From d197bf2bb4c09f705a63700ff5e1b19ec09e613b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 4 May 2025 22:28:38 +0300 Subject: [PATCH] feat: implement date formatting function and enhance PowerClampService with space-based data retrieval --- libs/common/src/helper/date-format.ts | 14 +++ .../helper/services/power.clamp.service.ts | 13 ++- ...onthly_space_energy_consumed_procedure.sql | 6 +- .../controllers/power-clamp.controller.ts | 28 +++++- src/power-clamp/dto/get-power-clamp.dto.ts | 20 +++- src/power-clamp/dto/power-clamp-params.dto.ts | 4 + src/power-clamp/power-clamp.module.ts | 98 ++++++++++++++++++- .../services/power-clamp.service.ts | 63 +++++++++++- src/space/services/space-device.service.ts | 44 ++++++++- 9 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 libs/common/src/helper/date-format.ts diff --git a/libs/common/src/helper/date-format.ts b/libs/common/src/helper/date-format.ts new file mode 100644 index 0000000..38c15b8 --- /dev/null +++ b/libs/common/src/helper/date-format.ts @@ -0,0 +1,14 @@ +export function toDDMMYYYY(dateString?: string | null): string | null { + if (!dateString) return null; + + // Ensure dateString is valid format YYYY-MM-DD + const regex = /^\d{4}-\d{2}-\d{2}$/; + if (!regex.test(dateString)) { + throw new Error( + `Invalid date format: ${dateString}. Expected format is YYYY-MM-DD`, + ); + } + + const [year, month, day] = dateString.split('-'); + return `${day}-${month}-${year}`; +} diff --git a/libs/common/src/helper/services/power.clamp.service.ts b/libs/common/src/helper/services/power.clamp.service.ts index b61b804..7c83208 100644 --- a/libs/common/src/helper/services/power.clamp.service.ts +++ b/libs/common/src/helper/services/power.clamp.service.ts @@ -46,16 +46,15 @@ export class PowerClampService { procedureFileName: string, params: (string | number | null)[], ): Promise { - const query = this.loadQuery(procedureFileName); + const query = this.loadQuery( + 'fact_device_energy_consumed', + procedureFileName, + ); await this.dataSource.query(query, params); console.log(`Procedure ${procedureFileName} executed successfully.`); } - private loadQuery(fileName: string): string { - return this.sqlLoader.loadQuery( - 'fact_device_energy_consumed', - fileName, - SQL_PROCEDURES_PATH, - ); + private loadQuery(folderName: string, fileName: string): string { + return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH); } } diff --git a/libs/common/src/sql/procedures/fact_space_energy_consumed/fact_monthly_space_energy_consumed_procedure.sql b/libs/common/src/sql/procedures/fact_space_energy_consumed/fact_monthly_space_energy_consumed_procedure.sql index 8002de6..8197ddd 100644 --- a/libs/common/src/sql/procedures/fact_space_energy_consumed/fact_monthly_space_energy_consumed_procedure.sql +++ b/libs/common/src/sql/procedures/fact_space_energy_consumed/fact_monthly_space_energy_consumed_procedure.sql @@ -1,8 +1,8 @@ WITH params AS ( SELECT - TO_DATE(NULLIF($2, ''), 'DD-MM-YYYY') AS start_date, - TO_DATE(NULLIF($3, ''), 'DD-MM-YYYY') AS end_date, - string_to_array(NULLIF($4, ''), ',') AS device_ids + TO_DATE(NULLIF($1, ''), 'DD-MM-YYYY') AS start_date, + TO_DATE(NULLIF($2, ''), 'DD-MM-YYYY') AS end_date, + string_to_array(NULLIF($3, ''), ',') AS device_ids ) SELECT TO_CHAR(A.date, 'MM-YYYY') AS month, diff --git a/src/power-clamp/controllers/power-clamp.controller.ts b/src/power-clamp/controllers/power-clamp.controller.ts index ca7b664..0f5dfdc 100644 --- a/src/power-clamp/controllers/power-clamp.controller.ts +++ b/src/power-clamp/controllers/power-clamp.controller.ts @@ -9,9 +9,15 @@ 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 { PowerClampService } from '../services/power-clamp.service'; -import { GetPowerClampDto } from '../dto/get-power-clamp.dto'; +import { + GetPowerClampBySpaceDto, + GetPowerClampDto, +} from '../dto/get-power-clamp.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { PowerClampParamsDto } from '../dto/power-clamp-params.dto'; +import { + PowerClampParamsDto, + SpaceParamsDto, +} from '../dto/power-clamp-params.dto'; @ApiTags('Power Clamp Module') @Controller({ @@ -39,4 +45,22 @@ export class PowerClampController { ): Promise { return await this.powerClampService.getPowerClampData(params, query); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('historical/space/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.PowerClamp.ACTIONS.GET_ENERGY_SUMMARY, + description: ControllerRoute.PowerClamp.ACTIONS.GET_ENERGY_DESCRIPTION, + }) + @ApiParam({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: true, + }) + async getPowerClampBySpaceData( + @Param() params: SpaceParamsDto, + @Query() query: GetPowerClampBySpaceDto, + ): Promise { + return await this.powerClampService.getPowerClampBySpaceData(params, query); + } } diff --git a/src/power-clamp/dto/get-power-clamp.dto.ts b/src/power-clamp/dto/get-power-clamp.dto.ts index baa6839..f4bb82e 100644 --- a/src/power-clamp/dto/get-power-clamp.dto.ts +++ b/src/power-clamp/dto/get-power-clamp.dto.ts @@ -1,5 +1,5 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsDateString, Matches } from 'class-validator'; +import { IsOptional, IsDateString, Matches, IsNotEmpty } from 'class-validator'; export class GetPowerClampDto { @ApiPropertyOptional({ @@ -33,3 +33,21 @@ export class GetPowerClampDto { }) year?: string; } +export class GetPowerClampBySpaceDto { + @ApiPropertyOptional({ + description: 'Input date in ISO format (YYYY-MM-DD) to filter the data', + example: '2025-04-23', + required: true, + }) + @IsDateString() + @IsNotEmpty() + public startDate: string; + @ApiPropertyOptional({ + description: 'Input date in ISO format (YYYY-MM-DD) to filter the data', + example: '2025-05-23', + required: true, + }) + @IsDateString() + @IsNotEmpty() + public endDate: string; +} diff --git a/src/power-clamp/dto/power-clamp-params.dto.ts b/src/power-clamp/dto/power-clamp-params.dto.ts index 42f8e11..586eee6 100644 --- a/src/power-clamp/dto/power-clamp-params.dto.ts +++ b/src/power-clamp/dto/power-clamp-params.dto.ts @@ -4,3 +4,7 @@ export class PowerClampParamsDto { @IsUUID('4', { message: 'Invalid UUID format' }) powerClampUuid: string; } +export class SpaceParamsDto { + @IsUUID('4', { message: 'Invalid UUID format' }) + spaceUuid: string; +} diff --git a/src/power-clamp/power-clamp.module.ts b/src/power-clamp/power-clamp.module.ts index 9cd6496..3f0595c 100644 --- a/src/power-clamp/power-clamp.module.ts +++ b/src/power-clamp/power-clamp.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { PowerClampService } from './services/power-clamp.service'; +import { PowerClampService as PowerClamp } from './services/power-clamp.service'; import { PowerClampController } from './controllers'; import { PowerClampDailyRepository, @@ -8,16 +8,108 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { + SpaceDeviceService, + SpaceLinkService, + SpaceService, + SubspaceDeviceService, + SubSpaceService, + ValidationService, +} from 'src/space/services'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { DeviceService } from 'src/device/services'; +import { + InviteSpaceRepository, + SpaceLinkRepository, + SpaceProductAllocationRepository, + SpaceRepository, + TagRepository, +} from '@app/common/modules/space'; +import { CommunityService } from 'src/community/services'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { + SpaceModelProductAllocationRepoitory, + SpaceModelRepository, + SubspaceModelProductAllocationRepoitory, + SubspaceModelRepository, +} from '@app/common/modules/space-model'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { SceneService } from 'src/scene/services'; +import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; +import { TagService } from 'src/tags/services'; +import { + SpaceModelService, + SubSpaceModelService, +} from 'src/space-model/services'; +import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { + SubspaceProductAllocationRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories/subspace.repository'; +import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service'; +import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; +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'; @Module({ imports: [ConfigModule], controllers: [PowerClampController], providers: [ - PowerClampService, + PowerClamp, PowerClampDailyRepository, PowerClampHourlyRepository, PowerClampMonthlyRepository, DeviceRepository, + SpaceDeviceService, + TuyaService, + ValidationService, + DeviceService, + SpaceRepository, + CommunityService, + ProjectRepository, + CommunityRepository, + SpaceModelRepository, + SceneDeviceRepository, + ProductRepository, + DeviceStatusFirebaseService, + SceneService, + SpaceService, + PowerClampService, + DeviceStatusLogRepository, + SceneIconRepository, + SceneRepository, + AutomationRepository, + InviteSpaceRepository, + SpaceLinkService, + SubSpaceService, + TagService, + SpaceModelService, + SpaceProductAllocationService, + SqlLoaderService, + SpaceLinkRepository, + SubspaceRepository, + SubspaceDeviceService, + SubspaceProductAllocationService, + NewTagRepository, + SubSpaceModelService, + SpaceModelProductAllocationService, + SpaceProductAllocationRepository, + SubspaceProductAllocationRepository, + TagRepository, + SubspaceModelRepository, + SubspaceModelProductAllocationService, + SpaceModelProductAllocationRepoitory, + SubspaceModelProductAllocationRepoitory, ], - exports: [PowerClampService], + exports: [PowerClamp], }) export class PowerClampModule {} diff --git a/src/power-clamp/services/power-clamp.service.ts b/src/power-clamp/services/power-clamp.service.ts index b95c9f0..1124aed 100644 --- a/src/power-clamp/services/power-clamp.service.ts +++ b/src/power-clamp/services/power-clamp.service.ts @@ -1,13 +1,24 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { GetPowerClampDto } from '../dto/get-power-clamp.dto'; +import { + GetPowerClampBySpaceDto, + GetPowerClampDto, +} from '../dto/get-power-clamp.dto'; import { PowerClampDailyRepository, PowerClampHourlyRepository, PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { PowerClampParamsDto } from '../dto/power-clamp-params.dto'; +import { + PowerClampParamsDto, + SpaceParamsDto, +} from '../dto/power-clamp-params.dto'; import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { SpaceDeviceService } from 'src/space/services'; +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 { toDDMMYYYY } from '@app/common/helper/date-format'; @Injectable() export class PowerClampService { @@ -16,8 +27,43 @@ export class PowerClampService { private readonly powerClampHourlyRepository: PowerClampHourlyRepository, private readonly powerClampMonthlyRepository: PowerClampMonthlyRepository, private readonly deviceRepository: DeviceRepository, + private readonly spaceDeviceService: SpaceDeviceService, + private readonly sqlLoader: SqlLoaderService, + private readonly dataSource: DataSource, ) {} + async getPowerClampBySpaceData( + params: SpaceParamsDto, + query: GetPowerClampBySpaceDto, + ) { + const { startDate, endDate } = query; + const { spaceUuid } = params; + + try { + const devices = + await this.spaceDeviceService.getAllDevicesBySpace(spaceUuid); + + const deviceUuids = devices.map((device) => device.uuid).join(','); + + const formattedStartDate = toDDMMYYYY(startDate); + const formattedEndDate = toDDMMYYYY(endDate); + const data = await this.executeProcedure( + 'fact_monthly_space_energy_consumed_procedure', + [formattedStartDate, formattedEndDate, deviceUuids], + ); + + return this.buildResponse( + `Power clamp data for space ${spaceUuid} fetched successfully`, + data, + ); + } catch (error) { + throw new HttpException( + error.message || 'Error fetching power clamp data by space', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getPowerClampData( params: PowerClampParamsDto, query: GetPowerClampDto, @@ -99,4 +145,17 @@ export class PowerClampService { statusCode: HttpStatus.OK, }); } + private async executeProcedure( + procedureFileName: string, + params: (string | number | null)[], + ): Promise { + const query = this.loadQuery( + 'fact_space_energy_consumed', + 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/space/services/space-device.service.ts b/src/space/services/space-device.service.ts index 891df3e..5f2b94d 100644 --- a/src/space/services/space-device.service.ts +++ b/src/space/services/space-device.service.ts @@ -1,6 +1,11 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + HttpException, + HttpStatus, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { GetSpaceParam } from '../dtos'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; @@ -9,6 +14,9 @@ import { ValidationService } from './space-validation.service'; import { ProductType } from '@app/common/constants/product-type.enum'; import { BatteryStatus } from '@app/common/constants/battery-status.enum'; import { DeviceService } from 'src/device/services'; +import { SpaceRepository } from '@app/common/modules/space'; +import { DeviceEntity } from '@app/common/modules/device/entities'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; @Injectable() export class SpaceDeviceService { @@ -16,6 +24,7 @@ export class SpaceDeviceService { private readonly tuyaService: TuyaService, private readonly validationService: ValidationService, private readonly deviceService: DeviceService, + private readonly spaceRepository: SpaceRepository, ) {} async listDevicesInSpace(params: GetSpaceParam): Promise { @@ -121,4 +130,37 @@ export class SpaceDeviceService { ); return batteryStatus ? batteryStatus.value : null; } + async getAllDevicesBySpace(spaceUuid: string): Promise { + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid }, + relations: ['children', 'devices'], + }); + + if (!space) { + throw new NotFoundException('Space not found'); + } + + const allDevices: DeviceEntity[] = [...space.devices]; + + // Recursive fetch function + const fetchChildren = async (parentSpace: SpaceEntity) => { + const children = await this.spaceRepository.find({ + where: { parent: { uuid: parentSpace.uuid } }, + relations: ['children', 'devices'], + }); + + for (const child of children) { + allDevices.push(...child.devices); + + if (child.children.length > 0) { + await fetchChildren(child); + } + } + }; + + // Start recursive fetch + await fetchChildren(space); + + return allDevices; + } }