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:
faris Aljohari
2025-06-25 03:20:25 -06:00
parent 43ab0030f0
commit 9bebcb2f3e
24 changed files with 368 additions and 128 deletions

View File

@ -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)
},

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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);
}

View File

@ -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,

View File

@ -1,6 +1,6 @@
WITH params AS (
SELECT
$2::date AS target_date
$1::date AS target_date
),
total_energy AS (
SELECT

View File

@ -1,6 +1,6 @@
WITH params AS (
SELECT
$2::date AS target_date
$1::date AS target_date
),
total_energy AS (
SELECT

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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: [
{

View File

@ -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: [],
})

View File

@ -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],
})

View File

@ -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],
})

View File

@ -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],
})

View File

@ -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],
})

View File

@ -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],
})

View File

@ -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],
})

View 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 {}

View 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');
}
}

View File

@ -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],
})

View File

@ -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],
})

View File

@ -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],
})