From ef2245eae1eec5725f49b64cbfe0ce9b9a714eb0 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 3 Jun 2025 23:37:52 -0600 Subject: [PATCH 1/4] Add AQI space daily pollutant stats module and related entities, DTOs, and repositories --- libs/common/src/database/database.module.ts | 2 + .../src/modules/aqi/aqi.repository.module.ts | 11 +++ libs/common/src/modules/aqi/dtos/aqi.dto.ts | 82 +++++++++++++++++ libs/common/src/modules/aqi/dtos/index.ts | 1 + .../src/modules/aqi/entities/aqi.entity.ts | 88 +++++++++++++++++++ libs/common/src/modules/aqi/entities/index.ts | 1 + .../src/modules/aqi/repositories/index.ts | 1 + .../presence-sensor.repository.ts | 19 ++++ 8 files changed, 205 insertions(+) create mode 100644 libs/common/src/modules/aqi/aqi.repository.module.ts create mode 100644 libs/common/src/modules/aqi/dtos/aqi.dto.ts create mode 100644 libs/common/src/modules/aqi/dtos/index.ts create mode 100644 libs/common/src/modules/aqi/entities/aqi.entity.ts create mode 100644 libs/common/src/modules/aqi/entities/index.ts create mode 100644 libs/common/src/modules/aqi/repositories/index.ts create mode 100644 libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 183fbcc..d25dbd8 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -55,6 +55,7 @@ import { PresenceSensorDailyDeviceEntity, PresenceSensorDailySpaceEntity, } from '../modules/presence-sensor/entities'; +import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -115,6 +116,7 @@ import { PowerClampMonthlyEntity, PresenceSensorDailyDeviceEntity, PresenceSensorDailySpaceEntity, + AqiSpaceDailyPollutantStatsEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/aqi/aqi.repository.module.ts b/libs/common/src/modules/aqi/aqi.repository.module.ts new file mode 100644 index 0000000..1fc820d --- /dev/null +++ b/libs/common/src/modules/aqi/aqi.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AqiSpaceDailyPollutantStatsEntity } from './entities/aqi.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([AqiSpaceDailyPollutantStatsEntity])], +}) +export class AqiRepositoryModule {} diff --git a/libs/common/src/modules/aqi/dtos/aqi.dto.ts b/libs/common/src/modules/aqi/dtos/aqi.dto.ts new file mode 100644 index 0000000..1745d60 --- /dev/null +++ b/libs/common/src/modules/aqi/dtos/aqi.dto.ts @@ -0,0 +1,82 @@ +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +export class AqiSpaceDailyPollutantStatsDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsNotEmpty() + @IsString() + spaceUuid: string; + + @IsNotEmpty() + @IsString() + eventDay: string; + + @IsNotEmpty() + @IsNumber() + eventHour: number; + + @IsNumber() + pm1Min: number; + + @IsNumber() + pm1Avg: number; + + @IsNumber() + pm1Max: number; + + @IsNumber() + pm10Min: number; + + @IsNumber() + pm10Avg: number; + + @IsNumber() + pm10Max: number; + + @IsNumber() + pm25Min: number; + + @IsNumber() + pm25Avg: number; + + @IsNumber() + pm25Max: number; + + @IsNumber() + ch2oMin: number; + + @IsNumber() + ch2oAvg: number; + + @IsNumber() + ch2oMax: number; + + @IsNumber() + vocMin: number; + + @IsNumber() + vocAvg: number; + + @IsNumber() + vocMax: number; + + @IsNumber() + co2Min: number; + + @IsNumber() + co2Avg: number; + + @IsNumber() + co2Max: number; + + @IsNumber() + aqiMin: number; + + @IsNumber() + aqiAvg: number; + + @IsNumber() + aqiMax: number; +} diff --git a/libs/common/src/modules/aqi/dtos/index.ts b/libs/common/src/modules/aqi/dtos/index.ts new file mode 100644 index 0000000..da89122 --- /dev/null +++ b/libs/common/src/modules/aqi/dtos/index.ts @@ -0,0 +1 @@ +export * from './aqi.dto'; diff --git a/libs/common/src/modules/aqi/entities/aqi.entity.ts b/libs/common/src/modules/aqi/entities/aqi.entity.ts new file mode 100644 index 0000000..701bcdd --- /dev/null +++ b/libs/common/src/modules/aqi/entities/aqi.entity.ts @@ -0,0 +1,88 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceEntity } from '../../space/entities/space.entity'; +import { AqiSpaceDailyPollutantStatsDto } from '../dtos'; + +@Entity({ name: 'space-daily-pollutant-stats' }) +@Unique(['spaceUuid', 'eventDay', 'eventHour']) +export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity { + @Column({ nullable: false }) + public spaceUuid: string; + + @ManyToOne(() => SpaceEntity, (space) => space.presenceSensorDaily) + space: SpaceEntity; + + @Column({ nullable: false }) + public eventDay: string; + + @Column({ nullable: false }) + public eventHour: number; + + @Column('float', { nullable: true }) + public pm1Min: number; + + @Column('float', { nullable: true }) + public pm1Avg: number; + + @Column('float', { nullable: true }) + public pm1Max: number; + + @Column('float', { nullable: true }) + public pm10Min: number; + + @Column('float', { nullable: true }) + public pm10Avg: number; + + @Column('float', { nullable: true }) + public pm10Max: number; + + @Column('float', { nullable: true }) + public pm25Min: number; + + @Column('float', { nullable: true }) + public pm25Avg: number; + + @Column('float', { nullable: true }) + public pm25Max: number; + + @Column('float', { nullable: true }) + public ch2oMin: number; + + @Column('float', { nullable: true }) + public ch2oAvg: number; + + @Column('float', { nullable: true }) + public ch2oMax: number; + + @Column('float', { nullable: true }) + public vocMin: number; + + @Column('float', { nullable: true }) + public vocAvg: number; + + @Column('float', { nullable: true }) + public vocMax: number; + + @Column('float', { nullable: true }) + public co2Min: number; + + @Column('float', { nullable: true }) + public co2Avg: number; + + @Column('float', { nullable: true }) + public co2Max: number; + + @Column('float', { nullable: true }) + public aqiMin: number; + + @Column('float', { nullable: true }) + public aqiAvg: number; + + @Column('float', { nullable: true }) + public aqiMax: number; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/aqi/entities/index.ts b/libs/common/src/modules/aqi/entities/index.ts new file mode 100644 index 0000000..655a0e7 --- /dev/null +++ b/libs/common/src/modules/aqi/entities/index.ts @@ -0,0 +1 @@ +export * from './aqi.entity'; diff --git a/libs/common/src/modules/aqi/repositories/index.ts b/libs/common/src/modules/aqi/repositories/index.ts new file mode 100644 index 0000000..8b64ee8 --- /dev/null +++ b/libs/common/src/modules/aqi/repositories/index.ts @@ -0,0 +1 @@ +export * from './presence-sensor.repository'; diff --git a/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts b/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts new file mode 100644 index 0000000..146eb59 --- /dev/null +++ b/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts @@ -0,0 +1,19 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { + PresenceSensorDailyDeviceEntity, + PresenceSensorDailySpaceEntity, +} from '../entities'; + +@Injectable() +export class PresenceSensorDailyDeviceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(PresenceSensorDailyDeviceEntity, dataSource.createEntityManager()); + } +} +@Injectable() +export class PresenceSensorDailySpaceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(PresenceSensorDailySpaceEntity, dataSource.createEntityManager()); + } +} From 0d6de2df43455711ab0061c91a2776ac3a040fd5 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:24:13 -0600 Subject: [PATCH 2/4] Refactor AqiSpaceDailyPollutantStatsEntity: update unique constraint and rename event fields for clarity --- .../src/modules/aqi/entities/aqi.entity.ts | 150 ++++++++++++++---- 1 file changed, 123 insertions(+), 27 deletions(-) diff --git a/libs/common/src/modules/aqi/entities/aqi.entity.ts b/libs/common/src/modules/aqi/entities/aqi.entity.ts index 701bcdd..fed3881 100644 --- a/libs/common/src/modules/aqi/entities/aqi.entity.ts +++ b/libs/common/src/modules/aqi/entities/aqi.entity.ts @@ -4,7 +4,7 @@ import { SpaceEntity } from '../../space/entities/space.entity'; import { AqiSpaceDailyPollutantStatsDto } from '../dtos'; @Entity({ name: 'space-daily-pollutant-stats' }) -@Unique(['spaceUuid', 'eventDay', 'eventHour']) +@Unique(['spaceUuid', 'eventDate']) export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity { @Column({ nullable: false }) public spaceUuid: string; @@ -12,74 +12,170 @@ export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity SpaceEntity, (space) => space.presenceSensorDaily) space: SpaceEntity; - @Column({ nullable: false }) - public eventDay: string; - - @Column({ nullable: false }) - public eventHour: number; + @Column({ type: 'date', nullable: false }) + public eventDate: Date; @Column('float', { nullable: true }) - public pm1Min: number; + public goodAqiPercentage: number; @Column('float', { nullable: true }) - public pm1Avg: number; + public moderateAqiPercentage: number; @Column('float', { nullable: true }) - public pm1Max: number; + public unhealthySensitiveAqiPercentage: number; @Column('float', { nullable: true }) - public pm10Min: number; + public unhealthyAqiPercentage: number; @Column('float', { nullable: true }) - public pm10Avg: number; + public veryUnhealthyAqiPercentage: number; @Column('float', { nullable: true }) - public pm10Max: number; + public hazardousAqiPercentage: number; @Column('float', { nullable: true }) - public pm25Min: number; + public dailyAvgAqi: number; @Column('float', { nullable: true }) - public pm25Avg: number; + public dailyMaxAqi: number; @Column('float', { nullable: true }) - public pm25Max: number; + public dailyMinAqi: number; @Column('float', { nullable: true }) - public ch2oMin: number; + public goodPm25Percentage: number; @Column('float', { nullable: true }) - public ch2oAvg: number; + public moderatePm25Percentage: number; @Column('float', { nullable: true }) - public ch2oMax: number; + public unhealthySensitivePm25Percentage: number; @Column('float', { nullable: true }) - public vocMin: number; + public unhealthyPm25Percentage: number; @Column('float', { nullable: true }) - public vocAvg: number; + public veryUnhealthyPm25Percentage: number; @Column('float', { nullable: true }) - public vocMax: number; + public hazardousPm25Percentage: number; @Column('float', { nullable: true }) - public co2Min: number; + public dailyAvgPm25: number; @Column('float', { nullable: true }) - public co2Avg: number; + public dailyMaxPm25: number; @Column('float', { nullable: true }) - public co2Max: number; + public dailyMinPm25: number; @Column('float', { nullable: true }) - public aqiMin: number; + public goodPm10Percentage: number; @Column('float', { nullable: true }) - public aqiAvg: number; + public moderatePm10Percentage: number; @Column('float', { nullable: true }) - public aqiMax: number; + public unhealthySensitivePm10Percentage: number; + + @Column('float', { nullable: true }) + public unhealthyPm10Percentage: number; + + @Column('float', { nullable: true }) + public veryUnhealthyPm10Percentage: number; + + @Column('float', { nullable: true }) + public hazardousPm10Percentage: number; + + @Column('float', { nullable: true }) + public dailyAvgPm10: number; + + @Column('float', { nullable: true }) + public dailyMaxPm10: number; + + @Column('float', { nullable: true }) + public dailyMinPm10: number; + + @Column('float', { nullable: true }) + public goodVocPercentage: number; + + @Column('float', { nullable: true }) + public moderateVocPercentage: number; + + @Column('float', { nullable: true }) + public unhealthySensitiveVocPercentage: number; + + @Column('float', { nullable: true }) + public unhealthyVocPercentage: number; + + @Column('float', { nullable: true }) + public veryUnhealthyVocPercentage: number; + + @Column('float', { nullable: true }) + public hazardousVocPercentage: number; + + @Column('float', { nullable: true }) + public dailyAvgVoc: number; + + @Column('float', { nullable: true }) + public dailyMaxVoc: number; + + @Column('float', { nullable: true }) + public dailyMinVoc: number; + + @Column('float', { nullable: true }) + public goodCo2Percentage: number; + + @Column('float', { nullable: true }) + public moderateCo2Percentage: number; + + @Column('float', { nullable: true }) + public unhealthySensitiveCo2Percentage: number; + + @Column('float', { nullable: true }) + public unhealthyCo2Percentage: number; + + @Column('float', { nullable: true }) + public veryUnhealthyCo2Percentage: number; + + @Column('float', { nullable: true }) + public hazardousCo2Percentage: number; + + @Column('float', { nullable: true }) + public dailyAvgCo2: number; + + @Column('float', { nullable: true }) + public dailyMaxCo2: number; + + @Column('float', { nullable: true }) + public dailyMinCo2: number; + + @Column('float', { nullable: true }) + public goodCh2oPercentage: number; + + @Column('float', { nullable: true }) + public moderateCh2oPercentage: number; + + @Column('float', { nullable: true }) + public unhealthySensitiveCh2oPercentage: number; + + @Column('float', { nullable: true }) + public unhealthyCh2oPercentage: number; + + @Column('float', { nullable: true }) + public veryUnhealthyCh2oPercentage: number; + + @Column('float', { nullable: true }) + public hazardousCh2oPercentage: number; + + @Column('float', { nullable: true }) + public dailyAvgCh2o: number; + + @Column('float', { nullable: true }) + public dailyMaxCh2o: number; + + @Column('float', { nullable: true }) + public dailyMinCh2o: number; constructor(partial: Partial) { super(); From c86be27576e6f4ea9c0fd4c32cc8ccf78a474998 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:19:34 -0600 Subject: [PATCH 3/4] Add AQI module and related services, controllers, and DTOs - Introduced AqiModule with AqiService and AqiController for handling AQI data. - Added DTOs for AQI requests: GetAqiDailyBySpaceDto and GetAqiPollutantBySpaceDto. - Implemented AqiDataService for managing AQI sensor historical data. - Updated existing modules to include AqiDataService where necessary. - Defined new routes for AQI data retrieval in ControllerRoute. --- libs/common/src/constants/controller-route.ts | 14 ++ .../common/src/constants/product-type.enum.ts | 1 + .../devices-status/devices-status.module.ts | 2 + .../services/devices-status.service.ts | 8 +- .../src/helper/services/aqi.data.service.ts | 47 ++++++ .../aqi/repositories/aqi.repository.ts | 10 ++ .../src/modules/aqi/repositories/index.ts | 2 +- .../presence-sensor.repository.ts | 19 --- src/app.module.ts | 2 + src/aqi/aqi.module.ts | 11 ++ src/aqi/controllers/aqi.controller.ts | 64 ++++++++ src/aqi/controllers/index.ts | 1 + src/aqi/dto/aqi-params.dto.ts | 7 + src/aqi/dto/get-aqi.dto.ts | 36 +++++ src/aqi/services/aqi.service.ts | 146 ++++++++++++++++++ src/aqi/services/index.ts | 1 + .../commission-device.module.ts | 2 + src/community/community.module.ts | 2 + src/door-lock/door.lock.module.ts | 2 + src/group/group.module.ts | 2 + src/invite-user/invite-user.module.ts | 2 + src/power-clamp/power-clamp.module.ts | 2 + src/project/project.module.ts | 2 + src/space-model/space-model.module.ts | 2 + src/space/space.module.ts | 2 + .../visitor-password.module.ts | 2 + 26 files changed, 370 insertions(+), 21 deletions(-) create mode 100644 libs/common/src/helper/services/aqi.data.service.ts create mode 100644 libs/common/src/modules/aqi/repositories/aqi.repository.ts delete mode 100644 libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts create mode 100644 src/aqi/aqi.module.ts create mode 100644 src/aqi/controllers/aqi.controller.ts create mode 100644 src/aqi/controllers/index.ts create mode 100644 src/aqi/dto/aqi-params.dto.ts create mode 100644 src/aqi/dto/get-aqi.dto.ts create mode 100644 src/aqi/services/aqi.service.ts create mode 100644 src/aqi/services/index.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 064deba..cc37eee 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -524,6 +524,20 @@ export class ControllerRoute { 'This endpoint retrieves the occupancy heat map data based on the provided parameters.'; }; }; + static AQI = class { + public static readonly ROUTE = 'aqi'; + + static ACTIONS = class { + public static readonly GET_AQI_RANGE_DATA_SUMMARY = 'Get AQI range data'; + public static readonly GET_AQI_RANGE_DATA_DESCRIPTION = + 'This endpoint retrieves the AQI (Air Quality Index) range data based on the provided parameters.'; + + public static readonly GET_AQI_DISTRIBUTION_DATA_SUMMARY = + 'Get AQI distribution data'; + public static readonly GET_AQI_DISTRIBUTION_DATA_DESCRIPTION = + 'This endpoint retrieves the AQI (Air Quality Index) distribution data based on the provided parameters.'; + }; + }; static DEVICE = class { public static readonly ROUTE = 'devices'; diff --git a/libs/common/src/constants/product-type.enum.ts b/libs/common/src/constants/product-type.enum.ts index fd0cc6f..1bb1394 100644 --- a/libs/common/src/constants/product-type.enum.ts +++ b/libs/common/src/constants/product-type.enum.ts @@ -19,4 +19,5 @@ export enum ProductType { FOUR_S = '4S', SIX_S = '6S', SOS = 'SOS', + AQI = 'AQI', } diff --git a/libs/common/src/firebase/devices-status/devices-status.module.ts b/libs/common/src/firebase/devices-status/devices-status.module.ts index 784b801..52f6123 100644 --- a/libs/common/src/firebase/devices-status/devices-status.module.ts +++ b/libs/common/src/firebase/devices-status/devices-status.module.ts @@ -11,6 +11,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; 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'; @Module({ providers: [ @@ -23,6 +24,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], controllers: [DeviceStatusFirebaseController], exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository], diff --git a/libs/common/src/firebase/devices-status/services/devices-status.service.ts b/libs/common/src/firebase/devices-status/services/devices-status.service.ts index 18c752c..4b0b0f7 100644 --- a/libs/common/src/firebase/devices-status/services/devices-status.service.ts +++ b/libs/common/src/firebase/devices-status/services/devices-status.service.ts @@ -23,6 +23,7 @@ import { PowerClampService } from '@app/common/helper/services/power.clamp.servi import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum'; import { PresenceSensorEnum } from '@app/common/constants/presence.sensor.enum'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Injectable() export class DeviceStatusFirebaseService { private tuya: TuyaContext; @@ -32,6 +33,7 @@ export class DeviceStatusFirebaseService { private readonly deviceRepository: DeviceRepository, private readonly powerClampService: PowerClampService, private readonly occupancyService: OccupancyService, + private readonly aqiDataService: AqiDataService, private deviceStatusLogRepository: DeviceStatusLogRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -262,7 +264,11 @@ export class DeviceStatusFirebaseService { ); } } - + if (addDeviceStatusDto.productType === ProductType.AQI) { + await this.aqiDataService.updateAQISensorHistoricalData( + addDeviceStatusDto.deviceUuid, + ); + } // Return the updated data const snapshot: DataSnapshot = await get(dataRef); return snapshot.val(); diff --git a/libs/common/src/helper/services/aqi.data.service.ts b/libs/common/src/helper/services/aqi.data.service.ts new file mode 100644 index 0000000..3e19b6c --- /dev/null +++ b/libs/common/src/helper/services/aqi.data.service.ts @@ -0,0 +1,47 @@ +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'; + +@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], + ); + } catch (err) { + console.error('Failed to insert or update aqi data:', err); + throw err; + } + } + + private async executeProcedure( + procedureFolderName: string, + procedureFileName: string, + params: (string | number | null)[], + ): Promise { + const query = this.loadQuery(procedureFolderName, procedureFileName); + await this.dataSource.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); + } + + private loadQuery(folderName: string, fileName: string): string { + return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH); + } +} diff --git a/libs/common/src/modules/aqi/repositories/aqi.repository.ts b/libs/common/src/modules/aqi/repositories/aqi.repository.ts new file mode 100644 index 0000000..4f02490 --- /dev/null +++ b/libs/common/src/modules/aqi/repositories/aqi.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { AqiSpaceDailyPollutantStatsEntity } from '../entities'; + +@Injectable() +export class AqiSpaceDailyPollutantStatsRepository extends Repository { + constructor(private dataSource: DataSource) { + super(AqiSpaceDailyPollutantStatsEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/aqi/repositories/index.ts b/libs/common/src/modules/aqi/repositories/index.ts index 8b64ee8..d1c0488 100644 --- a/libs/common/src/modules/aqi/repositories/index.ts +++ b/libs/common/src/modules/aqi/repositories/index.ts @@ -1 +1 @@ -export * from './presence-sensor.repository'; +export * from './aqi.repository'; diff --git a/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts b/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts deleted file mode 100644 index 146eb59..0000000 --- a/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Injectable } from '@nestjs/common'; -import { - PresenceSensorDailyDeviceEntity, - PresenceSensorDailySpaceEntity, -} from '../entities'; - -@Injectable() -export class PresenceSensorDailyDeviceRepository extends Repository { - constructor(private dataSource: DataSource) { - super(PresenceSensorDailyDeviceEntity, dataSource.createEntityManager()); - } -} -@Injectable() -export class PresenceSensorDailySpaceRepository extends Repository { - constructor(private dataSource: DataSource) { - super(PresenceSensorDailySpaceEntity, dataSource.createEntityManager()); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index bde273d..8d4b6e0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -39,6 +39,7 @@ import { HealthModule } from './health/health.module'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; import { OccupancyModule } from './occupancy/occupancy.module'; import { WeatherModule } from './weather/weather.module'; +import { AqiModule } from './aqi/aqi.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -81,6 +82,7 @@ import { WeatherModule } from './weather/weather.module'; HealthModule, OccupancyModule, WeatherModule, + AqiModule, ], providers: [ { diff --git a/src/aqi/aqi.module.ts b/src/aqi/aqi.module.ts new file mode 100644 index 0000000..fdf3a5f --- /dev/null +++ b/src/aqi/aqi.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { AqiService } from './services'; +import { AqiController } from './controllers'; +@Module({ + imports: [ConfigModule], + controllers: [AqiController], + providers: [AqiService, SqlLoaderService], +}) +export class AqiModule {} diff --git a/src/aqi/controllers/aqi.controller.ts b/src/aqi/controllers/aqi.controller.ts new file mode 100644 index 0000000..32d1d6c --- /dev/null +++ b/src/aqi/controllers/aqi.controller.ts @@ -0,0 +1,64 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { + ApiTags, + ApiBearerAuth, + ApiOperation, + ApiParam, +} from '@nestjs/swagger'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { AqiService } from '../services/aqi.service'; +import { + GetAqiDailyBySpaceDto, + GetAqiPollutantBySpaceDto, +} from '../dto/get-aqi.dto'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SpaceParamsDto } from '../dto/aqi-params.dto'; + +@ApiTags('AQI Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.AQI.ROUTE, +}) +export class AqiController { + constructor(private readonly aqiService: AqiService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('range/space/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_SUMMARY, + description: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_DESCRIPTION, + }) + @ApiParam({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: true, + }) + async getAQIRangeDataBySpace( + @Param() params: SpaceParamsDto, + @Query() query: GetAqiDailyBySpaceDto, + ): Promise { + return await this.aqiService.getAQIRangeDataBySpace(params, query); + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('distribution/space/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_SUMMARY, + description: + ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_DESCRIPTION, + }) + @ApiParam({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: true, + }) + async getAQIDistributionDataBySpace( + @Param() params: SpaceParamsDto, + @Query() query: GetAqiPollutantBySpaceDto, + ): Promise { + return await this.aqiService.getAQIDistributionDataBySpace(params, query); + } +} diff --git a/src/aqi/controllers/index.ts b/src/aqi/controllers/index.ts new file mode 100644 index 0000000..ffd182e --- /dev/null +++ b/src/aqi/controllers/index.ts @@ -0,0 +1 @@ +export * from './aqi.controller'; diff --git a/src/aqi/dto/aqi-params.dto.ts b/src/aqi/dto/aqi-params.dto.ts new file mode 100644 index 0000000..6e26d4f --- /dev/null +++ b/src/aqi/dto/aqi-params.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class SpaceParamsDto { + @IsUUID('4', { message: 'Invalid UUID format' }) + @IsNotEmpty() + spaceUuid: string; +} diff --git a/src/aqi/dto/get-aqi.dto.ts b/src/aqi/dto/get-aqi.dto.ts new file mode 100644 index 0000000..53d49ec --- /dev/null +++ b/src/aqi/dto/get-aqi.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Matches, IsNotEmpty, IsString } from 'class-validator'; + +export class GetAqiDailyBySpaceDto { + @ApiProperty({ + description: 'Month and year in format YYYY-MM', + example: '2025-03', + required: true, + }) + @Matches(/^\d{4}-(0[1-9]|1[0-2])$/, { + message: 'monthDate must be in YYYY-MM format', + }) + @IsNotEmpty() + monthDate: string; +} +export class GetAqiPollutantBySpaceDto { + @ApiProperty({ + description: 'Pollutant Type', + enum: ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o'], + example: 'aqi', + required: true, + }) + @IsString() + @IsNotEmpty() + public pollutantType: string; + @ApiProperty({ + description: 'Month and year in format YYYY-MM', + example: '2025-03', + required: true, + }) + @Matches(/^\d{4}-(0[1-9]|1[0-2])$/, { + message: 'monthDate must be in YYYY-MM format', + }) + @IsNotEmpty() + monthDate: string; +} diff --git a/src/aqi/services/aqi.service.ts b/src/aqi/services/aqi.service.ts new file mode 100644 index 0000000..a8435ed --- /dev/null +++ b/src/aqi/services/aqi.service.ts @@ -0,0 +1,146 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + GetAqiDailyBySpaceDto, + GetAqiPollutantBySpaceDto, +} from '../dto/get-aqi.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { SpaceParamsDto } from '../dto/aqi-params.dto'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { DataSource } from 'typeorm'; +import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; + +@Injectable() +export class AqiService { + constructor( + private readonly sqlLoader: SqlLoaderService, + private readonly dataSource: DataSource, + ) {} + async getAQIDistributionDataBySpace( + params: SpaceParamsDto, + query: GetAqiPollutantBySpaceDto, + ): Promise { + const { monthDate, pollutantType } = query; + const { spaceUuid } = params; + + // Validate pollutantType against the allowed values + const allowedPollutants = ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o']; + if (!allowedPollutants.includes(pollutantType.toLowerCase())) { + throw new HttpException( + `Invalid pollutant type. Allowed values: ${allowedPollutants.join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + + try { + const data = await this.executeProcedure( + 'fact_daily_space_aqi', + 'proceduce_select_daily_space_aqi', + [spaceUuid, monthDate], + ); + + const categories = [ + 'good', + 'moderate', + 'unhealthy_sensitive', + 'unhealthy', + 'very_unhealthy', + 'hazardous', + ]; + + const transformedData = data.map((item) => { + const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD + + const categoryData = categories.map((category) => { + const key = `${category}_${pollutantType.toLowerCase()}_percentage`; + return { + type: category, + percentage: item[key] ?? 0, + }; + }); + + return { date, data: categoryData }; + }); + + const response = this.buildResponse( + `AQI distribution data fetched successfully for ${spaceUuid} space and pollutant ${pollutantType}`, + transformedData, + ); + return response; + } catch (error) { + console.error('Failed to fetch AQI distribution data', { + error, + spaceUuid, + }); + throw new HttpException( + error.response?.message || 'Failed to fetch AQI distribution data', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getAQIRangeDataBySpace( + params: SpaceParamsDto, + query: GetAqiDailyBySpaceDto, + ): Promise { + const { monthDate } = query; + const { spaceUuid } = params; + + try { + const data = await this.executeProcedure( + 'fact_daily_space_aqi', + 'proceduce_select_daily_space_aqi', + [spaceUuid, monthDate], + ); + + // Define pollutants dynamically + const pollutants = ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o']; + + const transformedData = data.map((item) => { + const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD + const dailyData = pollutants.map((type) => ({ + type, + min: item[`daily_min_${type}`], + max: item[`daily_max_${type}`], + average: item[`daily_avg_${type}`], + })); + return { date, data: dailyData }; + }); + + const response = this.buildResponse( + `AQI data fetched successfully for ${spaceUuid} space`, + transformedData, + ); + return convertKeysToCamelCase(response); + } catch (error) { + console.error('Failed to fetch AQI data', { + error, + spaceUuid, + }); + throw new HttpException( + error.response?.message || 'Failed to fetch AQI data', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private buildResponse(message: string, data: any[]) { + return new SuccessResponseDto({ + message, + data, + statusCode: HttpStatus.OK, + }); + } + private async executeProcedure( + procedureFolderName: string, + procedureFileName: string, + params: (string | number | null)[], + ): Promise { + const query = this.loadQuery(procedureFolderName, procedureFileName); + return await this.dataSource.query(query, params); + } + private loadQuery(folderName: string, fileName: string): string { + return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH); + } +} diff --git a/src/aqi/services/index.ts b/src/aqi/services/index.ts new file mode 100644 index 0000000..f0ba82c --- /dev/null +++ b/src/aqi/services/index.ts @@ -0,0 +1 @@ +export * from './aqi.service'; diff --git a/src/commission-device/commission-device.module.ts b/src/commission-device/commission-device.module.ts index 8821410..3306705 100644 --- a/src/commission-device/commission-device.module.ts +++ b/src/commission-device/commission-device.module.ts @@ -29,6 +29,7 @@ import { import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; 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'; @Module({ imports: [ConfigModule, SpaceRepositoryModule], @@ -57,6 +58,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [], }) diff --git a/src/community/community.module.ts b/src/community/community.module.ts index a6f64df..edaddbb 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -64,6 +64,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; 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'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], @@ -116,6 +117,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [CommunityService, SpacePermissionService], }) diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index 3f2cee7..c2eaad1 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -29,6 +29,7 @@ import { 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'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -56,6 +57,7 @@ import { CommunityRepository } from '@app/common/modules/community/repositories' SqlLoaderService, OccupancyService, CommunityRepository, + AqiDataService, ], exports: [DoorLockService], }) diff --git a/src/group/group.module.ts b/src/group/group.module.ts index b50bac9..443ac31 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -27,6 +27,7 @@ import { 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'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [GroupController], @@ -53,6 +54,7 @@ import { CommunityRepository } from '@app/common/modules/community/repositories' SqlLoaderService, OccupancyService, CommunityRepository, + AqiDataService, ], exports: [GroupService], }) diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index fb1ba8d..a42922c 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -84,6 +84,7 @@ import { import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; 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'; @Module({ imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule], @@ -154,6 +155,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [InviteUserService], }) diff --git a/src/power-clamp/power-clamp.module.ts b/src/power-clamp/power-clamp.module.ts index e85ea1b..9910dd9 100644 --- a/src/power-clamp/power-clamp.module.ts +++ b/src/power-clamp/power-clamp.module.ts @@ -61,6 +61,7 @@ import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repos import { SpaceModelProductAllocationService } from 'src/space-model/services/space-model-product-allocation.service'; import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule], controllers: [PowerClampController], @@ -111,6 +112,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' SpaceModelProductAllocationRepoitory, SubspaceModelProductAllocationRepoitory, OccupancyService, + AqiDataService, ], exports: [PowerClamp], }) diff --git a/src/project/project.module.ts b/src/project/project.module.ts index fd9acb3..306a6c2 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -68,6 +68,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; 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'; const CommandHandlers = [CreateOrphanSpaceHandler]; @@ -126,6 +127,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [ProjectService, CqrsModule], }) diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index be57bb3..cdb713c 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -66,6 +66,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; 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'; const CommandHandlers = [ PropogateUpdateSpaceModelHandler, @@ -126,6 +127,7 @@ const CommandHandlers = [ PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [CqrsModule, SpaceModelService], }) diff --git a/src/space/space.module.ts b/src/space/space.module.ts index f74d993..0b33696 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -91,6 +91,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; 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'; export const CommandHandlers = [DisableSpaceHandler]; @@ -168,6 +169,7 @@ export const CommandHandlers = [DisableSpaceHandler]; PowerClampService, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [SpaceService], }) diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 6924713..c66ba39 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -31,6 +31,7 @@ import { 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'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -59,6 +60,7 @@ import { CommunityRepository } from '@app/common/modules/community/repositories' SqlLoaderService, OccupancyService, CommunityRepository, + AqiDataService, ], exports: [VisitorPasswordService], }) From 2cb77504ca27572f8a5321ee184d884ed4b9e2c1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:28:00 -0600 Subject: [PATCH 4/4] Add PollutantType enum and update AQI-related entities and services to use it --- libs/common/src/constants/pollutants.enum.ts | 8 ++ .../src/modules/aqi/entities/aqi.entity.ts | 110 +++++++++--------- .../modules/space/entities/space.entity.ts | 4 + src/aqi/dto/get-aqi.dto.ts | 5 +- src/aqi/services/aqi.service.ts | 12 +- 5 files changed, 72 insertions(+), 67 deletions(-) create mode 100644 libs/common/src/constants/pollutants.enum.ts diff --git a/libs/common/src/constants/pollutants.enum.ts b/libs/common/src/constants/pollutants.enum.ts new file mode 100644 index 0000000..a6c88ea --- /dev/null +++ b/libs/common/src/constants/pollutants.enum.ts @@ -0,0 +1,8 @@ +export enum PollutantType { + AQI = 'aqi', + PM25 = 'pm25', + PM10 = 'pm10', + VOC = 'voc', + CO2 = 'co2', + CH2O = 'ch2o', +} diff --git a/libs/common/src/modules/aqi/entities/aqi.entity.ts b/libs/common/src/modules/aqi/entities/aqi.entity.ts index fed3881..b4fb2bc 100644 --- a/libs/common/src/modules/aqi/entities/aqi.entity.ts +++ b/libs/common/src/modules/aqi/entities/aqi.entity.ts @@ -9,173 +9,173 @@ export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity SpaceEntity, (space) => space.presenceSensorDaily) + @ManyToOne(() => SpaceEntity, (space) => space.aqiSensorDaily) space: SpaceEntity; @Column({ type: 'date', nullable: false }) public eventDate: Date; @Column('float', { nullable: true }) - public goodAqiPercentage: number; + public goodAqiPercentage?: number; @Column('float', { nullable: true }) - public moderateAqiPercentage: number; + public moderateAqiPercentage?: number; @Column('float', { nullable: true }) - public unhealthySensitiveAqiPercentage: number; + public unhealthySensitiveAqiPercentage?: number; @Column('float', { nullable: true }) - public unhealthyAqiPercentage: number; + public unhealthyAqiPercentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyAqiPercentage: number; + public veryUnhealthyAqiPercentage?: number; @Column('float', { nullable: true }) - public hazardousAqiPercentage: number; + public hazardousAqiPercentage?: number; @Column('float', { nullable: true }) - public dailyAvgAqi: number; + public dailyAvgAqi?: number; @Column('float', { nullable: true }) - public dailyMaxAqi: number; + public dailyMaxAqi?: number; @Column('float', { nullable: true }) - public dailyMinAqi: number; + public dailyMinAqi?: number; @Column('float', { nullable: true }) - public goodPm25Percentage: number; + public goodPm25Percentage?: number; @Column('float', { nullable: true }) - public moderatePm25Percentage: number; + public moderatePm25Percentage?: number; @Column('float', { nullable: true }) - public unhealthySensitivePm25Percentage: number; + public unhealthySensitivePm25Percentage?: number; @Column('float', { nullable: true }) - public unhealthyPm25Percentage: number; + public unhealthyPm25Percentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyPm25Percentage: number; + public veryUnhealthyPm25Percentage?: number; @Column('float', { nullable: true }) - public hazardousPm25Percentage: number; + public hazardousPm25Percentage?: number; @Column('float', { nullable: true }) - public dailyAvgPm25: number; + public dailyAvgPm25?: number; @Column('float', { nullable: true }) - public dailyMaxPm25: number; + public dailyMaxPm25?: number; @Column('float', { nullable: true }) - public dailyMinPm25: number; + public dailyMinPm25?: number; @Column('float', { nullable: true }) - public goodPm10Percentage: number; + public goodPm10Percentage?: number; @Column('float', { nullable: true }) - public moderatePm10Percentage: number; + public moderatePm10Percentage?: number; @Column('float', { nullable: true }) - public unhealthySensitivePm10Percentage: number; + public unhealthySensitivePm10Percentage?: number; @Column('float', { nullable: true }) - public unhealthyPm10Percentage: number; + public unhealthyPm10Percentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyPm10Percentage: number; + public veryUnhealthyPm10Percentage?: number; @Column('float', { nullable: true }) - public hazardousPm10Percentage: number; + public hazardousPm10Percentage?: number; @Column('float', { nullable: true }) - public dailyAvgPm10: number; + public dailyAvgPm10?: number; @Column('float', { nullable: true }) - public dailyMaxPm10: number; + public dailyMaxPm10?: number; @Column('float', { nullable: true }) - public dailyMinPm10: number; + public dailyMinPm10?: number; @Column('float', { nullable: true }) - public goodVocPercentage: number; + public goodVocPercentage?: number; @Column('float', { nullable: true }) - public moderateVocPercentage: number; + public moderateVocPercentage?: number; @Column('float', { nullable: true }) - public unhealthySensitiveVocPercentage: number; + public unhealthySensitiveVocPercentage?: number; @Column('float', { nullable: true }) - public unhealthyVocPercentage: number; + public unhealthyVocPercentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyVocPercentage: number; + public veryUnhealthyVocPercentage?: number; @Column('float', { nullable: true }) - public hazardousVocPercentage: number; + public hazardousVocPercentage?: number; @Column('float', { nullable: true }) - public dailyAvgVoc: number; + public dailyAvgVoc?: number; @Column('float', { nullable: true }) - public dailyMaxVoc: number; + public dailyMaxVoc?: number; @Column('float', { nullable: true }) - public dailyMinVoc: number; + public dailyMinVoc?: number; @Column('float', { nullable: true }) - public goodCo2Percentage: number; + public goodCo2Percentage?: number; @Column('float', { nullable: true }) - public moderateCo2Percentage: number; + public moderateCo2Percentage?: number; @Column('float', { nullable: true }) - public unhealthySensitiveCo2Percentage: number; + public unhealthySensitiveCo2Percentage?: number; @Column('float', { nullable: true }) - public unhealthyCo2Percentage: number; + public unhealthyCo2Percentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyCo2Percentage: number; + public veryUnhealthyCo2Percentage?: number; @Column('float', { nullable: true }) - public hazardousCo2Percentage: number; + public hazardousCo2Percentage?: number; @Column('float', { nullable: true }) - public dailyAvgCo2: number; + public dailyAvgCo2?: number; @Column('float', { nullable: true }) - public dailyMaxCo2: number; + public dailyMaxCo2?: number; @Column('float', { nullable: true }) - public dailyMinCo2: number; + public dailyMinCo2?: number; @Column('float', { nullable: true }) - public goodCh2oPercentage: number; + public goodCh2oPercentage?: number; @Column('float', { nullable: true }) - public moderateCh2oPercentage: number; + public moderateCh2oPercentage?: number; @Column('float', { nullable: true }) - public unhealthySensitiveCh2oPercentage: number; + public unhealthySensitiveCh2oPercentage?: number; @Column('float', { nullable: true }) - public unhealthyCh2oPercentage: number; + public unhealthyCh2oPercentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyCh2oPercentage: number; + public veryUnhealthyCh2oPercentage?: number; @Column('float', { nullable: true }) - public hazardousCh2oPercentage: number; + public hazardousCh2oPercentage?: number; @Column('float', { nullable: true }) - public dailyAvgCh2o: number; + public dailyAvgCh2o?: number; @Column('float', { nullable: true }) - public dailyMaxCh2o: number; + public dailyMaxCh2o?: number; @Column('float', { nullable: true }) - public dailyMinCh2o: number; + public dailyMinCh2o?: number; constructor(partial: Partial) { super(); diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 5a7b4d2..e5e3b12 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -11,6 +11,7 @@ import { InviteUserSpaceEntity } from '../../Invite-user/entities'; import { SpaceProductAllocationEntity } from './space-product-allocation.entity'; import { SubspaceEntity } from './subspace/subspace.entity'; import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; +import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities'; @Entity({ name: 'space' }) export class SpaceEntity extends AbstractEntity { @@ -115,6 +116,9 @@ export class SpaceEntity extends AbstractEntity { @OneToMany(() => PresenceSensorDailySpaceEntity, (sensor) => sensor.space) presenceSensorDaily: PresenceSensorDailySpaceEntity[]; + @OneToMany(() => AqiSpaceDailyPollutantStatsEntity, (aqi) => aqi.space) + aqiSensorDaily: AqiSpaceDailyPollutantStatsEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/src/aqi/dto/get-aqi.dto.ts b/src/aqi/dto/get-aqi.dto.ts index 53d49ec..c25cb3a 100644 --- a/src/aqi/dto/get-aqi.dto.ts +++ b/src/aqi/dto/get-aqi.dto.ts @@ -1,3 +1,4 @@ +import { PollutantType } from '@app/common/constants/pollutants.enum'; import { ApiProperty } from '@nestjs/swagger'; import { Matches, IsNotEmpty, IsString } from 'class-validator'; @@ -16,8 +17,8 @@ export class GetAqiDailyBySpaceDto { export class GetAqiPollutantBySpaceDto { @ApiProperty({ description: 'Pollutant Type', - enum: ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o'], - example: 'aqi', + enum: PollutantType, + example: PollutantType.AQI, required: true, }) @IsString() diff --git a/src/aqi/services/aqi.service.ts b/src/aqi/services/aqi.service.ts index a8435ed..b583faf 100644 --- a/src/aqi/services/aqi.service.ts +++ b/src/aqi/services/aqi.service.ts @@ -10,6 +10,7 @@ import { DataSource } from 'typeorm'; import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { PollutantType } from '@app/common/constants/pollutants.enum'; @Injectable() export class AqiService { @@ -24,15 +25,6 @@ export class AqiService { const { monthDate, pollutantType } = query; const { spaceUuid } = params; - // Validate pollutantType against the allowed values - const allowedPollutants = ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o']; - if (!allowedPollutants.includes(pollutantType.toLowerCase())) { - throw new HttpException( - `Invalid pollutant type. Allowed values: ${allowedPollutants.join(', ')}`, - HttpStatus.BAD_REQUEST, - ); - } - try { const data = await this.executeProcedure( 'fact_daily_space_aqi', @@ -95,7 +87,7 @@ export class AqiService { ); // Define pollutants dynamically - const pollutants = ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o']; + const pollutants = Object.values(PollutantType); const transformedData = data.map((item) => { const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD