feat: implement date formatting function and enhance PowerClampService with space-based data retrieval

This commit is contained in:
faris Aljohari
2025-05-04 22:28:38 +03:00
parent e932d8a4a4
commit d197bf2bb4
9 changed files with 271 additions and 19 deletions

View File

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

View File

@ -46,16 +46,15 @@ export class PowerClampService {
procedureFileName: string, procedureFileName: string,
params: (string | number | null)[], params: (string | number | null)[],
): Promise<void> { ): Promise<void> {
const query = this.loadQuery(procedureFileName); const query = this.loadQuery(
'fact_device_energy_consumed',
procedureFileName,
);
await this.dataSource.query(query, params); await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`); console.log(`Procedure ${procedureFileName} executed successfully.`);
} }
private loadQuery(fileName: string): string { private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery( return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
'fact_device_energy_consumed',
fileName,
SQL_PROCEDURES_PATH,
);
} }
} }

View File

@ -1,8 +1,8 @@
WITH params AS ( WITH params AS (
SELECT SELECT
TO_DATE(NULLIF($2, ''), 'DD-MM-YYYY') AS start_date, TO_DATE(NULLIF($1, ''), 'DD-MM-YYYY') AS start_date,
TO_DATE(NULLIF($3, ''), 'DD-MM-YYYY') AS end_date, TO_DATE(NULLIF($2, ''), 'DD-MM-YYYY') AS end_date,
string_to_array(NULLIF($4, ''), ',') AS device_ids string_to_array(NULLIF($3, ''), ',') AS device_ids
) )
SELECT TO_CHAR(A.date, 'MM-YYYY') AS month, SELECT TO_CHAR(A.date, 'MM-YYYY') AS month,

View File

@ -9,9 +9,15 @@ import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route'; import { ControllerRoute } from '@app/common/constants/controller-route';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { PowerClampService } from '../services/power-clamp.service'; 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 { 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') @ApiTags('Power Clamp Module')
@Controller({ @Controller({
@ -39,4 +45,22 @@ export class PowerClampController {
): Promise<BaseResponseDto> { ): Promise<BaseResponseDto> {
return await this.powerClampService.getPowerClampData(params, query); 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<BaseResponseDto> {
return await this.powerClampService.getPowerClampBySpaceData(params, query);
}
} }

View File

@ -1,5 +1,5 @@
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsDateString, Matches } from 'class-validator'; import { IsOptional, IsDateString, Matches, IsNotEmpty } from 'class-validator';
export class GetPowerClampDto { export class GetPowerClampDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
@ -33,3 +33,21 @@ export class GetPowerClampDto {
}) })
year?: string; 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;
}

View File

@ -4,3 +4,7 @@ export class PowerClampParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' }) @IsUUID('4', { message: 'Invalid UUID format' })
powerClampUuid: string; powerClampUuid: string;
} }
export class SpaceParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' })
spaceUuid: string;
}

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; 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 { PowerClampController } from './controllers';
import { import {
PowerClampDailyRepository, PowerClampDailyRepository,
@ -8,16 +8,108 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { DeviceRepository } from '@app/common/modules/device/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({ @Module({
imports: [ConfigModule], imports: [ConfigModule],
controllers: [PowerClampController], controllers: [PowerClampController],
providers: [ providers: [
PowerClampService, PowerClamp,
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampHourlyRepository, PowerClampHourlyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
DeviceRepository, 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 {} export class PowerClampModule {}

View File

@ -1,13 +1,24 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 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 { import {
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampHourlyRepository, PowerClampHourlyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; 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 { 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() @Injectable()
export class PowerClampService { export class PowerClampService {
@ -16,8 +27,43 @@ export class PowerClampService {
private readonly powerClampHourlyRepository: PowerClampHourlyRepository, private readonly powerClampHourlyRepository: PowerClampHourlyRepository,
private readonly powerClampMonthlyRepository: PowerClampMonthlyRepository, private readonly powerClampMonthlyRepository: PowerClampMonthlyRepository,
private readonly deviceRepository: DeviceRepository, 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( async getPowerClampData(
params: PowerClampParamsDto, params: PowerClampParamsDto,
query: GetPowerClampDto, query: GetPowerClampDto,
@ -99,4 +145,17 @@ export class PowerClampService {
statusCode: HttpStatus.OK, statusCode: HttpStatus.OK,
}); });
} }
private async executeProcedure(
procedureFileName: string,
params: (string | number | null)[],
): Promise<any[]> {
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);
}
} }

View File

@ -1,6 +1,11 @@
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; 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 { GetSpaceParam } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.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 { ProductType } from '@app/common/constants/product-type.enum';
import { BatteryStatus } from '@app/common/constants/battery-status.enum'; import { BatteryStatus } from '@app/common/constants/battery-status.enum';
import { DeviceService } from 'src/device/services'; 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() @Injectable()
export class SpaceDeviceService { export class SpaceDeviceService {
@ -16,6 +24,7 @@ export class SpaceDeviceService {
private readonly tuyaService: TuyaService, private readonly tuyaService: TuyaService,
private readonly validationService: ValidationService, private readonly validationService: ValidationService,
private readonly deviceService: DeviceService, private readonly deviceService: DeviceService,
private readonly spaceRepository: SpaceRepository,
) {} ) {}
async listDevicesInSpace(params: GetSpaceParam): Promise<BaseResponseDto> { async listDevicesInSpace(params: GetSpaceParam): Promise<BaseResponseDto> {
@ -121,4 +130,37 @@ export class SpaceDeviceService {
); );
return batteryStatus ? batteryStatus.value : null; return batteryStatus ? batteryStatus.value : null;
} }
async getAllDevicesBySpace(spaceUuid: string): Promise<DeviceEntity[]> {
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;
}
} }