Merge pull request #364 from SyncrowIOT/revert-363-DATA-date-param-filtering

Revert "DATA-date-param-moved"
This commit is contained in:
Dona Maria Absi
2025-05-08 13:08:58 +03:00
committed by GitHub
31 changed files with 43 additions and 848 deletions

View File

@ -498,10 +498,6 @@ export class ControllerRoute {
'Get power clamp historical data';
public static readonly GET_ENERGY_DESCRIPTION =
'This endpoint retrieves the historical data of a power clamp device based on the provided parameters.';
public static readonly GET_ENERGY_BY_COMMUNITY_OR_SPACE_SUMMARY =
'Get power clamp historical data by community or space';
public static readonly GET_ENERGY_BY_COMMUNITY_OR_SPACE_DESCRIPTION =
'This endpoint retrieves the historical data of power clamp devices based on the provided community or space UUID.';
};
};
static DEVICE = class {

View File

@ -51,7 +51,6 @@ import {
PowerClampHourlyEntity,
PowerClampMonthlyEntity,
} from '../modules/power-clamp/entities/power-clamp.entity';
import { PresenceSensorDailyEntity } from '../modules/presence-sensor/entities';
@Module({
imports: [
TypeOrmModule.forRootAsync({
@ -110,7 +109,6 @@ import { PresenceSensorDailyEntity } from '../modules/presence-sensor/entities';
PowerClampHourlyEntity,
PowerClampDailyEntity,
PowerClampMonthlyEntity,
PresenceSensorDailyEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -1,38 +0,0 @@
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}`;
}
export function toMMYYYY(dateString?: string | null): string | null {
if (!dateString) return null;
// Ensure dateString is valid format YYYY-MM
const regex = /^\d{4}-\d{2}$/;
if (!regex.test(dateString)) {
throw new Error(
`Invalid date format: ${dateString}. Expected format is YYYY-MM`,
);
}
const [year, month] = dateString.split('-');
return `${month}-${year}`;
}
export function filterByMonth(data: any[], monthDate: string) {
const [year, month] = monthDate.split('-').map(Number);
return data.filter((item) => {
const itemDate = new Date(item.date);
return (
itemDate.getUTCFullYear() === year && itemDate.getUTCMonth() + 1 === month
);
});
}

View File

@ -22,20 +22,21 @@ export class PowerClampService {
})
.replace('/', '-'); // MM-YYYY
await this.executeProcedure(
'fact_hourly_device_energy_consumed_procedure',
[deviceUuid, dateStr, hour],
);
await this.executeProcedure('fact_hourly_energy_consumed_procedure', [
deviceUuid,
dateStr,
hour,
]);
await this.executeProcedure(
'fact_daily_device_energy_consumed_procedure',
[deviceUuid, dateStr],
);
await this.executeProcedure('fact_daily_energy_consumed_procedure', [
deviceUuid,
dateStr,
]);
await this.executeProcedure(
'fact_monthly_device_energy_consumed_procedure',
[deviceUuid, monthYear],
);
await this.executeProcedure('fact_monthly_energy_consumed_procedure', [
deviceUuid,
monthYear,
]);
} catch (err) {
console.error('Failed to insert or update energy data:', err);
throw err;
@ -46,15 +47,15 @@ export class PowerClampService {
procedureFileName: string,
params: (string | number | null)[],
): Promise<void> {
const query = this.loadQuery(
'fact_device_energy_consumed',
procedureFileName,
);
const query = this.loadQuery(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);
private loadQuery(fileName: string): string {
return this.sqlLoader.loadQuery(
'fact_energy_consumed',
fileName,
SQL_PROCEDURES_PATH,
);
}
}

View File

@ -1,43 +1,26 @@
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
import * as winston from 'winston';
const environment = process.env.NODE_ENV || 'local';
export const winstonLoggerOptions: winston.LoggerOptions = {
level:
environment === 'local'
? 'debug'
: environment === 'development'
? 'warn'
: 'error',
process.env.AZURE_POSTGRESQL_DATABASE === 'development' ? 'debug' : 'error',
transports: [
new winston.transports.Console({
level:
environment === 'local'
? 'debug'
: environment === 'development'
? 'warn'
: 'error',
format: winston.format.combine(
winston.format.timestamp(),
nestWinstonModuleUtilities.format.nestLike('MyApp', {
prettyPrint: environment === 'local',
prettyPrint: true,
}),
),
}),
// Only create file logs if NOT local
...(environment !== 'local'
? [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: winston.format.json(),
}),
new winston.transports.File({
filename: 'logs/combined.log',
level: 'info',
format: winston.format.json(),
}),
]
: []),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: winston.format.json(),
}),
new winston.transports.File({
filename: 'logs/combined.log',
format: winston.format.json(),
}),
],
};

View File

@ -18,7 +18,6 @@ import { SpaceEntity } from '../../space/entities/space.entity';
import { SubspaceEntity } from '../../space/entities/subspace/subspace.entity';
import { NewTagEntity } from '../../tag';
import { PowerClampHourlyEntity } from '../../power-clamp/entities/power-clamp.entity';
import { PresenceSensorDailyEntity } from '../../presence-sensor/entities';
@Entity({ name: 'device' })
@Unique(['deviceTuyaUuid'])
@ -83,8 +82,6 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
public tag: NewTagEntity;
@OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device)
powerClampHourly: PowerClampHourlyEntity[];
@OneToMany(() => PresenceSensorDailyEntity, (sensor) => sensor.device)
presenceSensorDaily: PresenceSensorDailyEntity[];
constructor(partial: Partial<DeviceEntity>) {
super();
Object.assign(this, partial);

View File

@ -1 +0,0 @@
export * from './presence-sensor.dto';

View File

@ -1,27 +0,0 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class PresenceSensorDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public deviceUuid: string;
@IsString()
@IsNotEmpty()
public eventDate: string;
@IsNumber()
@IsNotEmpty()
public CountMotionDetected: number;
@IsNumber()
@IsNotEmpty()
public CountPresenceDetected: number;
@IsNumber()
@IsNotEmpty()
public CountTotalPresenceDetected: number;
}

View File

@ -1 +0,0 @@
export * from './presence-sensor.entity';

View File

@ -1,31 +0,0 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { PresenceSensorDto } from '../dtos';
import { DeviceEntity } from '../../device/entities/device.entity';
@Entity({ name: 'presence-sensor-daily-detection' })
@Unique(['deviceUuid', 'eventDate'])
export class PresenceSensorDailyEntity extends AbstractEntity<PresenceSensorDto> {
@Column({ nullable: false })
public deviceUuid: string;
@Column({ nullable: false, type: 'date' })
public eventDate: string;
@Column({ nullable: false })
public CountMotionDetected: number;
@Column({ nullable: false })
public CountPresenceDetected: number;
@Column({ nullable: false })
public CountTotalPresenceDetected: number;
@ManyToOne(() => DeviceEntity, (device) => device.presenceSensorDaily)
device: DeviceEntity;
constructor(partial: Partial<PresenceSensorDailyEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PresenceSensorDailyEntity } from './entities/presence-sensor.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([PresenceSensorDailyEntity])],
})
export class PresenceSensorRepositoryModule {}

View File

@ -1 +0,0 @@
export * from './presence-sensor.repository';

View File

@ -1,10 +0,0 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { PresenceSensorDailyEntity } from '../entities';
@Injectable()
export class PresenceSensorDailyRepository extends Repository<PresenceSensorDailyEntity> {
constructor(private dataSource: DataSource) {
super(PresenceSensorDailyEntity, dataSource.createEntityManager());
}
}

View File

@ -1,20 +0,0 @@
WITH params AS (
SELECT
TO_DATE(NULLIF($1, ''), 'MM-YYYY') AS month,
string_to_array(NULLIF($2, ''), ',') AS device_ids
)
SELECT
A.date,
SUM(A.energy_consumed_kW::numeric) AS total_energy_consumed_KW,
SUM(A.energy_consumed_A::numeric) AS total_energy_consumed_A,
SUM(A.energy_consumed_B::numeric) AS total_energy_consumed_B,
SUM(A.energy_consumed_C::numeric) AS total_energy_consumed_C
FROM public."power-clamp-energy-consumed-daily" AS A
JOIN public.device AS B
ON A.device_uuid::TEXT = B."uuid"::TEXT
JOIN params P ON TRUE
WHERE B."uuid"::TEXT = ANY(P.device_ids)
AND (P.month IS NULL OR date_trunc('month', A.date)= P.month)
GROUP BY A.date
ORDER BY A.date;

View File

@ -1,21 +0,0 @@
WITH params AS (
SELECT
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,
SUM(A.energy_consumed_kW::numeric) AS total_energy_consumed_KW,
SUM(A.energy_consumed_A::numeric) AS total_energy_consumed_A,
SUM(A.energy_consumed_B::numeric) AS total_energy_consumed_B,
SUM(A.energy_consumed_C::numeric) AS total_energy_consumed_C
FROM public."power-clamp-energy-consumed-daily" AS A
JOIN public.device AS B
ON A.device_uuid::TEXT = B."uuid"::TEXT
JOIN params P ON TRUE
WHERE B."uuid"::TEXT = ANY(P.device_ids)
AND (P.start_date IS NULL OR A.date >= P.start_date)
AND (P.end_date IS NULL OR A.date <= P.end_date)
GROUP BY 1

View File

@ -1,115 +0,0 @@
WITH params AS (
SELECT
TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date,
$4::text AS device_id
),
device_logs AS (
SELECT
device.uuid AS device_id,
device.created_at,
device.device_tuya_uuid,
device.space_device_uuid AS space_id,
"device-status-log".event_id,
"device-status-log".event_time::timestamp,
"device-status-log".code,
"device-status-log".value,
"device-status-log".log,
LAG("device-status-log".event_time::timestamp)
OVER (PARTITION BY device.uuid
ORDER BY "device-status-log".event_time) AS prev_timestamp,
LAG("device-status-log".value)
OVER (PARTITION BY device.uuid
ORDER BY "device-status-log".event_time) AS prev_value
FROM device
LEFT JOIN "device-status-log"
ON device.uuid = "device-status-log".device_id
LEFT JOIN product
ON product.uuid = device.product_device_uuid
WHERE product.cat_name = 'hps'
AND "device-status-log".code = 'presence_state'
AND device.uuid::text = P.device_id
),
presence_detection AS (
SELECT *,
CASE
WHEN value = 'motion' AND prev_value = 'none' THEN 1 ELSE 0
END AS motion_detected,
CASE
WHEN value = 'presence' AND prev_value = 'none' THEN 1 ELSE 0
END AS presence_detected
FROM device_logs
),
presence_detection_summary AS (
SELECT
pd.device_id,
d.subspace_id,
pd.space_id,
pd.event_time::date AS event_date,
EXTRACT(HOUR FROM pd.event_time)::int AS event_hour,
SUM(motion_detected) AS count_motion_detected,
SUM(presence_detected) AS count_presence_detected,
SUM(motion_detected + presence_detected) AS count_total_presence_detected
FROM presence_detection pd
LEFT JOIN device d ON d.uuid = pd.device_id
JOIN params P ON TRUE
AND (P.event_date IS NULL OR pd.event_time::date = P.event_date)
GROUP BY 1, 2, 3, 4, 5
),
all_dates_and_hours AS (
SELECT device_id, subspace_id, space_id, event_date, event_hour
FROM (
SELECT DISTINCT device_id, subspace_id, space_id, event_date
FROM presence_detection_summary
) d
CROSS JOIN generate_series(0, 23) AS event_hour
),
table_final AS (
SELECT
adah.device_id,
adah.event_date,
COALESCE(pds.count_motion_detected, 0) AS count_motion_detected,
COALESCE(pds.count_presence_detected, 0) AS count_presence_detected,
COALESCE(pds.count_total_presence_detected, 0) AS count_total_presence_detected
FROM all_dates_and_hours adah
LEFT JOIN presence_detection_summary pds
ON pds.device_id = adah.device_id
AND pds.event_date = adah.event_date
AND pds.event_hour = adah.event_hour
),
daily_aggregates AS (
SELECT
device_id,
event_date,
SUM(count_motion_detected) AS count_motion_detected,
SUM(count_presence_detected) AS count_presence_detected,
SUM(count_total_presence_detected) AS count_total_presence_detected
FROM table_final
GROUP BY device_id, event_date
)
INSERT INTO public."presence-sensor-daily-detection" (
device_uuid,
event_date,
count_motion_detected,
count_presence_detected,
count_total_presence_detected
)
SELECT
device_id,
event_date,
count_motion_detected,
count_presence_detected,
count_total_presence_detected
FROM daily_aggregates
ON CONFLICT (device_uuid, event_date) DO UPDATE
SET
count_motion_detected = EXCLUDED.count_motion_detected,
count_presence_detected = EXCLUDED.count_presence_detected,
count_total_presence_detected = EXCLUDED.count_total_presence_detected;

View File

@ -1,19 +0,0 @@
-- will return the presence metrics for the days of the selected month
WITH params AS (
SELECT
TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS month,
string_to_array(NULLIF($4, ''), ',') AS device_ids
)
SELECT
A.device_uuid,
A.event_date,
A.count_motion_detected,
A.count_presence_detected,
A.count_total_presence_detected
FROM public."presence-sensor-daily-detection" AS A
JOIN params P ON TRUE
WHERE A.device_uuid::text = ANY(P.device_ids)
AND (P.month IS NULL
OR date_trunc('month', A.event_date) = P.month
)

View File

@ -1,11 +0,0 @@
SELECT
B.space_device_uuid AS space_id,
A."date",
SUM(A.energy_consumed_kW::numeric) AS total_energy_consumed_KW,
SUM(A.energy_consumed_A::numeric) AS total_energy_consumed_A,
SUM(A.energy_consumed_B::numeric) AS total_energy_consumed_B,
SUM(A.energy_consumed_C::numeric) AS total_energy_consumed_C
FROM public."power-clamp-energy-consumed-daily" AS A -- I want to change the source table in the future.
JOIN public.device AS B
ON A.device_uuid::TEXT = B."uuid"::TEXT
GROUP BY 1, 2;

View File

@ -1,106 +0,0 @@
-- This model shows the number of times a presence was detected per hour, per day
WITH device_logs AS (
SELECT
device.uuid AS device_id,
device.created_at,
device.device_tuya_uuid,
device.space_device_uuid AS space_id,
"device-status-log".event_id,
"device-status-log".event_time::timestamp,
"device-status-log".code,
"device-status-log".value,
"device-status-log".log,
LAG("device-status-log".event_time::timestamp)
OVER (PARTITION BY device.uuid
ORDER BY "device-status-log".event_time) AS prev_timestamp,
LAG("device-status-log".value)
OVER (PARTITION BY device.uuid
ORDER BY "device-status-log".event_time) AS prev_value
FROM device
LEFT JOIN "device-status-log"
ON device.uuid = "device-status-log".device_id
LEFT JOIN product
ON product.uuid = device.product_device_uuid
WHERE product.cat_name = 'hps'
AND "device-status-log".code = 'presence_state'
),
presence_detection AS (
SELECT *,
CASE
WHEN value = 'motion' AND prev_value = 'none' THEN 1 ELSE 0
END AS motion_detected,
CASE
WHEN value = 'presence' AND prev_value = 'none' THEN 1 ELSE 0
END AS presence_detected
FROM device_logs
),
presence_detection_summary AS (
SELECT
pd.device_id,
d.subspace_id,
pd.space_id,
pd.event_time::date AS event_date,
EXTRACT(HOUR FROM pd.event_time)::int AS event_hour,
SUM(motion_detected) AS count_motion_detected,
SUM(presence_detected) AS count_presence_detected,
SUM(motion_detected + presence_detected) AS count_total_presence_detected
FROM presence_detection pd
LEFT JOIN device d ON d.uuid = pd.device_id
GROUP BY 1, 2, 3, 4, 5
),
all_dates_and_hours AS (
SELECT device_id, subspace_id, space_id, event_date, event_hour
FROM (
SELECT DISTINCT device_id, subspace_id, space_id, event_date
FROM presence_detection_summary
) d
CROSS JOIN generate_series(0, 23) AS event_hour
),
table_final AS (
SELECT
adah.device_id,
adah.event_date,
COALESCE(pds.count_motion_detected, 0) AS count_motion_detected,
COALESCE(pds.count_presence_detected, 0) AS count_presence_detected,
COALESCE(pds.count_total_presence_detected, 0) AS count_total_presence_detected
FROM all_dates_and_hours adah
LEFT JOIN presence_detection_summary pds
ON pds.device_id = adah.device_id
AND pds.event_date = adah.event_date
AND pds.event_hour = adah.event_hour
),
daily_aggregate AS (
SELECT
device_id,
event_date,
SUM(count_motion_detected) AS count_motion_detected,
SUM(count_presence_detected) AS count_presence_detected,
SUM(count_total_presence_detected) AS count_total_presence_detected
FROM table_final
GROUP BY device_id, event_date
)
INSERT INTO public."presence-sensor-daily-detection" (
device_uuid,
event_date,
count_motion_detected,
count_presence_detected,
count_total_presence_detected
)
SELECT
device_id,
event_date,
count_motion_detected,
count_presence_detected,
count_total_presence_detected
FROM daily_aggregate
ON CONFLICT (device_uuid, event_date) DO UPDATE
SET
count_motion_detected = EXCLUDED.count_motion_detected,
count_presence_detected = EXCLUDED.count_presence_detected,
count_total_presence_detected = EXCLUDED.count_total_presence_detected;

View File

@ -1,9 +1,4 @@
import {
Injectable,
HttpException,
HttpStatus,
NotFoundException,
} from '@nestjs/common';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@ -21,8 +16,6 @@ import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
import { ILike, In, Not } from 'typeorm';
import { SpaceService } from 'src/space/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 CommunityService {
@ -310,58 +303,4 @@ export class CommunityService {
);
}
}
async getAllDevicesByCommunity(
communityUuid: string,
): Promise<DeviceEntity[]> {
// Fetch the community and its top-level spaces
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
relations: [
'spaces',
'spaces.children',
'spaces.devices',
'spaces.devices.productDevice',
],
});
if (!community) {
throw new NotFoundException('Community not found');
}
const allDevices: DeviceEntity[] = [];
// Recursive fetch function for spaces
const fetchSpaceDevices = async (space: SpaceEntity) => {
if (space.devices && space.devices.length > 0) {
allDevices.push(...space.devices);
}
if (space.children && space.children.length > 0) {
for (const childSpace of space.children) {
const fullChildSpace = await this.spaceRepository.findOne({
where: { uuid: childSpace.uuid },
relations: ['children', 'devices', 'devices.productDevice'],
});
if (fullChildSpace) {
await fetchSpaceDevices(fullChildSpace);
}
}
}
};
// Start recursive fetch for all top-level spaces
for (const space of community.spaces) {
const fullSpace = await this.spaceRepository.findOne({
where: { uuid: space.uuid },
relations: ['children', 'devices', 'devices.productDevice'],
});
if (fullSpace) {
await fetchSpaceDevices(fullSpace);
}
}
return allDevices;
}
}

View File

@ -4,21 +4,14 @@ import {
ApiBearerAuth,
ApiOperation,
ApiParam,
ApiQuery,
} 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 { PowerClampService } from '../services/power-clamp.service';
import {
GetPowerClampBySpaceDto,
GetPowerClampDto,
} from '../dto/get-power-clamp.dto';
import { GetPowerClampDto } from '../dto/get-power-clamp.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import {
PowerClampParamsDto,
ResourceParamsDto,
} from '../dto/power-clamp-params.dto';
import { PowerClampParamsDto } from '../dto/power-clamp-params.dto';
@ApiTags('Power Clamp Module')
@Controller({
@ -46,34 +39,4 @@ export class PowerClampController {
): Promise<BaseResponseDto> {
return await this.powerClampService.getPowerClampData(params, query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('historical')
@ApiOperation({
summary:
ControllerRoute.PowerClamp.ACTIONS
.GET_ENERGY_BY_COMMUNITY_OR_SPACE_SUMMARY,
description:
ControllerRoute.PowerClamp.ACTIONS
.GET_ENERGY_BY_COMMUNITY_OR_SPACE_DESCRIPTION,
})
@ApiQuery({
name: 'spaceUuid',
description: 'UUID of the Space',
required: false,
})
@ApiQuery({
name: 'communityUuid',
description: 'UUID of the Community',
required: false,
})
async getPowerClampDataBySpaceOrCommunity(
@Query() params: ResourceParamsDto,
@Query() query: GetPowerClampBySpaceDto,
): Promise<BaseResponseDto> {
return await this.powerClampService.getPowerClampDataBySpaceOrCommunity(
params,
query,
);
}
}

View File

@ -1,5 +1,5 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsDateString, Matches, IsNotEmpty } from 'class-validator';
import { IsOptional, IsDateString, Matches } from 'class-validator';
export class GetPowerClampDto {
@ApiPropertyOptional({
@ -33,13 +33,3 @@ export class GetPowerClampDto {
})
year?: string;
}
export class GetPowerClampBySpaceDto {
@ApiPropertyOptional({
description: 'monthDate must be in YYYY-MM format',
example: '2025-04',
required: true,
})
@IsDateString()
@IsNotEmpty()
public monthDate: string;
}

View File

@ -1,23 +1,6 @@
import { IsNotEmpty, IsOptional, IsUUID, ValidateIf } from 'class-validator';
import { IsUUID } from 'class-validator';
export class PowerClampParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' })
powerClampUuid: string;
}
export class SpaceParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' })
spaceUuid: string;
}
export class ResourceParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' })
@IsOptional()
spaceUuid?: string;
@IsUUID('4', { message: 'Invalid UUID format' })
@IsOptional()
communityUuid?: string;
@ValidateIf((o) => !o.spaceUuid && !o.communityUuid)
@IsNotEmpty({ message: 'Either spaceUuid or communityUuid must be provided' })
requireEither?: never; // This ensures at least one of them is provided
}

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PowerClampService as PowerClamp } from './services/power-clamp.service';
import { PowerClampService } from './services/power-clamp.service';
import { PowerClampController } from './controllers';
import {
PowerClampDailyRepository,
@ -8,108 +8,16 @@ 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: [
PowerClamp,
PowerClampService,
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: [PowerClamp],
exports: [PowerClampService],
})
export class PowerClampModule {}

View File

@ -1,32 +1,13 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import {
GetPowerClampBySpaceDto,
GetPowerClampDto,
} from '../dto/get-power-clamp.dto';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { 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,
ResourceParamsDto,
} from '../dto/power-clamp-params.dto';
import { PowerClampParamsDto } 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 { filterByMonth, toMMYYYY } from '@app/common/helper/date-format';
import { ProductType } from '@app/common/constants/product-type.enum';
import { CommunityService } from 'src/community/services';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@Injectable()
export class PowerClampService {
@ -35,85 +16,8 @@ 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,
private readonly communityService: CommunityService,
) {}
async getPowerClampDataBySpaceOrCommunity(
params: ResourceParamsDto,
query: GetPowerClampBySpaceDto,
): Promise<BaseResponseDto> {
const { monthDate } = query;
const { spaceUuid, communityUuid } = params;
try {
// Validate we have at least one identifier
if (!spaceUuid && !communityUuid) {
throw new BadRequestException(
'Either spaceUuid or communityUuid must be provided',
);
}
// Get devices based on space or community
const devices = spaceUuid
? await this.spaceDeviceService.getAllDevicesBySpace(spaceUuid)
: await this.communityService.getAllDevicesByCommunity(communityUuid);
if (!devices?.length) {
return this.buildResponse(
'No power clamp devices found for the specified criteria',
[],
);
}
// Filter and prepare device UUIDs
const deviceUuids = devices
.filter((device) => device.productDevice?.prodType === ProductType.PC)
.map((device) => device.uuid);
if (deviceUuids.length === 0) {
return this.buildResponse(
'No power clamp devices (PC type) found for the specified criteria',
[],
);
}
// Execute procedure
const formattedMonthDate = toMMYYYY(monthDate);
const data = await this.executeProcedure(
'fact_daily_space_energy_consumed_procedure',
[formattedMonthDate, deviceUuids.join(',')],
);
// Format and filter data
const formattedData = data.map((item) => ({
...item,
date: new Date(item.date).toLocaleDateString('en-CA'), // YYYY-MM-DD
}));
const resultData = monthDate
? filterByMonth(formattedData, monthDate)
: formattedData;
return this.buildResponse(
`Power clamp data fetched successfully for ${spaceUuid ? 'space' : 'community'}`,
resultData,
);
} catch (error) {
console.error('Error fetching power clamp data', {
error,
spaceUuid,
communityUuid,
});
throw new HttpException(
error.response?.message || 'Failed to fetch power clamp data',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getPowerClampData(
params: PowerClampParamsDto,
query: GetPowerClampDto,
@ -195,17 +99,4 @@ export class PowerClampService {
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,11 +1,6 @@
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import {
HttpException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { HttpException, HttpStatus, Injectable } 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';
@ -14,9 +9,6 @@ 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 {
@ -24,7 +16,6 @@ export class SpaceDeviceService {
private readonly tuyaService: TuyaService,
private readonly validationService: ValidationService,
private readonly deviceService: DeviceService,
private readonly spaceRepository: SpaceRepository,
) {}
async listDevicesInSpace(params: GetSpaceParam): Promise<BaseResponseDto> {
@ -130,37 +121,4 @@ export class SpaceDeviceService {
);
return batteryStatus ? batteryStatus.value : null;
}
async getAllDevicesBySpace(spaceUuid: string): Promise<DeviceEntity[]> {
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid },
relations: ['children', 'devices', 'devices.productDevice'],
});
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', 'devices.productDevice'],
});
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;
}
}