diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index dd25da9..e86ac6e 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -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) }, diff --git a/libs/common/src/helper/services/aqi.data.service.ts b/libs/common/src/helper/services/aqi.data.service.ts index 3e19b6c..9a18274 100644 --- a/libs/common/src/helper/services/aqi.data.service.ts +++ b/libs/common/src/helper/services/aqi.data.service.ts @@ -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 { - 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 { + 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 { - 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 { diff --git a/libs/common/src/helper/services/occupancy.service.ts b/libs/common/src/helper/services/occupancy.service.ts index ea99b7c..cec1560 100644 --- a/libs/common/src/helper/services/occupancy.service.ts +++ b/libs/common/src/helper/services/occupancy.service.ts @@ -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 { - 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 { + 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 { - 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 { - 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 { diff --git a/libs/common/src/helper/services/power.clamp.service.ts b/libs/common/src/helper/services/power.clamp.service.ts index 7c83208..7805dd5 100644 --- a/libs/common/src/helper/services/power.clamp.service.ts +++ b/libs/common/src/helper/services/power.clamp.service.ts @@ -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 { + async updateEnergyConsumedHistoricalData(): Promise { 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 { - 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 { diff --git a/libs/common/src/helper/services/sos.handler.service.ts b/libs/common/src/helper/services/sos.handler.service.ts index e5f9df9..dd69f33 100644 --- a/libs/common/src/helper/services/sos.handler.service.ts +++ b/libs/common/src/helper/services/sos.handler.service.ts @@ -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 { - 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); } diff --git a/libs/common/src/helper/services/tuya.web.socket.service.ts b/libs/common/src/helper/services/tuya.web.socket.service.ts index 3f56a63..1db1991 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -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, diff --git a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_daily_device_energy_consumed_procedure.sql b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_daily_device_energy_consumed_procedure.sql index ab9e7d2..233b24d 100644 --- a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_daily_device_energy_consumed_procedure.sql +++ b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_daily_device_energy_consumed_procedure.sql @@ -1,6 +1,6 @@ WITH params AS ( SELECT - $2::date AS target_date + $1::date AS target_date ), total_energy AS ( SELECT diff --git a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_hourly_device_energy_consumed_procedure.sql b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_hourly_device_energy_consumed_procedure.sql index c056a0f..afe6e4d 100644 --- a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_hourly_device_energy_consumed_procedure.sql +++ b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_hourly_device_energy_consumed_procedure.sql @@ -1,6 +1,6 @@ WITH params AS ( SELECT - $2::date AS target_date + $1::date AS target_date ), total_energy AS ( SELECT diff --git a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_monthly_device_energy_consumed_procedure.sql b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_monthly_device_energy_consumed_procedure.sql index 691de79..8deddda 100644 --- a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_monthly_device_energy_consumed_procedure.sql +++ b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_monthly_device_energy_consumed_procedure.sql @@ -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 diff --git a/package-lock.json b/package-lock.json index eaf972a..e3305e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index eaec865..55d546b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app.module.ts b/src/app.module.ts index 712531f..a7ae475 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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: [ { diff --git a/src/commission-device/commission-device.module.ts b/src/commission-device/commission-device.module.ts index 3306705..a189f71 100644 --- a/src/commission-device/commission-device.module.ts +++ b/src/commission-device/commission-device.module.ts @@ -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: [], }) diff --git a/src/community/community.module.ts b/src/community/community.module.ts index 0567ffb..e41f78d 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -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], }) diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index c2eaad1..407fced 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -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], }) diff --git a/src/group/group.module.ts b/src/group/group.module.ts index 443ac31..7f9f6ab 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -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], }) diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index 31e3a9b..d8655c5 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -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], }) diff --git a/src/power-clamp/power-clamp.module.ts b/src/power-clamp/power-clamp.module.ts index 120e368..bb8317b 100644 --- a/src/power-clamp/power-clamp.module.ts +++ b/src/power-clamp/power-clamp.module.ts @@ -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], }) diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 7c7f7d3..8860e11 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -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], }) diff --git a/src/scheduler/scheduler.module.ts b/src/scheduler/scheduler.module.ts new file mode 100644 index 0000000..f5bbb09 --- /dev/null +++ b/src/scheduler/scheduler.module.ts @@ -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 {} diff --git a/src/scheduler/scheduler.service.ts b/src/scheduler/scheduler.service.ts new file mode 100644 index 0000000..e8e4337 --- /dev/null +++ b/src/scheduler/scheduler.service.ts @@ -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, + 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[]) { + 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'); + } +} diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index 4cd6435..1b0a576 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -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], }) diff --git a/src/space/space.module.ts b/src/space/space.module.ts index 8229879..288706e 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -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], }) diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index c66ba39..b1d927c 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -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], })