Merge pull request #350 from SyncrowIOT/SP-1355-powr-clap-historical-data

SP-1355-powr-clap-historical-data
This commit is contained in:
faljawhary
2025-04-24 15:09:11 +03:00
committed by GitHub
32 changed files with 875 additions and 34 deletions

View File

@ -494,9 +494,10 @@ export class ControllerRoute {
public static readonly ROUTE = 'power-clamp';
static ACTIONS = class {
public static readonly GET_ENERGY_SUMMARY = 'Get power clamp data';
public static readonly GET_ENERGY_SUMMARY =
'Get power clamp historical data';
public static readonly GET_ENERGY_DESCRIPTION =
'This endpoint retrieves power clamp data for a specific device.';
'This endpoint retrieves the historical data of a power clamp device based on the provided parameters.';
};
};
static DEVICE = class {

View File

@ -0,0 +1,6 @@
export enum PowerClampEnergyEnum {
ENERGY_CONSUMED = 'EnergyConsumed',
ENERGY_CONSUMED_A = 'EnergyConsumedA',
ENERGY_CONSUMED_B = 'EnergyConsumedB',
ENERGY_CONSUMED_C = 'EnergyConsumedC',
}

View File

@ -1 +1,2 @@
export const SQL_QUERIES_PATH = 'libs/common/src/sql/queries';
export const SQL_PROCEDURES_PATH = 'libs/common/src/sql/procedures';

View File

@ -46,6 +46,11 @@ import { ClientEntity } from '../modules/client/entities';
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger';
import { createLogger } from 'winston';
import { winstonLoggerOptions } from '../logger/services/winston.logger';
import {
PowerClampDailyEntity,
PowerClampHourlyEntity,
PowerClampMonthlyEntity,
} from '../modules/power-clamp/entities/power-clamp.entity';
@Module({
imports: [
TypeOrmModule.forRootAsync({
@ -101,6 +106,9 @@ import { winstonLoggerOptions } from '../logger/services/winston.logger';
SpaceProductAllocationEntity,
SubspaceProductAllocationEntity,
ClientEntity,
PowerClampHourlyEntity,
PowerClampDailyEntity,
PowerClampMonthlyEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -3,12 +3,24 @@ import { DeviceStatusFirebaseController } from './controllers/devices-status.con
import { DeviceStatusFirebaseService } from './services/devices-status.service';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories/device-status.repository';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
@Module({
providers: [
DeviceStatusFirebaseService,
DeviceRepository,
DeviceStatusLogRepository,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
controllers: [DeviceStatusFirebaseController],
exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository],

View File

@ -18,6 +18,9 @@ import {
runTransaction,
} from 'firebase/database';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { ProductType } from '@app/common/constants/product-type.enum';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum';
@Injectable()
export class DeviceStatusFirebaseService {
private tuya: TuyaContext;
@ -25,6 +28,7 @@ export class DeviceStatusFirebaseService {
constructor(
private readonly configService: ConfigService,
private readonly deviceRepository: DeviceRepository,
private readonly powerClampService: PowerClampService,
private deviceStatusLogRepository: DeviceStatusLogRepository,
) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
@ -79,6 +83,7 @@ export class DeviceStatusFirebaseService {
return await this.createDeviceStatusFirebase({
deviceUuid: device.uuid,
...addDeviceStatusDto,
productType: device.productDevice.prodType,
});
}
// Return null if device not found or no UUID
@ -216,6 +221,25 @@ export class DeviceStatusFirebaseService {
});
await this.deviceStatusLogRepository.save(newLogs);
if (addDeviceStatusDto.productType === ProductType.PC) {
const energyCodes = new Set([
PowerClampEnergyEnum.ENERGY_CONSUMED,
PowerClampEnergyEnum.ENERGY_CONSUMED_A,
PowerClampEnergyEnum.ENERGY_CONSUMED_B,
PowerClampEnergyEnum.ENERGY_CONSUMED_C,
]);
const energyStatus = addDeviceStatusDto?.log?.properties?.find((status) =>
energyCodes.has(status.code),
);
if (energyStatus) {
await this.powerClampService.updateEnergyConsumedHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
}
// Return the updated data
const snapshot: DataSnapshot = await get(dataRef);
return snapshot.val();

View File

@ -0,0 +1,61 @@
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 PowerClampService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async updateEnergyConsumedHistoricalData(deviceUuid: string): Promise<void> {
try {
const now = new Date();
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
const hour = now.getHours();
const monthYear = now
.toLocaleDateString('en-US', {
month: '2-digit',
year: 'numeric',
})
.replace('/', '-'); // MM-YYYY
await this.executeProcedure('fact_hourly_energy_consumed_procedure', [
deviceUuid,
dateStr,
hour,
]);
await this.executeProcedure('fact_daily_energy_consumed_procedure', [
deviceUuid,
dateStr,
]);
await this.executeProcedure('fact_monthly_energy_consumed_procedure', [
deviceUuid,
monthYear,
]);
} catch (err) {
console.error('Failed to insert or update energy data:', err);
throw err;
}
}
private async executeProcedure(
procedureFileName: string,
params: (string | number | null)[],
): Promise<void> {
const query = this.loadQuery(procedureFileName);
await this.dataSource.query(query, params);
}
private loadQuery(fileName: string): string {
return this.sqlLoader.loadQuery(
'fact_energy_consumed',
fileName,
SQL_PROCEDURES_PATH,
);
}
}

View File

@ -1,4 +1,3 @@
import { SQL_QUERIES_PATH } from '@app/common/constants/sql-query-path';
import { Injectable, Logger } from '@nestjs/common';
import { readFileSync } from 'fs';
import { join } from 'path';
@ -8,13 +7,8 @@ export class SqlLoaderService {
private readonly logger = new Logger(SqlLoaderService.name);
private readonly sqlRootPath = join(__dirname, '../sql/queries');
loadQuery(module: string, queryName: string): string {
const filePath = join(
process.cwd(),
SQL_QUERIES_PATH,
module,
`${queryName}.sql`,
);
loadQuery(module: string, queryName: string, path: string): string {
const filePath = join(process.cwd(), path, module, `${queryName}.sql`);
try {
return readFileSync(filePath, 'utf8');
} catch (error) {

View File

@ -17,6 +17,7 @@ import { SceneDeviceEntity } from '../../scene-device/entities';
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';
@Entity({ name: 'device' })
@Unique(['deviceTuyaUuid'])
@ -79,7 +80,8 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
@OneToMany(() => NewTagEntity, (tag) => tag.devices)
// @JoinTable({ name: 'device_tags' })
public tag: NewTagEntity;
@OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device)
powerClampHourly: PowerClampHourlyEntity[];
constructor(partial: Partial<DeviceEntity>) {
super();
Object.assign(this, partial);

View File

@ -0,0 +1 @@
export * from './power-clamp.dto';

View File

@ -0,0 +1,43 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class PowerClampDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public deviceUuid: string;
@IsString()
@IsOptional()
public hour?: string;
@IsString()
@IsOptional()
public day?: string;
@IsString()
@IsOptional()
public month?: string;
@IsString()
@IsNotEmpty()
public energyConsumedKw: string;
@IsString()
@IsNotEmpty()
public energyConsumedA: string;
@IsString()
@IsNotEmpty()
public energyConsumedB: string;
@IsString()
@IsNotEmpty()
public energyConsumedC: string;
@IsString()
@IsNotEmpty()
public prodType: string;
}

View File

@ -0,0 +1 @@
export * from './power-clamp.entity';

View File

@ -0,0 +1,95 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { PowerClampDto } from '../dtos';
import { DeviceEntity } from '../../device/entities/device.entity';
@Entity({ name: 'power-clamp-energy-consumed-hourly' })
@Unique(['deviceUuid', 'date', 'hour'])
export class PowerClampHourlyEntity extends AbstractEntity<PowerClampDto> {
@Column({ nullable: false })
public deviceUuid: string;
@Column({ nullable: false })
public hour: string;
@Column({ nullable: false, type: 'date' })
public date: string;
@Column({ nullable: true })
public energyConsumedKw: string;
@Column({ nullable: false })
public energyConsumedA: string;
@Column({ nullable: false })
public energyConsumedB: string;
@Column({ nullable: false })
public energyConsumedC: string;
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
device: DeviceEntity;
constructor(partial: Partial<PowerClampHourlyEntity>) {
super();
Object.assign(this, partial);
}
}
@Entity({ name: 'power-clamp-energy-consumed-daily' })
@Unique(['deviceUuid', 'date'])
export class PowerClampDailyEntity extends AbstractEntity<PowerClampDto> {
@Column({ nullable: false })
public deviceUuid: string;
@Column({ nullable: false, type: 'date' })
public date: string;
@Column({ nullable: true })
public energyConsumedKw: string;
@Column({ nullable: false })
public energyConsumedA: string;
@Column({ nullable: false })
public energyConsumedB: string;
@Column({ nullable: false })
public energyConsumedC: string;
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
device: DeviceEntity;
constructor(partial: Partial<PowerClampHourlyEntity>) {
super();
Object.assign(this, partial);
}
}
@Entity({ name: 'power-clamp-energy-consumed-monthly' })
@Unique(['deviceUuid', 'month'])
export class PowerClampMonthlyEntity extends AbstractEntity<PowerClampDto> {
@Column({ nullable: false })
public deviceUuid: string;
@Column({ nullable: false })
public month: string;
@Column({ nullable: true })
public energyConsumedKw: string;
@Column({ nullable: false })
public energyConsumedA: string;
@Column({ nullable: false })
public energyConsumedB: string;
@Column({ nullable: false })
public energyConsumedC: string;
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
device: DeviceEntity;
constructor(partial: Partial<PowerClampHourlyEntity>) {
super();
Object.assign(this, partial);
}
}

View File

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

View File

@ -0,0 +1 @@
export * from './power-clamp.repository';

View File

@ -0,0 +1,28 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import {
PowerClampDailyEntity,
PowerClampHourlyEntity,
PowerClampMonthlyEntity,
} from '../entities/power-clamp.entity';
@Injectable()
export class PowerClampHourlyRepository extends Repository<PowerClampHourlyEntity> {
constructor(private dataSource: DataSource) {
super(PowerClampHourlyEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class PowerClampDailyRepository extends Repository<PowerClampDailyEntity> {
constructor(private dataSource: DataSource) {
super(PowerClampDailyEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class PowerClampMonthlyRepository extends Repository<PowerClampMonthlyEntity> {
constructor(private dataSource: DataSource) {
super(PowerClampMonthlyEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,111 @@
WITH params AS (
SELECT
$1::uuid AS device_id,
$2::date AS target_date
),
total_energy AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
GROUP BY 1,2,3,4,5
),
energy_phase_A AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
GROUP BY 1,2,3,4,5
),
energy_phase_B AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
GROUP BY 1,2,3,4,5
),
energy_phase_C AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
GROUP BY 1,2,3,4,5
),
final_data AS (
SELECT
t.device_id,
t.date,
t.event_year::text,
t.event_month,
t.hour,
(t.max_value - t.min_value) AS energy_consumed_kW,
(a.max_value - a.min_value) AS energy_consumed_A,
(b.max_value - b.min_value) AS energy_consumed_B,
(c.max_value - c.min_value) AS energy_consumed_C
FROM total_energy t
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
)
-- Final upsert into daily table
INSERT INTO public."power-clamp-energy-consumed-daily" (
device_uuid,
energy_consumed_kw,
energy_consumed_a,
energy_consumed_b,
energy_consumed_c,
date
)
SELECT
device_id,
SUM(CAST(energy_consumed_kW AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_A AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_B AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_C AS NUMERIC))::VARCHAR,
date
FROM final_data
GROUP BY device_id, date
ON CONFLICT (device_uuid, date) DO UPDATE
SET
energy_consumed_kw = EXCLUDED.energy_consumed_kw,
energy_consumed_a = EXCLUDED.energy_consumed_a,
energy_consumed_b = EXCLUDED.energy_consumed_b,
energy_consumed_c = EXCLUDED.energy_consumed_c;

View File

@ -0,0 +1,113 @@
WITH params AS (
SELECT
$1::uuid AS device_id,
$2::date AS target_date,
$3::text AS target_hour
),
total_energy AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time)::text AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
GROUP BY 1,2,3,4,5
),
energy_phase_A AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time)::text AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
GROUP BY 1,2,3,4,5
),
energy_phase_B AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time)::text AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
GROUP BY 1,2,3,4,5
),
energy_phase_C AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time)::text AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
GROUP BY 1,2,3,4,5
),
final_data AS (
SELECT
t.device_id,
t.date,
t.event_year::text,
t.event_month,
t.hour,
(t.max_value - t.min_value) AS energy_consumed_kW,
(a.max_value - a.min_value) AS energy_consumed_A,
(b.max_value - b.min_value) AS energy_consumed_B,
(c.max_value - c.min_value) AS energy_consumed_C
FROM total_energy t
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
)
-- UPSERT into hourly table
INSERT INTO public."power-clamp-energy-consumed-hourly" (
device_uuid,
energy_consumed_kw,
energy_consumed_a,
energy_consumed_b,
energy_consumed_c,
date,
hour
)
SELECT
device_id,
SUM(CAST(energy_consumed_kW AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_A AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_B AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_C AS NUMERIC))::VARCHAR,
date,
hour
FROM final_data
GROUP BY 1,6,7
ON CONFLICT (device_uuid, date, hour) DO UPDATE
SET
energy_consumed_kw = EXCLUDED.energy_consumed_kw,
energy_consumed_a = EXCLUDED.energy_consumed_a,
energy_consumed_b = EXCLUDED.energy_consumed_b,
energy_consumed_c = EXCLUDED.energy_consumed_c;

View File

@ -0,0 +1,107 @@
WITH params AS (
SELECT
$1::uuid AS device_id,
$2::text AS target_month -- Format should match 'MM-YYYY'
),
total_energy AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND log.device_id = params.device_id
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
GROUP BY 1,2,3,4,5
),
energy_phase_A AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND log.device_id = params.device_id
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
GROUP BY 1,2,3,4,5
),
energy_phase_B AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND log.device_id = params.device_id
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
GROUP BY 1,2,3,4,5
),
energy_phase_C AS (
SELECT
log.device_id,
log.event_time::date AS date,
EXTRACT(HOUR FROM log.event_time) AS hour,
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
MIN(log.value)::integer AS min_value,
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND log.device_id = params.device_id
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
GROUP BY 1,2,3,4,5
),
final_data AS (
SELECT
t.device_id,
t.date,
t.event_year::text,
t.event_month,
t.hour,
(t.max_value - t.min_value) AS energy_consumed_kW,
(a.max_value - a.min_value) AS energy_consumed_A,
(b.max_value - b.min_value) AS energy_consumed_B,
(c.max_value - c.min_value) AS energy_consumed_C
FROM total_energy t
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
)
-- Final monthly UPSERT
INSERT INTO public."power-clamp-energy-consumed-monthly" (
device_uuid,
energy_consumed_kw,
energy_consumed_a,
energy_consumed_b,
energy_consumed_c,
month
)
SELECT
device_id,
SUM(CAST(energy_consumed_kW AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_A AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_B AS NUMERIC))::VARCHAR,
SUM(CAST(energy_consumed_C AS NUMERIC))::VARCHAR,
event_month
FROM final_data
GROUP BY device_id, event_month
ON CONFLICT (device_uuid, month) DO UPDATE
SET
energy_consumed_kw = EXCLUDED.energy_consumed_kw,
energy_consumed_a = EXCLUDED.energy_consumed_a,
energy_consumed_b = EXCLUDED.energy_consumed_b,
energy_consumed_c = EXCLUDED.energy_consumed_c;

View File

@ -21,6 +21,13 @@ import {
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
@Module({
imports: [ConfigModule, SpaceRepositoryModule],
@ -43,6 +50,11 @@ import { SubspaceRepository } from '@app/common/modules/space/repositories/subsp
AutomationRepository,
CommunityRepository,
SubspaceRepository,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
exports: [],
})

View File

@ -56,6 +56,13 @@ import {
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
@ -102,6 +109,11 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
SceneIconRepository,
SceneRepository,
AutomationRepository,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
exports: [CommunityService, SpacePermissionService],
})

View File

@ -20,6 +20,13 @@ import {
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
@Module({
imports: [ConfigModule, DeviceRepositoryModule],
controllers: [DoorLockController],
@ -40,6 +47,11 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories';
SceneRepository,
SceneDeviceRepository,
AutomationRepository,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
exports: [DoorLockService],
})

View File

@ -18,6 +18,13 @@ import {
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
@Module({
imports: [ConfigModule, DeviceRepositoryModule],
controllers: [GroupController],
@ -37,6 +44,11 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
SceneIconRepository,
SceneRepository,
AutomationRepository,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
exports: [GroupService],
})

View File

@ -76,6 +76,13 @@ import {
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
@Module({
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
@ -140,6 +147,11 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
SceneIconRepository,
SceneRepository,
AutomationRepository,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
exports: [InviteUserService],
})

View File

@ -1,9 +1,11 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } 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 { GetPowerClampDto } from '../dto/get-power-clamp.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@ApiTags('Power Clamp Module')
@Controller({
@ -15,12 +17,18 @@ export class PowerClampController {
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
@Get(':powerClampUuid/historical')
@ApiOperation({
summary: ControllerRoute.PowerClamp.ACTIONS.GET_ENERGY_SUMMARY,
description: ControllerRoute.PowerClamp.ACTIONS.GET_ENERGY_DESCRIPTION,
})
async getPowerClampData() {
return await this.powerClampService.getPowerClampData();
async getPowerClampData(
@Param('powerClampUuid') powerClampUuid: string,
@Query() params: GetPowerClampDto,
): Promise<BaseResponseDto> {
return await this.powerClampService.getPowerClampData(
powerClampUuid,
params,
);
}
}

View File

@ -0,0 +1,35 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsDateString, Matches } from 'class-validator';
export class GetPowerClampDto {
@ApiPropertyOptional({
description: 'Input date in ISO format (YYYY-MM-DD) to filter the data',
example: '2025-04-23',
required: false,
})
@IsOptional()
@IsDateString()
dayDate?: string;
@ApiPropertyOptional({
description: 'Month and year in format YYYY-MM',
example: '2025-03',
required: false,
})
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/, {
message: 'monthDate must be in YYYY-MM format',
})
@IsOptional()
monthDate?: string;
@ApiPropertyOptional({
description: 'Input year in YYYY format to filter the data',
example: '2025',
required: false,
})
@IsOptional()
@Matches(/^\d{4}$/, {
message: 'Year must be in YYYY format',
})
year?: string;
}

View File

@ -2,11 +2,20 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PowerClampService } from './services/power-clamp.service';
import { PowerClampController } from './controllers';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import {
PowerClampDailyRepository,
PowerClampHourlyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
@Module({
imports: [ConfigModule],
controllers: [PowerClampController],
providers: [PowerClampService, SqlLoaderService],
providers: [
PowerClampService,
PowerClampDailyRepository,
PowerClampHourlyRepository,
PowerClampMonthlyRepository,
],
exports: [PowerClampService],
})
export class PowerClampModule {}

View File

@ -1,27 +1,88 @@
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
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';
@Injectable()
export class PowerClampService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
private readonly powerClampDailyRepository: PowerClampDailyRepository,
private readonly powerClampHourlyRepository: PowerClampHourlyRepository,
private readonly powerClampMonthlyRepository: PowerClampMonthlyRepository,
) {}
async getPowerClampData() {
const sql = this.sqlLoader.loadQuery(
'fact_daily_energy_consumed',
'fact_daily_energy_consumed',
);
return this.dataSource.manager.query(sql);
async getPowerClampData(powerClampUuid: string, params: GetPowerClampDto) {
const { dayDate, monthDate, year } = params;
try {
if (dayDate) {
const data = await this.powerClampHourlyRepository
.createQueryBuilder('hourly')
.where('hourly.deviceUuid = :deviceUuid', {
deviceUuid: powerClampUuid,
})
.andWhere('hourly.date = :date', { date: dayDate })
.orderBy('CAST(hourly.hour AS INTEGER)', 'ASC')
.getMany();
return this.buildResponse(
`Power clamp data for day ${dayDate} fetched successfully`,
data,
);
}
if (monthDate) {
const data = await this.powerClampDailyRepository
.createQueryBuilder('daily')
.where('daily.deviceUuid = :deviceUuid', {
deviceUuid: powerClampUuid,
})
.andWhere("TO_CHAR(daily.date, 'YYYY-MM') = :monthDate", {
monthDate,
})
.orderBy('daily.date', 'ASC')
.getMany();
return this.buildResponse(
`Power clamp data for month ${monthDate} fetched successfully`,
data,
);
}
if (year) {
const data = await this.powerClampMonthlyRepository
.createQueryBuilder('monthly')
.where('monthly.deviceUuid = :deviceUuid', {
deviceUuid: powerClampUuid,
})
.andWhere('RIGHT(monthly.month, 4) = :year', { year })
.orderBy('monthly.month', 'ASC')
.getMany();
return this.buildResponse(
`Power clamp data for year ${year} fetched successfully`,
data,
);
}
return this.buildResponse(`Power clamp data fetched successfully`, []);
} catch (error) {
throw new HttpException(
'Error fetching power clamp data',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getEnergyConsumed(code: string) {
const sql = this.sqlLoader.loadQuery(
'energy',
'energy_consumed_with_params',
);
return this.dataSource.manager.query(sql, [code]);
private buildResponse(message: string, data: any[]) {
return new SuccessResponseDto({
message,
data,
statusCode: HttpStatus.OK,
});
}
}

View File

@ -60,6 +60,13 @@ import {
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampDailyRepository,
PowerClampHourlyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
const CommandHandlers = [CreateOrphanSpaceHandler];
@ -112,6 +119,11 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
SceneRepository,
AutomationRepository,
SubspaceModelProductAllocationRepoitory,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
exports: [ProjectService, CqrsModule],
})

View File

@ -58,6 +58,13 @@ import {
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { CommunityModule } from 'src/community/community.module';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
const CommandHandlers = [
PropogateUpdateSpaceModelHandler,
@ -112,6 +119,11 @@ const CommandHandlers = [
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
SubSpaceService,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
exports: [CqrsModule, SpaceModelService],
})

View File

@ -83,6 +83,13 @@ import { SubspaceModelProductAllocationService } from 'src/space-model/services/
import { SpaceProductAllocationService } from './services/space-product-allocation.service';
import { SubspaceProductAllocationService } from './services/subspace/subspace-product-allocation.service';
import { SpaceValidationController } from './controllers/space-validation.controller';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
export const CommandHandlers = [DisableSpaceHandler];
@ -154,6 +161,11 @@ export const CommandHandlers = [DisableSpaceHandler];
SubspaceProductAllocationService,
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
PowerClampService,
SqlLoaderService,
],
exports: [SpaceService],
})

View File

@ -22,6 +22,13 @@ import {
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
@Module({
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
controllers: [VisitorPasswordController],
@ -43,6 +50,11 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories';
SceneDeviceRepository,
AutomationRepository,
ProjectRepository,
PowerClampService,
PowerClampHourlyRepository,
PowerClampDailyRepository,
PowerClampMonthlyRepository,
SqlLoaderService,
],
exports: [VisitorPasswordService],
})