mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-10 15:17:41 +00:00
feat: implement scheduler for periodic data updates and optimize database procedures
- Added SchedulerModule and SchedulerService to handle hourly data updates for AQI, occupancy, and energy consumption. - Refactored existing services to remove unused device repository dependencies and streamline procedure execution. - Updated SQL procedures to use correct parameter indexing. - Enhanced error handling and logging for scheduled tasks. - Integrated new repositories for presence sensor and AQI pollutant stats across multiple modules. - Added NestJS schedule package for task scheduling capabilities.
This commit is contained in:
@ -126,7 +126,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
|
||||
extra: {
|
||||
charset: 'utf8mb4',
|
||||
max: 100, // set pool max size
|
||||
idleTimeoutMillis: 5000, // close idle clients after 5 second
|
||||
idleTimeoutMillis: 3000, // close idle clients after 5 second
|
||||
connectionTimeoutMillis: 12_000, // return an error after 11 second if connection could not be established
|
||||
maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)
|
||||
},
|
||||
|
@ -1,44 +1,63 @@
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SqlLoaderService } from './sql-loader.service';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
|
||||
import { SqlLoaderService } from './sql-loader.service';
|
||||
|
||||
@Injectable()
|
||||
export class AqiDataService {
|
||||
constructor(
|
||||
private readonly sqlLoader: SqlLoaderService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly deviceRepository: DeviceRepository,
|
||||
) {}
|
||||
async updateAQISensorHistoricalData(deviceUuid: string): Promise<void> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||
const device = await this.deviceRepository.findOne({
|
||||
where: { uuid: deviceUuid },
|
||||
relations: ['spaceDevice'],
|
||||
});
|
||||
|
||||
await this.executeProcedure(
|
||||
'fact_daily_space_aqi',
|
||||
'proceduce_update_daily_space_aqi',
|
||||
[dateStr, device.spaceDevice?.uuid],
|
||||
);
|
||||
async updateAQISensorHistoricalData(): Promise<void> {
|
||||
try {
|
||||
const { dateStr } = this.getFormattedDates();
|
||||
|
||||
// Execute all procedures in parallel
|
||||
await Promise.all([
|
||||
this.executeProcedureWithRetry(
|
||||
'proceduce_update_daily_space_aqi',
|
||||
[dateStr],
|
||||
'fact_daily_space_aqi',
|
||||
),
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error('Failed to insert or update aqi data:', err);
|
||||
console.error('Failed to update AQI sensor historical data:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeProcedure(
|
||||
procedureFolderName: string,
|
||||
private getFormattedDates(): { dateStr: string } {
|
||||
const now = new Date();
|
||||
return {
|
||||
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
|
||||
};
|
||||
}
|
||||
private async executeProcedureWithRetry(
|
||||
procedureFileName: string,
|
||||
params: (string | number | null)[],
|
||||
folderName: string,
|
||||
retries = 3,
|
||||
): Promise<void> {
|
||||
const query = this.loadQuery(procedureFolderName, procedureFileName);
|
||||
await this.dataSource.query(query, params);
|
||||
console.log(`Procedure ${procedureFileName} executed successfully.`);
|
||||
try {
|
||||
const query = this.loadQuery(folderName, procedureFileName);
|
||||
await this.dataSource.query(query, params);
|
||||
console.log(`Procedure ${procedureFileName} executed successfully.`);
|
||||
} catch (err) {
|
||||
if (retries > 0) {
|
||||
const delayMs = 1000 * (4 - retries); // Exponential backoff
|
||||
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
return this.executeProcedureWithRetry(
|
||||
procedureFileName,
|
||||
params,
|
||||
folderName,
|
||||
retries - 1,
|
||||
);
|
||||
}
|
||||
console.error(`Failed to execute ${procedureFileName}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private loadQuery(folderName: string, fileName: string): string {
|
||||
|
@ -1,65 +1,68 @@
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SqlLoaderService } from './sql-loader.service';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
|
||||
import { SqlLoaderService } from './sql-loader.service';
|
||||
|
||||
@Injectable()
|
||||
export class OccupancyService {
|
||||
constructor(
|
||||
private readonly sqlLoader: SqlLoaderService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly deviceRepository: DeviceRepository,
|
||||
) {}
|
||||
async updateOccupancySensorHistoricalDurationData(
|
||||
deviceUuid: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||
const device = await this.deviceRepository.findOne({
|
||||
where: { uuid: deviceUuid },
|
||||
relations: ['spaceDevice'],
|
||||
});
|
||||
|
||||
await this.executeProcedure(
|
||||
'fact_daily_space_occupancy_duration',
|
||||
'procedure_update_daily_space_occupancy_duration',
|
||||
[dateStr, device.spaceDevice?.uuid],
|
||||
);
|
||||
async updateOccupancyDataProcedures(): Promise<void> {
|
||||
try {
|
||||
const { dateStr } = this.getFormattedDates();
|
||||
|
||||
// Execute all procedures in parallel
|
||||
await Promise.all([
|
||||
this.executeProcedureWithRetry(
|
||||
'procedure_update_fact_space_occupancy',
|
||||
[dateStr],
|
||||
'fact_space_occupancy_count',
|
||||
),
|
||||
this.executeProcedureWithRetry(
|
||||
'procedure_update_daily_space_occupancy_duration',
|
||||
[dateStr],
|
||||
'fact_daily_space_occupancy_duration',
|
||||
),
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error('Failed to insert or update occupancy duration data:', err);
|
||||
console.error('Failed to update occupancy data:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
async updateOccupancySensorHistoricalData(deviceUuid: string): Promise<void> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||
const device = await this.deviceRepository.findOne({
|
||||
where: { uuid: deviceUuid },
|
||||
relations: ['spaceDevice'],
|
||||
});
|
||||
|
||||
await this.executeProcedure(
|
||||
'fact_space_occupancy_count',
|
||||
'procedure_update_fact_space_occupancy',
|
||||
[dateStr, device.spaceDevice?.uuid],
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to insert or update occupancy data:', err);
|
||||
throw err;
|
||||
}
|
||||
private getFormattedDates(): { dateStr: string } {
|
||||
const now = new Date();
|
||||
return {
|
||||
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
|
||||
};
|
||||
}
|
||||
|
||||
private async executeProcedure(
|
||||
procedureFolderName: string,
|
||||
private async executeProcedureWithRetry(
|
||||
procedureFileName: string,
|
||||
params: (string | number | null)[],
|
||||
folderName: string,
|
||||
retries = 3,
|
||||
): Promise<void> {
|
||||
const query = this.loadQuery(procedureFolderName, procedureFileName);
|
||||
await this.dataSource.query(query, params);
|
||||
console.log(`Procedure ${procedureFileName} executed successfully.`);
|
||||
try {
|
||||
const query = this.loadQuery(folderName, procedureFileName);
|
||||
await this.dataSource.query(query, params);
|
||||
console.log(`Procedure ${procedureFileName} executed successfully.`);
|
||||
} catch (err) {
|
||||
if (retries > 0) {
|
||||
const delayMs = 1000 * (4 - retries); // Exponential backoff
|
||||
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
return this.executeProcedureWithRetry(
|
||||
procedureFileName,
|
||||
params,
|
||||
folderName,
|
||||
retries - 1,
|
||||
);
|
||||
}
|
||||
console.error(`Failed to execute ${procedureFileName}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private loadQuery(folderName: string, fileName: string): string {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SqlLoaderService } from './sql-loader.service';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
|
||||
import { SqlLoaderService } from './sql-loader.service';
|
||||
|
||||
@Injectable()
|
||||
export class PowerClampService {
|
||||
@ -10,48 +10,72 @@ export class PowerClampService {
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async updateEnergyConsumedHistoricalData(deviceUuid: string): Promise<void> {
|
||||
async updateEnergyConsumedHistoricalData(): Promise<void> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||
const hour = now.getHours();
|
||||
const monthYear = now
|
||||
.toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
.replace('/', '-'); // MM-YYYY
|
||||
const { dateStr, monthYear } = this.getFormattedDates();
|
||||
|
||||
await this.executeProcedure(
|
||||
'fact_hourly_device_energy_consumed_procedure',
|
||||
[deviceUuid, dateStr, hour],
|
||||
);
|
||||
|
||||
await this.executeProcedure(
|
||||
'fact_daily_device_energy_consumed_procedure',
|
||||
[deviceUuid, dateStr],
|
||||
);
|
||||
|
||||
await this.executeProcedure(
|
||||
'fact_monthly_device_energy_consumed_procedure',
|
||||
[deviceUuid, monthYear],
|
||||
);
|
||||
// Execute all procedures in parallel
|
||||
await Promise.all([
|
||||
this.executeProcedureWithRetry(
|
||||
'fact_hourly_device_energy_consumed_procedure',
|
||||
[dateStr],
|
||||
'fact_device_energy_consumed',
|
||||
),
|
||||
this.executeProcedureWithRetry(
|
||||
'fact_daily_device_energy_consumed_procedure',
|
||||
[dateStr],
|
||||
'fact_device_energy_consumed',
|
||||
),
|
||||
this.executeProcedureWithRetry(
|
||||
'fact_monthly_device_energy_consumed_procedure',
|
||||
[monthYear],
|
||||
'fact_device_energy_consumed',
|
||||
),
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error('Failed to insert or update energy data:', err);
|
||||
console.error('Failed to update energy consumption data:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeProcedure(
|
||||
private getFormattedDates(): { dateStr: string; monthYear: string } {
|
||||
const now = new Date();
|
||||
return {
|
||||
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
|
||||
monthYear: now
|
||||
.toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
.replace('/', '-'), // MM-YYYY
|
||||
};
|
||||
}
|
||||
|
||||
private async executeProcedureWithRetry(
|
||||
procedureFileName: string,
|
||||
params: (string | number | null)[],
|
||||
folderName: string,
|
||||
retries = 3,
|
||||
): Promise<void> {
|
||||
const query = this.loadQuery(
|
||||
'fact_device_energy_consumed',
|
||||
procedureFileName,
|
||||
);
|
||||
await this.dataSource.query(query, params);
|
||||
console.log(`Procedure ${procedureFileName} executed successfully.`);
|
||||
try {
|
||||
const query = this.loadQuery(folderName, procedureFileName);
|
||||
await this.dataSource.query(query, params);
|
||||
console.log(`Procedure ${procedureFileName} executed successfully.`);
|
||||
} catch (err) {
|
||||
if (retries > 0) {
|
||||
const delayMs = 1000 * (4 - retries); // Exponential backoff
|
||||
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
return this.executeProcedureWithRetry(
|
||||
procedureFileName,
|
||||
params,
|
||||
folderName,
|
||||
retries - 1,
|
||||
);
|
||||
}
|
||||
console.error(`Failed to execute ${procedureFileName}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private loadQuery(folderName: string, fileName: string): string {
|
||||
|
@ -23,6 +23,13 @@ export class SosHandlerService {
|
||||
status: [{ code: 'sos', value: true }],
|
||||
log: logData,
|
||||
});
|
||||
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([
|
||||
{
|
||||
deviceTuyaUuid: devId,
|
||||
status: [{ code: 'sos', value: true }],
|
||||
log: logData,
|
||||
},
|
||||
]);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
@ -31,30 +38,13 @@ export class SosHandlerService {
|
||||
status: [{ code: 'sos', value: false }],
|
||||
log: logData,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to send SOS false value', err);
|
||||
}
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to send SOS true value', err);
|
||||
}
|
||||
}
|
||||
|
||||
async handleSosEventOurDb(devId: string, logData: any): Promise<void> {
|
||||
try {
|
||||
await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({
|
||||
deviceTuyaUuid: devId,
|
||||
status: [{ code: 'sos', value: true }],
|
||||
log: logData,
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({
|
||||
deviceTuyaUuid: devId,
|
||||
status: [{ code: 'sos', value: false }],
|
||||
log: logData,
|
||||
});
|
||||
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([
|
||||
{
|
||||
deviceTuyaUuid: devId,
|
||||
status: [{ code: 'sos', value: false }],
|
||||
log: logData,
|
||||
},
|
||||
]);
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to send SOS false value', err);
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ export class TuyaWebSocketService {
|
||||
|
||||
try {
|
||||
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb(
|
||||
batch.map((item) => ({
|
||||
batch?.map((item) => ({
|
||||
deviceTuyaUuid: item.devId,
|
||||
status: item.status,
|
||||
log: item.logData,
|
||||
|
@ -1,6 +1,6 @@
|
||||
WITH params AS (
|
||||
SELECT
|
||||
$2::date AS target_date
|
||||
$1::date AS target_date
|
||||
),
|
||||
total_energy AS (
|
||||
SELECT
|
||||
|
@ -1,6 +1,6 @@
|
||||
WITH params AS (
|
||||
SELECT
|
||||
$2::date AS target_date
|
||||
$1::date AS target_date
|
||||
),
|
||||
total_energy AS (
|
||||
SELECT
|
||||
|
@ -1,6 +1,6 @@
|
||||
WITH params AS (
|
||||
SELECT
|
||||
$2::text AS target_month -- Format should match 'MM-YYYY'
|
||||
$1::text AS target_month -- Format should match 'MM-YYYY'
|
||||
),
|
||||
total_energy AS (
|
||||
SELECT
|
||||
|
42
package-lock.json
generated
42
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/schedule": "^6.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
@ -2538,6 +2539,19 @@
|
||||
"@nestjs/core": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schedule": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz",
|
||||
"integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron": "4.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schematics": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz",
|
||||
@ -3215,6 +3229,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
|
||||
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
@ -5426,6 +5446,19 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cron": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz",
|
||||
"integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/luxon": "~3.6.0",
|
||||
"luxon": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@ -9777,6 +9810,15 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
|
||||
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.8",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
|
||||
|
@ -30,6 +30,7 @@
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/schedule": "^6.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
|
@ -42,6 +42,8 @@ import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston
|
||||
import { AqiModule } from './aqi/aqi.module';
|
||||
import { OccupancyModule } from './occupancy/occupancy.module';
|
||||
import { WeatherModule } from './weather/weather.module';
|
||||
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule'; // ✅ الباكيج الرسمي
|
||||
import { SchedulerModule } from './scheduler/scheduler.module';
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
@ -94,6 +96,8 @@ import { WeatherModule } from './weather/weather.module';
|
||||
OccupancyModule,
|
||||
WeatherModule,
|
||||
AqiModule,
|
||||
SchedulerModule,
|
||||
NestScheduleModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -30,6 +30,8 @@ import { PowerClampService } from '@app/common/helper/services/power.clamp.servi
|
||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, SpaceRepositoryModule],
|
||||
@ -59,6 +61,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
SqlLoaderService,
|
||||
OccupancyService,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
|
@ -64,6 +64,8 @@ import {
|
||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
|
||||
@ -118,6 +120,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
SqlLoaderService,
|
||||
OccupancyService,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [CommunityService, SpacePermissionService],
|
||||
})
|
||||
|
@ -30,6 +30,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
@Module({
|
||||
imports: [ConfigModule, DeviceRepositoryModule],
|
||||
controllers: [DoorLockController],
|
||||
@ -58,6 +60,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
OccupancyService,
|
||||
CommunityRepository,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [DoorLockService],
|
||||
})
|
||||
|
@ -28,6 +28,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
@Module({
|
||||
imports: [ConfigModule, DeviceRepositoryModule],
|
||||
controllers: [GroupController],
|
||||
@ -55,6 +57,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
OccupancyService,
|
||||
CommunityRepository,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [GroupService],
|
||||
})
|
||||
|
@ -82,6 +82,8 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su
|
||||
import { TagService as NewTagService } from 'src/tags/services';
|
||||
import { UserDevicePermissionService } from 'src/user-device-permission/services';
|
||||
import { UserService, UserSpaceService } from 'src/users/services';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
|
||||
@ -150,6 +152,8 @@ import { UserService, UserSpaceService } from 'src/users/services';
|
||||
SqlLoaderService,
|
||||
OccupancyService,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [InviteUserService],
|
||||
})
|
||||
|
@ -60,6 +60,8 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su
|
||||
import { TagService } from 'src/tags/services';
|
||||
import { PowerClampController } from './controllers';
|
||||
import { PowerClampService as PowerClamp } from './services/power-clamp.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
controllers: [PowerClampController],
|
||||
@ -109,6 +111,8 @@ import { PowerClampService as PowerClamp } from './services/power-clamp.service'
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
OccupancyService,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [PowerClamp],
|
||||
})
|
||||
|
@ -67,6 +67,8 @@ import { ProjectUserController } from './controllers/project-user.controller';
|
||||
import { CreateOrphanSpaceHandler } from './handler';
|
||||
import { ProjectService } from './services';
|
||||
import { ProjectUserService } from './services/project-user.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
|
||||
const CommandHandlers = [CreateOrphanSpaceHandler];
|
||||
|
||||
@ -124,6 +126,8 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
|
||||
SqlLoaderService,
|
||||
OccupancyService,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [ProjectService, CqrsModule],
|
||||
})
|
||||
|
25
src/scheduler/scheduler.module.ts
Normal file
25
src/scheduler/scheduler.module.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { DatabaseModule } from '@app/common/database/database.module';
|
||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SchedulerService } from './scheduler.service';
|
||||
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule'; // ✅ الباكيج الرسمي
|
||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestScheduleModule.forRoot(),
|
||||
TypeOrmModule.forFeature([]),
|
||||
DatabaseModule,
|
||||
],
|
||||
providers: [
|
||||
SchedulerService,
|
||||
SqlLoaderService,
|
||||
PowerClampService,
|
||||
OccupancyService,
|
||||
AqiDataService,
|
||||
],
|
||||
})
|
||||
export class SchedulerModule {}
|
92
src/scheduler/scheduler.service.ts
Normal file
92
src/scheduler/scheduler.service.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||
|
||||
@Injectable()
|
||||
export class SchedulerService {
|
||||
constructor(
|
||||
private readonly powerClampService: PowerClampService,
|
||||
private readonly occupancyService: OccupancyService,
|
||||
private readonly aqiDataService: AqiDataService,
|
||||
) {
|
||||
console.log('SchedulerService initialized!');
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_HOUR)
|
||||
async runHourlyProcedures() {
|
||||
console.log('\n======== Starting Procedures ========');
|
||||
console.log(new Date().toISOString(), 'Scheduler running...');
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
this.executeTask(
|
||||
() => this.powerClampService.updateEnergyConsumedHistoricalData(),
|
||||
'Energy Consumption',
|
||||
),
|
||||
this.executeTask(
|
||||
() => this.occupancyService.updateOccupancyDataProcedures(),
|
||||
'Occupancy Data',
|
||||
),
|
||||
this.executeTask(
|
||||
() => this.aqiDataService.updateAQISensorHistoricalData(),
|
||||
'AQI Data',
|
||||
),
|
||||
]);
|
||||
|
||||
this.logResults(results);
|
||||
} catch (error) {
|
||||
console.error('MAIN SCHEDULER ERROR:', error);
|
||||
if (error.stack) {
|
||||
console.error('Error stack:', error.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async executeTask(
|
||||
task: () => Promise<void>,
|
||||
name: string,
|
||||
): Promise<{ name: string; status: string }> {
|
||||
try {
|
||||
console.log(`[${new Date().toISOString()}] Starting ${name} task...`);
|
||||
await task();
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ${name} task completed successfully`,
|
||||
);
|
||||
return { name, status: 'success' };
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ${name} task failed:`,
|
||||
error.message,
|
||||
);
|
||||
if (error.stack) {
|
||||
console.error('Task error stack:', error.stack);
|
||||
}
|
||||
return { name, status: 'failed' };
|
||||
}
|
||||
}
|
||||
|
||||
private logResults(results: PromiseSettledResult<any>[]) {
|
||||
const successCount = results.filter((r) => r.status === 'fulfilled').length;
|
||||
const failedCount = results.length - successCount;
|
||||
|
||||
console.log('\n======== Task Results ========');
|
||||
console.log(`Successful tasks: ${successCount}`);
|
||||
console.log(`Failed tasks: ${failedCount}`);
|
||||
|
||||
if (failedCount > 0) {
|
||||
console.log('\n======== Failed Tasks Details ========');
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.error(`Task ${index + 1} failed:`, result.reason);
|
||||
if (result.reason.stack) {
|
||||
console.error('Error stack:', result.reason.stack);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n======== Scheduler Completed ========\n');
|
||||
}
|
||||
}
|
@ -63,6 +63,8 @@ import {
|
||||
import { SpaceModelService, SubSpaceModelService } from './services';
|
||||
import { SpaceModelProductAllocationService } from './services/space-model-product-allocation.service';
|
||||
import { SubspaceModelProductAllocationService } from './services/subspace/subspace-model-product-allocation.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
|
||||
const CommandHandlers = [
|
||||
PropogateUpdateSpaceModelHandler,
|
||||
@ -120,6 +122,8 @@ const CommandHandlers = [
|
||||
SqlLoaderService,
|
||||
OccupancyService,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [CqrsModule, SpaceModelService],
|
||||
})
|
||||
|
@ -88,6 +88,8 @@ import {
|
||||
} from './services';
|
||||
import { SpaceProductAllocationService } from './services/space-product-allocation.service';
|
||||
import { SubspaceProductAllocationService } from './services/subspace/subspace-product-allocation.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
|
||||
export const CommandHandlers = [DisableSpaceHandler];
|
||||
|
||||
@ -161,6 +163,8 @@ export const CommandHandlers = [DisableSpaceHandler];
|
||||
SqlLoaderService,
|
||||
OccupancyService,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [SpaceService],
|
||||
})
|
||||
|
@ -32,6 +32,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
||||
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
|
||||
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
|
||||
@Module({
|
||||
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
|
||||
controllers: [VisitorPasswordController],
|
||||
@ -61,6 +63,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||
OccupancyService,
|
||||
CommunityRepository,
|
||||
AqiDataService,
|
||||
PresenceSensorDailySpaceRepository,
|
||||
AqiSpaceDailyPollutantStatsRepository,
|
||||
],
|
||||
exports: [VisitorPasswordService],
|
||||
})
|
||||
|
Reference in New Issue
Block a user