mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-11-27 02:54:54 +00:00
Compare commits
48 Commits
DATA-daily
...
DATA-bug-f
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c15ce77fe | |||
| a8bb161ee2 | |||
| fe891030aa | |||
| f2e515b180 | |||
| f7fd96afa1 | |||
| 5292271721 | |||
| 180d16eeb1 | |||
| dca3db0c59 | |||
| 56e78683b3 | |||
| e575e51c4c | |||
| 8750da7e62 | |||
| e3e9fe82fc | |||
| e253d1ca03 | |||
| b50d7682f3 | |||
| 1bb3803229 | |||
| 92ee6ee951 | |||
| c06be4736c | |||
| 5cb4295f8a | |||
| 67331aa92a | |||
| 7ec41f8311 | |||
| 4aa3d04478 | |||
| 799fcb6fb9 | |||
| 921770ea79 | |||
| 7ec4171e1a | |||
| c0a6e9ab63 | |||
| 208281386d | |||
| fb5084ba3a | |||
| 9c3abdd08a | |||
| 3535c1d8c5 | |||
| 2ba5700fdd | |||
| 1479c74423 | |||
| 8030644fee | |||
| d43e860867 | |||
| f8269df3fb | |||
| c085514d27 | |||
| fa3cb578df | |||
| b4572beec2 | |||
| b3e86ec56f | |||
| 45b8cdcaae | |||
| 5ed59e4fcc | |||
| 91abfb41ab | |||
| d40fb7a762 | |||
| 71f795babe | |||
| 0d48505eac | |||
| e538f2b829 | |||
| 23af8e9de3 | |||
| d197bf2bb4 | |||
| 2a1f1f52f6 |
@ -498,6 +498,21 @@ 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 Occupancy = class {
|
||||||
|
public static readonly ROUTE = 'occupancy';
|
||||||
|
|
||||||
|
static ACTIONS = class {
|
||||||
|
public static readonly GET_OCCUPANCY_HEAT_MAP_SUMMARY =
|
||||||
|
'Get occupancy heat map data';
|
||||||
|
public static readonly GET_OCCUPANCY_HEAT_MAP_DESCRIPTION =
|
||||||
|
'This endpoint retrieves the occupancy heat map data based on the provided parameters.';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
static DEVICE = class {
|
static DEVICE = class {
|
||||||
@ -609,6 +624,17 @@ export class ControllerRoute {
|
|||||||
'This endpoint retrieves all devices in the system.';
|
'This endpoint retrieves all devices in the system.';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
static DEVICE_SPACE_COMMUNITY = class {
|
||||||
|
public static readonly ROUTE = 'devices-space-community';
|
||||||
|
|
||||||
|
static ACTIONS = class {
|
||||||
|
public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_SUMMARY =
|
||||||
|
'Get all devices by space or community with recursive child';
|
||||||
|
|
||||||
|
public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_DESCRIPTION =
|
||||||
|
'This endpoint retrieves all devices in the system by space or community with recursive child.';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
static DEVICE_PERMISSION = class {
|
static DEVICE_PERMISSION = class {
|
||||||
public static readonly ROUTE = 'device-permission';
|
public static readonly ROUTE = 'device-permission';
|
||||||
|
|||||||
4
libs/common/src/constants/presence.sensor.enum.ts
Normal file
4
libs/common/src/constants/presence.sensor.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum PresenceSensorEnum {
|
||||||
|
PRESENCE_STATE = 'presence_state',
|
||||||
|
SENSITIVITY = 'sensitivity',
|
||||||
|
}
|
||||||
@ -51,6 +51,10 @@ import {
|
|||||||
PowerClampHourlyEntity,
|
PowerClampHourlyEntity,
|
||||||
PowerClampMonthlyEntity,
|
PowerClampMonthlyEntity,
|
||||||
} from '../modules/power-clamp/entities/power-clamp.entity';
|
} from '../modules/power-clamp/entities/power-clamp.entity';
|
||||||
|
import {
|
||||||
|
PresenceSensorDailyDeviceEntity,
|
||||||
|
PresenceSensorDailySpaceEntity,
|
||||||
|
} from '../modules/presence-sensor/entities';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
@ -109,6 +113,8 @@ import {
|
|||||||
PowerClampHourlyEntity,
|
PowerClampHourlyEntity,
|
||||||
PowerClampDailyEntity,
|
PowerClampDailyEntity,
|
||||||
PowerClampMonthlyEntity,
|
PowerClampMonthlyEntity,
|
||||||
|
PresenceSensorDailyDeviceEntity,
|
||||||
|
PresenceSensorDailySpaceEntity,
|
||||||
],
|
],
|
||||||
namingStrategy: new SnakeNamingStrategy(),
|
namingStrategy: new SnakeNamingStrategy(),
|
||||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@ -21,6 +22,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
],
|
],
|
||||||
controllers: [DeviceStatusFirebaseController],
|
controllers: [DeviceStatusFirebaseController],
|
||||||
exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository],
|
exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository],
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log
|
|||||||
import { ProductType } from '@app/common/constants/product-type.enum';
|
import { ProductType } from '@app/common/constants/product-type.enum';
|
||||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||||
import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum';
|
import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum';
|
||||||
|
import { PresenceSensorEnum } from '@app/common/constants/presence.sensor.enum';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeviceStatusFirebaseService {
|
export class DeviceStatusFirebaseService {
|
||||||
private tuya: TuyaContext;
|
private tuya: TuyaContext;
|
||||||
@ -29,6 +31,7 @@ export class DeviceStatusFirebaseService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly deviceRepository: DeviceRepository,
|
private readonly deviceRepository: DeviceRepository,
|
||||||
private readonly powerClampService: PowerClampService,
|
private readonly powerClampService: PowerClampService,
|
||||||
|
private readonly occupancyService: OccupancyService,
|
||||||
private deviceStatusLogRepository: DeviceStatusLogRepository,
|
private deviceStatusLogRepository: DeviceStatusLogRepository,
|
||||||
) {
|
) {
|
||||||
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
|
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
|
||||||
@ -240,6 +243,26 @@ export class DeviceStatusFirebaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
addDeviceStatusDto.productType === ProductType.CPS ||
|
||||||
|
addDeviceStatusDto.productType === ProductType.WPS
|
||||||
|
) {
|
||||||
|
const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]);
|
||||||
|
|
||||||
|
const occupancyStatus = addDeviceStatusDto?.log?.properties?.find(
|
||||||
|
(status) => occupancyCodes.has(status.code),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (occupancyStatus) {
|
||||||
|
await this.occupancyService.updateOccupancySensorHistoricalData(
|
||||||
|
addDeviceStatusDto.deviceUuid,
|
||||||
|
);
|
||||||
|
await this.occupancyService.updateOccupancySensorHistoricalDurationData(
|
||||||
|
addDeviceStatusDto.deviceUuid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return the updated data
|
// Return the updated data
|
||||||
const snapshot: DataSnapshot = await get(dataRef);
|
const snapshot: DataSnapshot = await get(dataRef);
|
||||||
return snapshot.val();
|
return snapshot.val();
|
||||||
|
|||||||
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
68
libs/common/src/helper/services/occupancy.service.ts
Normal file
68
libs/common/src/helper/services/occupancy.service.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||||
|
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 OccupancyService {
|
||||||
|
constructor(
|
||||||
|
private readonly sqlLoader: SqlLoaderService,
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
private readonly deviceRepository: DeviceRepository,
|
||||||
|
) {}
|
||||||
|
async updateOccupancySensorHistoricalDurationData(
|
||||||
|
deviceUuid: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||||
|
const device = await this.deviceRepository.findOne({
|
||||||
|
where: { uuid: deviceUuid },
|
||||||
|
relations: ['spaceDevice'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.executeProcedure(
|
||||||
|
'fact_daily_space_occupancy_duration',
|
||||||
|
'procedure_update_daily_space_occupancy_duration',
|
||||||
|
[dateStr, device.spaceDevice?.uuid],
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to insert or update occupancy duration data:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async updateOccupancySensorHistoricalData(deviceUuid: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||||
|
const device = await this.deviceRepository.findOne({
|
||||||
|
where: { uuid: deviceUuid },
|
||||||
|
relations: ['spaceDevice'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.executeProcedure(
|
||||||
|
'fact_space_occupancy_count',
|
||||||
|
'procedure_update_fact_space_occupancy',
|
||||||
|
[dateStr, device.spaceDevice?.uuid],
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to insert or update occupancy data:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeProcedure(
|
||||||
|
procedureFolderName: string,
|
||||||
|
procedureFileName: string,
|
||||||
|
params: (string | number | null)[],
|
||||||
|
): Promise<void> {
|
||||||
|
const query = this.loadQuery(procedureFolderName, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,16 +46,15 @@ export class PowerClampService {
|
|||||||
procedureFileName: string,
|
procedureFileName: string,
|
||||||
params: (string | number | null)[],
|
params: (string | number | null)[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const query = this.loadQuery(procedureFileName);
|
const query = this.loadQuery(
|
||||||
|
'fact_device_energy_consumed',
|
||||||
|
procedureFileName,
|
||||||
|
);
|
||||||
await this.dataSource.query(query, params);
|
await this.dataSource.query(query, params);
|
||||||
console.log(`Procedure ${procedureFileName} executed successfully.`);
|
console.log(`Procedure ${procedureFileName} executed successfully.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadQuery(fileName: string): string {
|
private loadQuery(folderName: string, fileName: string): string {
|
||||||
return this.sqlLoader.loadQuery(
|
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
|
||||||
'fact_device_energy_consumed',
|
|
||||||
fileName,
|
|
||||||
SQL_PROCEDURES_PATH,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { PresenceSensorDailyDeviceEntity } 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(() => PresenceSensorDailyDeviceEntity, (sensor) => sensor.device)
|
||||||
|
presenceSensorDaily: PresenceSensorDailyDeviceEntity[];
|
||||||
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,58 @@
|
|||||||
|
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';
|
||||||
|
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'presence-sensor-daily-device-detection' })
|
||||||
|
@Unique(['deviceUuid', 'eventDate'])
|
||||||
|
export class PresenceSensorDailyDeviceEntity 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<PresenceSensorDailyDeviceEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Entity({ name: 'presence-sensor-daily-space-detection' })
|
||||||
|
@Unique(['spaceUuid', 'eventDate'])
|
||||||
|
export class PresenceSensorDailySpaceEntity extends AbstractEntity<PresenceSensorDto> {
|
||||||
|
@Column({ nullable: false })
|
||||||
|
public spaceUuid: 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(() => SpaceEntity, (space) => space.presenceSensorDaily)
|
||||||
|
space: SpaceEntity;
|
||||||
|
|
||||||
|
constructor(partial: Partial<PresenceSensorDailySpaceEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import {
|
||||||
|
PresenceSensorDailyDeviceEntity,
|
||||||
|
PresenceSensorDailySpaceEntity,
|
||||||
|
} from './entities/presence-sensor.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [],
|
||||||
|
exports: [],
|
||||||
|
controllers: [],
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
PresenceSensorDailyDeviceEntity,
|
||||||
|
PresenceSensorDailySpaceEntity,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PresenceSensorRepositoryModule {}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './presence-sensor.repository';
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
PresenceSensorDailyDeviceEntity,
|
||||||
|
PresenceSensorDailySpaceEntity,
|
||||||
|
} from '../entities';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PresenceSensorDailyDeviceRepository extends Repository<PresenceSensorDailyDeviceEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(PresenceSensorDailyDeviceEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Injectable()
|
||||||
|
export class PresenceSensorDailySpaceRepository extends Repository<PresenceSensorDailySpaceEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(PresenceSensorDailySpaceEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import { SpaceModelEntity } from '../../space-model';
|
|||||||
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
||||||
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
||||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||||
|
import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities';
|
||||||
|
|
||||||
@Entity({ name: 'space' })
|
@Entity({ name: 'space' })
|
||||||
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||||
@ -111,6 +112,9 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
|||||||
)
|
)
|
||||||
public productAllocations: SpaceProductAllocationEntity[];
|
public productAllocations: SpaceProductAllocationEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => PresenceSensorDailySpaceEntity, (sensor) => sensor.space)
|
||||||
|
presenceSensorDaily: PresenceSensorDailySpaceEntity[];
|
||||||
|
|
||||||
constructor(partial: Partial<SpaceEntity>) {
|
constructor(partial: Partial<SpaceEntity>) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, partial);
|
Object.assign(this, partial);
|
||||||
|
|||||||
@ -0,0 +1,116 @@
|
|||||||
|
-- Step 1: Get device presence events with previous timestamps
|
||||||
|
WITH start_date AS (
|
||||||
|
SELECT
|
||||||
|
d.uuid AS device_id,
|
||||||
|
d.space_device_uuid AS space_id,
|
||||||
|
l.value,
|
||||||
|
l.event_time::timestamp AS event_time,
|
||||||
|
LAG(l.event_time::timestamp) OVER (PARTITION BY d.uuid ORDER BY l.event_time) AS prev_timestamp
|
||||||
|
FROM device d
|
||||||
|
LEFT JOIN "device-status-log" l
|
||||||
|
ON d.uuid = l.device_id
|
||||||
|
LEFT JOIN product p
|
||||||
|
ON p.uuid = d.product_device_uuid
|
||||||
|
WHERE p.cat_name = 'hps'
|
||||||
|
AND l.code = 'presence_state'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Step 2: Identify periods when device reports "none"
|
||||||
|
device_none_periods AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
device_id,
|
||||||
|
event_time AS empty_from,
|
||||||
|
LEAD(event_time) OVER (PARTITION BY device_id ORDER BY event_time) AS empty_until
|
||||||
|
FROM start_date
|
||||||
|
WHERE value = 'none'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Step 3: Clip the "none" periods to the edges of each day
|
||||||
|
clipped_device_none_periods AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
GREATEST(empty_from, DATE_TRUNC('day', empty_from)) AS clipped_from,
|
||||||
|
LEAST(empty_until, DATE_TRUNC('day', empty_until) + INTERVAL '1 day') AS clipped_until
|
||||||
|
FROM device_none_periods
|
||||||
|
WHERE empty_until IS NOT NULL
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Step 4: Break multi-day periods into daily intervals
|
||||||
|
generated_daily_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
gs::date AS day,
|
||||||
|
GREATEST(clipped_from, gs) AS interval_start,
|
||||||
|
LEAST(clipped_until, gs + INTERVAL '1 day') AS interval_end
|
||||||
|
FROM clipped_device_none_periods,
|
||||||
|
LATERAL generate_series(DATE_TRUNC('day', clipped_from), DATE_TRUNC('day', clipped_until), INTERVAL '1 day') AS gs
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Step 5: Merge overlapping or adjacent intervals per day
|
||||||
|
merged_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
day,
|
||||||
|
interval_start,
|
||||||
|
interval_end
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
day,
|
||||||
|
interval_start,
|
||||||
|
interval_end,
|
||||||
|
LAG(interval_end) OVER (PARTITION BY space_id, day ORDER BY interval_start) AS prev_end
|
||||||
|
FROM generated_daily_intervals
|
||||||
|
) sub
|
||||||
|
WHERE prev_end IS NULL OR interval_start > prev_end
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Step 6: Sum up total missing seconds (device reported "none") per day
|
||||||
|
missing_seconds_per_day AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
day AS missing_date,
|
||||||
|
SUM(EXTRACT(EPOCH FROM (interval_end - interval_start))) AS total_missing_seconds
|
||||||
|
FROM merged_intervals
|
||||||
|
GROUP BY space_id, day
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Step 7: Calculate total occupied time per day (86400 - missing)
|
||||||
|
occupied_seconds_per_day AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
missing_date as event_date,
|
||||||
|
86400 - total_missing_seconds AS total_occupied_seconds,
|
||||||
|
(86400 - total_missing_seconds)/86400*100 as occupancy_prct
|
||||||
|
FROM missing_seconds_per_day
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Final Output
|
||||||
|
, final_data as (
|
||||||
|
SELECT space_id,
|
||||||
|
event_date,
|
||||||
|
total_occupied_seconds,
|
||||||
|
occupancy_prct
|
||||||
|
FROM occupied_seconds_per_day
|
||||||
|
ORDER BY 1,2
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO public."space-daily-occupancy-duration" (
|
||||||
|
space_uuid,
|
||||||
|
event_date,
|
||||||
|
occupied_seconds,
|
||||||
|
occupancy_percentage
|
||||||
|
)
|
||||||
|
select space_id,
|
||||||
|
event_date,
|
||||||
|
total_occupied_seconds,
|
||||||
|
occupancy_prct
|
||||||
|
FROM final_data
|
||||||
|
ON CONFLICT (space_uuid, event_date) DO UPDATE
|
||||||
|
SET
|
||||||
|
occupancy_percentage = EXCLUDED.occupancy_percentage,
|
||||||
|
occupied_seconds = EXCLUDED.occupied_seconds;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
$1::uuid AS space_uuid,
|
||||||
|
TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SELECT sdo.space_uuid,
|
||||||
|
event_date,
|
||||||
|
occupancy_percentage,
|
||||||
|
occupied_seconds
|
||||||
|
FROM public."space-daily-occupancy-duration" as sdo
|
||||||
|
JOIN params P ON true
|
||||||
|
where (sdo.space_uuid = P.space_uuid
|
||||||
|
OR P.event_month IS null)
|
||||||
|
AND TO_CHAR(sdo.event_date, 'YYYY-MM') = TO_CHAR(P.event_month, 'YYYY-MM')
|
||||||
|
ORDER BY sdo.space_uuid, sdo.event_date;
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date,
|
||||||
|
$2::uuid AS space_id
|
||||||
|
)
|
||||||
|
|
||||||
|
, start_date AS (
|
||||||
|
SELECT
|
||||||
|
d.uuid AS device_id,
|
||||||
|
d.space_device_uuid AS space_id,
|
||||||
|
l.value,
|
||||||
|
l.event_time::timestamp AS event_time,
|
||||||
|
LAG(l.event_time::timestamp) OVER (PARTITION BY d.uuid ORDER BY l.event_time) AS prev_timestamp
|
||||||
|
FROM device d
|
||||||
|
LEFT JOIN "device-status-log" l
|
||||||
|
ON d.uuid = l.device_id
|
||||||
|
LEFT JOIN product p
|
||||||
|
ON p.uuid = d.product_device_uuid
|
||||||
|
WHERE p.cat_name = 'hps'
|
||||||
|
AND l.code = 'presence_state'
|
||||||
|
)
|
||||||
|
|
||||||
|
, device_none_periods AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
device_id,
|
||||||
|
event_time AS empty_from,
|
||||||
|
LEAD(event_time) OVER (PARTITION BY device_id ORDER BY event_time) AS empty_until
|
||||||
|
FROM start_date
|
||||||
|
WHERE value = 'none'
|
||||||
|
)
|
||||||
|
|
||||||
|
, clipped_device_none_periods AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
GREATEST(empty_from, DATE_TRUNC('day', empty_from)) AS clipped_from,
|
||||||
|
LEAST(empty_until, DATE_TRUNC('day', empty_until) + INTERVAL '1 day') AS clipped_until
|
||||||
|
FROM device_none_periods
|
||||||
|
WHERE empty_until IS NOT NULL
|
||||||
|
)
|
||||||
|
|
||||||
|
, generated_daily_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
gs::date AS day,
|
||||||
|
GREATEST(clipped_from, gs) AS interval_start,
|
||||||
|
LEAST(clipped_until, gs + INTERVAL '1 day') AS interval_end
|
||||||
|
FROM clipped_device_none_periods,
|
||||||
|
LATERAL generate_series(DATE_TRUNC('day', clipped_from), DATE_TRUNC('day', clipped_until), INTERVAL '1 day') AS gs
|
||||||
|
)
|
||||||
|
|
||||||
|
, merged_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
day,
|
||||||
|
interval_start,
|
||||||
|
interval_end
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
day,
|
||||||
|
interval_start,
|
||||||
|
interval_end,
|
||||||
|
LAG(interval_end) OVER (PARTITION BY space_id, day ORDER BY interval_start) AS prev_end
|
||||||
|
FROM generated_daily_intervals
|
||||||
|
) sub
|
||||||
|
WHERE prev_end IS NULL OR interval_start > prev_end
|
||||||
|
)
|
||||||
|
|
||||||
|
, missing_seconds_per_day AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
day AS missing_date,
|
||||||
|
SUM(EXTRACT(EPOCH FROM (interval_end - interval_start))) AS total_missing_seconds
|
||||||
|
FROM merged_intervals
|
||||||
|
GROUP BY space_id, day
|
||||||
|
)
|
||||||
|
|
||||||
|
, occupied_seconds_per_day AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
missing_date as event_date,
|
||||||
|
86400 - total_missing_seconds AS total_occupied_seconds,
|
||||||
|
(86400 - total_missing_seconds)/86400*100 as occupancy_percentage
|
||||||
|
FROM missing_seconds_per_day
|
||||||
|
)
|
||||||
|
|
||||||
|
, final_data as (
|
||||||
|
SELECT
|
||||||
|
occupied_seconds_per_day.space_id,
|
||||||
|
occupied_seconds_per_day.event_date,
|
||||||
|
total_occupied_seconds,
|
||||||
|
occupancy_percentage
|
||||||
|
FROM occupied_seconds_per_day
|
||||||
|
JOIN params p
|
||||||
|
ON p.space_id = occupied_seconds_per_day.space_id
|
||||||
|
AND p.event_date = occupied_seconds_per_day.event_date
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO public."space-daily-occupancy-duration" (
|
||||||
|
space_uuid,
|
||||||
|
event_date,
|
||||||
|
occupied_seconds,
|
||||||
|
occupancy_percentage
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
total_occupied_seconds,
|
||||||
|
occupancy_percentage
|
||||||
|
FROM final_data
|
||||||
|
ON CONFLICT (space_uuid, event_date) DO UPDATE
|
||||||
|
SET
|
||||||
|
occupied_seconds = EXCLUDED.occupied_seconds,
|
||||||
|
occupancy_percentage = EXCLUDED.occupancy_percentage;
|
||||||
@ -11,7 +11,7 @@ WITH params AS (
|
|||||||
A.count_motion_detected,
|
A.count_motion_detected,
|
||||||
A.count_presence_detected,
|
A.count_presence_detected,
|
||||||
A.count_total_presence_detected
|
A.count_total_presence_detected
|
||||||
FROM public."presence-sensor-daily-detection" AS A
|
FROM public."presence-sensor-daily-device-detection" AS A
|
||||||
JOIN params P ON TRUE
|
JOIN params P ON TRUE
|
||||||
WHERE A.device_uuid::text = ANY(P.device_ids)
|
WHERE A.device_uuid::text = ANY(P.device_ids)
|
||||||
AND (P.month IS NULL
|
AND (P.month IS NULL
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
TO_DATE(NULLIF($2, ''), 'YYYY') AS year,
|
||||||
|
string_to_array(NULLIF($4, ''), ',') AS device_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
A.device_uuid,
|
||||||
|
TO_CHAR(date_trunc('month', A.event_date), 'YYYY-MM') AS event_month,
|
||||||
|
SUM(A.count_motion_detected) AS total_motion_detected,
|
||||||
|
SUM(A.count_presence_detected) AS total_presence_detected,
|
||||||
|
SUM(A.count_total_presence_detected) AS total_overall_presence
|
||||||
|
FROM public."presence-sensor-daily-device-detection" AS A
|
||||||
|
JOIN params P ON TRUE
|
||||||
|
WHERE A.device_uuid::text = ANY(P.device_ids)
|
||||||
|
AND (
|
||||||
|
P.year IS NULL
|
||||||
|
OR date_trunc('year', A.event_date) = P.year
|
||||||
|
)
|
||||||
|
GROUP BY 1,2
|
||||||
|
ORDER BY 1,2;
|
||||||
@ -26,6 +26,7 @@ device_logs AS (
|
|||||||
ON device.uuid = "device-status-log".device_id
|
ON device.uuid = "device-status-log".device_id
|
||||||
LEFT JOIN product
|
LEFT JOIN product
|
||||||
ON product.uuid = device.product_device_uuid
|
ON product.uuid = device.product_device_uuid
|
||||||
|
JOIN params P ON TRUE
|
||||||
WHERE product.cat_name = 'hps'
|
WHERE product.cat_name = 'hps'
|
||||||
AND "device-status-log".code = 'presence_state'
|
AND "device-status-log".code = 'presence_state'
|
||||||
AND device.uuid::text = P.device_id
|
AND device.uuid::text = P.device_id
|
||||||
@ -93,7 +94,7 @@ daily_aggregates AS (
|
|||||||
GROUP BY device_id, event_date
|
GROUP BY device_id, event_date
|
||||||
)
|
)
|
||||||
|
|
||||||
INSERT INTO public."presence-sensor-daily-detection" (
|
INSERT INTO public."presence-sensor-daily-device-detection" (
|
||||||
device_uuid,
|
device_uuid,
|
||||||
event_date,
|
event_date,
|
||||||
count_motion_detected,
|
count_motion_detected,
|
||||||
@ -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;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
WITH params AS (
|
WITH params AS (
|
||||||
SELECT
|
SELECT
|
||||||
TO_DATE(NULLIF($2, ''), 'DD-MM-YYYY') AS start_date,
|
TO_DATE(NULLIF($1, ''), 'DD-MM-YYYY') AS start_date,
|
||||||
TO_DATE(NULLIF($3, ''), 'DD-MM-YYYY') AS end_date,
|
TO_DATE(NULLIF($2, ''), 'DD-MM-YYYY') AS end_date,
|
||||||
string_to_array(NULLIF($4, ''), ',') AS device_ids
|
string_to_array(NULLIF($3, ''), ',') AS device_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
SELECT TO_CHAR(A.date, 'MM-YYYY') AS month,
|
SELECT TO_CHAR(A.date, 'MM-YYYY') AS month,
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
$1::uuid AS space_id,
|
||||||
|
TO_DATE(NULLIF($2, ''), 'YYYY') AS event_year
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT psdsd.*
|
||||||
|
FROM public."presence-sensor-daily-space-detection" psdsd
|
||||||
|
JOIN params P ON true
|
||||||
|
WHERE psdsd.space_uuid = P.space_id
|
||||||
|
AND (
|
||||||
|
P.event_year IS NULL
|
||||||
|
OR TO_CHAR(psdsd.event_date, 'YYYY') = TO_CHAR(P.event_year, 'YYYY')
|
||||||
|
)
|
||||||
|
ORDER BY space_uuid, event_date
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
WITH device_logs AS (
|
||||||
|
SELECT
|
||||||
|
device.uuid AS device_id,
|
||||||
|
device.space_device_uuid AS space_id,
|
||||||
|
"device-status-log".event_time::timestamp AS event_time,
|
||||||
|
"device-status-log".value,
|
||||||
|
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'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 1. All 'none' → presence or motion
|
||||||
|
presence_transitions AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_time,
|
||||||
|
event_time::date AS event_date,
|
||||||
|
value
|
||||||
|
FROM device_logs
|
||||||
|
WHERE (value = 'motion' OR value = 'presence') AND prev_value = 'none'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 2. Cluster events per space_id within 30s
|
||||||
|
clustered_events AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_time,
|
||||||
|
event_date,
|
||||||
|
value,
|
||||||
|
SUM(new_cluster_flag) OVER (PARTITION BY space_id ORDER BY event_time) AS cluster_id
|
||||||
|
FROM (
|
||||||
|
SELECT *,
|
||||||
|
CASE
|
||||||
|
WHEN event_time - LAG(event_time) OVER (PARTITION BY space_id ORDER BY event_time) > INTERVAL '30 seconds'
|
||||||
|
THEN 1 ELSE 0
|
||||||
|
END AS new_cluster_flag
|
||||||
|
FROM presence_transitions
|
||||||
|
) marked
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 3. Determine dominant type (motion vs presence) per cluster
|
||||||
|
cluster_type AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
cluster_id,
|
||||||
|
COUNT(*) FILTER (WHERE value = 'motion') AS motion_count,
|
||||||
|
COUNT(*) FILTER (WHERE value = 'presence') AS presence_count,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(*) FILTER (WHERE value = 'motion') > COUNT(*) FILTER (WHERE value = 'presence') THEN 'motion'
|
||||||
|
ELSE 'presence'
|
||||||
|
END AS dominant_type
|
||||||
|
FROM clustered_events
|
||||||
|
GROUP BY space_id, event_date, cluster_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 4. Count clusters by dominant type
|
||||||
|
summary AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
COUNT(*) FILTER (WHERE dominant_type = 'motion') AS count_motion_detected,
|
||||||
|
COUNT(*) FILTER (WHERE dominant_type = 'presence') AS count_presence_detected,
|
||||||
|
COUNT(*) AS count_total_presence_detected
|
||||||
|
FROM cluster_type
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
)
|
||||||
|
|
||||||
|
-- 5. Output
|
||||||
|
, final_table as (
|
||||||
|
SELECT *
|
||||||
|
FROM summary
|
||||||
|
ORDER BY space_id, event_date)
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO public."presence-sensor-daily-space-detection" (
|
||||||
|
space_uuid,
|
||||||
|
event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
FROM final_table
|
||||||
|
ON CONFLICT (space_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,113 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date,
|
||||||
|
$2::uuid AS space_id
|
||||||
|
),
|
||||||
|
|
||||||
|
device_logs AS (
|
||||||
|
SELECT
|
||||||
|
device.uuid AS device_id,
|
||||||
|
device.space_device_uuid AS space_id,
|
||||||
|
"device-status-log".event_time::timestamp AS event_time,
|
||||||
|
"device-status-log".value,
|
||||||
|
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'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 1. All 'none' → presence or motion
|
||||||
|
presence_transitions AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_time,
|
||||||
|
event_time::date AS event_date,
|
||||||
|
value
|
||||||
|
FROM device_logs
|
||||||
|
WHERE (value = 'motion' OR value = 'presence') AND prev_value = 'none'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 2. Cluster events per space_id within 30s
|
||||||
|
clustered_events AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_time,
|
||||||
|
event_date,
|
||||||
|
value,
|
||||||
|
SUM(new_cluster_flag) OVER (PARTITION BY space_id ORDER BY event_time) AS cluster_id
|
||||||
|
FROM (
|
||||||
|
SELECT *,
|
||||||
|
CASE
|
||||||
|
WHEN event_time - LAG(event_time) OVER (PARTITION BY space_id ORDER BY event_time) > INTERVAL '30 seconds'
|
||||||
|
THEN 1 ELSE 0
|
||||||
|
END AS new_cluster_flag
|
||||||
|
FROM presence_transitions
|
||||||
|
) marked
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 3. Determine dominant type (motion vs presence) per cluster
|
||||||
|
cluster_type AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
cluster_id,
|
||||||
|
COUNT(*) FILTER (WHERE value = 'motion') AS motion_count,
|
||||||
|
COUNT(*) FILTER (WHERE value = 'presence') AS presence_count,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(*) FILTER (WHERE value = 'motion') > COUNT(*) FILTER (WHERE value = 'presence') THEN 'motion'
|
||||||
|
ELSE 'presence'
|
||||||
|
END AS dominant_type
|
||||||
|
FROM clustered_events
|
||||||
|
GROUP BY space_id, event_date, cluster_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 4. Count clusters by dominant type
|
||||||
|
summary AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
COUNT(*) FILTER (WHERE dominant_type = 'motion') AS count_motion_detected,
|
||||||
|
COUNT(*) FILTER (WHERE dominant_type = 'presence') AS count_presence_detected,
|
||||||
|
COUNT(*) AS count_total_presence_detected
|
||||||
|
FROM cluster_type
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
)
|
||||||
|
|
||||||
|
-- 5. Output
|
||||||
|
, final_table as (
|
||||||
|
SELECT summary.space_id,
|
||||||
|
summary.event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
FROM summary
|
||||||
|
JOIN params P ON true
|
||||||
|
where summary.space_id = P.space_id
|
||||||
|
and (P.event_date IS NULL or summary.event_date::date = P.event_date)
|
||||||
|
ORDER BY space_id, event_date)
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO public."presence-sensor-daily-space-detection" (
|
||||||
|
space_uuid,
|
||||||
|
event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
FROM final_table
|
||||||
|
ON CONFLICT (space_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;
|
||||||
@ -85,7 +85,7 @@ daily_aggregate AS (
|
|||||||
GROUP BY device_id, event_date
|
GROUP BY device_id, event_date
|
||||||
)
|
)
|
||||||
|
|
||||||
INSERT INTO public."presence-sensor-daily-detection" (
|
INSERT INTO public."presence-sensor-daily-device-detection" (
|
||||||
device_uuid,
|
device_uuid,
|
||||||
event_date,
|
event_date,
|
||||||
count_motion_detected,
|
count_motion_detected,
|
||||||
|
|||||||
@ -37,12 +37,13 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
|||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
|
|
||||||
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
|
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
|
||||||
|
import { OccupancyModule } from './occupancy/occupancy.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
load: config,
|
load: config,
|
||||||
}),
|
}),
|
||||||
/* ThrottlerModule.forRoot({
|
/* ThrottlerModule.forRoot({
|
||||||
throttlers: [{ ttl: 100000, limit: 30 }],
|
throttlers: [{ ttl: 100000, limit: 30 }],
|
||||||
}), */
|
}), */
|
||||||
WinstonModule.forRoot(winstonLoggerOptions),
|
WinstonModule.forRoot(winstonLoggerOptions),
|
||||||
@ -77,13 +78,14 @@ import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston
|
|||||||
DeviceCommissionModule,
|
DeviceCommissionModule,
|
||||||
PowerClampModule,
|
PowerClampModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
|
OccupancyModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: APP_INTERCEPTOR,
|
provide: APP_INTERCEPTOR,
|
||||||
useClass: LoggingInterceptor,
|
useClass: LoggingInterceptor,
|
||||||
},
|
},
|
||||||
/* {
|
/* {
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: ThrottlerGuard,
|
useClass: ThrottlerGuard,
|
||||||
}, */
|
}, */
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito
|
|||||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories/project.repository';
|
import { ProjectRepository } from '@app/common/modules/project/repositiories/project.repository';
|
||||||
import { AutomationSpaceController } from './controllers/automation-space.controller';
|
import { AutomationSpaceController } from './controllers/automation-space.controller';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
|
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
|
||||||
@ -35,6 +36,7 @@ import { AutomationSpaceController } from './controllers/automation-space.contro
|
|||||||
SceneDeviceRepository,
|
SceneDeviceRepository,
|
||||||
AutomationRepository,
|
AutomationRepository,
|
||||||
ProjectRepository,
|
ProjectRepository,
|
||||||
|
CommunityRepository,
|
||||||
],
|
],
|
||||||
exports: [AutomationService],
|
exports: [AutomationService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import {
|
|||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, SpaceRepositoryModule],
|
imports: [ConfigModule, SpaceRepositoryModule],
|
||||||
@ -55,6 +56,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -63,6 +63,7 @@ import {
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
|
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
|
||||||
@ -114,6 +115,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
],
|
],
|
||||||
exports: [CommunityService, SpacePermissionService],
|
exports: [CommunityService, SpacePermissionService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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,53 @@ export class CommunityService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async getAllDevicesByCommunity(
|
||||||
|
communityUuid: string,
|
||||||
|
): Promise<DeviceEntity[]> {
|
||||||
|
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[] = [];
|
||||||
|
const visitedSpaceUuids = new Set<string>();
|
||||||
|
|
||||||
|
// Recursive fetch function with visited check
|
||||||
|
const fetchSpaceDevices = async (space: SpaceEntity) => {
|
||||||
|
if (visitedSpaceUuids.has(space.uuid)) return;
|
||||||
|
visitedSpaceUuids.add(space.uuid);
|
||||||
|
|
||||||
|
if (space.devices?.length) {
|
||||||
|
allDevices.push(...space.devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (space.children?.length) {
|
||||||
|
for (const child of space.children) {
|
||||||
|
const fullChild = await this.spaceRepository.findOne({
|
||||||
|
where: { uuid: child.uuid },
|
||||||
|
relations: ['children', 'devices', 'devices.productDevice'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fullChild) {
|
||||||
|
await fetchSpaceDevices(fullChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const space of community.spaces) {
|
||||||
|
await fetchSpaceDevices(space);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDevices;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/device/controllers/device-space-community.controller.ts
Normal file
52
src/device/controllers/device-space-community.controller.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { DeviceService } from '../services/device.service';
|
||||||
|
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOperation,
|
||||||
|
ApiQuery,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||||
|
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||||
|
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||||
|
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||||
|
import { GetDevicesBySpaceOrCommunityDto } from '../dtos';
|
||||||
|
|
||||||
|
@ApiTags('Device Module')
|
||||||
|
@Controller({
|
||||||
|
version: EnableDisableStatusEnum.ENABLED,
|
||||||
|
path: ControllerRoute.DEVICE_SPACE_COMMUNITY.ROUTE,
|
||||||
|
})
|
||||||
|
export class DeviceSpaceOrCommunityController {
|
||||||
|
constructor(private readonly deviceService: DeviceService) {}
|
||||||
|
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(PermissionsGuard)
|
||||||
|
@Permissions('DEVICE_VIEW')
|
||||||
|
@Get('recursive-child')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
ControllerRoute.DEVICE_SPACE_COMMUNITY.ACTIONS
|
||||||
|
.GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_SUMMARY,
|
||||||
|
description:
|
||||||
|
ControllerRoute.DEVICE_SPACE_COMMUNITY.ACTIONS
|
||||||
|
.GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_DESCRIPTION,
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'spaceUuid',
|
||||||
|
description: 'UUID of the Space',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'communityUuid',
|
||||||
|
description: 'UUID of the Community',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
async getAllDevicesBySpaceOrCommunityWithChild(
|
||||||
|
@Query() query: GetDevicesBySpaceOrCommunityDto,
|
||||||
|
) {
|
||||||
|
return await this.deviceService.getAllDevicesBySpaceOrCommunityWithChild(
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,8 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito
|
|||||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
import { DeviceProjectController } from './controllers/device-project.controller';
|
import { DeviceProjectController } from './controllers/device-project.controller';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
import { DeviceSpaceOrCommunityController } from './controllers/device-space-community.controller';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
@ -30,7 +32,11 @@ import { DeviceProjectController } from './controllers/device-project.controller
|
|||||||
DeviceRepositoryModule,
|
DeviceRepositoryModule,
|
||||||
DeviceStatusFirebaseModule,
|
DeviceStatusFirebaseModule,
|
||||||
],
|
],
|
||||||
controllers: [DeviceController, DeviceProjectController],
|
controllers: [
|
||||||
|
DeviceController,
|
||||||
|
DeviceProjectController,
|
||||||
|
DeviceSpaceOrCommunityController,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
DeviceService,
|
DeviceService,
|
||||||
ProductRepository,
|
ProductRepository,
|
||||||
@ -46,6 +52,7 @@ import { DeviceProjectController } from './controllers/device-project.controller
|
|||||||
SceneRepository,
|
SceneRepository,
|
||||||
SceneDeviceRepository,
|
SceneDeviceRepository,
|
||||||
AutomationRepository,
|
AutomationRepository,
|
||||||
|
CommunityRepository,
|
||||||
],
|
],
|
||||||
exports: [DeviceService],
|
exports: [DeviceService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
|
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUUID,
|
||||||
|
ValidateIf,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class GetDeviceBySpaceUuidDto {
|
export class GetDeviceBySpaceUuidDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
@ -44,3 +51,24 @@ export class GetDoorLockDevices {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
public deviceType: DeviceTypeEnum;
|
public deviceType: DeviceTypeEnum;
|
||||||
}
|
}
|
||||||
|
export class GetDevicesBySpaceOrCommunityDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Device Product Type',
|
||||||
|
example: 'PC',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public productType: string;
|
||||||
|
@IsUUID('4', { message: 'Invalid space UUID format' })
|
||||||
|
@IsOptional()
|
||||||
|
spaceUuid?: string;
|
||||||
|
|
||||||
|
@IsUUID('4', { message: 'Invalid community 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
|
||||||
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
GetDeviceBySpaceUuidDto,
|
GetDeviceBySpaceUuidDto,
|
||||||
GetDeviceLogsDto,
|
GetDeviceLogsDto,
|
||||||
|
GetDevicesBySpaceOrCommunityDto,
|
||||||
GetDoorLockDevices,
|
GetDoorLockDevices,
|
||||||
} from '../dtos/get.device.dto';
|
} from '../dtos/get.device.dto';
|
||||||
import {
|
import {
|
||||||
@ -65,6 +66,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
|||||||
import { ProjectParam } from '../dtos';
|
import { ProjectParam } from '../dtos';
|
||||||
import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum';
|
import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum';
|
||||||
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
|
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeviceService {
|
export class DeviceService {
|
||||||
@ -80,6 +82,7 @@ export class DeviceService {
|
|||||||
private readonly sceneService: SceneService,
|
private readonly sceneService: SceneService,
|
||||||
private readonly tuyaService: TuyaService,
|
private readonly tuyaService: TuyaService,
|
||||||
private readonly projectRepository: ProjectRepository,
|
private readonly projectRepository: ProjectRepository,
|
||||||
|
private readonly communityRepository: CommunityRepository,
|
||||||
) {
|
) {
|
||||||
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
|
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
|
||||||
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
|
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
|
||||||
@ -1722,4 +1725,137 @@ export class DeviceService {
|
|||||||
statusCode: HttpStatus.OK,
|
statusCode: HttpStatus.OK,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async getAllDevicesBySpaceOrCommunityWithChild(
|
||||||
|
query: GetDevicesBySpaceOrCommunityDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
try {
|
||||||
|
const { spaceUuid, communityUuid, productType } = query;
|
||||||
|
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.getAllDevicesBySpace(spaceUuid)
|
||||||
|
: await this.getAllDevicesByCommunity(communityUuid);
|
||||||
|
|
||||||
|
if (!devices?.length) {
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
message: `No devices found for ${spaceUuid ? 'space' : 'community'}`,
|
||||||
|
data: [],
|
||||||
|
statusCode: HttpStatus.CREATED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const devicesFilterd = devices.filter(
|
||||||
|
(device) => device.productDevice?.prodType === productType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (devicesFilterd.length === 0) {
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
message: `No ${productType} devices found for ${spaceUuid ? 'space' : 'community'}`,
|
||||||
|
data: [],
|
||||||
|
statusCode: HttpStatus.CREATED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
message: `Devices fetched successfully`,
|
||||||
|
data: devicesFilterd,
|
||||||
|
statusCode: HttpStatus.OK,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Internal server error',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
async getAllDevicesByCommunity(
|
||||||
|
communityUuid: string,
|
||||||
|
): Promise<DeviceEntity[]> {
|
||||||
|
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[] = [];
|
||||||
|
const visitedSpaceUuids = new Set<string>();
|
||||||
|
|
||||||
|
// Recursive fetch function with visited check
|
||||||
|
const fetchSpaceDevices = async (space: SpaceEntity) => {
|
||||||
|
if (visitedSpaceUuids.has(space.uuid)) return;
|
||||||
|
visitedSpaceUuids.add(space.uuid);
|
||||||
|
|
||||||
|
if (space.devices?.length) {
|
||||||
|
allDevices.push(...space.devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (space.children?.length) {
|
||||||
|
for (const child of space.children) {
|
||||||
|
const fullChild = await this.spaceRepository.findOne({
|
||||||
|
where: { uuid: child.uuid },
|
||||||
|
relations: ['children', 'devices', 'devices.productDevice'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fullChild) {
|
||||||
|
await fetchSpaceDevices(fullChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const space of community.spaces) {
|
||||||
|
await fetchSpaceDevices(space);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDevices;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,8 @@ import {
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, DeviceRepositoryModule],
|
imports: [ConfigModule, DeviceRepositoryModule],
|
||||||
controllers: [DoorLockController],
|
controllers: [DoorLockController],
|
||||||
@ -52,6 +54,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
CommunityRepository,
|
||||||
],
|
],
|
||||||
exports: [DoorLockService],
|
exports: [DoorLockService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -25,6 +25,8 @@ import {
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, DeviceRepositoryModule],
|
imports: [ConfigModule, DeviceRepositoryModule],
|
||||||
controllers: [GroupController],
|
controllers: [GroupController],
|
||||||
@ -49,6 +51,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
CommunityRepository,
|
||||||
],
|
],
|
||||||
exports: [GroupService],
|
exports: [GroupService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -83,6 +83,7 @@ import {
|
|||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
|
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
|
||||||
@ -152,6 +153,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
],
|
],
|
||||||
exports: [InviteUserService],
|
exports: [InviteUserService],
|
||||||
})
|
})
|
||||||
|
|||||||
1
src/occupancy/controllers/index.ts
Normal file
1
src/occupancy/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './occupancy.controller';
|
||||||
71
src/occupancy/controllers/occupancy.controller.ts
Normal file
71
src/occupancy/controllers/occupancy.controller.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
} 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 { OccupancyService } from '../services/occupancy.service';
|
||||||
|
import {
|
||||||
|
GetOccupancyDurationBySpaceDto,
|
||||||
|
GetOccupancyHeatMapBySpaceDto,
|
||||||
|
} from '../dto/get-occupancy.dto';
|
||||||
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
|
import { SpaceParamsDto } from '../dto/occupancy-params.dto';
|
||||||
|
|
||||||
|
@ApiTags('Occupancy Module')
|
||||||
|
@Controller({
|
||||||
|
version: EnableDisableStatusEnum.ENABLED,
|
||||||
|
path: ControllerRoute.Occupancy.ROUTE,
|
||||||
|
})
|
||||||
|
export class OccupancyController {
|
||||||
|
constructor(private readonly occupancyService: OccupancyService) {}
|
||||||
|
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('heat-map/space/:spaceUuid')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_SUMMARY,
|
||||||
|
description:
|
||||||
|
ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_DESCRIPTION,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'spaceUuid',
|
||||||
|
description: 'UUID of the Space',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
async getOccupancyHeatMapDataBySpace(
|
||||||
|
@Param() params: SpaceParamsDto,
|
||||||
|
@Query() query: GetOccupancyHeatMapBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
return await this.occupancyService.getOccupancyHeatMapDataBySpace(
|
||||||
|
params,
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('duration/space/:spaceUuid')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_SUMMARY,
|
||||||
|
description:
|
||||||
|
ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_DESCRIPTION,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'spaceUuid',
|
||||||
|
description: 'UUID of the Space',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
async getOccupancyDurationDataBySpace(
|
||||||
|
@Param() params: SpaceParamsDto,
|
||||||
|
@Query() query: GetOccupancyDurationBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
return await this.occupancyService.getOccupancyDurationDataBySpace(
|
||||||
|
params,
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/occupancy/dto/get-occupancy.dto.ts
Normal file
27
src/occupancy/dto/get-occupancy.dto.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Matches, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class GetOccupancyHeatMapBySpaceDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Input year in YYYY format to filter the data',
|
||||||
|
example: '2025',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Matches(/^\d{4}$/, {
|
||||||
|
message: 'Year must be in YYYY format',
|
||||||
|
})
|
||||||
|
year: string;
|
||||||
|
}
|
||||||
|
export class GetOccupancyDurationBySpaceDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Month and year in format YYYY-MM',
|
||||||
|
example: '2025-03',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/, {
|
||||||
|
message: 'monthDate must be in YYYY-MM format',
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
monthDate: string;
|
||||||
|
}
|
||||||
7
src/occupancy/dto/occupancy-params.dto.ts
Normal file
7
src/occupancy/dto/occupancy-params.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
export class SpaceParamsDto {
|
||||||
|
@IsUUID('4', { message: 'Invalid UUID format' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
spaceUuid: string;
|
||||||
|
}
|
||||||
11
src/occupancy/occupancy.module.ts
Normal file
11
src/occupancy/occupancy.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { OccupancyController } from './controllers';
|
||||||
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from './services';
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
controllers: [OccupancyController],
|
||||||
|
providers: [OccupancyService, SqlLoaderService],
|
||||||
|
})
|
||||||
|
export class OccupancyModule {}
|
||||||
1
src/occupancy/services/index.ts
Normal file
1
src/occupancy/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './occupancy.service';
|
||||||
103
src/occupancy/services/occupancy.service.ts
Normal file
103
src/occupancy/services/occupancy.service.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
GetOccupancyDurationBySpaceDto,
|
||||||
|
GetOccupancyHeatMapBySpaceDto,
|
||||||
|
} from '../dto/get-occupancy.dto';
|
||||||
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
|
import { SpaceParamsDto } from '../dto/occupancy-params.dto';
|
||||||
|
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 { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OccupancyService {
|
||||||
|
constructor(
|
||||||
|
private readonly sqlLoader: SqlLoaderService,
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getOccupancyDurationDataBySpace(
|
||||||
|
params: SpaceParamsDto,
|
||||||
|
query: GetOccupancyDurationBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
const { monthDate } = query;
|
||||||
|
const { spaceUuid } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.executeProcedure(
|
||||||
|
'fact_daily_space_occupancy_duration',
|
||||||
|
'procedure_select_daily_space_occupancy_duration',
|
||||||
|
[spaceUuid, monthDate],
|
||||||
|
);
|
||||||
|
const formattedData = data.map((item) => ({
|
||||||
|
...item,
|
||||||
|
event_date: new Date(item.event_date).toLocaleDateString('en-CA'), // YYYY-MM-DD
|
||||||
|
}));
|
||||||
|
return this.buildResponse(
|
||||||
|
`Occupancy duration data fetched successfully for ${spaceUuid} space`,
|
||||||
|
formattedData,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch occupancy duration data', {
|
||||||
|
error,
|
||||||
|
spaceUuid,
|
||||||
|
});
|
||||||
|
throw new HttpException(
|
||||||
|
error.response?.message || 'Failed to fetch occupancy duration data',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getOccupancyHeatMapDataBySpace(
|
||||||
|
params: SpaceParamsDto,
|
||||||
|
query: GetOccupancyHeatMapBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
const { year } = query;
|
||||||
|
const { spaceUuid } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.executeProcedure(
|
||||||
|
'fact_space_occupancy_count',
|
||||||
|
'proceduce_select_fact_space_occupancy',
|
||||||
|
[spaceUuid, year],
|
||||||
|
);
|
||||||
|
const formattedData = data.map((item) => ({
|
||||||
|
...item,
|
||||||
|
event_date: new Date(item.event_date).toLocaleDateString('en-CA'), // YYYY-MM-DD
|
||||||
|
}));
|
||||||
|
return this.buildResponse(
|
||||||
|
`Occupancy heat map data fetched successfully for ${spaceUuid} space`,
|
||||||
|
formattedData,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch occupancy heat map data', {
|
||||||
|
error,
|
||||||
|
spaceUuid,
|
||||||
|
});
|
||||||
|
throw new HttpException(
|
||||||
|
error.response?.message || 'Failed to fetch occupancy heat map data',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildResponse(message: string, data: any[]) {
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
message,
|
||||||
|
data,
|
||||||
|
statusCode: HttpStatus.OK,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private async executeProcedure(
|
||||||
|
procedureFolderName: string,
|
||||||
|
procedureFileName: string,
|
||||||
|
params: (string | number | null)[],
|
||||||
|
): Promise<any[]> {
|
||||||
|
const query = this.loadQuery(procedureFolderName, procedureFileName);
|
||||||
|
return await this.dataSource.query(query, params);
|
||||||
|
}
|
||||||
|
private loadQuery(folderName: string, fileName: string): string {
|
||||||
|
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,13 @@
|
|||||||
|
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
|
||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsOptional, IsDateString, Matches } from 'class-validator';
|
import { Transform } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsOptional,
|
||||||
|
IsDateString,
|
||||||
|
Matches,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsBoolean,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class GetPowerClampDto {
|
export class GetPowerClampDto {
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
@ -33,3 +41,26 @@ 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;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: true,
|
||||||
|
description: 'Whether to group results by device or not',
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@Transform((value) => {
|
||||||
|
return value.obj.groupByDevice === BooleanValues.TRUE;
|
||||||
|
})
|
||||||
|
public groupByDevice?: boolean = false;
|
||||||
|
}
|
||||||
|
|||||||
@ -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,110 @@ 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';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.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,
|
||||||
|
OccupancyService,
|
||||||
],
|
],
|
||||||
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,119 @@ 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, groupByDevice } = 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 devices found for ${spaceUuid ? 'space' : 'community'}`,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter power clamp devices
|
||||||
|
const powerClampDevices = devices.filter(
|
||||||
|
(device) => device.productDevice?.prodType === ProductType.PC,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (powerClampDevices.length === 0) {
|
||||||
|
return this.buildResponse(
|
||||||
|
`No power clamp devices found for ${spaceUuid ? 'space' : 'community'}`,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedMonthDate = toMMYYYY(monthDate);
|
||||||
|
|
||||||
|
if (groupByDevice) {
|
||||||
|
// Handle per-device response
|
||||||
|
const deviceDataPromises = powerClampDevices.map(async (device) => {
|
||||||
|
const data = await this.executeProcedure(
|
||||||
|
'fact_daily_space_energy_consumed_procedure',
|
||||||
|
[formattedMonthDate, device.uuid],
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedData = data.map((item) => ({
|
||||||
|
...item,
|
||||||
|
date: new Date(item.date).toLocaleDateString('en-CA'), // YYYY-MM-DD
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filteredData = monthDate
|
||||||
|
? filterByMonth(formattedData, monthDate)
|
||||||
|
: formattedData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceUuid: device.uuid,
|
||||||
|
deviceName: device.name || `Power Clamp Device`,
|
||||||
|
data: filteredData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceDataResults = await Promise.all(deviceDataPromises);
|
||||||
|
|
||||||
|
return this.buildResponse(
|
||||||
|
`Power clamp data fetched successfully for ${spaceUuid ? 'space' : 'community'}`,
|
||||||
|
deviceDataResults,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Original behavior - all devices together
|
||||||
|
const deviceUuids = powerClampDevices.map((device) => device.uuid);
|
||||||
|
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 +229,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,4 +1,6 @@
|
|||||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||||
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
|
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -13,7 +15,6 @@ import {
|
|||||||
Res,
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Response } from 'express';
|
|
||||||
import {
|
import {
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
@ -21,11 +22,10 @@ import {
|
|||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { ProjectService } from '../services';
|
import { Response } from 'express';
|
||||||
import { CreateProjectDto, GetProjectParam } from '../dto';
|
import { CreateProjectDto, GetProjectParam } from '../dto';
|
||||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
import { ListProjectsDto } from '../dto/list-project.dto';
|
||||||
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
|
import { ProjectService } from '../services';
|
||||||
import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto';
|
|
||||||
|
|
||||||
@ApiTags('Project Module')
|
@ApiTags('Project Module')
|
||||||
@Controller({
|
@Controller({
|
||||||
@ -80,9 +80,7 @@ export class ProjectController {
|
|||||||
description: ControllerRoute.PROJECT.ACTIONS.LIST_PROJECTS_DESCRIPTION,
|
description: ControllerRoute.PROJECT.ACTIONS.LIST_PROJECTS_DESCRIPTION,
|
||||||
})
|
})
|
||||||
@Get()
|
@Get()
|
||||||
async list(
|
async list(@Query() query: ListProjectsDto): Promise<BaseResponseDto> {
|
||||||
@Query() query: PaginationRequestGetListDto,
|
|
||||||
): Promise<BaseResponseDto> {
|
|
||||||
return this.projectService.listProjects(query);
|
return this.projectService.listProjects(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
src/project/dto/list-project.dto.ts
Normal file
7
src/project/dto/list-project.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto';
|
||||||
|
import { PickType } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ListProjectsDto extends PickType(PaginationRequestGetListDto, [
|
||||||
|
'page',
|
||||||
|
'size',
|
||||||
|
]) {}
|
||||||
@ -67,6 +67,7 @@ import {
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
|
||||||
const CommandHandlers = [CreateOrphanSpaceHandler];
|
const CommandHandlers = [CreateOrphanSpaceHandler];
|
||||||
|
|
||||||
@ -124,6 +125,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
],
|
],
|
||||||
exports: [ProjectService, CqrsModule],
|
exports: [ProjectService, CqrsModule],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service
|
|||||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
|
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
|
||||||
@ -32,6 +33,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
|||||||
ProjectRepository,
|
ProjectRepository,
|
||||||
SceneDeviceRepository,
|
SceneDeviceRepository,
|
||||||
AutomationRepository,
|
AutomationRepository,
|
||||||
|
CommunityRepository,
|
||||||
],
|
],
|
||||||
exports: [SceneService],
|
exports: [SceneService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -65,6 +65,7 @@ import {
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
|
||||||
const CommandHandlers = [
|
const CommandHandlers = [
|
||||||
PropogateUpdateSpaceModelHandler,
|
PropogateUpdateSpaceModelHandler,
|
||||||
@ -124,6 +125,7 @@ const CommandHandlers = [
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
],
|
],
|
||||||
exports: [CqrsModule, SpaceModelService],
|
exports: [CqrsModule, SpaceModelService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
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 { SpaceDeviceService } from '../services';
|
import { SpaceDeviceService } from '../services';
|
||||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||||
|
import { GetDevicesBySpaceDto } from '../dtos/device.space.dto';
|
||||||
|
|
||||||
@ApiTags('Space Module')
|
@ApiTags('Space Module')
|
||||||
@Controller({
|
@Controller({
|
||||||
@ -26,7 +27,8 @@ export class SpaceDeviceController {
|
|||||||
@Get()
|
@Get()
|
||||||
async listDevicesInSpace(
|
async listDevicesInSpace(
|
||||||
@Param() params: GetSpaceParam,
|
@Param() params: GetSpaceParam,
|
||||||
|
@Query() query: GetDevicesBySpaceDto,
|
||||||
): Promise<BaseResponseDto> {
|
): Promise<BaseResponseDto> {
|
||||||
return await this.spaceDeviceService.listDevicesInSpace(params);
|
return await this.spaceDeviceService.listDevicesInSpace(params, query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/space/dtos/device.space.dto.ts
Normal file
13
src/space/dtos/device.space.dto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class GetDevicesBySpaceDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Device Product Type',
|
||||||
|
example: 'PC',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
public productType?: string;
|
||||||
|
}
|
||||||
@ -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,10 @@ 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';
|
||||||
|
import { GetDevicesBySpaceDto } from '../dtos/device.space.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceDeviceService {
|
export class SpaceDeviceService {
|
||||||
@ -16,11 +25,15 @@ 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,
|
||||||
|
query: GetDevicesBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
const { spaceUuid, communityUuid, projectUuid } = params;
|
const { spaceUuid, communityUuid, projectUuid } = params;
|
||||||
|
const { productType } = query;
|
||||||
try {
|
try {
|
||||||
// Validate community, project, and fetch space including devices in a single query
|
// Validate community, project, and fetch space including devices in a single query
|
||||||
const space = await this.validationService.fetchSpaceDevices(spaceUuid);
|
const space = await this.validationService.fetchSpaceDevices(spaceUuid);
|
||||||
@ -42,7 +55,23 @@ export class SpaceDeviceService {
|
|||||||
const detailedDevices = (await Promise.allSettled(deviceDetailsPromises))
|
const detailedDevices = (await Promise.allSettled(deviceDetailsPromises))
|
||||||
.filter((result) => result.status === 'fulfilled' && result.value)
|
.filter((result) => result.status === 'fulfilled' && result.value)
|
||||||
.map((result) => (result as PromiseFulfilledResult<any>).value);
|
.map((result) => (result as PromiseFulfilledResult<any>).value);
|
||||||
|
console.log('detailedDevices', detailedDevices);
|
||||||
|
if (productType) {
|
||||||
|
const devicesFilterd = detailedDevices.filter(
|
||||||
|
(device) => device.productType === productType,
|
||||||
|
);
|
||||||
|
if (devicesFilterd.length === 0) {
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
message: `No ${productType} devices found for ${spaceUuid ? 'space' : 'community'}`,
|
||||||
|
data: [],
|
||||||
|
statusCode: HttpStatus.CREATED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
data: devicesFilterd,
|
||||||
|
message: 'Successfully retrieved list of devices.',
|
||||||
|
});
|
||||||
|
}
|
||||||
return new SuccessResponseDto({
|
return new SuccessResponseDto({
|
||||||
data: detailedDevices,
|
data: detailedDevices,
|
||||||
message: 'Successfully retrieved list of devices.',
|
message: 'Successfully retrieved list of devices.',
|
||||||
@ -121,4 +150,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,6 +90,7 @@ import {
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
|
||||||
export const CommandHandlers = [DisableSpaceHandler];
|
export const CommandHandlers = [DisableSpaceHandler];
|
||||||
|
|
||||||
@ -166,6 +167,7 @@ export const CommandHandlers = [DisableSpaceHandler];
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
PowerClampService,
|
PowerClampService,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
],
|
],
|
||||||
exports: [SpaceService],
|
exports: [SpaceService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -29,6 +29,8 @@ import {
|
|||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
|
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
|
||||||
controllers: [VisitorPasswordController],
|
controllers: [VisitorPasswordController],
|
||||||
@ -55,6 +57,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
CommunityRepository,
|
||||||
],
|
],
|
||||||
exports: [VisitorPasswordService],
|
exports: [VisitorPasswordService],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user