mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-10 15:17:41 +00:00
Merge pull request #363 from SyncrowIOT/DATA-date-param-filtering
DATA-date-param-moved
This commit is contained in:
@ -498,6 +498,10 @@ export class ControllerRoute {
|
|||||||
'Get power clamp historical data';
|
'Get power clamp historical data';
|
||||||
public static readonly GET_ENERGY_DESCRIPTION =
|
public static readonly GET_ENERGY_DESCRIPTION =
|
||||||
'This endpoint retrieves the historical data of a power clamp device based on the provided parameters.';
|
'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 {
|
static DEVICE = class {
|
||||||
|
@ -51,6 +51,7 @@ import {
|
|||||||
PowerClampHourlyEntity,
|
PowerClampHourlyEntity,
|
||||||
PowerClampMonthlyEntity,
|
PowerClampMonthlyEntity,
|
||||||
} from '../modules/power-clamp/entities/power-clamp.entity';
|
} from '../modules/power-clamp/entities/power-clamp.entity';
|
||||||
|
import { PresenceSensorDailyEntity } from '../modules/presence-sensor/entities';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
@ -109,6 +110,7 @@ import {
|
|||||||
PowerClampHourlyEntity,
|
PowerClampHourlyEntity,
|
||||||
PowerClampDailyEntity,
|
PowerClampDailyEntity,
|
||||||
PowerClampMonthlyEntity,
|
PowerClampMonthlyEntity,
|
||||||
|
PresenceSensorDailyEntity,
|
||||||
],
|
],
|
||||||
namingStrategy: new SnakeNamingStrategy(),
|
namingStrategy: new SnakeNamingStrategy(),
|
||||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||||
|
38
libs/common/src/helper/date-format.ts
Normal file
38
libs/common/src/helper/date-format.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -22,21 +22,20 @@ export class PowerClampService {
|
|||||||
})
|
})
|
||||||
.replace('/', '-'); // MM-YYYY
|
.replace('/', '-'); // MM-YYYY
|
||||||
|
|
||||||
await this.executeProcedure('fact_hourly_energy_consumed_procedure', [
|
await this.executeProcedure(
|
||||||
deviceUuid,
|
'fact_hourly_device_energy_consumed_procedure',
|
||||||
dateStr,
|
[deviceUuid, dateStr, hour],
|
||||||
hour,
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
await this.executeProcedure('fact_daily_energy_consumed_procedure', [
|
await this.executeProcedure(
|
||||||
deviceUuid,
|
'fact_daily_device_energy_consumed_procedure',
|
||||||
dateStr,
|
[deviceUuid, dateStr],
|
||||||
]);
|
);
|
||||||
|
|
||||||
await this.executeProcedure('fact_monthly_energy_consumed_procedure', [
|
await this.executeProcedure(
|
||||||
deviceUuid,
|
'fact_monthly_device_energy_consumed_procedure',
|
||||||
monthYear,
|
[deviceUuid, monthYear],
|
||||||
]);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to insert or update energy data:', err);
|
console.error('Failed to insert or update energy data:', err);
|
||||||
throw err;
|
throw err;
|
||||||
@ -47,15 +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.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
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_energy_consumed',
|
|
||||||
fileName,
|
|
||||||
SQL_PROCEDURES_PATH,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,43 @@
|
|||||||
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||||
import * as winston from 'winston';
|
import * as winston from 'winston';
|
||||||
|
const environment = process.env.NODE_ENV || 'local';
|
||||||
|
|
||||||
export const winstonLoggerOptions: winston.LoggerOptions = {
|
export const winstonLoggerOptions: winston.LoggerOptions = {
|
||||||
level:
|
level:
|
||||||
process.env.AZURE_POSTGRESQL_DATABASE === 'development' ? 'debug' : 'error',
|
environment === 'local'
|
||||||
|
? 'debug'
|
||||||
|
: environment === 'development'
|
||||||
|
? 'warn'
|
||||||
|
: 'error',
|
||||||
transports: [
|
transports: [
|
||||||
new winston.transports.Console({
|
new winston.transports.Console({
|
||||||
|
level:
|
||||||
|
environment === 'local'
|
||||||
|
? 'debug'
|
||||||
|
: environment === 'development'
|
||||||
|
? 'warn'
|
||||||
|
: 'error',
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.timestamp(),
|
winston.format.timestamp(),
|
||||||
nestWinstonModuleUtilities.format.nestLike('MyApp', {
|
nestWinstonModuleUtilities.format.nestLike('MyApp', {
|
||||||
prettyPrint: true,
|
prettyPrint: environment === 'local',
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
new winston.transports.File({
|
// Only create file logs if NOT local
|
||||||
filename: 'logs/error.log',
|
...(environment !== 'local'
|
||||||
level: 'error',
|
? [
|
||||||
format: winston.format.json(),
|
new winston.transports.File({
|
||||||
}),
|
filename: 'logs/error.log',
|
||||||
new winston.transports.File({
|
level: 'error',
|
||||||
filename: 'logs/combined.log',
|
format: winston.format.json(),
|
||||||
format: winston.format.json(),
|
}),
|
||||||
}),
|
new winston.transports.File({
|
||||||
|
filename: 'logs/combined.log',
|
||||||
|
level: 'info',
|
||||||
|
format: winston.format.json(),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,7 @@ import { SpaceEntity } from '../../space/entities/space.entity';
|
|||||||
import { SubspaceEntity } from '../../space/entities/subspace/subspace.entity';
|
import { SubspaceEntity } from '../../space/entities/subspace/subspace.entity';
|
||||||
import { NewTagEntity } from '../../tag';
|
import { NewTagEntity } from '../../tag';
|
||||||
import { PowerClampHourlyEntity } from '../../power-clamp/entities/power-clamp.entity';
|
import { PowerClampHourlyEntity } from '../../power-clamp/entities/power-clamp.entity';
|
||||||
|
import { PresenceSensorDailyEntity } from '../../presence-sensor/entities';
|
||||||
|
|
||||||
@Entity({ name: 'device' })
|
@Entity({ name: 'device' })
|
||||||
@Unique(['deviceTuyaUuid'])
|
@Unique(['deviceTuyaUuid'])
|
||||||
@ -82,6 +83,8 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
|
|||||||
public tag: NewTagEntity;
|
public tag: NewTagEntity;
|
||||||
@OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device)
|
@OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device)
|
||||||
powerClampHourly: PowerClampHourlyEntity[];
|
powerClampHourly: PowerClampHourlyEntity[];
|
||||||
|
@OneToMany(() => PresenceSensorDailyEntity, (sensor) => sensor.device)
|
||||||
|
presenceSensorDaily: PresenceSensorDailyEntity[];
|
||||||
constructor(partial: Partial<DeviceEntity>) {
|
constructor(partial: Partial<DeviceEntity>) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, partial);
|
Object.assign(this, partial);
|
||||||
|
1
libs/common/src/modules/presence-sensor/dtos/index.ts
Normal file
1
libs/common/src/modules/presence-sensor/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './presence-sensor.dto';
|
@ -0,0 +1,27 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './presence-sensor.entity';
|
@ -0,0 +1,31 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
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 {}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './presence-sensor.repository';
|
@ -0,0 +1,10 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
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;
|
@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
|
@ -0,0 +1,115 @@
|
|||||||
|
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;
|
@ -0,0 +1,19 @@
|
|||||||
|
-- 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
|
||||||
|
)
|
@ -0,0 +1,11 @@
|
|||||||
|
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;
|
@ -0,0 +1,106 @@
|
|||||||
|
-- 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;
|
@ -1,4 +1,9 @@
|
|||||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
|
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
|
||||||
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
|
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
|
||||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
@ -16,6 +21,8 @@ import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
|
|||||||
import { ILike, In, Not } from 'typeorm';
|
import { ILike, In, Not } from 'typeorm';
|
||||||
import { SpaceService } from 'src/space/services';
|
import { SpaceService } from 'src/space/services';
|
||||||
import { SpaceRepository } from '@app/common/modules/space';
|
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 CommunityService {
|
export class CommunityService {
|
||||||
@ -303,4 +310,58 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,21 @@ import {
|
|||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiParam,
|
ApiParam,
|
||||||
|
ApiQuery,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
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,
|
||||||
|
ResourceParamsDto,
|
||||||
|
} from '../dto/power-clamp-params.dto';
|
||||||
|
|
||||||
@ApiTags('Power Clamp Module')
|
@ApiTags('Power Clamp Module')
|
||||||
@Controller({
|
@Controller({
|
||||||
@ -39,4 +46,34 @@ 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')
|
||||||
|
@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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,13 @@ export class GetPowerClampDto {
|
|||||||
})
|
})
|
||||||
year?: string;
|
year?: string;
|
||||||
}
|
}
|
||||||
|
export class GetPowerClampBySpaceDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'monthDate must be in YYYY-MM format',
|
||||||
|
example: '2025-04',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@IsDateString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public monthDate: string;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,23 @@
|
|||||||
import { IsUUID } from 'class-validator';
|
import { IsNotEmpty, IsOptional, IsUUID, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class PowerClampParamsDto {
|
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;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
@ -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 {}
|
||||||
|
@ -1,13 +1,32 @@
|
|||||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
import {
|
||||||
import { GetPowerClampDto } from '../dto/get-power-clamp.dto';
|
BadRequestException,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
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,
|
||||||
|
ResourceParamsDto,
|
||||||
|
} 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 { 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()
|
@Injectable()
|
||||||
export class PowerClampService {
|
export class PowerClampService {
|
||||||
@ -16,8 +35,85 @@ 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,
|
||||||
|
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(
|
async getPowerClampData(
|
||||||
params: PowerClampParamsDto,
|
params: PowerClampParamsDto,
|
||||||
query: GetPowerClampDto,
|
query: GetPowerClampDto,
|
||||||
@ -99,4 +195,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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', '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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user