mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-10 07:07:21 +00:00
Merge branch 'dev'
This commit is contained in:
@ -1,18 +1,18 @@
|
|||||||
|
import { PlatformType } from '@app/common/constants/platform-type.enum';
|
||||||
|
import { RoleType } from '@app/common/constants/role.type.enum';
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import { HelperHashService } from '../../helper/services';
|
|
||||||
import { UserRepository } from '../../../../common/src/modules/user/repositories';
|
|
||||||
import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository';
|
|
||||||
import { UserSessionEntity } from '../../../../common/src/modules/session/entities';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { OAuth2Client } from 'google-auth-library';
|
import { OAuth2Client } from 'google-auth-library';
|
||||||
import { PlatformType } from '@app/common/constants/platform-type.enum';
|
import { UserSessionEntity } from '../../../../common/src/modules/session/entities';
|
||||||
import { RoleType } from '@app/common/constants/role.type.enum';
|
import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository';
|
||||||
|
import { UserRepository } from '../../../../common/src/modules/user/repositories';
|
||||||
|
import { HelperHashService } from '../../helper/services';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -40,16 +40,17 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
relations: ['roleType', 'project'],
|
relations: ['roleType', 'project'],
|
||||||
});
|
});
|
||||||
if (
|
|
||||||
platform === PlatformType.WEB &&
|
|
||||||
(user.roleType.type === RoleType.SPACE_OWNER ||
|
|
||||||
user.roleType.type === RoleType.SPACE_MEMBER)
|
|
||||||
) {
|
|
||||||
throw new UnauthorizedException('Access denied for web platform');
|
|
||||||
}
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new BadRequestException('Invalid credentials');
|
throw new BadRequestException('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
platform === PlatformType.WEB &&
|
||||||
|
[RoleType.SPACE_OWNER, RoleType.SPACE_MEMBER].includes(
|
||||||
|
user.roleType.type as RoleType,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedException('Access denied for web platform');
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.isUserVerified) {
|
if (!user.isUserVerified) {
|
||||||
throw new BadRequestException('User is not verified');
|
throw new BadRequestException('User is not verified');
|
||||||
|
@ -465,7 +465,16 @@ export class ControllerRoute {
|
|||||||
'This endpoint retrieves the terms and conditions for the application.';
|
'This endpoint retrieves the terms and conditions for the application.';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
static WEATHER = class {
|
||||||
|
public static readonly ROUTE = 'weather';
|
||||||
|
|
||||||
|
static ACTIONS = class {
|
||||||
|
public static readonly FETCH_WEATHER_DETAILS_SUMMARY =
|
||||||
|
'Fetch Weather Details';
|
||||||
|
public static readonly FETCH_WEATHER_DETAILS_DESCRIPTION =
|
||||||
|
'This endpoint retrieves the current weather details for a specified location like temperature, humidity, etc.';
|
||||||
|
};
|
||||||
|
};
|
||||||
static PRIVACY_POLICY = class {
|
static PRIVACY_POLICY = class {
|
||||||
public static readonly ROUTE = 'policy';
|
public static readonly ROUTE = 'policy';
|
||||||
|
|
||||||
@ -500,6 +509,31 @@ export class ControllerRoute {
|
|||||||
'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.';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 AQI = class {
|
||||||
|
public static readonly ROUTE = 'aqi';
|
||||||
|
|
||||||
|
static ACTIONS = class {
|
||||||
|
public static readonly GET_AQI_RANGE_DATA_SUMMARY = 'Get AQI range data';
|
||||||
|
public static readonly GET_AQI_RANGE_DATA_DESCRIPTION =
|
||||||
|
'This endpoint retrieves the AQI (Air Quality Index) range data based on the provided parameters.';
|
||||||
|
|
||||||
|
public static readonly GET_AQI_DISTRIBUTION_DATA_SUMMARY =
|
||||||
|
'Get AQI distribution data';
|
||||||
|
public static readonly GET_AQI_DISTRIBUTION_DATA_DESCRIPTION =
|
||||||
|
'This endpoint retrieves the AQI (Air Quality Index) distribution data based on the provided parameters.';
|
||||||
|
};
|
||||||
|
};
|
||||||
static DEVICE = class {
|
static DEVICE = class {
|
||||||
public static readonly ROUTE = 'devices';
|
public static readonly ROUTE = 'devices';
|
||||||
|
|
||||||
@ -609,6 +643,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';
|
||||||
|
8
libs/common/src/constants/pollutants.enum.ts
Normal file
8
libs/common/src/constants/pollutants.enum.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export enum PollutantType {
|
||||||
|
AQI = 'aqi',
|
||||||
|
PM25 = 'pm25',
|
||||||
|
PM10 = 'pm10',
|
||||||
|
VOC = 'voc',
|
||||||
|
CO2 = 'co2',
|
||||||
|
CH2O = 'ch2o',
|
||||||
|
}
|
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',
|
||||||
|
}
|
@ -19,4 +19,5 @@ export enum ProductType {
|
|||||||
FOUR_S = '4S',
|
FOUR_S = '4S',
|
||||||
SIX_S = '6S',
|
SIX_S = '6S',
|
||||||
SOS = 'SOS',
|
SOS = 'SOS',
|
||||||
|
AQI = 'AQI',
|
||||||
}
|
}
|
||||||
|
@ -1,56 +1,64 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { SnakeNamingStrategy } from './strategies';
|
|
||||||
import { UserEntity } from '../modules/user/entities/user.entity';
|
|
||||||
import { UserSessionEntity } from '../modules/session/entities/session.entity';
|
|
||||||
import { UserOtpEntity } from '../modules/user/entities';
|
|
||||||
import { ProductEntity } from '../modules/product/entities';
|
|
||||||
import { DeviceEntity } from '../modules/device/entities';
|
import { DeviceEntity } from '../modules/device/entities';
|
||||||
import { PermissionTypeEntity } from '../modules/permission/entities';
|
import { PermissionTypeEntity } from '../modules/permission/entities';
|
||||||
|
import { ProductEntity } from '../modules/product/entities';
|
||||||
|
import { UserSessionEntity } from '../modules/session/entities/session.entity';
|
||||||
|
import { UserOtpEntity } from '../modules/user/entities';
|
||||||
|
import { UserEntity } from '../modules/user/entities/user.entity';
|
||||||
|
import { SnakeNamingStrategy } from './strategies';
|
||||||
|
|
||||||
import { UserSpaceEntity } from '../modules/user/entities';
|
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger';
|
||||||
import { DeviceUserPermissionEntity } from '../modules/device/entities';
|
import { createLogger } from 'winston';
|
||||||
import { RoleTypeEntity } from '../modules/role-type/entities';
|
import { winstonLoggerOptions } from '../logger/services/winston.logger';
|
||||||
import { UserNotificationEntity } from '../modules/user/entities';
|
import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities';
|
||||||
import { DeviceNotificationEntity } from '../modules/device/entities';
|
import { AutomationEntity } from '../modules/automation/entities';
|
||||||
import { RegionEntity } from '../modules/region/entities';
|
import { ClientEntity } from '../modules/client/entities';
|
||||||
import { TimeZoneEntity } from '../modules/timezone/entities';
|
|
||||||
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
|
|
||||||
import { CommunityEntity } from '../modules/community/entities';
|
import { CommunityEntity } from '../modules/community/entities';
|
||||||
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
|
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
|
||||||
import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
|
|
||||||
import { SceneDeviceEntity } from '../modules/scene-device/entities';
|
|
||||||
import { ProjectEntity } from '../modules/project/entities';
|
|
||||||
import {
|
import {
|
||||||
SpaceModelEntity,
|
DeviceNotificationEntity,
|
||||||
SubspaceModelEntity,
|
DeviceUserPermissionEntity,
|
||||||
TagModel,
|
} from '../modules/device/entities';
|
||||||
SpaceModelProductAllocationEntity,
|
|
||||||
SubspaceModelProductAllocationEntity,
|
|
||||||
} from '../modules/space-model/entities';
|
|
||||||
import {
|
import {
|
||||||
InviteUserEntity,
|
InviteUserEntity,
|
||||||
InviteUserSpaceEntity,
|
InviteUserSpaceEntity,
|
||||||
} from '../modules/Invite-user/entities';
|
} from '../modules/Invite-user/entities';
|
||||||
import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity';
|
|
||||||
import { AutomationEntity } from '../modules/automation/entities';
|
|
||||||
import { SpaceProductAllocationEntity } from '../modules/space/entities/space-product-allocation.entity';
|
|
||||||
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
|
|
||||||
import { SpaceEntity } from '../modules/space/entities/space.entity';
|
|
||||||
import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity';
|
|
||||||
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
|
|
||||||
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
|
|
||||||
import { TagEntity } from '../modules/space/entities/tag.entity';
|
|
||||||
import { ClientEntity } from '../modules/client/entities';
|
|
||||||
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger';
|
|
||||||
import { createLogger } from 'winston';
|
|
||||||
import { winstonLoggerOptions } from '../logger/services/winston.logger';
|
|
||||||
import {
|
import {
|
||||||
PowerClampDailyEntity,
|
PowerClampDailyEntity,
|
||||||
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';
|
||||||
|
import { ProjectEntity } from '../modules/project/entities';
|
||||||
|
import { RegionEntity } from '../modules/region/entities';
|
||||||
|
import { RoleTypeEntity } from '../modules/role-type/entities';
|
||||||
|
import { SceneDeviceEntity } from '../modules/scene-device/entities';
|
||||||
|
import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
|
||||||
|
import {
|
||||||
|
SpaceModelEntity,
|
||||||
|
SpaceModelProductAllocationEntity,
|
||||||
|
SubspaceModelEntity,
|
||||||
|
SubspaceModelProductAllocationEntity,
|
||||||
|
} from '../modules/space-model/entities';
|
||||||
|
import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity';
|
||||||
|
import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity';
|
||||||
|
import { SpaceProductAllocationEntity } from '../modules/space/entities/space-product-allocation.entity';
|
||||||
|
import { SpaceEntity } from '../modules/space/entities/space.entity';
|
||||||
|
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||||
|
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
|
||||||
|
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
|
||||||
|
import { TimeZoneEntity } from '../modules/timezone/entities';
|
||||||
|
import {
|
||||||
|
UserNotificationEntity,
|
||||||
|
UserSpaceEntity,
|
||||||
|
} from '../modules/user/entities';
|
||||||
|
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
|
||||||
|
import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
@ -81,7 +89,6 @@ import {
|
|||||||
SpaceEntity,
|
SpaceEntity,
|
||||||
SpaceLinkEntity,
|
SpaceLinkEntity,
|
||||||
SubspaceEntity,
|
SubspaceEntity,
|
||||||
TagEntity,
|
|
||||||
UserSpaceEntity,
|
UserSpaceEntity,
|
||||||
DeviceUserPermissionEntity,
|
DeviceUserPermissionEntity,
|
||||||
RoleTypeEntity,
|
RoleTypeEntity,
|
||||||
@ -96,7 +103,6 @@ import {
|
|||||||
SceneDeviceEntity,
|
SceneDeviceEntity,
|
||||||
SpaceModelEntity,
|
SpaceModelEntity,
|
||||||
SubspaceModelEntity,
|
SubspaceModelEntity,
|
||||||
TagModel,
|
|
||||||
InviteUserEntity,
|
InviteUserEntity,
|
||||||
InviteUserSpaceEntity,
|
InviteUserSpaceEntity,
|
||||||
InviteSpaceEntity,
|
InviteSpaceEntity,
|
||||||
@ -109,6 +115,10 @@ import {
|
|||||||
PowerClampHourlyEntity,
|
PowerClampHourlyEntity,
|
||||||
PowerClampDailyEntity,
|
PowerClampDailyEntity,
|
||||||
PowerClampMonthlyEntity,
|
PowerClampMonthlyEntity,
|
||||||
|
PresenceSensorDailyDeviceEntity,
|
||||||
|
PresenceSensorDailySpaceEntity,
|
||||||
|
AqiSpaceDailyPollutantStatsEntity,
|
||||||
|
SpaceDailyOccupancyDurationEntity,
|
||||||
],
|
],
|
||||||
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,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 { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@ -21,6 +23,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
AqiDataService,
|
||||||
],
|
],
|
||||||
controllers: [DeviceStatusFirebaseController],
|
controllers: [DeviceStatusFirebaseController],
|
||||||
exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository],
|
exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository],
|
||||||
|
@ -21,6 +21,9 @@ 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';
|
||||||
|
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeviceStatusFirebaseService {
|
export class DeviceStatusFirebaseService {
|
||||||
private tuya: TuyaContext;
|
private tuya: TuyaContext;
|
||||||
@ -29,6 +32,8 @@ 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 readonly aqiDataService: AqiDataService,
|
||||||
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 +245,30 @@ 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addDeviceStatusDto.productType === ProductType.AQI) {
|
||||||
|
await this.aqiDataService.updateAQISensorHistoricalData(
|
||||||
|
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();
|
||||||
|
47
libs/common/src/helper/services/aqi.data.service.ts
Normal file
47
libs/common/src/helper/services/aqi.data.service.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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 AqiDataService {
|
||||||
|
constructor(
|
||||||
|
private readonly sqlLoader: SqlLoaderService,
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
private readonly deviceRepository: DeviceRepository,
|
||||||
|
) {}
|
||||||
|
async updateAQISensorHistoricalData(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_aqi',
|
||||||
|
'proceduce_update_daily_space_aqi',
|
||||||
|
[dateStr, device.spaceDevice?.uuid],
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to insert or update aqi 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);
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
11
libs/common/src/modules/aqi/aqi.repository.module.ts
Normal file
11
libs/common/src/modules/aqi/aqi.repository.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AqiSpaceDailyPollutantStatsEntity } from './entities/aqi.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [],
|
||||||
|
exports: [],
|
||||||
|
controllers: [],
|
||||||
|
imports: [TypeOrmModule.forFeature([AqiSpaceDailyPollutantStatsEntity])],
|
||||||
|
})
|
||||||
|
export class AqiRepositoryModule {}
|
82
libs/common/src/modules/aqi/dtos/aqi.dto.ts
Normal file
82
libs/common/src/modules/aqi/dtos/aqi.dto.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class AqiSpaceDailyPollutantStatsDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public uuid: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
spaceUuid: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
eventDay: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
eventHour: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm1Min: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm1Avg: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm1Max: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm10Min: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm10Avg: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm10Max: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm25Min: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm25Avg: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
pm25Max: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
ch2oMin: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
ch2oAvg: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
ch2oMax: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
vocMin: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
vocAvg: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
vocMax: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
co2Min: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
co2Avg: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
co2Max: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
aqiMin: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
aqiAvg: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
aqiMax: number;
|
||||||
|
}
|
1
libs/common/src/modules/aqi/dtos/index.ts
Normal file
1
libs/common/src/modules/aqi/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './aqi.dto';
|
184
libs/common/src/modules/aqi/entities/aqi.entity.ts
Normal file
184
libs/common/src/modules/aqi/entities/aqi.entity.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
|
||||||
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
|
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||||
|
import { AqiSpaceDailyPollutantStatsDto } from '../dtos';
|
||||||
|
|
||||||
|
@Entity({ name: 'space-daily-pollutant-stats' })
|
||||||
|
@Unique(['spaceUuid', 'eventDate'])
|
||||||
|
export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity<AqiSpaceDailyPollutantStatsDto> {
|
||||||
|
@Column({ nullable: false })
|
||||||
|
public spaceUuid: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => SpaceEntity, (space) => space.aqiSensorDaily)
|
||||||
|
space: SpaceEntity;
|
||||||
|
|
||||||
|
@Column({ type: 'date', nullable: false })
|
||||||
|
public eventDate: Date;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public goodAqiPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public moderateAqiPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthySensitiveAqiPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthyAqiPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public veryUnhealthyAqiPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public hazardousAqiPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyAvgAqi?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMaxAqi?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMinAqi?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public goodPm25Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public moderatePm25Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthySensitivePm25Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthyPm25Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public veryUnhealthyPm25Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public hazardousPm25Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyAvgPm25?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMaxPm25?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMinPm25?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public goodPm10Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public moderatePm10Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthySensitivePm10Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthyPm10Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public veryUnhealthyPm10Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public hazardousPm10Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyAvgPm10?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMaxPm10?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMinPm10?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public goodVocPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public moderateVocPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthySensitiveVocPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthyVocPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public veryUnhealthyVocPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public hazardousVocPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyAvgVoc?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMaxVoc?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMinVoc?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public goodCo2Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public moderateCo2Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthySensitiveCo2Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthyCo2Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public veryUnhealthyCo2Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public hazardousCo2Percentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyAvgCo2?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMaxCo2?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMinCo2?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public goodCh2oPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public moderateCh2oPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthySensitiveCh2oPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public unhealthyCh2oPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public veryUnhealthyCh2oPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public hazardousCh2oPercentage?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyAvgCh2o?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMaxCh2o?: number;
|
||||||
|
|
||||||
|
@Column('float', { nullable: true })
|
||||||
|
public dailyMinCh2o?: number;
|
||||||
|
|
||||||
|
constructor(partial: Partial<AqiSpaceDailyPollutantStatsEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
1
libs/common/src/modules/aqi/entities/index.ts
Normal file
1
libs/common/src/modules/aqi/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './aqi.entity';
|
10
libs/common/src/modules/aqi/repositories/aqi.repository.ts
Normal file
10
libs/common/src/modules/aqi/repositories/aqi.repository.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AqiSpaceDailyPollutantStatsEntity } from '../entities';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AqiSpaceDailyPollutantStatsRepository extends Repository<AqiSpaceDailyPollutantStatsEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(AqiSpaceDailyPollutantStatsEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
1
libs/common/src/modules/aqi/repositories/index.ts
Normal file
1
libs/common/src/modules/aqi/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './aqi.repository';
|
@ -2,15 +2,15 @@ import { SourceType } from '@app/common/constants/source-type.enum';
|
|||||||
import { Entity, Column, PrimaryColumn, Unique } from 'typeorm';
|
import { Entity, Column, PrimaryColumn, Unique } from 'typeorm';
|
||||||
|
|
||||||
@Entity('device-status-log')
|
@Entity('device-status-log')
|
||||||
@Unique('event_time_idx', ['eventTime'])
|
@Unique('event_time_idx', ['eventTime', 'deviceId', 'code', 'value'])
|
||||||
export class DeviceStatusLogEntity {
|
export class DeviceStatusLogEntity {
|
||||||
@Column({ type: 'int', generated: true, unsigned: true })
|
@PrimaryColumn({ type: 'int', generated: true, unsigned: true })
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column({ type: 'text' })
|
@Column({ type: 'text' })
|
||||||
eventId: string;
|
eventId: string;
|
||||||
|
|
||||||
@PrimaryColumn({ type: 'timestamptz' })
|
@Column({ type: 'timestamptz' })
|
||||||
eventTime: Date;
|
eventTime: Date;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
|
@ -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'])
|
||||||
@ -77,11 +78,13 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
|
|||||||
@OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {})
|
@OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {})
|
||||||
sceneDevices: SceneDeviceEntity[];
|
sceneDevices: SceneDeviceEntity[];
|
||||||
|
|
||||||
@OneToMany(() => NewTagEntity, (tag) => tag.devices)
|
@ManyToOne(() => NewTagEntity, (tag) => tag.devices)
|
||||||
// @JoinTable({ name: 'device_tags' })
|
@JoinColumn({ name: 'tag_uuid' })
|
||||||
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/occupancy/dtos/index.ts
Normal file
1
libs/common/src/modules/occupancy/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './occupancy.dto';
|
23
libs/common/src/modules/occupancy/dtos/occupancy.dto.ts
Normal file
23
libs/common/src/modules/occupancy/dtos/occupancy.dto.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class SpaceDailyOccupancyDurationDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public uuid: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public spaceUuid: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public eventDate: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public occupancyPercentage: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public occupiedSeconds: number;
|
||||||
|
}
|
1
libs/common/src/modules/occupancy/entities/index.ts
Normal file
1
libs/common/src/modules/occupancy/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './occupancy.entity';
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
|
||||||
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
|
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||||
|
import { SpaceDailyOccupancyDurationDto } from '../dtos';
|
||||||
|
|
||||||
|
@Entity({ name: 'space-daily-occupancy-duration' })
|
||||||
|
@Unique(['spaceUuid', 'eventDate'])
|
||||||
|
export class SpaceDailyOccupancyDurationEntity extends AbstractEntity<SpaceDailyOccupancyDurationDto> {
|
||||||
|
@Column({ nullable: false })
|
||||||
|
public spaceUuid: string;
|
||||||
|
|
||||||
|
@Column({ nullable: false, type: 'date' })
|
||||||
|
public eventDate: string;
|
||||||
|
|
||||||
|
public CountTotalPresenceDetected: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => SpaceEntity, (space) => space.presenceSensorDaily)
|
||||||
|
space: SpaceEntity;
|
||||||
|
|
||||||
|
@Column({ type: 'int' })
|
||||||
|
occupancyPercentage: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
occupiedSeconds?: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
deviceCount?: number;
|
||||||
|
constructor(partial: Partial<SpaceDailyOccupancyDurationEntity>) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, partial);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { SpaceDailyOccupancyDurationEntity } from './entities/occupancy.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [],
|
||||||
|
exports: [],
|
||||||
|
controllers: [],
|
||||||
|
imports: [TypeOrmModule.forFeature([SpaceDailyOccupancyDurationEntity])],
|
||||||
|
})
|
||||||
|
export class SpaceDailyOccupancyDurationRepositoryModule {}
|
1
libs/common/src/modules/occupancy/repositories/index.ts
Normal file
1
libs/common/src/modules/occupancy/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './occupancy.repository';
|
@ -0,0 +1,10 @@
|
|||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { SpaceDailyOccupancyDurationEntity } from '../entities/occupancy.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SpaceDailyOccupancyDurationEntityRepository extends Repository<SpaceDailyOccupancyDurationEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(SpaceDailyOccupancyDurationEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
@ -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,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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,7 @@
|
|||||||
import { Column, Entity, OneToMany } from 'typeorm';
|
import { Column, Entity, OneToMany } from 'typeorm';
|
||||||
import { ProductDto } from '../dtos';
|
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
import { DeviceEntity } from '../../device/entities';
|
import { DeviceEntity } from '../../device/entities';
|
||||||
import { TagModel } from '../../space-model';
|
import { ProductDto } from '../dtos';
|
||||||
import { TagEntity } from '../../space/entities/tag.entity';
|
|
||||||
import { NewTagEntity } from '../../tag/entities';
|
|
||||||
@Entity({ name: 'product' })
|
@Entity({ name: 'product' })
|
||||||
export class ProductEntity extends AbstractEntity<ProductDto> {
|
export class ProductEntity extends AbstractEntity<ProductDto> {
|
||||||
@Column({
|
@Column({
|
||||||
@ -28,15 +25,6 @@ export class ProductEntity extends AbstractEntity<ProductDto> {
|
|||||||
})
|
})
|
||||||
public prodType: string;
|
public prodType: string;
|
||||||
|
|
||||||
@OneToMany(() => NewTagEntity, (tag) => tag.product, { cascade: true })
|
|
||||||
public newTags: NewTagEntity[];
|
|
||||||
|
|
||||||
@OneToMany(() => TagModel, (tag) => tag.product)
|
|
||||||
tagModels: TagModel[];
|
|
||||||
|
|
||||||
@OneToMany(() => TagEntity, (tag) => tag.product)
|
|
||||||
tags: TagEntity[];
|
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => DeviceEntity,
|
() => DeviceEntity,
|
||||||
(devicesProductEntity) => devicesProductEntity.productDevice,
|
(devicesProductEntity) => devicesProductEntity.productDevice,
|
||||||
|
@ -12,6 +12,7 @@ export class RoleTypeEntity extends AbstractEntity<RoleTypeDto> {
|
|||||||
nullable: false,
|
nullable: false,
|
||||||
enum: Object.values(RoleType),
|
enum: Object.values(RoleType),
|
||||||
})
|
})
|
||||||
|
// why is this ts-type string not enum?
|
||||||
type: string;
|
type: string;
|
||||||
@OneToMany(() => UserEntity, (inviteUser) => inviteUser.roleType, {
|
@OneToMany(() => UserEntity, (inviteUser) => inviteUser.roleType, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class TagModelDto {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
public uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
public name: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
public productUuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
spaceModelUuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
subspaceModelUuid: string;
|
|
||||||
}
|
|
@ -1,4 +1,3 @@
|
|||||||
|
export * from './space-model-product-allocation.entity';
|
||||||
export * from './space-model.entity';
|
export * from './space-model.entity';
|
||||||
export * from './subspace-model';
|
export * from './subspace-model';
|
||||||
export * from './tag-model.entity';
|
|
||||||
export * from './space-model-product-allocation.entity';
|
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
import {
|
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm';
|
||||||
Entity,
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
Column,
|
|
||||||
ManyToOne,
|
|
||||||
ManyToMany,
|
|
||||||
JoinTable,
|
|
||||||
OneToMany,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { SpaceModelEntity } from './space-model.entity';
|
|
||||||
import { NewTagEntity } from '../../tag/entities/tag.entity';
|
|
||||||
import { ProductEntity } from '../../product/entities/product.entity';
|
import { ProductEntity } from '../../product/entities/product.entity';
|
||||||
import { SpaceProductAllocationEntity } from '../../space/entities/space-product-allocation.entity';
|
import { SpaceProductAllocationEntity } from '../../space/entities/space-product-allocation.entity';
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
import { NewTagEntity } from '../../tag/entities/tag.entity';
|
||||||
|
import { SpaceModelEntity } from './space-model.entity';
|
||||||
|
|
||||||
@Entity({ name: 'space_model_product_allocation' })
|
@Entity({ name: 'space_model_product_allocation' })
|
||||||
|
@Unique(['spaceModel', 'product', 'tag'])
|
||||||
export class SpaceModelProductAllocationEntity extends AbstractEntity<SpaceModelProductAllocationEntity> {
|
export class SpaceModelProductAllocationEntity extends AbstractEntity<SpaceModelProductAllocationEntity> {
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
@ -31,9 +25,8 @@ export class SpaceModelProductAllocationEntity extends AbstractEntity<SpaceModel
|
|||||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||||
public product: ProductEntity;
|
public product: ProductEntity;
|
||||||
|
|
||||||
@ManyToMany(() => NewTagEntity, { cascade: true, onDelete: 'CASCADE' })
|
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||||
@JoinTable({ name: 'space_model_product_tags' })
|
public tag: NewTagEntity;
|
||||||
public tags: NewTagEntity[];
|
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => SpaceProductAllocationEntity,
|
() => SpaceProductAllocationEntity,
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { Entity, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
|
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
import { SpaceModelDto } from '../dtos';
|
|
||||||
import { SubspaceModelEntity } from './subspace-model';
|
|
||||||
import { ProjectEntity } from '../../project/entities';
|
import { ProjectEntity } from '../../project/entities';
|
||||||
import { TagModel } from './tag-model.entity';
|
|
||||||
import { SpaceModelProductAllocationEntity } from './space-model-product-allocation.entity';
|
|
||||||
import { SpaceEntity } from '../../space/entities/space.entity';
|
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||||
|
import { SpaceModelDto } from '../dtos';
|
||||||
|
import { SpaceModelProductAllocationEntity } from './space-model-product-allocation.entity';
|
||||||
|
import { SubspaceModelEntity } from './subspace-model';
|
||||||
|
|
||||||
@Entity({ name: 'space-model' })
|
@Entity({ name: 'space-model' })
|
||||||
export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> {
|
export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> {
|
||||||
@ -49,9 +48,6 @@ export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> {
|
|||||||
})
|
})
|
||||||
public spaces: SpaceEntity[];
|
public spaces: SpaceEntity[];
|
||||||
|
|
||||||
@OneToMany(() => TagModel, (tag) => tag.spaceModel)
|
|
||||||
tags: TagModel[];
|
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => SpaceModelProductAllocationEntity,
|
() => SpaceModelProductAllocationEntity,
|
||||||
(allocation) => allocation.spaceModel,
|
(allocation) => allocation.spaceModel,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
|
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
|
||||||
import { SubspaceModelEntity } from './subspace-model.entity';
|
|
||||||
import { ProductEntity } from '@app/common/modules/product/entities/product.entity';
|
import { ProductEntity } from '@app/common/modules/product/entities/product.entity';
|
||||||
import { NewTagEntity } from '@app/common/modules/tag/entities/tag.entity';
|
import { NewTagEntity } from '@app/common/modules/tag/entities/tag.entity';
|
||||||
|
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
|
||||||
import { SubspaceModelProductAllocationDto } from '../../dtos/subspace-model/subspace-model-product-allocation.dto';
|
import { SubspaceModelProductAllocationDto } from '../../dtos/subspace-model/subspace-model-product-allocation.dto';
|
||||||
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
|
import { SubspaceModelEntity } from './subspace-model.entity';
|
||||||
|
|
||||||
@Entity({ name: 'subspace_model_product_allocation' })
|
@Entity({ name: 'subspace_model_product_allocation' })
|
||||||
|
@Unique(['subspaceModel', 'product', 'tag'])
|
||||||
export class SubspaceModelProductAllocationEntity extends AbstractEntity<SubspaceModelProductAllocationDto> {
|
export class SubspaceModelProductAllocationEntity extends AbstractEntity<SubspaceModelProductAllocationDto> {
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
@ -27,12 +28,8 @@ export class SubspaceModelProductAllocationEntity extends AbstractEntity<Subspac
|
|||||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||||
public product: ProductEntity;
|
public product: ProductEntity;
|
||||||
|
|
||||||
@ManyToMany(() => NewTagEntity, (tag) => tag.subspaceModelAllocations, {
|
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||||
cascade: true,
|
public tag: NewTagEntity;
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinTable({ name: 'subspace_model_product_tags' })
|
|
||||||
public tags: NewTagEntity[];
|
|
||||||
|
|
||||||
constructor(partial: Partial<SubspaceModelProductAllocationEntity>) {
|
constructor(partial: Partial<SubspaceModelProductAllocationEntity>) {
|
||||||
super();
|
super();
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
|
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
|
||||||
|
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
||||||
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
|
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
|
||||||
import { SubSpaceModelDto } from '../../dtos';
|
import { SubSpaceModelDto } from '../../dtos';
|
||||||
import { SpaceModelEntity } from '../space-model.entity';
|
import { SpaceModelEntity } from '../space-model.entity';
|
||||||
import { TagModel } from '../tag-model.entity';
|
|
||||||
import { SubspaceModelProductAllocationEntity } from './subspace-model-product-allocation.entity';
|
import { SubspaceModelProductAllocationEntity } from './subspace-model-product-allocation.entity';
|
||||||
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
|
||||||
|
|
||||||
@Entity({ name: 'subspace-model' })
|
@Entity({ name: 'subspace-model' })
|
||||||
export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
|
export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
|
||||||
@ -41,9 +40,6 @@ export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
|
|||||||
})
|
})
|
||||||
public disabled: boolean;
|
public disabled: boolean;
|
||||||
|
|
||||||
@OneToMany(() => TagModel, (tag) => tag.subspaceModel)
|
|
||||||
tags: TagModel[];
|
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => SubspaceModelProductAllocationEntity,
|
() => SubspaceModelProductAllocationEntity,
|
||||||
(allocation) => allocation.subspaceModel,
|
(allocation) => allocation.subspaceModel,
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
|
||||||
import { TagModelDto } from '../dtos/tag-model.dto';
|
|
||||||
import { SpaceModelEntity } from './space-model.entity';
|
|
||||||
import { SubspaceModelEntity } from './subspace-model';
|
|
||||||
import { ProductEntity } from '../../product/entities';
|
|
||||||
import { TagEntity } from '../../space/entities/tag.entity';
|
|
||||||
|
|
||||||
@Entity({ name: 'tag_model' })
|
|
||||||
export class TagModel extends AbstractEntity<TagModelDto> {
|
|
||||||
@Column({ type: 'varchar', length: 255 })
|
|
||||||
tag: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => ProductEntity, (product) => product.tagModels, {
|
|
||||||
nullable: false,
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'product_id' })
|
|
||||||
product: ProductEntity;
|
|
||||||
|
|
||||||
@ManyToOne(() => SpaceModelEntity, (space) => space.tags, { nullable: true })
|
|
||||||
@JoinColumn({ name: 'space_model_id' })
|
|
||||||
spaceModel: SpaceModelEntity;
|
|
||||||
|
|
||||||
@ManyToOne(() => SubspaceModelEntity, (subspace) => subspace.tags, {
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'subspace_model_id' })
|
|
||||||
subspaceModel: SubspaceModelEntity;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
nullable: false,
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
public disabled: boolean;
|
|
||||||
|
|
||||||
@OneToMany(() => TagEntity, (tag) => tag.model)
|
|
||||||
tags: TagEntity[];
|
|
||||||
}
|
|
@ -1,11 +1,10 @@
|
|||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
SpaceModelEntity,
|
SpaceModelEntity,
|
||||||
SpaceModelProductAllocationEntity,
|
SpaceModelProductAllocationEntity,
|
||||||
SubspaceModelEntity,
|
SubspaceModelEntity,
|
||||||
SubspaceModelProductAllocationEntity,
|
SubspaceModelProductAllocationEntity,
|
||||||
TagModel,
|
|
||||||
} from '../entities';
|
} from '../entities';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -21,13 +20,6 @@ export class SubspaceModelRepository extends Repository<SubspaceModelEntity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TagModelRepository extends Repository<TagModel> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(TagModel, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceModelProductAllocationRepoitory extends Repository<SpaceModelProductAllocationEntity> {
|
export class SpaceModelProductAllocationRepoitory extends Repository<SpaceModelProductAllocationEntity> {
|
||||||
constructor(private dataSource: DataSource) {
|
constructor(private dataSource: DataSource) {
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { SpaceModelEntity, SubspaceModelEntity, TagModel } from './entities';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { SpaceModelEntity, SubspaceModelEntity } from './entities';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [],
|
exports: [],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
imports: [
|
imports: [TypeOrmModule.forFeature([SpaceModelEntity, SubspaceModelEntity])],
|
||||||
TypeOrmModule.forFeature([SpaceModelEntity, SubspaceModelEntity, TagModel]),
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class SpaceModelRepositoryModule {}
|
export class SpaceModelRepositoryModule {}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
|
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
|
||||||
import { SpaceEntity } from './space.entity';
|
|
||||||
import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity';
|
|
||||||
import { ProductEntity } from '../../product/entities/product.entity';
|
|
||||||
import { NewTagEntity } from '../../tag/entities/tag.entity';
|
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
|
import { ProductEntity } from '../../product/entities/product.entity';
|
||||||
|
import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity';
|
||||||
|
import { NewTagEntity } from '../../tag/entities/tag.entity';
|
||||||
import { SpaceProductAllocationDto } from '../dtos/space-product-allocation.dto';
|
import { SpaceProductAllocationDto } from '../dtos/space-product-allocation.dto';
|
||||||
|
import { SpaceEntity } from './space.entity';
|
||||||
|
|
||||||
@Entity({ name: 'space_product_allocation' })
|
@Entity({ name: 'space_product_allocation' })
|
||||||
|
@Unique(['space', 'product', 'tag'], {})
|
||||||
export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAllocationDto> {
|
export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAllocationDto> {
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
@ -30,9 +31,8 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
|
|||||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||||
public product: ProductEntity;
|
public product: ProductEntity;
|
||||||
|
|
||||||
@ManyToMany(() => NewTagEntity)
|
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||||
@JoinTable({ name: 'space_product_tags' })
|
public tag: NewTagEntity;
|
||||||
public tags: NewTagEntity[];
|
|
||||||
|
|
||||||
constructor(partial: Partial<SpaceProductAllocationEntity>) {
|
constructor(partial: Partial<SpaceProductAllocationEntity>) {
|
||||||
super();
|
super();
|
||||||
|
@ -10,6 +10,9 @@ 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';
|
||||||
|
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
|
||||||
|
import { SpaceDailyOccupancyDurationEntity } from '../../occupancy/entities';
|
||||||
|
|
||||||
@Entity({ name: 'space' })
|
@Entity({ name: 'space' })
|
||||||
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||||
@ -111,6 +114,18 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
|||||||
)
|
)
|
||||||
public productAllocations: SpaceProductAllocationEntity[];
|
public productAllocations: SpaceProductAllocationEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => PresenceSensorDailySpaceEntity, (sensor) => sensor.space)
|
||||||
|
presenceSensorDaily: PresenceSensorDailySpaceEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => AqiSpaceDailyPollutantStatsEntity, (aqi) => aqi.space)
|
||||||
|
aqiSensorDaily: AqiSpaceDailyPollutantStatsEntity[];
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => SpaceDailyOccupancyDurationEntity,
|
||||||
|
(occupancy) => occupancy.space,
|
||||||
|
)
|
||||||
|
occupancyDaily: SpaceDailyOccupancyDurationEntity[];
|
||||||
|
|
||||||
constructor(partial: Partial<SpaceEntity>) {
|
constructor(partial: Partial<SpaceEntity>) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, partial);
|
Object.assign(this, partial);
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
import {
|
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
|
||||||
Entity,
|
|
||||||
Column,
|
|
||||||
ManyToOne,
|
|
||||||
ManyToMany,
|
|
||||||
JoinTable,
|
|
||||||
Unique,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { SubspaceEntity } from './subspace.entity';
|
|
||||||
import { ProductEntity } from '@app/common/modules/product/entities';
|
import { ProductEntity } from '@app/common/modules/product/entities';
|
||||||
import { SubspaceModelProductAllocationEntity } from '@app/common/modules/space-model';
|
import { SubspaceModelProductAllocationEntity } from '@app/common/modules/space-model';
|
||||||
import { NewTagEntity } from '@app/common/modules/tag/entities/tag.entity';
|
import { NewTagEntity } from '@app/common/modules/tag/entities/tag.entity';
|
||||||
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
|
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
|
||||||
import { SubspaceProductAllocationDto } from '../../dtos/subspace-product-allocation.dto';
|
import { SubspaceProductAllocationDto } from '../../dtos/subspace-product-allocation.dto';
|
||||||
|
import { SubspaceEntity } from './subspace.entity';
|
||||||
|
|
||||||
@Entity({ name: 'subspace_product_allocation' })
|
@Entity({ name: 'subspace_product_allocation' })
|
||||||
@Unique(['subspace', 'product'])
|
@Unique(['subspace', 'product', 'tag'])
|
||||||
export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProductAllocationDto> {
|
export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProductAllocationDto> {
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
@ -38,9 +31,8 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
|
|||||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||||
public product: ProductEntity;
|
public product: ProductEntity;
|
||||||
|
|
||||||
@ManyToMany(() => NewTagEntity)
|
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||||
@JoinTable({ name: 'subspace_product_tags' })
|
public tag: NewTagEntity;
|
||||||
public tags: NewTagEntity[];
|
|
||||||
|
|
||||||
constructor(partial: Partial<SubspaceProductAllocationEntity>) {
|
constructor(partial: Partial<SubspaceProductAllocationEntity>) {
|
||||||
super();
|
super();
|
||||||
|
@ -4,7 +4,6 @@ import { SubspaceModelEntity } from '@app/common/modules/space-model';
|
|||||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||||
import { SubspaceDto } from '../../dtos';
|
import { SubspaceDto } from '../../dtos';
|
||||||
import { SpaceEntity } from '../space.entity';
|
import { SpaceEntity } from '../space.entity';
|
||||||
import { TagEntity } from '../tag.entity';
|
|
||||||
import { SubspaceProductAllocationEntity } from './subspace-product-allocation.entity';
|
import { SubspaceProductAllocationEntity } from './subspace-product-allocation.entity';
|
||||||
|
|
||||||
@Entity({ name: 'subspace' })
|
@Entity({ name: 'subspace' })
|
||||||
@ -43,9 +42,6 @@ export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
|
|||||||
})
|
})
|
||||||
subSpaceModel?: SubspaceModelEntity;
|
subSpaceModel?: SubspaceModelEntity;
|
||||||
|
|
||||||
@OneToMany(() => TagEntity, (tag) => tag.subspace)
|
|
||||||
tags: TagEntity[];
|
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => SubspaceProductAllocationEntity,
|
() => SubspaceProductAllocationEntity,
|
||||||
(allocation) => allocation.subspace,
|
(allocation) => allocation.subspace,
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import { Entity, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm';
|
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
|
||||||
import { ProductEntity } from '../../product/entities';
|
|
||||||
import { TagDto } from '../dtos';
|
|
||||||
import { TagModel } from '../../space-model/entities/tag-model.entity';
|
|
||||||
import { DeviceEntity } from '../../device/entities';
|
|
||||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
|
||||||
|
|
||||||
@Entity({ name: 'tag' })
|
|
||||||
export class TagEntity extends AbstractEntity<TagDto> {
|
|
||||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
|
||||||
tag: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => TagModel, (model) => model.tags, {
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
model: TagModel;
|
|
||||||
|
|
||||||
@ManyToOne(() => ProductEntity, (product) => product.tags, {
|
|
||||||
nullable: false,
|
|
||||||
})
|
|
||||||
product: ProductEntity;
|
|
||||||
|
|
||||||
@ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, {
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'subspace_id' })
|
|
||||||
subspace: SubspaceEntity;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
nullable: false,
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
public disabled: boolean;
|
|
||||||
|
|
||||||
@OneToOne(() => DeviceEntity, (device) => device.tag, {
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'device_id' })
|
|
||||||
device: DeviceEntity;
|
|
||||||
}
|
|
@ -1,10 +1,9 @@
|
|||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
import { InviteSpaceEntity } from '../entities/invite-space.entity';
|
import { InviteSpaceEntity } from '../entities/invite-space.entity';
|
||||||
import { SpaceLinkEntity } from '../entities/space-link.entity';
|
import { SpaceLinkEntity } from '../entities/space-link.entity';
|
||||||
import { SpaceEntity } from '../entities/space.entity';
|
|
||||||
import { TagEntity } from '../entities/tag.entity';
|
|
||||||
import { SpaceProductAllocationEntity } from '../entities/space-product-allocation.entity';
|
import { SpaceProductAllocationEntity } from '../entities/space-product-allocation.entity';
|
||||||
|
import { SpaceEntity } from '../entities/space.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceRepository extends Repository<SpaceEntity> {
|
export class SpaceRepository extends Repository<SpaceEntity> {
|
||||||
@ -20,13 +19,6 @@ export class SpaceLinkRepository extends Repository<SpaceLinkEntity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TagRepository extends Repository<TagEntity> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(TagEntity, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InviteSpaceRepository extends Repository<InviteSpaceEntity> {
|
export class InviteSpaceRepository extends Repository<InviteSpaceEntity> {
|
||||||
constructor(private dataSource: DataSource) {
|
constructor(private dataSource: DataSource) {
|
||||||
|
@ -6,7 +6,6 @@ import { SpaceProductAllocationEntity } from './entities/space-product-allocatio
|
|||||||
import { SpaceEntity } from './entities/space.entity';
|
import { SpaceEntity } from './entities/space.entity';
|
||||||
import { SubspaceProductAllocationEntity } from './entities/subspace/subspace-product-allocation.entity';
|
import { SubspaceProductAllocationEntity } from './entities/subspace/subspace-product-allocation.entity';
|
||||||
import { SubspaceEntity } from './entities/subspace/subspace.entity';
|
import { SubspaceEntity } from './entities/subspace/subspace.entity';
|
||||||
import { TagEntity } from './entities/tag.entity';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [],
|
providers: [],
|
||||||
@ -16,7 +15,6 @@ import { TagEntity } from './entities/tag.entity';
|
|||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
SpaceEntity,
|
SpaceEntity,
|
||||||
SubspaceEntity,
|
SubspaceEntity,
|
||||||
TagEntity,
|
|
||||||
InviteSpaceEntity,
|
InviteSpaceEntity,
|
||||||
SpaceProductAllocationEntity,
|
SpaceProductAllocationEntity,
|
||||||
SubspaceProductAllocationEntity,
|
SubspaceProductAllocationEntity,
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { Entity, Column, ManyToOne, Unique, ManyToMany } from 'typeorm';
|
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm';
|
||||||
import { ProductEntity } from '../../product/entities';
|
|
||||||
import { ProjectEntity } from '../../project/entities';
|
|
||||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||||
import { NewTagDto } from '../dtos/tag.dto';
|
import { DeviceEntity } from '../../device/entities/device.entity';
|
||||||
|
import { ProjectEntity } from '../../project/entities';
|
||||||
import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity';
|
import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity';
|
||||||
import { SubspaceModelProductAllocationEntity } from '../../space-model/entities/subspace-model/subspace-model-product-allocation.entity';
|
import { SubspaceModelProductAllocationEntity } from '../../space-model/entities/subspace-model/subspace-model-product-allocation.entity';
|
||||||
import { DeviceEntity } from '../../device/entities/device.entity';
|
import { NewTagDto } from '../dtos/tag.dto';
|
||||||
|
|
||||||
@Entity({ name: 'new_tag' })
|
@Entity({ name: 'new_tag' })
|
||||||
@Unique(['name', 'project'])
|
@Unique(['name', 'project'])
|
||||||
@ -24,31 +23,25 @@ export class NewTagEntity extends AbstractEntity<NewTagDto> {
|
|||||||
})
|
})
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ManyToOne(() => ProductEntity, (product) => product.newTags, {
|
|
||||||
nullable: false,
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
public product: ProductEntity;
|
|
||||||
|
|
||||||
@ManyToOne(() => ProjectEntity, (project) => project.tags, {
|
@ManyToOne(() => ProjectEntity, (project) => project.tags, {
|
||||||
nullable: false,
|
nullable: false,
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
public project: ProjectEntity;
|
public project: ProjectEntity;
|
||||||
|
|
||||||
@ManyToMany(
|
@OneToMany(
|
||||||
() => SpaceModelProductAllocationEntity,
|
() => SpaceModelProductAllocationEntity,
|
||||||
(allocation) => allocation.tags,
|
(allocation) => allocation.tag,
|
||||||
)
|
)
|
||||||
public spaceModelAllocations: SpaceModelProductAllocationEntity[];
|
public spaceModelAllocations: SpaceModelProductAllocationEntity[];
|
||||||
|
|
||||||
@ManyToMany(
|
@OneToMany(
|
||||||
() => SubspaceModelProductAllocationEntity,
|
() => SubspaceModelProductAllocationEntity,
|
||||||
(allocation) => allocation.tags,
|
(allocation) => allocation.tag,
|
||||||
)
|
)
|
||||||
public subspaceModelAllocations: SubspaceModelProductAllocationEntity[];
|
public subspaceModelAllocations: SubspaceModelProductAllocationEntity[];
|
||||||
|
|
||||||
@ManyToOne(() => DeviceEntity, (device) => device.tag)
|
@OneToMany(() => DeviceEntity, (device) => device.tag)
|
||||||
public devices: DeviceEntity[];
|
public devices: DeviceEntity[];
|
||||||
|
|
||||||
constructor(partial: Partial<NewTagEntity>) {
|
constructor(partial: Partial<NewTagEntity>) {
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
$1::uuid AS space_uuid,
|
||||||
|
TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
sdp.space_uuid,
|
||||||
|
sdp.event_date,
|
||||||
|
sdp.good_aqi_percentage, sdp.moderate_aqi_percentage, sdp.unhealthy_sensitive_aqi_percentage, sdp.unhealthy_aqi_percentage,
|
||||||
|
sdp.very_unhealthy_aqi_percentage, sdp.hazardous_aqi_percentage,
|
||||||
|
sdp.daily_avg_aqi, sdp.daily_max_aqi, sdp.daily_min_aqi,
|
||||||
|
|
||||||
|
sdp.good_pm25_percentage, sdp.moderate_pm25_percentage, sdp.unhealthy_sensitive_pm25_percentage, sdp.unhealthy_pm25_percentage,
|
||||||
|
sdp.very_unhealthy_pm25_percentage, sdp.hazardous_pm25_percentage,
|
||||||
|
sdp.daily_avg_pm25, sdp.daily_max_pm25, sdp.daily_min_pm25,
|
||||||
|
|
||||||
|
sdp.good_pm10_percentage, sdp.moderate_pm10_percentage, sdp.unhealthy_sensitive_pm10_percentage, sdp.unhealthy_pm10_percentage,
|
||||||
|
sdp.very_unhealthy_pm10_percentage, sdp.hazardous_pm10_percentage,
|
||||||
|
sdp.daily_avg_pm10, sdp.daily_max_pm10, sdp.daily_min_pm10,
|
||||||
|
|
||||||
|
sdp.good_voc_percentage, sdp.moderate_voc_percentage, sdp.unhealthy_sensitive_voc_percentage, sdp.unhealthy_voc_percentage,
|
||||||
|
sdp.very_unhealthy_voc_percentage, sdp.hazardous_voc_percentage,
|
||||||
|
sdp.daily_avg_voc, sdp.daily_max_voc, sdp.daily_min_voc,
|
||||||
|
|
||||||
|
sdp.good_co2_percentage, sdp.moderate_co2_percentage, sdp.unhealthy_sensitive_co2_percentage, sdp.unhealthy_co2_percentage,
|
||||||
|
sdp.very_unhealthy_co2_percentage, sdp.hazardous_co2_percentage,
|
||||||
|
sdp.daily_avg_co2, sdp.daily_max_co2, sdp.daily_min_co2,
|
||||||
|
|
||||||
|
sdp.good_ch2o_percentage, sdp.moderate_ch2o_percentage, sdp.unhealthy_sensitive_ch2o_percentage, sdp.unhealthy_ch2o_percentage,
|
||||||
|
sdp.very_unhealthy_ch2o_percentage, sdp.hazardous_ch2o_percentage,
|
||||||
|
sdp.daily_avg_ch2o, sdp.daily_max_ch2o, sdp.daily_min_ch2o
|
||||||
|
|
||||||
|
FROM public."space-daily-pollutant-stats" AS sdp
|
||||||
|
CROSS JOIN params p
|
||||||
|
WHERE
|
||||||
|
(p.space_uuid IS NULL OR sdp.space_uuid = p.space_uuid)
|
||||||
|
AND (p.event_month IS NULL OR TO_CHAR(sdp.event_date, 'YYYY-MM') = TO_CHAR(p.event_month, 'YYYY-MM'))
|
||||||
|
ORDER BY sdp.space_uuid, sdp.event_date;
|
@ -0,0 +1,374 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date,
|
||||||
|
$2::uuid AS space_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Query Pipeline Starts Here
|
||||||
|
device_space 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".code,
|
||||||
|
"device-status-log".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 = 'hjjcy'
|
||||||
|
),
|
||||||
|
|
||||||
|
average_pollutants AS (
|
||||||
|
SELECT
|
||||||
|
event_time::date AS event_date,
|
||||||
|
date_trunc('hour', event_time) AS event_hour,
|
||||||
|
space_id,
|
||||||
|
|
||||||
|
-- PM1
|
||||||
|
MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min,
|
||||||
|
AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max,
|
||||||
|
|
||||||
|
-- PM25
|
||||||
|
MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min,
|
||||||
|
AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max,
|
||||||
|
|
||||||
|
-- PM10
|
||||||
|
MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min,
|
||||||
|
AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max,
|
||||||
|
|
||||||
|
-- VOC
|
||||||
|
MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min,
|
||||||
|
AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg,
|
||||||
|
MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max,
|
||||||
|
|
||||||
|
-- CH2O
|
||||||
|
MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min,
|
||||||
|
AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg,
|
||||||
|
MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max,
|
||||||
|
|
||||||
|
-- CO2
|
||||||
|
MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min,
|
||||||
|
AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg,
|
||||||
|
MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max
|
||||||
|
|
||||||
|
FROM device_space
|
||||||
|
GROUP BY space_id, event_hour, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
filled_pollutants AS (
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
-- AVG
|
||||||
|
COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f,
|
||||||
|
COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f,
|
||||||
|
COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f,
|
||||||
|
COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f,
|
||||||
|
COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f,
|
||||||
|
|
||||||
|
-- MIN
|
||||||
|
COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f,
|
||||||
|
COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f,
|
||||||
|
COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f,
|
||||||
|
COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f,
|
||||||
|
COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f,
|
||||||
|
|
||||||
|
-- MAX
|
||||||
|
COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f,
|
||||||
|
COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f,
|
||||||
|
COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f,
|
||||||
|
COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f,
|
||||||
|
COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f
|
||||||
|
FROM average_pollutants
|
||||||
|
),
|
||||||
|
|
||||||
|
hourly_results AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
event_hour,
|
||||||
|
pm1_min, pm1_avg, pm1_max,
|
||||||
|
pm25_min_f, pm25_avg_f, pm25_max_f,
|
||||||
|
pm10_min_f, pm10_avg_f, pm10_max_f,
|
||||||
|
voc_min_f, voc_avg_f, voc_max_f,
|
||||||
|
co2_min_f, co2_avg_f, co2_max_f,
|
||||||
|
ch2o_min_f, ch2o_avg_f, ch2o_max_f,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_min_f),
|
||||||
|
calculate_aqi('pm10', pm10_min_f)
|
||||||
|
) AS hourly_min_aqi,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_avg_f),
|
||||||
|
calculate_aqi('pm10', pm10_avg_f)
|
||||||
|
) AS hourly_avg_aqi,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_max_f),
|
||||||
|
calculate_aqi('pm10', pm10_max_f)
|
||||||
|
) AS hourly_max_aqi,
|
||||||
|
|
||||||
|
classify_aqi(GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_avg_f),
|
||||||
|
calculate_aqi('pm10', pm10_avg_f)
|
||||||
|
)) AS aqi_category,
|
||||||
|
|
||||||
|
classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category,
|
||||||
|
classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category,
|
||||||
|
classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category,
|
||||||
|
classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category,
|
||||||
|
classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category
|
||||||
|
|
||||||
|
FROM filled_pollutants
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_category_counts AS (
|
||||||
|
SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, aqi_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, pm25_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, pm10_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, voc_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, co2_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, ch2o_category
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_totals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
SUM(category_count) AS total_count
|
||||||
|
FROM daily_category_counts
|
||||||
|
where pollutant = 'aqi'
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Pivot Categories into Columns
|
||||||
|
daily_percentages AS (
|
||||||
|
select
|
||||||
|
dt.space_id,
|
||||||
|
dt.event_date,
|
||||||
|
-- AQI CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage,
|
||||||
|
-- PM25 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage,
|
||||||
|
-- PM10 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage,
|
||||||
|
-- VOC CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage,
|
||||||
|
-- CO2 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage,
|
||||||
|
-- CH20 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage
|
||||||
|
FROM daily_totals dt
|
||||||
|
LEFT JOIN daily_category_counts dcc
|
||||||
|
ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date
|
||||||
|
GROUP BY dt.space_id, dt.event_date, dt.total_count
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_averages AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
-- AQI
|
||||||
|
ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi,
|
||||||
|
ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi,
|
||||||
|
ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi,
|
||||||
|
-- PM25
|
||||||
|
ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25,
|
||||||
|
ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25,
|
||||||
|
ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25,
|
||||||
|
-- PM10
|
||||||
|
ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10,
|
||||||
|
ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10,
|
||||||
|
ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10,
|
||||||
|
-- VOC
|
||||||
|
ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc,
|
||||||
|
ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc,
|
||||||
|
ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc,
|
||||||
|
-- CO2
|
||||||
|
ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2,
|
||||||
|
ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2,
|
||||||
|
ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2,
|
||||||
|
-- CH2O
|
||||||
|
ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o,
|
||||||
|
ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o,
|
||||||
|
ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o
|
||||||
|
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
final_data as(
|
||||||
|
SELECT
|
||||||
|
p.space_id,
|
||||||
|
p.event_date,
|
||||||
|
p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage,
|
||||||
|
a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi,
|
||||||
|
p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage,
|
||||||
|
a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25,
|
||||||
|
p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage,
|
||||||
|
a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10,
|
||||||
|
p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage,
|
||||||
|
a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc,
|
||||||
|
p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage,
|
||||||
|
a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2,
|
||||||
|
p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage,
|
||||||
|
a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o
|
||||||
|
FROM daily_percentages p
|
||||||
|
LEFT JOIN daily_averages a
|
||||||
|
ON p.space_id = a.space_id AND p.event_date = a.event_date
|
||||||
|
ORDER BY p.space_id, p.event_date)
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO public."space-daily-pollutant-stats" (
|
||||||
|
space_uuid,
|
||||||
|
event_date,
|
||||||
|
good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage,
|
||||||
|
daily_avg_aqi, daily_max_aqi, daily_min_aqi,
|
||||||
|
good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage,
|
||||||
|
daily_avg_pm25, daily_max_pm25, daily_min_pm25,
|
||||||
|
good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage,
|
||||||
|
daily_avg_pm10, daily_max_pm10, daily_min_pm10,
|
||||||
|
good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage,
|
||||||
|
daily_avg_voc, daily_max_voc, daily_min_voc,
|
||||||
|
good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage,
|
||||||
|
daily_avg_co2, daily_max_co2, daily_min_co2,
|
||||||
|
good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage,
|
||||||
|
daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage,
|
||||||
|
daily_avg_aqi, daily_max_aqi, daily_min_aqi,
|
||||||
|
good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage,
|
||||||
|
daily_avg_pm25, daily_max_pm25, daily_min_pm25,
|
||||||
|
good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage,
|
||||||
|
daily_avg_pm10, daily_max_pm10, daily_min_pm10,
|
||||||
|
good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage,
|
||||||
|
daily_avg_voc, daily_max_voc, daily_min_voc,
|
||||||
|
good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage,
|
||||||
|
daily_avg_co2, daily_max_co2, daily_min_co2,
|
||||||
|
good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage,
|
||||||
|
daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o
|
||||||
|
FROM final_data
|
||||||
|
ON CONFLICT (space_uuid, event_date) DO UPDATE
|
||||||
|
SET
|
||||||
|
good_aqi_percentage = EXCLUDED.good_aqi_percentage,
|
||||||
|
moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage,
|
||||||
|
unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage,
|
||||||
|
unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage,
|
||||||
|
very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage,
|
||||||
|
hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage,
|
||||||
|
daily_avg_aqi = EXCLUDED.daily_avg_aqi,
|
||||||
|
daily_max_aqi = EXCLUDED.daily_max_aqi,
|
||||||
|
daily_min_aqi = EXCLUDED.daily_min_aqi,
|
||||||
|
good_pm25_percentage = EXCLUDED.good_pm25_percentage,
|
||||||
|
moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage,
|
||||||
|
unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage,
|
||||||
|
unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage,
|
||||||
|
very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage,
|
||||||
|
hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage,
|
||||||
|
daily_avg_pm25 = EXCLUDED.daily_avg_pm25,
|
||||||
|
daily_max_pm25 = EXCLUDED.daily_max_pm25,
|
||||||
|
daily_min_pm25 = EXCLUDED.daily_min_pm25,
|
||||||
|
good_pm10_percentage = EXCLUDED.good_pm10_percentage,
|
||||||
|
moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage,
|
||||||
|
unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage,
|
||||||
|
unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage,
|
||||||
|
very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage,
|
||||||
|
hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage,
|
||||||
|
daily_avg_pm10 = EXCLUDED.daily_avg_pm10,
|
||||||
|
daily_max_pm10 = EXCLUDED.daily_max_pm10,
|
||||||
|
daily_min_pm10 = EXCLUDED.daily_min_pm10,
|
||||||
|
good_voc_percentage = EXCLUDED.good_voc_percentage,
|
||||||
|
moderate_voc_percentage = EXCLUDED.moderate_voc_percentage,
|
||||||
|
unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage,
|
||||||
|
unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage,
|
||||||
|
very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage,
|
||||||
|
hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage,
|
||||||
|
daily_avg_voc = EXCLUDED.daily_avg_voc,
|
||||||
|
daily_max_voc = EXCLUDED.daily_max_voc,
|
||||||
|
daily_min_voc = EXCLUDED.daily_min_voc,
|
||||||
|
good_co2_percentage = EXCLUDED.good_co2_percentage,
|
||||||
|
moderate_co2_percentage = EXCLUDED.moderate_co2_percentage,
|
||||||
|
unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage,
|
||||||
|
unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage,
|
||||||
|
very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage,
|
||||||
|
hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage,
|
||||||
|
daily_avg_co2 = EXCLUDED.daily_avg_co2,
|
||||||
|
daily_max_co2 = EXCLUDED.daily_max_co2,
|
||||||
|
daily_min_co2 = EXCLUDED.daily_min_co2,
|
||||||
|
good_ch2o_percentage = EXCLUDED.good_ch2o_percentage,
|
||||||
|
moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage,
|
||||||
|
unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage,
|
||||||
|
unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage,
|
||||||
|
very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage,
|
||||||
|
hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage,
|
||||||
|
daily_avg_ch2o = EXCLUDED.daily_avg_ch2o,
|
||||||
|
daily_max_ch2o = EXCLUDED.daily_max_ch2o,
|
||||||
|
daily_min_ch2o = EXCLUDED.daily_min_ch2o;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,367 @@
|
|||||||
|
-- Query Pipeline Starts Here
|
||||||
|
WITH device_space 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".code,
|
||||||
|
"device-status-log".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 = 'hjjcy'
|
||||||
|
),
|
||||||
|
|
||||||
|
average_pollutants AS (
|
||||||
|
SELECT
|
||||||
|
event_time::date AS event_date,
|
||||||
|
date_trunc('hour', event_time) AS event_hour,
|
||||||
|
space_id,
|
||||||
|
|
||||||
|
-- PM1
|
||||||
|
MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min,
|
||||||
|
AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max,
|
||||||
|
|
||||||
|
-- PM25
|
||||||
|
MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min,
|
||||||
|
AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max,
|
||||||
|
|
||||||
|
-- PM10
|
||||||
|
MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min,
|
||||||
|
AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max,
|
||||||
|
|
||||||
|
-- VOC
|
||||||
|
MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min,
|
||||||
|
AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg,
|
||||||
|
MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max,
|
||||||
|
|
||||||
|
-- CH2O
|
||||||
|
MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min,
|
||||||
|
AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg,
|
||||||
|
MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max,
|
||||||
|
|
||||||
|
-- CO2
|
||||||
|
MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min,
|
||||||
|
AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg,
|
||||||
|
MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max
|
||||||
|
|
||||||
|
FROM device_space
|
||||||
|
GROUP BY space_id, event_hour, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
filled_pollutants AS (
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
-- AVG
|
||||||
|
COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f,
|
||||||
|
COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f,
|
||||||
|
COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f,
|
||||||
|
COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f,
|
||||||
|
COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f,
|
||||||
|
|
||||||
|
-- MIN
|
||||||
|
COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f,
|
||||||
|
COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f,
|
||||||
|
COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f,
|
||||||
|
COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f,
|
||||||
|
COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f,
|
||||||
|
|
||||||
|
-- MAX
|
||||||
|
COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f,
|
||||||
|
COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f,
|
||||||
|
COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f,
|
||||||
|
COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f,
|
||||||
|
COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f
|
||||||
|
FROM average_pollutants
|
||||||
|
),
|
||||||
|
|
||||||
|
hourly_results AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
event_hour,
|
||||||
|
pm1_min, pm1_avg, pm1_max,
|
||||||
|
pm25_min_f, pm25_avg_f, pm25_max_f,
|
||||||
|
pm10_min_f, pm10_avg_f, pm10_max_f,
|
||||||
|
voc_min_f, voc_avg_f, voc_max_f,
|
||||||
|
co2_min_f, co2_avg_f, co2_max_f,
|
||||||
|
ch2o_min_f, ch2o_avg_f, ch2o_max_f,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_min_f),
|
||||||
|
calculate_aqi('pm10', pm10_min_f)
|
||||||
|
) AS hourly_min_aqi,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_avg_f),
|
||||||
|
calculate_aqi('pm10', pm10_avg_f)
|
||||||
|
) AS hourly_avg_aqi,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_max_f),
|
||||||
|
calculate_aqi('pm10', pm10_max_f)
|
||||||
|
) AS hourly_max_aqi,
|
||||||
|
|
||||||
|
classify_aqi(GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_avg_f),
|
||||||
|
calculate_aqi('pm10', pm10_avg_f)
|
||||||
|
)) AS aqi_category,
|
||||||
|
|
||||||
|
classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category,
|
||||||
|
classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category,
|
||||||
|
classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category,
|
||||||
|
classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category,
|
||||||
|
classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category
|
||||||
|
|
||||||
|
FROM filled_pollutants
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_category_counts AS (
|
||||||
|
SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, aqi_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, pm25_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, pm10_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, voc_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, co2_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, ch2o_category
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_totals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
SUM(category_count) AS total_count
|
||||||
|
FROM daily_category_counts
|
||||||
|
where pollutant = 'aqi'
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Pivot Categories into Columns
|
||||||
|
daily_percentages AS (
|
||||||
|
select
|
||||||
|
dt.space_id,
|
||||||
|
dt.event_date,
|
||||||
|
-- AQI CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage,
|
||||||
|
-- PM25 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage,
|
||||||
|
-- PM10 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage,
|
||||||
|
-- VOC CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage,
|
||||||
|
-- CO2 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage,
|
||||||
|
-- CH20 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage
|
||||||
|
FROM daily_totals dt
|
||||||
|
LEFT JOIN daily_category_counts dcc
|
||||||
|
ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date
|
||||||
|
GROUP BY dt.space_id, dt.event_date, dt.total_count
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_averages AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
-- AQI
|
||||||
|
ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi,
|
||||||
|
ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi,
|
||||||
|
ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi,
|
||||||
|
-- PM25
|
||||||
|
ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25,
|
||||||
|
ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25,
|
||||||
|
ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25,
|
||||||
|
-- PM10
|
||||||
|
ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10,
|
||||||
|
ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10,
|
||||||
|
ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10,
|
||||||
|
-- VOC
|
||||||
|
ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc,
|
||||||
|
ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc,
|
||||||
|
ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc,
|
||||||
|
-- CO2
|
||||||
|
ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2,
|
||||||
|
ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2,
|
||||||
|
ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2,
|
||||||
|
-- CH2O
|
||||||
|
ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o,
|
||||||
|
ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o,
|
||||||
|
ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o
|
||||||
|
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
final_data as(
|
||||||
|
SELECT
|
||||||
|
p.space_id,
|
||||||
|
p.event_date,
|
||||||
|
p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage,
|
||||||
|
a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi,
|
||||||
|
p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage,
|
||||||
|
a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25,
|
||||||
|
p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage,
|
||||||
|
a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10,
|
||||||
|
p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage,
|
||||||
|
a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc,
|
||||||
|
p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage,
|
||||||
|
a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2,
|
||||||
|
p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage,
|
||||||
|
a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o
|
||||||
|
FROM daily_percentages p
|
||||||
|
LEFT JOIN daily_averages a
|
||||||
|
ON p.space_id = a.space_id AND p.event_date = a.event_date
|
||||||
|
ORDER BY p.space_id, p.event_date)
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO public."space-daily-pollutant-stats" (
|
||||||
|
space_uuid,
|
||||||
|
event_date,
|
||||||
|
good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage,
|
||||||
|
daily_avg_aqi, daily_max_aqi, daily_min_aqi,
|
||||||
|
good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage,
|
||||||
|
daily_avg_pm25, daily_max_pm25, daily_min_pm25,
|
||||||
|
good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage,
|
||||||
|
daily_avg_pm10, daily_max_pm10, daily_min_pm10,
|
||||||
|
good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage,
|
||||||
|
daily_avg_voc, daily_max_voc, daily_min_voc,
|
||||||
|
good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage,
|
||||||
|
daily_avg_co2, daily_max_co2, daily_min_co2,
|
||||||
|
good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage,
|
||||||
|
daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage,
|
||||||
|
daily_avg_aqi, daily_max_aqi, daily_min_aqi,
|
||||||
|
good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage,
|
||||||
|
daily_avg_pm25, daily_max_pm25, daily_min_pm25,
|
||||||
|
good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage,
|
||||||
|
daily_avg_pm10, daily_max_pm10, daily_min_pm10,
|
||||||
|
good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage,
|
||||||
|
daily_avg_voc, daily_max_voc, daily_min_voc,
|
||||||
|
good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage,
|
||||||
|
daily_avg_co2, daily_max_co2, daily_min_co2,
|
||||||
|
good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage,
|
||||||
|
daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o
|
||||||
|
FROM final_data
|
||||||
|
ON CONFLICT (space_uuid, event_date) DO UPDATE
|
||||||
|
SET
|
||||||
|
good_aqi_percentage = EXCLUDED.good_aqi_percentage,
|
||||||
|
moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage,
|
||||||
|
unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage,
|
||||||
|
unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage,
|
||||||
|
very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage,
|
||||||
|
hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage,
|
||||||
|
daily_avg_aqi = EXCLUDED.daily_avg_aqi,
|
||||||
|
daily_max_aqi = EXCLUDED.daily_max_aqi,
|
||||||
|
daily_min_aqi = EXCLUDED.daily_min_aqi,
|
||||||
|
good_pm25_percentage = EXCLUDED.good_pm25_percentage,
|
||||||
|
moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage,
|
||||||
|
unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage,
|
||||||
|
unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage,
|
||||||
|
very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage,
|
||||||
|
hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage,
|
||||||
|
daily_avg_pm25 = EXCLUDED.daily_avg_pm25,
|
||||||
|
daily_max_pm25 = EXCLUDED.daily_max_pm25,
|
||||||
|
daily_min_pm25 = EXCLUDED.daily_min_pm25,
|
||||||
|
good_pm10_percentage = EXCLUDED.good_pm10_percentage,
|
||||||
|
moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage,
|
||||||
|
unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage,
|
||||||
|
unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage,
|
||||||
|
very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage,
|
||||||
|
hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage,
|
||||||
|
daily_avg_pm10 = EXCLUDED.daily_avg_pm10,
|
||||||
|
daily_max_pm10 = EXCLUDED.daily_max_pm10,
|
||||||
|
daily_min_pm10 = EXCLUDED.daily_min_pm10,
|
||||||
|
good_voc_percentage = EXCLUDED.good_voc_percentage,
|
||||||
|
moderate_voc_percentage = EXCLUDED.moderate_voc_percentage,
|
||||||
|
unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage,
|
||||||
|
unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage,
|
||||||
|
very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage,
|
||||||
|
hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage,
|
||||||
|
daily_avg_voc = EXCLUDED.daily_avg_voc,
|
||||||
|
daily_max_voc = EXCLUDED.daily_max_voc,
|
||||||
|
daily_min_voc = EXCLUDED.daily_min_voc,
|
||||||
|
good_co2_percentage = EXCLUDED.good_co2_percentage,
|
||||||
|
moderate_co2_percentage = EXCLUDED.moderate_co2_percentage,
|
||||||
|
unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage,
|
||||||
|
unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage,
|
||||||
|
very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage,
|
||||||
|
hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage,
|
||||||
|
daily_avg_co2 = EXCLUDED.daily_avg_co2,
|
||||||
|
daily_max_co2 = EXCLUDED.daily_max_co2,
|
||||||
|
daily_min_co2 = EXCLUDED.daily_min_co2,
|
||||||
|
good_ch2o_percentage = EXCLUDED.good_ch2o_percentage,
|
||||||
|
moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage,
|
||||||
|
unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage,
|
||||||
|
unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage,
|
||||||
|
very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage,
|
||||||
|
hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage,
|
||||||
|
daily_avg_ch2o = EXCLUDED.daily_avg_ch2o,
|
||||||
|
daily_max_ch2o = EXCLUDED.daily_max_ch2o,
|
||||||
|
daily_min_ch2o = EXCLUDED.daily_min_ch2o;
|
||||||
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
|||||||
|
WITH presence_logs AS (
|
||||||
|
SELECT
|
||||||
|
d.space_device_uuid AS space_id,
|
||||||
|
l.device_id,
|
||||||
|
l.event_time,
|
||||||
|
l.value,
|
||||||
|
LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time,
|
||||||
|
LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value
|
||||||
|
FROM device d
|
||||||
|
JOIN "device-status-log" l ON d.uuid = l.device_id
|
||||||
|
JOIN product p ON p.uuid = d.product_device_uuid
|
||||||
|
WHERE l.code = 'presence_state'
|
||||||
|
AND p.cat_name = 'hps'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Intervals when device was in 'presence' (between prev_time and event_time when value='none')
|
||||||
|
presence_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
prev_time AS start_time,
|
||||||
|
event_time AS end_time
|
||||||
|
FROM presence_logs
|
||||||
|
WHERE value = 'none'
|
||||||
|
AND prev_value = 'presence'
|
||||||
|
AND prev_time IS NOT NULL
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Split intervals across days
|
||||||
|
split_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
generate_series(
|
||||||
|
date_trunc('day', start_time),
|
||||||
|
date_trunc('day', end_time),
|
||||||
|
interval '1 day'
|
||||||
|
)::date AS event_date,
|
||||||
|
GREATEST(start_time, date_trunc('day', start_time)) AS interval_start,
|
||||||
|
LEAST(end_time, date_trunc('day', end_time) + interval '1 day') AS interval_end
|
||||||
|
FROM presence_intervals
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Mark and group overlapping intervals per space per day
|
||||||
|
ordered_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
interval_start,
|
||||||
|
interval_end,
|
||||||
|
LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end
|
||||||
|
FROM split_intervals
|
||||||
|
),
|
||||||
|
|
||||||
|
grouped_intervals AS (
|
||||||
|
SELECT *,
|
||||||
|
SUM(CASE
|
||||||
|
WHEN prev_end IS NULL OR interval_start > prev_end THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp
|
||||||
|
FROM ordered_intervals
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Merge overlapping intervals per group
|
||||||
|
merged_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
MIN(interval_start) AS merged_start,
|
||||||
|
MAX(interval_end) AS merged_end
|
||||||
|
FROM grouped_intervals
|
||||||
|
GROUP BY space_id, event_date, grp
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Sum durations of merged intervals
|
||||||
|
summed_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds
|
||||||
|
FROM merged_intervals
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
final_data AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
LEAST(raw_occupied_seconds, 86400) AS occupied_seconds,
|
||||||
|
ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage
|
||||||
|
FROM summed_intervals
|
||||||
|
ORDER BY space_id, event_date)
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO public."space-daily-occupancy-duration" (
|
||||||
|
space_uuid,
|
||||||
|
event_date,
|
||||||
|
occupied_seconds,
|
||||||
|
occupancy_percentage
|
||||||
|
)
|
||||||
|
select space_id,
|
||||||
|
event_date,
|
||||||
|
occupied_seconds,
|
||||||
|
occupancy_percentage
|
||||||
|
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,109 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date,
|
||||||
|
$2::uuid AS space_id
|
||||||
|
),
|
||||||
|
|
||||||
|
presence_logs AS (
|
||||||
|
SELECT
|
||||||
|
d.space_device_uuid AS space_id,
|
||||||
|
l.device_id,
|
||||||
|
l.event_time,
|
||||||
|
l.value,
|
||||||
|
LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time
|
||||||
|
FROM device d
|
||||||
|
JOIN "device-status-log" l ON d.uuid = l.device_id
|
||||||
|
JOIN product p ON p.uuid = d.product_device_uuid
|
||||||
|
WHERE l.code = 'presence_state'
|
||||||
|
AND p.cat_name = 'hps'
|
||||||
|
),
|
||||||
|
|
||||||
|
presence_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
prev_time AS start_time,
|
||||||
|
event_time AS end_time
|
||||||
|
FROM presence_logs
|
||||||
|
WHERE value = 'none' AND prev_time IS NOT NULL
|
||||||
|
),
|
||||||
|
|
||||||
|
split_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
generate_series(
|
||||||
|
date_trunc('day', start_time),
|
||||||
|
date_trunc('day', end_time),
|
||||||
|
interval '1 day'
|
||||||
|
)::date AS event_date,
|
||||||
|
GREATEST(start_time, date_trunc('day', start_time)) AS interval_start,
|
||||||
|
LEAST(end_time, date_trunc('day', end_time) + INTERVAL '1 day') AS interval_end
|
||||||
|
FROM presence_intervals
|
||||||
|
),
|
||||||
|
|
||||||
|
ordered_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
interval_start,
|
||||||
|
interval_end,
|
||||||
|
LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end
|
||||||
|
FROM split_intervals
|
||||||
|
),
|
||||||
|
|
||||||
|
grouped_intervals AS (
|
||||||
|
SELECT *,
|
||||||
|
SUM(CASE
|
||||||
|
WHEN prev_end IS NULL OR interval_start > prev_end THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp
|
||||||
|
FROM ordered_intervals
|
||||||
|
),
|
||||||
|
|
||||||
|
merged_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
MIN(interval_start) AS merged_start,
|
||||||
|
MAX(interval_end) AS merged_end
|
||||||
|
FROM grouped_intervals
|
||||||
|
GROUP BY space_id, event_date, grp
|
||||||
|
),
|
||||||
|
|
||||||
|
summed_intervals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds
|
||||||
|
FROM merged_intervals
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
final_data AS (
|
||||||
|
SELECT
|
||||||
|
s.space_id,
|
||||||
|
s.event_date,
|
||||||
|
LEAST(raw_occupied_seconds, 86400) AS occupied_seconds,
|
||||||
|
ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage
|
||||||
|
FROM summed_intervals s
|
||||||
|
JOIN params p
|
||||||
|
ON p.space_id = s.space_id
|
||||||
|
AND p.event_date = s.event_date
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO public."space-daily-occupancy-duration" (
|
||||||
|
space_uuid,
|
||||||
|
event_date,
|
||||||
|
occupied_seconds,
|
||||||
|
occupancy_percentage
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
occupied_seconds,
|
||||||
|
occupancy_percentage
|
||||||
|
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,20 @@
|
|||||||
|
-- will return the presence metrics for the days of the selected month
|
||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS month,
|
||||||
|
string_to_array(NULLIF($4, ''), ',') AS device_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
A.device_uuid,
|
||||||
|
A.event_date,
|
||||||
|
A.count_motion_detected,
|
||||||
|
A.count_presence_detected,
|
||||||
|
A.count_total_presence_detected
|
||||||
|
FROM public."presence-sensor-daily-device-detection" AS A
|
||||||
|
JOIN params P ON TRUE
|
||||||
|
WHERE A.device_uuid::text = ANY(P.device_ids)
|
||||||
|
AND (P.month IS NULL
|
||||||
|
OR date_trunc('month', A.event_date) = P.month
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,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;
|
@ -0,0 +1,115 @@
|
|||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date,
|
||||||
|
$4::text AS device_id
|
||||||
|
),
|
||||||
|
|
||||||
|
device_logs AS (
|
||||||
|
SELECT
|
||||||
|
device.uuid AS device_id,
|
||||||
|
device.created_at,
|
||||||
|
device.device_tuya_uuid,
|
||||||
|
device.space_device_uuid AS space_id,
|
||||||
|
"device-status-log".event_id,
|
||||||
|
"device-status-log".event_time::timestamp,
|
||||||
|
"device-status-log".code,
|
||||||
|
"device-status-log".value,
|
||||||
|
"device-status-log".log,
|
||||||
|
LAG("device-status-log".event_time::timestamp)
|
||||||
|
OVER (PARTITION BY device.uuid
|
||||||
|
ORDER BY "device-status-log".event_time) AS prev_timestamp,
|
||||||
|
LAG("device-status-log".value)
|
||||||
|
OVER (PARTITION BY device.uuid
|
||||||
|
ORDER BY "device-status-log".event_time) AS prev_value
|
||||||
|
FROM device
|
||||||
|
LEFT JOIN "device-status-log"
|
||||||
|
ON device.uuid = "device-status-log".device_id
|
||||||
|
LEFT JOIN product
|
||||||
|
ON product.uuid = device.product_device_uuid
|
||||||
|
JOIN params P ON TRUE
|
||||||
|
WHERE product.cat_name = 'hps'
|
||||||
|
AND "device-status-log".code = 'presence_state'
|
||||||
|
AND device.uuid::text = P.device_id
|
||||||
|
),
|
||||||
|
|
||||||
|
presence_detection AS (
|
||||||
|
SELECT *,
|
||||||
|
CASE
|
||||||
|
WHEN value = 'motion' AND prev_value = 'none' THEN 1 ELSE 0
|
||||||
|
END AS motion_detected,
|
||||||
|
CASE
|
||||||
|
WHEN value = 'presence' AND prev_value = 'none' THEN 1 ELSE 0
|
||||||
|
END AS presence_detected
|
||||||
|
FROM device_logs
|
||||||
|
),
|
||||||
|
|
||||||
|
presence_detection_summary AS (
|
||||||
|
SELECT
|
||||||
|
pd.device_id,
|
||||||
|
d.subspace_id,
|
||||||
|
pd.space_id,
|
||||||
|
pd.event_time::date AS event_date,
|
||||||
|
EXTRACT(HOUR FROM pd.event_time)::int AS event_hour,
|
||||||
|
SUM(motion_detected) AS count_motion_detected,
|
||||||
|
SUM(presence_detected) AS count_presence_detected,
|
||||||
|
SUM(motion_detected + presence_detected) AS count_total_presence_detected
|
||||||
|
FROM presence_detection pd
|
||||||
|
LEFT JOIN device d ON d.uuid = pd.device_id
|
||||||
|
JOIN params P ON TRUE
|
||||||
|
AND (P.event_date IS NULL OR pd.event_time::date = P.event_date)
|
||||||
|
GROUP BY 1, 2, 3, 4, 5
|
||||||
|
),
|
||||||
|
|
||||||
|
all_dates_and_hours AS (
|
||||||
|
SELECT device_id, subspace_id, space_id, event_date, event_hour
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT device_id, subspace_id, space_id, event_date
|
||||||
|
FROM presence_detection_summary
|
||||||
|
) d
|
||||||
|
CROSS JOIN generate_series(0, 23) AS event_hour
|
||||||
|
),
|
||||||
|
|
||||||
|
table_final AS (
|
||||||
|
SELECT
|
||||||
|
adah.device_id,
|
||||||
|
adah.event_date,
|
||||||
|
COALESCE(pds.count_motion_detected, 0) AS count_motion_detected,
|
||||||
|
COALESCE(pds.count_presence_detected, 0) AS count_presence_detected,
|
||||||
|
COALESCE(pds.count_total_presence_detected, 0) AS count_total_presence_detected
|
||||||
|
FROM all_dates_and_hours adah
|
||||||
|
LEFT JOIN presence_detection_summary pds
|
||||||
|
ON pds.device_id = adah.device_id
|
||||||
|
AND pds.event_date = adah.event_date
|
||||||
|
AND pds.event_hour = adah.event_hour
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_aggregates AS (
|
||||||
|
SELECT
|
||||||
|
device_id,
|
||||||
|
event_date,
|
||||||
|
SUM(count_motion_detected) AS count_motion_detected,
|
||||||
|
SUM(count_presence_detected) AS count_presence_detected,
|
||||||
|
SUM(count_total_presence_detected) AS count_total_presence_detected
|
||||||
|
FROM table_final
|
||||||
|
GROUP BY device_id, event_date
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO public."presence-sensor-daily-device-detection" (
|
||||||
|
device_uuid,
|
||||||
|
event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
device_id,
|
||||||
|
event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
FROM daily_aggregates
|
||||||
|
ON CONFLICT (device_uuid, event_date) DO UPDATE
|
||||||
|
SET
|
||||||
|
count_motion_detected = EXCLUDED.count_motion_detected,
|
||||||
|
count_presence_detected = EXCLUDED.count_presence_detected,
|
||||||
|
count_total_presence_detected = EXCLUDED.count_total_presence_detected;
|
@ -0,0 +1,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;
|
@ -0,0 +1,362 @@
|
|||||||
|
-- Function to calculate AQI
|
||||||
|
CREATE OR REPLACE FUNCTION calculate_aqi(p_pollutant TEXT, concentration NUMERIC)
|
||||||
|
RETURNS NUMERIC AS $$
|
||||||
|
DECLARE
|
||||||
|
c_low NUMERIC;
|
||||||
|
c_high NUMERIC;
|
||||||
|
i_low INT;
|
||||||
|
i_high INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT v.c_low, v.c_high, v.i_low, v.i_high
|
||||||
|
INTO c_low, c_high, i_low, i_high
|
||||||
|
FROM (
|
||||||
|
VALUES
|
||||||
|
-- PM2.5
|
||||||
|
('pm25', 0.0, 12.0, 0, 50),
|
||||||
|
('pm25', 12.1, 35.4, 51, 100),
|
||||||
|
('pm25', 35.5, 55.4, 101, 150),
|
||||||
|
('pm25', 55.5, 150.4, 151, 200),
|
||||||
|
('pm25', 150.5, 250.4, 201, 300),
|
||||||
|
('pm25', 250.5, 500.4, 301, 500),
|
||||||
|
|
||||||
|
-- PM10
|
||||||
|
('pm10', 0, 54, 0, 50),
|
||||||
|
('pm10', 55, 154, 51, 100),
|
||||||
|
('pm10', 155, 254, 101, 150),
|
||||||
|
('pm10', 255, 354, 151, 200),
|
||||||
|
|
||||||
|
-- VOC
|
||||||
|
('voc', 0, 200, 0, 50),
|
||||||
|
('voc', 201, 400, 51, 100),
|
||||||
|
('voc', 401, 600, 101, 150),
|
||||||
|
('voc', 601, 1000, 151, 200),
|
||||||
|
|
||||||
|
-- CH2O
|
||||||
|
('ch2o', 0, 2, 0, 50),
|
||||||
|
('ch2o', 2.1, 4, 51, 100),
|
||||||
|
('ch2o', 4.1, 6, 101, 150),
|
||||||
|
|
||||||
|
-- CO2
|
||||||
|
('co2', 350, 1000, 0, 50),
|
||||||
|
('co2', 1001, 1250, 51, 100),
|
||||||
|
('co2', 1251, 1500, 101, 150),
|
||||||
|
('co2', 1501, 2000, 151, 200)
|
||||||
|
) AS v(pollutant, c_low, c_high, i_low, i_high)
|
||||||
|
WHERE v.pollutant = LOWER(p_pollutant)
|
||||||
|
AND concentration BETWEEN v.c_low AND v.c_high
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
RETURN ROUND(((i_high - i_low) * (concentration - c_low) / (c_high - c_low)) + i_low);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
|
||||||
|
-- Function to classify AQI
|
||||||
|
CREATE OR REPLACE FUNCTION classify_aqi(aqi NUMERIC)
|
||||||
|
RETURNS TEXT AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN CASE
|
||||||
|
WHEN aqi BETWEEN 0 AND 50 THEN 'Good'
|
||||||
|
WHEN aqi BETWEEN 51 AND 100 THEN 'Moderate'
|
||||||
|
WHEN aqi BETWEEN 101 AND 150 THEN 'Unhealthy for Sensitive Groups'
|
||||||
|
WHEN aqi BETWEEN 151 AND 200 THEN 'Unhealthy'
|
||||||
|
WHEN aqi BETWEEN 201 AND 300 THEN 'Very Unhealthy'
|
||||||
|
WHEN aqi >= 301 THEN 'Hazardous'
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
|
||||||
|
-- Function to convert AQI level string to number
|
||||||
|
CREATE OR REPLACE FUNCTION level_to_numeric(level_text TEXT)
|
||||||
|
RETURNS NUMERIC AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN CAST(regexp_replace(level_text, '[^0-9]', '', 'g') AS NUMERIC);
|
||||||
|
EXCEPTION WHEN others THEN
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
|
||||||
|
-- Query Pipeline Starts Here
|
||||||
|
WITH device_space 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".code,
|
||||||
|
"device-status-log".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 = 'hjjcy'
|
||||||
|
),
|
||||||
|
|
||||||
|
average_pollutants AS (
|
||||||
|
SELECT
|
||||||
|
event_time::date AS event_date,
|
||||||
|
date_trunc('hour', event_time) AS event_hour,
|
||||||
|
device_id,
|
||||||
|
space_id,
|
||||||
|
|
||||||
|
-- PM1
|
||||||
|
MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min,
|
||||||
|
AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max,
|
||||||
|
|
||||||
|
-- PM25
|
||||||
|
MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min,
|
||||||
|
AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max,
|
||||||
|
|
||||||
|
-- PM10
|
||||||
|
MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min,
|
||||||
|
AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max,
|
||||||
|
|
||||||
|
-- VOC
|
||||||
|
MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min,
|
||||||
|
AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg,
|
||||||
|
MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max,
|
||||||
|
|
||||||
|
-- CH2O
|
||||||
|
MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min,
|
||||||
|
AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg,
|
||||||
|
MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max,
|
||||||
|
|
||||||
|
-- CO2
|
||||||
|
MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min,
|
||||||
|
AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg,
|
||||||
|
MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max
|
||||||
|
|
||||||
|
FROM device_space
|
||||||
|
GROUP BY device_id, space_id, event_hour, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
filled_pollutants AS (
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
-- AVG
|
||||||
|
COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_avg_f,
|
||||||
|
COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_avg_f,
|
||||||
|
COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_avg_f,
|
||||||
|
COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_avg_f,
|
||||||
|
COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_avg_f,
|
||||||
|
|
||||||
|
-- MIN
|
||||||
|
COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_min_f,
|
||||||
|
COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_min_f,
|
||||||
|
COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_min_f,
|
||||||
|
COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_min_f,
|
||||||
|
COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_min_f,
|
||||||
|
|
||||||
|
-- MAX
|
||||||
|
COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_max_f,
|
||||||
|
COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_max_f,
|
||||||
|
COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_max_f,
|
||||||
|
COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_max_f,
|
||||||
|
COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_max_f
|
||||||
|
FROM average_pollutants
|
||||||
|
),
|
||||||
|
|
||||||
|
hourly_results AS (
|
||||||
|
SELECT
|
||||||
|
device_id,
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
event_hour,
|
||||||
|
pm1_min, pm1_avg, pm1_max,
|
||||||
|
pm25_min_f, pm25_avg_f, pm25_max_f,
|
||||||
|
pm10_min_f, pm10_avg_f, pm10_max_f,
|
||||||
|
voc_min_f, voc_avg_f, voc_max_f,
|
||||||
|
co2_min_f, co2_avg_f, co2_max_f,
|
||||||
|
ch2o_min_f, ch2o_avg_f, ch2o_max_f,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_min_f),
|
||||||
|
calculate_aqi('pm10', pm10_min_f)
|
||||||
|
) AS hourly_min_aqi,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_avg_f),
|
||||||
|
calculate_aqi('pm10', pm10_avg_f)
|
||||||
|
) AS hourly_avg_aqi,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_max_f),
|
||||||
|
calculate_aqi('pm10', pm10_max_f)
|
||||||
|
) AS hourly_max_aqi,
|
||||||
|
|
||||||
|
classify_aqi(GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_avg_f),
|
||||||
|
calculate_aqi('pm10', pm10_avg_f)
|
||||||
|
)) AS aqi_category,
|
||||||
|
|
||||||
|
classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category,
|
||||||
|
classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category,
|
||||||
|
classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category,
|
||||||
|
classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category,
|
||||||
|
classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category
|
||||||
|
|
||||||
|
FROM filled_pollutants
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_category_counts AS (
|
||||||
|
SELECT device_id, space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY device_id, space_id, event_date, aqi_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT device_id, space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY device_id, space_id, event_date, pm25_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT device_id, space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY device_id, space_id, event_date, pm10_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT device_id, space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY device_id, space_id, event_date, voc_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT device_id, space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY device_id, space_id, event_date, co2_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT device_id, space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY device_id, space_id, event_date, ch2o_category
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_totals AS (
|
||||||
|
SELECT
|
||||||
|
device_id,
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
SUM(category_count) AS total_count
|
||||||
|
FROM daily_category_counts
|
||||||
|
where pollutant = 'aqi'
|
||||||
|
GROUP BY device_id, space_id, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Pivot Categories into Columns
|
||||||
|
daily_percentages AS (
|
||||||
|
select
|
||||||
|
dt.device_id,
|
||||||
|
dt.space_id,
|
||||||
|
dt.event_date,
|
||||||
|
-- AQI CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage,
|
||||||
|
-- PM25 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage,
|
||||||
|
-- PM10 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage,
|
||||||
|
-- VOC CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage,
|
||||||
|
-- CO2 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage,
|
||||||
|
-- CH20 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage
|
||||||
|
FROM daily_totals dt
|
||||||
|
LEFT JOIN daily_category_counts dcc
|
||||||
|
ON dt.device_id = dcc.device_id AND dt.event_date = dcc.event_date
|
||||||
|
GROUP BY dt.device_id, dt.space_id, dt.event_date, dt.total_count
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_averages AS (
|
||||||
|
SELECT
|
||||||
|
device_id,
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
-- AQI
|
||||||
|
ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi,
|
||||||
|
ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi,
|
||||||
|
ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi,
|
||||||
|
-- PM25
|
||||||
|
ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25,
|
||||||
|
ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25,
|
||||||
|
ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25,
|
||||||
|
-- PM10
|
||||||
|
ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10,
|
||||||
|
ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10,
|
||||||
|
ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10,
|
||||||
|
-- VOC
|
||||||
|
ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc,
|
||||||
|
ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc,
|
||||||
|
ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc,
|
||||||
|
-- CO2
|
||||||
|
ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2,
|
||||||
|
ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2,
|
||||||
|
ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2,
|
||||||
|
-- CH2O
|
||||||
|
ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o,
|
||||||
|
ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o,
|
||||||
|
ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o
|
||||||
|
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY device_id, space_id, event_date
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
p.device_id,
|
||||||
|
p.space_id,
|
||||||
|
p.event_date,
|
||||||
|
p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage,
|
||||||
|
a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi,
|
||||||
|
p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage,
|
||||||
|
a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25,
|
||||||
|
p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage,
|
||||||
|
a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10,
|
||||||
|
p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage,
|
||||||
|
a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc,
|
||||||
|
p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage,
|
||||||
|
a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2,
|
||||||
|
p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage,
|
||||||
|
a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o
|
||||||
|
FROM daily_percentages p
|
||||||
|
LEFT JOIN daily_averages a
|
||||||
|
ON p.device_id = a.device_id AND p.event_date = a.event_date
|
||||||
|
ORDER BY p.space_id, p.event_date;
|
||||||
|
|
||||||
|
|
@ -0,0 +1,275 @@
|
|||||||
|
-- Query Pipeline Starts Here
|
||||||
|
WITH device_space 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".code,
|
||||||
|
"device-status-log".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 = 'hjjcy'
|
||||||
|
),
|
||||||
|
|
||||||
|
average_pollutants AS (
|
||||||
|
SELECT
|
||||||
|
event_time::date AS event_date,
|
||||||
|
date_trunc('hour', event_time) AS event_hour,
|
||||||
|
space_id,
|
||||||
|
|
||||||
|
-- PM1
|
||||||
|
MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min,
|
||||||
|
AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max,
|
||||||
|
|
||||||
|
-- PM25
|
||||||
|
MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min,
|
||||||
|
AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max,
|
||||||
|
|
||||||
|
-- PM10
|
||||||
|
MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min,
|
||||||
|
AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg,
|
||||||
|
MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max,
|
||||||
|
|
||||||
|
-- VOC
|
||||||
|
MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min,
|
||||||
|
AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg,
|
||||||
|
MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max,
|
||||||
|
|
||||||
|
-- CH2O
|
||||||
|
MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min,
|
||||||
|
AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg,
|
||||||
|
MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max,
|
||||||
|
|
||||||
|
-- CO2
|
||||||
|
MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min,
|
||||||
|
AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg,
|
||||||
|
MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max
|
||||||
|
|
||||||
|
FROM device_space
|
||||||
|
GROUP BY space_id, event_hour, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
filled_pollutants AS (
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
-- AVG
|
||||||
|
COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f,
|
||||||
|
COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f,
|
||||||
|
COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f,
|
||||||
|
COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f,
|
||||||
|
COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f,
|
||||||
|
|
||||||
|
-- MIN
|
||||||
|
COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f,
|
||||||
|
COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f,
|
||||||
|
COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f,
|
||||||
|
COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f,
|
||||||
|
COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f,
|
||||||
|
|
||||||
|
-- MAX
|
||||||
|
COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f,
|
||||||
|
COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f,
|
||||||
|
COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f,
|
||||||
|
COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f,
|
||||||
|
COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f
|
||||||
|
FROM average_pollutants
|
||||||
|
),
|
||||||
|
|
||||||
|
hourly_results AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
event_hour,
|
||||||
|
pm1_min, pm1_avg, pm1_max,
|
||||||
|
pm25_min_f, pm25_avg_f, pm25_max_f,
|
||||||
|
pm10_min_f, pm10_avg_f, pm10_max_f,
|
||||||
|
voc_min_f, voc_avg_f, voc_max_f,
|
||||||
|
co2_min_f, co2_avg_f, co2_max_f,
|
||||||
|
ch2o_min_f, ch2o_avg_f, ch2o_max_f,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_min_f),
|
||||||
|
calculate_aqi('pm10', pm10_min_f)
|
||||||
|
) AS hourly_min_aqi,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_avg_f),
|
||||||
|
calculate_aqi('pm10', pm10_avg_f)
|
||||||
|
) AS hourly_avg_aqi,
|
||||||
|
|
||||||
|
GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_max_f),
|
||||||
|
calculate_aqi('pm10', pm10_max_f)
|
||||||
|
) AS hourly_max_aqi,
|
||||||
|
|
||||||
|
classify_aqi(GREATEST(
|
||||||
|
calculate_aqi('pm25', pm25_avg_f),
|
||||||
|
calculate_aqi('pm10', pm10_avg_f)
|
||||||
|
)) AS aqi_category,
|
||||||
|
|
||||||
|
classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category,
|
||||||
|
classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category,
|
||||||
|
classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category,
|
||||||
|
classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category,
|
||||||
|
classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category
|
||||||
|
|
||||||
|
FROM filled_pollutants
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_category_counts AS (
|
||||||
|
SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, aqi_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, pm25_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, pm10_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, voc_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, co2_category
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date, ch2o_category
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_totals AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
SUM(category_count) AS total_count
|
||||||
|
FROM daily_category_counts
|
||||||
|
where pollutant = 'aqi'
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Pivot Categories into Columns
|
||||||
|
daily_percentages AS (
|
||||||
|
select
|
||||||
|
dt.space_id,
|
||||||
|
dt.event_date,
|
||||||
|
-- AQI CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage,
|
||||||
|
-- PM25 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage,
|
||||||
|
-- PM10 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage,
|
||||||
|
-- VOC CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage,
|
||||||
|
-- CO2 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage,
|
||||||
|
-- CH20 CATEGORIES
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage,
|
||||||
|
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage
|
||||||
|
FROM daily_totals dt
|
||||||
|
LEFT JOIN daily_category_counts dcc
|
||||||
|
ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date
|
||||||
|
GROUP BY dt.space_id, dt.event_date, dt.total_count
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_averages AS (
|
||||||
|
SELECT
|
||||||
|
space_id,
|
||||||
|
event_date,
|
||||||
|
-- AQI
|
||||||
|
ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi,
|
||||||
|
ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi,
|
||||||
|
ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi,
|
||||||
|
-- PM25
|
||||||
|
ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25,
|
||||||
|
ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25,
|
||||||
|
ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25,
|
||||||
|
-- PM10
|
||||||
|
ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10,
|
||||||
|
ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10,
|
||||||
|
ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10,
|
||||||
|
-- VOC
|
||||||
|
ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc,
|
||||||
|
ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc,
|
||||||
|
ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc,
|
||||||
|
-- CO2
|
||||||
|
ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2,
|
||||||
|
ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2,
|
||||||
|
ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2,
|
||||||
|
-- CH2O
|
||||||
|
ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o,
|
||||||
|
ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o,
|
||||||
|
ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o
|
||||||
|
|
||||||
|
FROM hourly_results
|
||||||
|
GROUP BY space_id, event_date
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
p.space_id,
|
||||||
|
p.event_date,
|
||||||
|
p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage,
|
||||||
|
a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi,
|
||||||
|
p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage,
|
||||||
|
a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25,
|
||||||
|
p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage,
|
||||||
|
a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10,
|
||||||
|
p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage,
|
||||||
|
a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc,
|
||||||
|
p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage,
|
||||||
|
a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2,
|
||||||
|
p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage,
|
||||||
|
a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o
|
||||||
|
FROM daily_percentages p
|
||||||
|
LEFT JOIN daily_averages a
|
||||||
|
ON p.space_id = a.space_id AND p.event_date = a.event_date
|
||||||
|
ORDER BY p.space_id, p.event_date;
|
||||||
|
|
||||||
|
|
@ -1,91 +1,90 @@
|
|||||||
-- Step 1: Get device presence events with previous timestamps
|
WITH presence_logs AS (
|
||||||
WITH start_date AS (
|
SELECT
|
||||||
SELECT
|
d.space_device_uuid AS space_id,
|
||||||
d.uuid AS device_id,
|
l.device_id,
|
||||||
d.space_device_uuid AS space_id,
|
l.event_time,
|
||||||
l.value,
|
l.value,
|
||||||
l.event_time::timestamp AS event_time,
|
LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time,
|
||||||
LAG(l.event_time::timestamp) OVER (PARTITION BY d.uuid ORDER BY l.event_time) AS prev_timestamp
|
LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value
|
||||||
FROM device d
|
FROM device d
|
||||||
LEFT JOIN "device-status-log" l
|
JOIN "device-status-log" l ON d.uuid = l.device_id
|
||||||
ON d.uuid = l.device_id
|
JOIN product p ON p.uuid = d.product_device_uuid
|
||||||
LEFT JOIN product p
|
WHERE l.code = 'presence_state'
|
||||||
ON p.uuid = d.product_device_uuid
|
AND p.cat_name = 'hps'
|
||||||
WHERE p.cat_name = 'hps'
|
|
||||||
AND l.code = 'presence_state'
|
|
||||||
),
|
),
|
||||||
|
|
||||||
-- Step 2: Identify periods when device reports "none"
|
-- Intervals when device was in 'presence' (between prev_time and event_time when value='none')
|
||||||
device_none_periods AS (
|
presence_intervals AS (
|
||||||
SELECT
|
SELECT
|
||||||
space_id,
|
space_id,
|
||||||
device_id,
|
prev_time AS start_time,
|
||||||
event_time AS empty_from,
|
event_time AS end_time
|
||||||
LEAD(event_time) OVER (PARTITION BY device_id ORDER BY event_time) AS empty_until
|
FROM presence_logs
|
||||||
FROM start_date
|
WHERE value = 'none'
|
||||||
WHERE value = 'none'
|
AND prev_value = 'presence'
|
||||||
|
AND prev_time IS NOT NULL
|
||||||
),
|
),
|
||||||
|
|
||||||
-- Step 3: Clip the "none" periods to the edges of each day
|
-- Split intervals across days
|
||||||
clipped_device_none_periods AS (
|
split_intervals AS (
|
||||||
SELECT
|
SELECT
|
||||||
space_id,
|
space_id,
|
||||||
GREATEST(empty_from, DATE_TRUNC('day', empty_from)) AS clipped_from,
|
generate_series(
|
||||||
LEAST(empty_until, DATE_TRUNC('day', empty_until) + INTERVAL '1 day') AS clipped_until
|
date_trunc('day', start_time),
|
||||||
FROM device_none_periods
|
date_trunc('day', end_time),
|
||||||
WHERE empty_until IS NOT NULL
|
interval '1 day'
|
||||||
|
)::date AS event_date,
|
||||||
|
GREATEST(start_time, date_trunc('day', start_time)) AS interval_start,
|
||||||
|
LEAST(end_time, date_trunc('day', end_time) + interval '1 day') AS interval_end
|
||||||
|
FROM presence_intervals
|
||||||
),
|
),
|
||||||
|
|
||||||
-- Step 4: Break multi-day periods into daily intervals
|
-- Mark and group overlapping intervals per space per day
|
||||||
generated_daily_intervals AS (
|
ordered_intervals AS (
|
||||||
SELECT
|
SELECT
|
||||||
space_id,
|
space_id,
|
||||||
gs::date AS day,
|
event_date,
|
||||||
GREATEST(clipped_from, gs) AS interval_start,
|
interval_start,
|
||||||
LEAST(clipped_until, gs + INTERVAL '1 day') AS interval_end
|
interval_end,
|
||||||
FROM clipped_device_none_periods,
|
LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end
|
||||||
LATERAL generate_series(DATE_TRUNC('day', clipped_from), DATE_TRUNC('day', clipped_until), INTERVAL '1 day') AS gs
|
FROM split_intervals
|
||||||
),
|
),
|
||||||
|
|
||||||
-- Step 5: Merge overlapping or adjacent intervals per day
|
grouped_intervals AS (
|
||||||
|
SELECT *,
|
||||||
|
SUM(CASE
|
||||||
|
WHEN prev_end IS NULL OR interval_start > prev_end THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp
|
||||||
|
FROM ordered_intervals
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Merge overlapping intervals per group
|
||||||
merged_intervals AS (
|
merged_intervals AS (
|
||||||
SELECT
|
SELECT
|
||||||
space_id,
|
space_id,
|
||||||
day,
|
event_date,
|
||||||
interval_start,
|
MIN(interval_start) AS merged_start,
|
||||||
interval_end
|
MAX(interval_end) AS merged_end
|
||||||
FROM (
|
FROM grouped_intervals
|
||||||
SELECT
|
GROUP BY space_id, event_date, grp
|
||||||
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
|
-- Sum durations of merged intervals
|
||||||
missing_seconds_per_day AS (
|
summed_intervals AS (
|
||||||
SELECT
|
SELECT
|
||||||
space_id,
|
space_id,
|
||||||
day AS missing_date,
|
event_date,
|
||||||
SUM(EXTRACT(EPOCH FROM (interval_end - interval_start))) AS total_missing_seconds
|
SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds
|
||||||
FROM merged_intervals
|
FROM merged_intervals
|
||||||
GROUP BY space_id, day
|
GROUP BY space_id, event_date
|
||||||
),
|
|
||||||
|
|
||||||
-- Step 7: Calculate total occupied time per day (86400 - missing)
|
|
||||||
occupied_seconds_per_day AS (
|
|
||||||
SELECT
|
|
||||||
space_id,
|
|
||||||
missing_date as date,
|
|
||||||
86400 - total_missing_seconds AS total_occupied_seconds
|
|
||||||
FROM missing_seconds_per_day
|
|
||||||
)
|
)
|
||||||
|
|
||||||
-- Final Output
|
-- Final output with capped seconds and percentage
|
||||||
SELECT *
|
SELECT
|
||||||
FROM occupied_seconds_per_day
|
space_id,
|
||||||
ORDER BY 1,2;
|
event_date,
|
||||||
|
LEAST(raw_occupied_seconds, 86400) AS occupied_seconds,
|
||||||
|
ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage
|
||||||
|
FROM summed_intervals
|
||||||
|
ORDER BY space_id, event_date;
|
@ -0,0 +1,106 @@
|
|||||||
|
-- This model shows the number of times a presence was detected per hour, per day
|
||||||
|
WITH device_logs AS (
|
||||||
|
SELECT
|
||||||
|
device.uuid AS device_id,
|
||||||
|
device.created_at,
|
||||||
|
device.device_tuya_uuid,
|
||||||
|
device.space_device_uuid AS space_id,
|
||||||
|
"device-status-log".event_id,
|
||||||
|
"device-status-log".event_time::timestamp,
|
||||||
|
"device-status-log".code,
|
||||||
|
"device-status-log".value,
|
||||||
|
"device-status-log".log,
|
||||||
|
LAG("device-status-log".event_time::timestamp)
|
||||||
|
OVER (PARTITION BY device.uuid
|
||||||
|
ORDER BY "device-status-log".event_time) AS prev_timestamp,
|
||||||
|
LAG("device-status-log".value)
|
||||||
|
OVER (PARTITION BY device.uuid
|
||||||
|
ORDER BY "device-status-log".event_time) AS prev_value
|
||||||
|
FROM device
|
||||||
|
LEFT JOIN "device-status-log"
|
||||||
|
ON device.uuid = "device-status-log".device_id
|
||||||
|
LEFT JOIN product
|
||||||
|
ON product.uuid = device.product_device_uuid
|
||||||
|
WHERE product.cat_name = 'hps'
|
||||||
|
AND "device-status-log".code = 'presence_state'
|
||||||
|
),
|
||||||
|
|
||||||
|
presence_detection AS (
|
||||||
|
SELECT *,
|
||||||
|
CASE
|
||||||
|
WHEN value = 'motion' AND prev_value = 'none' THEN 1 ELSE 0
|
||||||
|
END AS motion_detected,
|
||||||
|
CASE
|
||||||
|
WHEN value = 'presence' AND prev_value = 'none' THEN 1 ELSE 0
|
||||||
|
END AS presence_detected
|
||||||
|
FROM device_logs
|
||||||
|
),
|
||||||
|
|
||||||
|
presence_detection_summary AS (
|
||||||
|
SELECT
|
||||||
|
pd.device_id,
|
||||||
|
d.subspace_id,
|
||||||
|
pd.space_id,
|
||||||
|
pd.event_time::date AS event_date,
|
||||||
|
EXTRACT(HOUR FROM pd.event_time)::int AS event_hour,
|
||||||
|
SUM(motion_detected) AS count_motion_detected,
|
||||||
|
SUM(presence_detected) AS count_presence_detected,
|
||||||
|
SUM(motion_detected + presence_detected) AS count_total_presence_detected
|
||||||
|
FROM presence_detection pd
|
||||||
|
LEFT JOIN device d ON d.uuid = pd.device_id
|
||||||
|
GROUP BY 1, 2, 3, 4, 5
|
||||||
|
),
|
||||||
|
|
||||||
|
all_dates_and_hours AS (
|
||||||
|
SELECT device_id, subspace_id, space_id, event_date, event_hour
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT device_id, subspace_id, space_id, event_date
|
||||||
|
FROM presence_detection_summary
|
||||||
|
) d
|
||||||
|
CROSS JOIN generate_series(0, 23) AS event_hour
|
||||||
|
),
|
||||||
|
|
||||||
|
table_final AS (
|
||||||
|
SELECT
|
||||||
|
adah.device_id,
|
||||||
|
adah.event_date,
|
||||||
|
COALESCE(pds.count_motion_detected, 0) AS count_motion_detected,
|
||||||
|
COALESCE(pds.count_presence_detected, 0) AS count_presence_detected,
|
||||||
|
COALESCE(pds.count_total_presence_detected, 0) AS count_total_presence_detected
|
||||||
|
FROM all_dates_and_hours adah
|
||||||
|
LEFT JOIN presence_detection_summary pds
|
||||||
|
ON pds.device_id = adah.device_id
|
||||||
|
AND pds.event_date = adah.event_date
|
||||||
|
AND pds.event_hour = adah.event_hour
|
||||||
|
),
|
||||||
|
|
||||||
|
daily_aggregate AS (
|
||||||
|
SELECT
|
||||||
|
device_id,
|
||||||
|
event_date,
|
||||||
|
SUM(count_motion_detected) AS count_motion_detected,
|
||||||
|
SUM(count_presence_detected) AS count_presence_detected,
|
||||||
|
SUM(count_total_presence_detected) AS count_total_presence_detected
|
||||||
|
FROM table_final
|
||||||
|
GROUP BY device_id, event_date
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO public."presence-sensor-daily-device-detection" (
|
||||||
|
device_uuid,
|
||||||
|
event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
device_id,
|
||||||
|
event_date,
|
||||||
|
count_motion_detected,
|
||||||
|
count_presence_detected,
|
||||||
|
count_total_presence_detected
|
||||||
|
FROM daily_aggregate
|
||||||
|
ON CONFLICT (device_uuid, event_date) DO UPDATE
|
||||||
|
SET
|
||||||
|
count_motion_detected = EXCLUDED.count_motion_detected,
|
||||||
|
count_presence_detected = EXCLUDED.count_presence_detected,
|
||||||
|
count_total_presence_detected = EXCLUDED.count_total_presence_detected;
|
18
libs/common/src/util/calculate.aqi.ts
Normal file
18
libs/common/src/util/calculate.aqi.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export function calculateAQI(pm2_5: number): number {
|
||||||
|
const breakpoints = [
|
||||||
|
{ pmLow: 0.0, pmHigh: 12.0, aqiLow: 0, aqiHigh: 50 },
|
||||||
|
{ pmLow: 12.1, pmHigh: 35.4, aqiLow: 51, aqiHigh: 100 },
|
||||||
|
{ pmLow: 35.5, pmHigh: 55.4, aqiLow: 101, aqiHigh: 150 },
|
||||||
|
{ pmLow: 55.5, pmHigh: 150.4, aqiLow: 151, aqiHigh: 200 },
|
||||||
|
{ pmLow: 150.5, pmHigh: 250.4, aqiLow: 201, aqiHigh: 300 },
|
||||||
|
{ pmLow: 250.5, pmHigh: 500.4, aqiLow: 301, aqiHigh: 500 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const bp = breakpoints.find((b) => pm2_5 >= b.pmLow && pm2_5 <= b.pmHigh);
|
||||||
|
if (!bp) return pm2_5 > 500.4 ? 500 : 0; // Handle out-of-range values
|
||||||
|
|
||||||
|
return Math.round(
|
||||||
|
((bp.aqiHigh - bp.aqiLow) / (bp.pmHigh - bp.pmLow)) * (pm2_5 - bp.pmLow) +
|
||||||
|
bp.aqiLow,
|
||||||
|
);
|
||||||
|
}
|
11
libs/common/src/util/device-utils.ts
Normal file
11
libs/common/src/util/device-utils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { DeviceEntity } from '../modules/device/entities';
|
||||||
|
|
||||||
|
export function addSpaceUuidToDevices(
|
||||||
|
devices: DeviceEntity[],
|
||||||
|
spaceUuid: string,
|
||||||
|
): DeviceEntity[] {
|
||||||
|
return devices.map((device) => {
|
||||||
|
(device as any).spaceUuid = spaceUuid;
|
||||||
|
return device;
|
||||||
|
});
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as nodemailer from 'nodemailer';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import * as nodemailer from 'nodemailer';
|
||||||
import {
|
import {
|
||||||
SEND_EMAIL_API_URL_DEV,
|
SEND_EMAIL_API_URL_DEV,
|
||||||
SEND_EMAIL_API_URL_PROD,
|
SEND_EMAIL_API_URL_PROD,
|
||||||
@ -83,12 +83,17 @@ export class EmailService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async sendEmailWithTemplate(
|
async sendEmailWithTemplate({
|
||||||
email: string,
|
email,
|
||||||
name: string,
|
name,
|
||||||
isEnable: boolean,
|
isEnable,
|
||||||
isDelete: boolean,
|
isDelete,
|
||||||
): Promise<void> {
|
}: {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
isEnable: boolean;
|
||||||
|
isDelete: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
const API_TOKEN = this.configService.get<string>(
|
const API_TOKEN = this.configService.get<string>(
|
||||||
'email-config.MAILTRAP_API_TOKEN',
|
'email-config.MAILTRAP_API_TOKEN',
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"",
|
"format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"",
|
||||||
"start": "npm run test && node dist/main",
|
"start": "npm run test && node dist/main",
|
||||||
"start:dev": "npm run test && npx nest start --watch",
|
"start:dev": "npm run test && npx nest start --watch",
|
||||||
|
"dev": "npx nest start --watch",
|
||||||
"start:debug": "npm run test && npx nest start --debug --watch",
|
"start:debug": "npm run test && npx nest start --debug --watch",
|
||||||
"start:prod": "npm run test && node dist/main",
|
"start:prod": "npm run test && node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
@ -1,48 +1,50 @@
|
|||||||
|
import { SeederModule } from '@app/common/seed/seeder.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import config from './config';
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
import { WinstonModule } from 'nest-winston';
|
||||||
import { AuthenticationModule } from './auth/auth.module';
|
import { AuthenticationModule } from './auth/auth.module';
|
||||||
import { UserModule } from './users/user.module';
|
|
||||||
import { GroupModule } from './group/group.module';
|
|
||||||
import { DeviceModule } from './device/device.module';
|
|
||||||
import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module';
|
|
||||||
import { CommunityModule } from './community/community.module';
|
|
||||||
import { SeederModule } from '@app/common/seed/seeder.module';
|
|
||||||
import { UserNotificationModule } from './user-notification/user-notification.module';
|
|
||||||
import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module';
|
|
||||||
import { SceneModule } from './scene/scene.module';
|
|
||||||
import { DoorLockModule } from './door-lock/door.lock.module';
|
|
||||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
|
||||||
import { LoggingInterceptor } from './interceptors/logging.interceptor';
|
|
||||||
import { AutomationModule } from './automation/automation.module';
|
import { AutomationModule } from './automation/automation.module';
|
||||||
import { RegionModule } from './region/region.module';
|
|
||||||
import { TimeZoneModule } from './timezone/timezone.module';
|
|
||||||
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
|
|
||||||
import { ScheduleModule } from './schedule/schedule.module';
|
|
||||||
import { SpaceModule } from './space/space.module';
|
|
||||||
import { ProductModule } from './product';
|
|
||||||
import { ProjectModule } from './project';
|
|
||||||
import { SpaceModelModule } from './space-model';
|
|
||||||
import { InviteUserModule } from './invite-user/invite-user.module';
|
|
||||||
import { PermissionModule } from './permission/permission.module';
|
|
||||||
import { RoleModule } from './role/role.module';
|
|
||||||
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
|
|
||||||
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
|
|
||||||
import { TagModule } from './tags/tags.module';
|
|
||||||
import { ClientModule } from './client/client.module';
|
import { ClientModule } from './client/client.module';
|
||||||
import { DeviceCommissionModule } from './commission-device/commission-device.module';
|
import { DeviceCommissionModule } from './commission-device/commission-device.module';
|
||||||
import { PowerClampModule } from './power-clamp/power-clamp.module';
|
import { CommunityModule } from './community/community.module';
|
||||||
import { WinstonModule } from 'nest-winston';
|
import config from './config';
|
||||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module';
|
||||||
|
import { DeviceModule } from './device/device.module';
|
||||||
|
import { DoorLockModule } from './door-lock/door.lock.module';
|
||||||
|
import { GroupModule } from './group/group.module';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
|
import { LoggingInterceptor } from './interceptors/logging.interceptor';
|
||||||
|
import { InviteUserModule } from './invite-user/invite-user.module';
|
||||||
|
import { PermissionModule } from './permission/permission.module';
|
||||||
|
import { PowerClampModule } from './power-clamp/power-clamp.module';
|
||||||
|
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
|
||||||
|
import { ProductModule } from './product';
|
||||||
|
import { ProjectModule } from './project';
|
||||||
|
import { RegionModule } from './region/region.module';
|
||||||
|
import { RoleModule } from './role/role.module';
|
||||||
|
import { SceneModule } from './scene/scene.module';
|
||||||
|
import { ScheduleModule } from './schedule/schedule.module';
|
||||||
|
import { SpaceModelModule } from './space-model';
|
||||||
|
import { SpaceModule } from './space/space.module';
|
||||||
|
import { TagModule } from './tags/tags.module';
|
||||||
|
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
|
||||||
|
import { TimeZoneModule } from './timezone/timezone.module';
|
||||||
|
import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module';
|
||||||
|
import { UserNotificationModule } from './user-notification/user-notification.module';
|
||||||
|
import { UserModule } from './users/user.module';
|
||||||
|
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
|
||||||
|
|
||||||
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
|
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
|
||||||
|
import { AqiModule } from './aqi/aqi.module';
|
||||||
|
import { OccupancyModule } from './occupancy/occupancy.module';
|
||||||
|
import { WeatherModule } from './weather/weather.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 +79,16 @@ import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston
|
|||||||
DeviceCommissionModule,
|
DeviceCommissionModule,
|
||||||
PowerClampModule,
|
PowerClampModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
|
OccupancyModule,
|
||||||
|
WeatherModule,
|
||||||
|
AqiModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: APP_INTERCEPTOR,
|
provide: APP_INTERCEPTOR,
|
||||||
useClass: LoggingInterceptor,
|
useClass: LoggingInterceptor,
|
||||||
},
|
},
|
||||||
/* {
|
/* {
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: ThrottlerGuard,
|
useClass: ThrottlerGuard,
|
||||||
}, */
|
}, */
|
||||||
|
11
src/aqi/aqi.module.ts
Normal file
11
src/aqi/aqi.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { AqiService } from './services';
|
||||||
|
import { AqiController } from './controllers';
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
controllers: [AqiController],
|
||||||
|
providers: [AqiService, SqlLoaderService],
|
||||||
|
})
|
||||||
|
export class AqiModule {}
|
64
src/aqi/controllers/aqi.controller.ts
Normal file
64
src/aqi/controllers/aqi.controller.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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 { AqiService } from '../services/aqi.service';
|
||||||
|
import {
|
||||||
|
GetAqiDailyBySpaceDto,
|
||||||
|
GetAqiPollutantBySpaceDto,
|
||||||
|
} from '../dto/get-aqi.dto';
|
||||||
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
|
import { SpaceParamsDto } from '../dto/aqi-params.dto';
|
||||||
|
|
||||||
|
@ApiTags('AQI Module')
|
||||||
|
@Controller({
|
||||||
|
version: EnableDisableStatusEnum.ENABLED,
|
||||||
|
path: ControllerRoute.AQI.ROUTE,
|
||||||
|
})
|
||||||
|
export class AqiController {
|
||||||
|
constructor(private readonly aqiService: AqiService) {}
|
||||||
|
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('range/space/:spaceUuid')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_SUMMARY,
|
||||||
|
description: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_DESCRIPTION,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'spaceUuid',
|
||||||
|
description: 'UUID of the Space',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
async getAQIRangeDataBySpace(
|
||||||
|
@Param() params: SpaceParamsDto,
|
||||||
|
@Query() query: GetAqiDailyBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
return await this.aqiService.getAQIRangeDataBySpace(params, query);
|
||||||
|
}
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('distribution/space/:spaceUuid')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_SUMMARY,
|
||||||
|
description:
|
||||||
|
ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_DESCRIPTION,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'spaceUuid',
|
||||||
|
description: 'UUID of the Space',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
async getAQIDistributionDataBySpace(
|
||||||
|
@Param() params: SpaceParamsDto,
|
||||||
|
@Query() query: GetAqiPollutantBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
return await this.aqiService.getAQIDistributionDataBySpace(params, query);
|
||||||
|
}
|
||||||
|
}
|
1
src/aqi/controllers/index.ts
Normal file
1
src/aqi/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './aqi.controller';
|
7
src/aqi/dto/aqi-params.dto.ts
Normal file
7
src/aqi/dto/aqi-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;
|
||||||
|
}
|
37
src/aqi/dto/get-aqi.dto.ts
Normal file
37
src/aqi/dto/get-aqi.dto.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { PollutantType } from '@app/common/constants/pollutants.enum';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Matches, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class GetAqiDailyBySpaceDto {
|
||||||
|
@ApiProperty({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
export class GetAqiPollutantBySpaceDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Pollutant Type',
|
||||||
|
enum: PollutantType,
|
||||||
|
example: PollutantType.AQI,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
public pollutantType: string;
|
||||||
|
@ApiProperty({
|
||||||
|
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;
|
||||||
|
}
|
138
src/aqi/services/aqi.service.ts
Normal file
138
src/aqi/services/aqi.service.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
GetAqiDailyBySpaceDto,
|
||||||
|
GetAqiPollutantBySpaceDto,
|
||||||
|
} from '../dto/get-aqi.dto';
|
||||||
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
|
import { SpaceParamsDto } from '../dto/aqi-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';
|
||||||
|
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
||||||
|
import { PollutantType } from '@app/common/constants/pollutants.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AqiService {
|
||||||
|
constructor(
|
||||||
|
private readonly sqlLoader: SqlLoaderService,
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
async getAQIDistributionDataBySpace(
|
||||||
|
params: SpaceParamsDto,
|
||||||
|
query: GetAqiPollutantBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
const { monthDate, pollutantType } = query;
|
||||||
|
const { spaceUuid } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.executeProcedure(
|
||||||
|
'fact_daily_space_aqi',
|
||||||
|
'proceduce_select_daily_space_aqi',
|
||||||
|
[spaceUuid, monthDate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
'good',
|
||||||
|
'moderate',
|
||||||
|
'unhealthy_sensitive',
|
||||||
|
'unhealthy',
|
||||||
|
'very_unhealthy',
|
||||||
|
'hazardous',
|
||||||
|
];
|
||||||
|
|
||||||
|
const transformedData = data.map((item) => {
|
||||||
|
const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||||
|
|
||||||
|
const categoryData = categories.map((category) => {
|
||||||
|
const key = `${category}_${pollutantType.toLowerCase()}_percentage`;
|
||||||
|
return {
|
||||||
|
type: category,
|
||||||
|
percentage: item[key] ?? 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { date, data: categoryData };
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = this.buildResponse(
|
||||||
|
`AQI distribution data fetched successfully for ${spaceUuid} space and pollutant ${pollutantType}`,
|
||||||
|
transformedData,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch AQI distribution data', {
|
||||||
|
error,
|
||||||
|
spaceUuid,
|
||||||
|
});
|
||||||
|
throw new HttpException(
|
||||||
|
error.response?.message || 'Failed to fetch AQI distribution data',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAQIRangeDataBySpace(
|
||||||
|
params: SpaceParamsDto,
|
||||||
|
query: GetAqiDailyBySpaceDto,
|
||||||
|
): Promise<BaseResponseDto> {
|
||||||
|
const { monthDate } = query;
|
||||||
|
const { spaceUuid } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.executeProcedure(
|
||||||
|
'fact_daily_space_aqi',
|
||||||
|
'proceduce_select_daily_space_aqi',
|
||||||
|
[spaceUuid, monthDate],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Define pollutants dynamically
|
||||||
|
const pollutants = Object.values(PollutantType);
|
||||||
|
|
||||||
|
const transformedData = data.map((item) => {
|
||||||
|
const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||||
|
const dailyData = pollutants.map((type) => ({
|
||||||
|
type,
|
||||||
|
min: item[`daily_min_${type}`],
|
||||||
|
max: item[`daily_max_${type}`],
|
||||||
|
average: item[`daily_avg_${type}`],
|
||||||
|
}));
|
||||||
|
return { date, data: dailyData };
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = this.buildResponse(
|
||||||
|
`AQI data fetched successfully for ${spaceUuid} space`,
|
||||||
|
transformedData,
|
||||||
|
);
|
||||||
|
return convertKeysToCamelCase(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch AQI data', {
|
||||||
|
error,
|
||||||
|
spaceUuid,
|
||||||
|
});
|
||||||
|
throw new HttpException(
|
||||||
|
error.response?.message || 'Failed to fetch AQI 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);
|
||||||
|
}
|
||||||
|
}
|
1
src/aqi/services/index.ts
Normal file
1
src/aqi/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './aqi.service';
|
@ -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,8 @@ 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';
|
||||||
|
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, SpaceRepositoryModule],
|
imports: [ConfigModule, SpaceRepositoryModule],
|
||||||
@ -55,6 +57,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
AqiDataService,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as csv from 'csv-parser';
|
import * as csv from 'csv-parser';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||||
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||||
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
|
import { SpaceRepository } from '@app/common/modules/space';
|
||||||
|
import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity';
|
||||||
|
import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||||
|
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
||||||
|
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { DeviceService } from 'src/device/services';
|
import { DeviceService } from 'src/device/services';
|
||||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
|
||||||
import { SpaceRepository } from '@app/common/modules/space';
|
|
||||||
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
|
||||||
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
|
||||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
|
||||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeviceCommissionService {
|
export class DeviceCommissionService {
|
||||||
@ -118,7 +120,7 @@ export class DeviceCommissionService {
|
|||||||
where: { uuid: spaceId },
|
where: { uuid: spaceId },
|
||||||
relations: [
|
relations: [
|
||||||
'productAllocations',
|
'productAllocations',
|
||||||
'productAllocations.tags',
|
'productAllocations.tag',
|
||||||
'productAllocations.product',
|
'productAllocations.product',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -135,7 +137,7 @@ export class DeviceCommissionService {
|
|||||||
where: { uuid: subspaceId },
|
where: { uuid: subspaceId },
|
||||||
relations: [
|
relations: [
|
||||||
'productAllocations',
|
'productAllocations',
|
||||||
'productAllocations.tags',
|
'productAllocations.tag',
|
||||||
'productAllocations.product',
|
'productAllocations.product',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -151,19 +153,23 @@ export class DeviceCommissionService {
|
|||||||
subspace?.productAllocations || space.productAllocations;
|
subspace?.productAllocations || space.productAllocations;
|
||||||
|
|
||||||
const match = allocations
|
const match = allocations
|
||||||
.flatMap((pa) =>
|
.map(
|
||||||
(pa.tags || []).map((tag) => ({ product: pa.product, tag })),
|
({
|
||||||
|
product,
|
||||||
|
tag,
|
||||||
|
}:
|
||||||
|
| SpaceProductAllocationEntity
|
||||||
|
| SubspaceProductAllocationEntity) => ({ product, tag }),
|
||||||
)
|
)
|
||||||
.find(({ tag }) => tag.name === tagName);
|
.find(
|
||||||
|
({ tag, product }) =>
|
||||||
|
tag.name === tagName && product.name === productName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
console.error(`No matching tag found for Device ID: ${rawDeviceId}`);
|
console.error(
|
||||||
failureCount.value++;
|
`No matching tag-product combination found for Device ID: ${rawDeviceId}`,
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (match.product.name !== productName) {
|
|
||||||
console.error(`Product name mismatch for Device ID: ${rawDeviceId}`);
|
|
||||||
failureCount.value++;
|
failureCount.value++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
SpaceLinkRepository,
|
SpaceLinkRepository,
|
||||||
SpaceProductAllocationRepository,
|
SpaceProductAllocationRepository,
|
||||||
SpaceRepository,
|
SpaceRepository,
|
||||||
TagRepository,
|
|
||||||
} from '@app/common/modules/space/repositories';
|
} from '@app/common/modules/space/repositories';
|
||||||
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
|
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
|
||||||
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
|
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
|
||||||
@ -63,6 +62,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 { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
|
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
|
||||||
@ -85,6 +86,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
SpaceProductAllocationService,
|
SpaceProductAllocationService,
|
||||||
SpaceLinkRepository,
|
SpaceLinkRepository,
|
||||||
SubspaceRepository,
|
SubspaceRepository,
|
||||||
|
// Todo: find out why this is needed
|
||||||
TagService,
|
TagService,
|
||||||
SubspaceDeviceService,
|
SubspaceDeviceService,
|
||||||
SubspaceProductAllocationService,
|
SubspaceProductAllocationService,
|
||||||
@ -96,7 +98,6 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
SpaceModelProductAllocationService,
|
SpaceModelProductAllocationService,
|
||||||
SpaceProductAllocationRepository,
|
SpaceProductAllocationRepository,
|
||||||
SubspaceProductAllocationRepository,
|
SubspaceProductAllocationRepository,
|
||||||
TagRepository,
|
|
||||||
SubspaceModelRepository,
|
SubspaceModelRepository,
|
||||||
SubspaceModelProductAllocationService,
|
SubspaceModelProductAllocationService,
|
||||||
SpaceModelProductAllocationRepoitory,
|
SpaceModelProductAllocationRepoitory,
|
||||||
@ -114,6 +115,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
AqiDataService,
|
||||||
],
|
],
|
||||||
exports: [CommunityService, SpacePermissionService],
|
exports: [CommunityService, SpacePermissionService],
|
||||||
})
|
})
|
||||||
|
@ -1,21 +1,30 @@
|
|||||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
|
||||||
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
|
|
||||||
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';
|
||||||
|
import { PageResponse } from '@app/common/dto/pagination.response.dto';
|
||||||
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
import {
|
import {
|
||||||
ExtendedTypeORMCustomModelFindAllQuery,
|
ExtendedTypeORMCustomModelFindAllQuery,
|
||||||
TypeORMCustomModel,
|
TypeORMCustomModel,
|
||||||
} from '@app/common/models/typeOrmCustom.model';
|
} from '@app/common/models/typeOrmCustom.model';
|
||||||
import { PageResponse } from '@app/common/dto/pagination.response.dto';
|
|
||||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
|
||||||
import { CommunityDto } from '@app/common/modules/community/dtos';
|
import { CommunityDto } from '@app/common/modules/community/dtos';
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
import { CommunityEntity } from '@app/common/modules/community/entities';
|
||||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
import { DeviceEntity } from '@app/common/modules/device/entities';
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
|
|
||||||
import { ILike, In, Not } from 'typeorm';
|
|
||||||
import { SpaceService } from 'src/space/services';
|
|
||||||
import { SpaceRepository } from '@app/common/modules/space';
|
import { SpaceRepository } from '@app/common/modules/space';
|
||||||
|
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||||
|
import { addSpaceUuidToDevices } from '@app/common/util/device-utils';
|
||||||
|
import {
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { SpaceService } from 'src/space/services';
|
||||||
|
import { SelectQueryBuilder } from 'typeorm';
|
||||||
|
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
|
||||||
|
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityService {
|
export class CommunityService {
|
||||||
@ -85,56 +94,36 @@ export class CommunityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCommunities(
|
async getCommunities(
|
||||||
param: ProjectParam,
|
{ projectUuid }: ProjectParam,
|
||||||
pageable: Partial<ExtendedTypeORMCustomModelFindAllQuery>,
|
pageable: Partial<ExtendedTypeORMCustomModelFindAllQuery>,
|
||||||
): Promise<BaseResponseDto> {
|
): Promise<BaseResponseDto> {
|
||||||
try {
|
try {
|
||||||
const project = await this.validateProject(param.projectUuid);
|
const project = await this.validateProject(projectUuid);
|
||||||
|
|
||||||
pageable.modelName = 'community';
|
/**
|
||||||
pageable.where = {
|
* TODO: removing this breaks the code (should be fixed when refactoring @see TypeORMCustomModel
|
||||||
project: { uuid: param.projectUuid },
|
*/
|
||||||
name: Not(`${ORPHAN_COMMUNITY_NAME}-${project.name}`),
|
pageable.where = {};
|
||||||
};
|
let qb: undefined | SelectQueryBuilder<CommunityEntity> = undefined;
|
||||||
|
|
||||||
|
qb = this.communityRepository
|
||||||
|
.createQueryBuilder('c')
|
||||||
|
.leftJoin('c.spaces', 's', 's.disabled = false')
|
||||||
|
.where('c.project = :projectUuid', { projectUuid })
|
||||||
|
.andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`)
|
||||||
|
.distinct(true);
|
||||||
if (pageable.search) {
|
if (pageable.search) {
|
||||||
const matchingCommunities = await this.communityRepository.find({
|
qb.andWhere(
|
||||||
where: {
|
`c.name ILIKE '%${pageable.search}%' OR s.space_name ILIKE '%${pageable.search}%'`,
|
||||||
project: { uuid: param.projectUuid },
|
);
|
||||||
name: ILike(`%${pageable.search}%`),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const matchingSpaces = await this.spaceRepository.find({
|
|
||||||
where: {
|
|
||||||
spaceName: ILike(`%${pageable.search}%`),
|
|
||||||
disabled: false,
|
|
||||||
community: { project: { uuid: param.projectUuid } },
|
|
||||||
},
|
|
||||||
relations: ['community'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const spaceCommunityUuids = [
|
|
||||||
...new Set(matchingSpaces.map((space) => space.community.uuid)),
|
|
||||||
];
|
|
||||||
|
|
||||||
const allMatchedCommunityUuids = [
|
|
||||||
...new Set([
|
|
||||||
...matchingCommunities.map((c) => c.uuid),
|
|
||||||
...spaceCommunityUuids,
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
|
|
||||||
pageable.where = {
|
|
||||||
...pageable.where,
|
|
||||||
uuid: In(allMatchedCommunityUuids),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const customModel = TypeORMCustomModel(this.communityRepository);
|
const customModel = TypeORMCustomModel(this.communityRepository);
|
||||||
|
|
||||||
const { baseResponseDto, paginationResponseDto } =
|
const { baseResponseDto, paginationResponseDto } =
|
||||||
await customModel.findAll(pageable);
|
await customModel.findAll({ ...pageable, modelName: 'community' }, qb);
|
||||||
|
|
||||||
|
// todo: refactor this to minimize the number of queries
|
||||||
if (pageable.includeSpaces) {
|
if (pageable.includeSpaces) {
|
||||||
const communitiesWithSpaces = await Promise.all(
|
const communitiesWithSpaces = await Promise.all(
|
||||||
baseResponseDto.data.map(async (community: CommunityDto) => {
|
baseResponseDto.data.map(async (community: CommunityDto) => {
|
||||||
@ -142,7 +131,7 @@ export class CommunityService {
|
|||||||
await this.spaceService.getSpacesHierarchyForCommunity(
|
await this.spaceService.getSpacesHierarchyForCommunity(
|
||||||
{
|
{
|
||||||
communityUuid: community.uuid,
|
communityUuid: community.uuid,
|
||||||
projectUuid: param.projectUuid,
|
projectUuid: projectUuid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onlyWithDevices: false,
|
onlyWithDevices: false,
|
||||||
@ -303,4 +292,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(...addSpaceUuidToDevices(space.devices, space.uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import AuthConfig from './auth.config';
|
import AuthConfig from './auth.config';
|
||||||
import AppConfig from './app.config';
|
import AppConfig from './app.config';
|
||||||
import JwtConfig from './jwt.config';
|
import JwtConfig from './jwt.config';
|
||||||
export default [AuthConfig, AppConfig, JwtConfig];
|
import WeatherOpenConfig from './weather.open.config';
|
||||||
|
export default [AuthConfig, AppConfig, JwtConfig, WeatherOpenConfig];
|
||||||
|
9
src/config/weather.open.config.ts
Normal file
9
src/config/weather.open.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs(
|
||||||
|
'openweather-config',
|
||||||
|
(): Record<string, any> => ({
|
||||||
|
OPEN_WEATHER_MAP_API_KEY: process.env.OPEN_WEATHER_MAP_API_KEY,
|
||||||
|
WEATHER_API_URL: process.env.WEATHER_API_URL,
|
||||||
|
}),
|
||||||
|
);
|
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
|
||||||
|
}
|
||||||
|
@ -1,38 +1,46 @@
|
|||||||
import { ORPHAN_SPACE_NAME } from './../../../libs/common/src/constants/orphan-constant';
|
import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum';
|
||||||
import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository';
|
import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum';
|
||||||
|
import { BatteryStatus } from '@app/common/constants/battery-status.enum';
|
||||||
|
import { DeviceStatuses } from '@app/common/constants/device-status.enum';
|
||||||
|
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
|
||||||
|
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
|
||||||
|
import { ProductType } from '@app/common/constants/product-type.enum';
|
||||||
|
import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum';
|
||||||
|
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||||
|
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||||
|
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||||
|
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
||||||
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
import { DeviceEntity } from '@app/common/modules/device/entities';
|
||||||
|
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||||
|
import { ProjectEntity } from '@app/common/modules/project/entities';
|
||||||
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
|
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||||
|
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||||
|
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||||
|
import { addSpaceUuidToDevices } from '@app/common/util/device-utils';
|
||||||
import {
|
import {
|
||||||
Injectable,
|
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
|
||||||
NotFoundException,
|
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
Inject,
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
|
||||||
|
import { AddAutomationDto } from 'src/automation/dtos';
|
||||||
|
import { SceneService } from 'src/scene/services';
|
||||||
|
import { In, Not, QueryRunner } from 'typeorm';
|
||||||
|
import { ProjectParam } from '../dtos';
|
||||||
import {
|
import {
|
||||||
AddDeviceDto,
|
AddDeviceDto,
|
||||||
AddSceneToFourSceneDeviceDto,
|
AddSceneToFourSceneDeviceDto,
|
||||||
UpdateDeviceDto,
|
|
||||||
AssignDeviceToSpaceDto,
|
AssignDeviceToSpaceDto,
|
||||||
|
UpdateDeviceDto,
|
||||||
} from '../dtos/add.device.dto';
|
} from '../dtos/add.device.dto';
|
||||||
import {
|
|
||||||
DeviceInstructionResponse,
|
|
||||||
GetDeviceDetailsFunctionsInterface,
|
|
||||||
GetDeviceDetailsFunctionsStatusInterface,
|
|
||||||
GetDeviceDetailsInterface,
|
|
||||||
GetMacAddressInterface,
|
|
||||||
GetPowerClampFunctionsStatusInterface,
|
|
||||||
controlDeviceInterface,
|
|
||||||
getDeviceLogsInterface,
|
|
||||||
updateDeviceFirmwareInterface,
|
|
||||||
} from '../interfaces/get.device.interface';
|
|
||||||
import {
|
|
||||||
GetDeviceBySpaceUuidDto,
|
|
||||||
GetDeviceLogsDto,
|
|
||||||
GetDoorLockDevices,
|
|
||||||
} from '../dtos/get.device.dto';
|
|
||||||
import {
|
import {
|
||||||
BatchControlDevicesDto,
|
BatchControlDevicesDto,
|
||||||
BatchFactoryResetDevicesDto,
|
BatchFactoryResetDevicesDto,
|
||||||
@ -40,31 +48,29 @@ import {
|
|||||||
ControlDeviceDto,
|
ControlDeviceDto,
|
||||||
GetSceneFourSceneDeviceDto,
|
GetSceneFourSceneDeviceDto,
|
||||||
} from '../dtos/control.device.dto';
|
} from '../dtos/control.device.dto';
|
||||||
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
|
||||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
|
||||||
import { In, Not, QueryRunner } from 'typeorm';
|
|
||||||
import { ProductType } from '@app/common/constants/product-type.enum';
|
|
||||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
|
||||||
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
|
||||||
import { DeviceStatuses } from '@app/common/constants/device-status.enum';
|
|
||||||
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
|
|
||||||
import { BatteryStatus } from '@app/common/constants/battery-status.enum';
|
|
||||||
import { SceneService } from 'src/scene/services';
|
|
||||||
import { AddAutomationDto } from 'src/automation/dtos';
|
|
||||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
|
||||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
|
||||||
import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum';
|
|
||||||
import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum';
|
|
||||||
import { DeviceSceneParamDto } from '../dtos/device.param.dto';
|
|
||||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
|
||||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
|
||||||
import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto';
|
import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto';
|
||||||
import { DeviceEntity } from '@app/common/modules/device/entities';
|
import { DeviceSceneParamDto } from '../dtos/device.param.dto';
|
||||||
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
import {
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
GetDeviceLogsDto,
|
||||||
import { ProjectParam } from '../dtos';
|
GetDevicesBySpaceOrCommunityDto,
|
||||||
import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum';
|
GetDoorLockDevices,
|
||||||
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
|
} from '../dtos/get.device.dto';
|
||||||
|
import {
|
||||||
|
controlDeviceInterface,
|
||||||
|
DeviceInstructionResponse,
|
||||||
|
GetDeviceDetailsFunctionsInterface,
|
||||||
|
GetDeviceDetailsFunctionsStatusInterface,
|
||||||
|
GetDeviceDetailsInterface,
|
||||||
|
getDeviceLogsInterface,
|
||||||
|
GetMacAddressInterface,
|
||||||
|
GetPowerClampFunctionsStatusInterface,
|
||||||
|
updateDeviceFirmwareInterface,
|
||||||
|
} from '../interfaces/get.device.interface';
|
||||||
|
import {
|
||||||
|
ORPHAN_COMMUNITY_NAME,
|
||||||
|
ORPHAN_SPACE_NAME,
|
||||||
|
} from './../../../libs/common/src/constants/orphan-constant';
|
||||||
|
import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeviceService {
|
export class DeviceService {
|
||||||
@ -80,6 +86,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');
|
||||||
@ -194,46 +201,6 @@ export class DeviceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDevicesBySpaceId(
|
|
||||||
getDeviceBySpaceUuidDto: GetDeviceBySpaceUuidDto,
|
|
||||||
): Promise<GetDeviceDetailsInterface[]> {
|
|
||||||
try {
|
|
||||||
const devices = await this.deviceRepository.find({
|
|
||||||
where: {
|
|
||||||
spaceDevice: { uuid: getDeviceBySpaceUuidDto.spaceUuid },
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
relations: [
|
|
||||||
'spaceDevice',
|
|
||||||
'productDevice',
|
|
||||||
'permission',
|
|
||||||
'permission.permissionType',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const devicesData = await Promise.all(
|
|
||||||
devices.map(async (device) => {
|
|
||||||
return {
|
|
||||||
haveRoom: device.spaceDevice ? true : false,
|
|
||||||
productUuid: device.productDevice.uuid,
|
|
||||||
productType: device.productDevice.prodType,
|
|
||||||
permissionType: device.permission[0].permissionType.type,
|
|
||||||
...(await this.getDeviceDetailsByDeviceIdTuya(
|
|
||||||
device.deviceTuyaUuid,
|
|
||||||
)),
|
|
||||||
uuid: device.uuid,
|
|
||||||
} as GetDeviceDetailsInterface;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return devicesData;
|
|
||||||
} catch (error) {
|
|
||||||
// Handle the error here
|
|
||||||
throw new HttpException(
|
|
||||||
'Error fetching devices by space',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async transferDeviceInSpaces(
|
async transferDeviceInSpaces(
|
||||||
assignDeviceToSpaceDto: AssignDeviceToSpaceDto,
|
assignDeviceToSpaceDto: AssignDeviceToSpaceDto,
|
||||||
projectUuid: string,
|
projectUuid: string,
|
||||||
@ -1195,39 +1162,6 @@ export class DeviceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFullSpaceHierarchy(
|
|
||||||
space: SpaceEntity,
|
|
||||||
): Promise<{ uuid: string; spaceName: string }[]> {
|
|
||||||
try {
|
|
||||||
// Fetch only the relevant spaces, starting with the target space
|
|
||||||
const targetSpace = await this.spaceRepository.findOne({
|
|
||||||
where: { uuid: space.uuid },
|
|
||||||
relations: ['parent', 'children'],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch only the ancestors of the target space
|
|
||||||
const ancestors = await this.fetchAncestors(targetSpace);
|
|
||||||
|
|
||||||
// Optionally, fetch descendants if required
|
|
||||||
const descendants = await this.fetchDescendants(targetSpace);
|
|
||||||
|
|
||||||
const fullHierarchy = [...ancestors, targetSpace, ...descendants].map(
|
|
||||||
(space) => ({
|
|
||||||
uuid: space.uuid,
|
|
||||||
spaceName: space.spaceName,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return fullHierarchy;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching space hierarchy:', error.message);
|
|
||||||
throw new HttpException(
|
|
||||||
'Error fetching space hierarchy',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPowerClampInstructionStatus(deviceDetails: any) {
|
async getPowerClampInstructionStatus(deviceDetails: any) {
|
||||||
try {
|
try {
|
||||||
const deviceStatus = await this.getPowerClampInstructionStatusTuya(
|
const deviceStatus = await this.getPowerClampInstructionStatusTuya(
|
||||||
@ -1327,27 +1261,6 @@ export class DeviceService {
|
|||||||
return ancestors.reverse();
|
return ancestors.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchDescendants(space: SpaceEntity): Promise<SpaceEntity[]> {
|
|
||||||
const descendants: SpaceEntity[] = [];
|
|
||||||
|
|
||||||
// Fetch the immediate children of the current space
|
|
||||||
const children = await this.spaceRepository.find({
|
|
||||||
where: { parent: { uuid: space.uuid } },
|
|
||||||
relations: ['children'], // To continue fetching downwards
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const child of children) {
|
|
||||||
// Add the child to the descendants list
|
|
||||||
descendants.push(child);
|
|
||||||
|
|
||||||
// Recursively fetch the child's descendants
|
|
||||||
const childDescendants = await this.fetchDescendants(child);
|
|
||||||
descendants.push(...childDescendants);
|
|
||||||
}
|
|
||||||
|
|
||||||
return descendants;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addSceneToSceneDevice(
|
async addSceneToSceneDevice(
|
||||||
deviceUuid: string,
|
deviceUuid: string,
|
||||||
addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto,
|
addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto,
|
||||||
@ -1650,22 +1563,6 @@ export class DeviceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveDevicesToSpace(
|
|
||||||
targetSpace: SpaceEntity,
|
|
||||||
deviceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
if (!deviceIds || deviceIds.length === 0) {
|
|
||||||
throw new HttpException(
|
|
||||||
'No device IDs provided for transfer',
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.deviceRepository.update(
|
|
||||||
{ uuid: In(deviceIds) },
|
|
||||||
{ spaceDevice: targetSpace },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
async getDoorLockDevices(projectUuid: string) {
|
async getDoorLockDevices(projectUuid: string) {
|
||||||
await this.validateProject(projectUuid);
|
await this.validateProject(projectUuid);
|
||||||
|
|
||||||
@ -1722,4 +1619,173 @@ 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[] = [];
|
||||||
|
allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid));
|
||||||
|
|
||||||
|
// 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(...addSpaceUuidToDevices(child.devices, child.uuid));
|
||||||
|
|
||||||
|
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(...addSpaceUuidToDevices(space.devices, space.uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addDevicesToOrphanSpace(
|
||||||
|
space: SpaceEntity,
|
||||||
|
project: ProjectEntity,
|
||||||
|
queryRunner: QueryRunner,
|
||||||
|
) {
|
||||||
|
const spaceRepository = queryRunner.manager.getRepository(SpaceEntity);
|
||||||
|
const deviceRepository = queryRunner.manager.getRepository(DeviceEntity);
|
||||||
|
try {
|
||||||
|
const orphanSpace = await spaceRepository.findOne({
|
||||||
|
where: {
|
||||||
|
community: {
|
||||||
|
name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
|
||||||
|
},
|
||||||
|
spaceName: ORPHAN_SPACE_NAME,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orphanSpace) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Orphan space not found in community ${project.name}`,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await deviceRepository.update(
|
||||||
|
{ uuid: In(space.devices.map((device) => device.uuid)) },
|
||||||
|
{ spaceDevice: orphanSpace },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to add devices to orphan spaces: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,9 @@ 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';
|
||||||
|
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, DeviceRepositoryModule],
|
imports: [ConfigModule, DeviceRepositoryModule],
|
||||||
controllers: [DoorLockController],
|
controllers: [DoorLockController],
|
||||||
@ -52,6 +55,9 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
CommunityRepository,
|
||||||
|
AqiDataService,
|
||||||
],
|
],
|
||||||
exports: [DoorLockService],
|
exports: [DoorLockService],
|
||||||
})
|
})
|
||||||
|
@ -25,6 +25,9 @@ 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';
|
||||||
|
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, DeviceRepositoryModule],
|
imports: [ConfigModule, DeviceRepositoryModule],
|
||||||
controllers: [GroupController],
|
controllers: [GroupController],
|
||||||
@ -49,6 +52,9 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
CommunityRepository,
|
||||||
|
AqiDataService,
|
||||||
],
|
],
|
||||||
exports: [GroupService],
|
exports: [GroupService],
|
||||||
})
|
})
|
||||||
|
@ -1,18 +1,75 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { InviteUserService } from './services/invite-user.service';
|
|
||||||
import { InviteUserController } from './controllers/invite-user.controller';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { InviteUserController } from './controllers/invite-user.controller';
|
||||||
|
import { InviteUserService } from './services/invite-user.service';
|
||||||
|
|
||||||
|
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||||
|
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
|
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||||
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||||
import {
|
import {
|
||||||
UserRepository,
|
DeviceRepository,
|
||||||
UserSpaceRepository,
|
DeviceUserPermissionRepository,
|
||||||
} from '@app/common/modules/user/repositories';
|
} from '@app/common/modules/device/repositories';
|
||||||
import { InviteUserRepositoryModule } from '@app/common/modules/Invite-user/Invite-user.repository.module';
|
import { InviteUserRepositoryModule } from '@app/common/modules/Invite-user/Invite-user.repository.module';
|
||||||
import {
|
import {
|
||||||
InviteUserRepository,
|
InviteUserRepository,
|
||||||
InviteUserSpaceRepository,
|
InviteUserSpaceRepository,
|
||||||
} from '@app/common/modules/Invite-user/repositiories';
|
} from '@app/common/modules/Invite-user/repositiories';
|
||||||
|
import { PermissionTypeRepository } from '@app/common/modules/permission/repositories';
|
||||||
|
import {
|
||||||
|
PowerClampDailyRepository,
|
||||||
|
PowerClampHourlyRepository,
|
||||||
|
PowerClampMonthlyRepository,
|
||||||
|
} from '@app/common/modules/power-clamp/repositories';
|
||||||
|
import { ProductRepository } from '@app/common/modules/product/repositories';
|
||||||
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
|
import { RegionRepository } from '@app/common/modules/region/repositories';
|
||||||
|
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
||||||
|
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||||
|
import {
|
||||||
|
SceneIconRepository,
|
||||||
|
SceneRepository,
|
||||||
|
} from '@app/common/modules/scene/repositories';
|
||||||
|
import {
|
||||||
|
InviteSpaceRepository,
|
||||||
|
SpaceLinkRepository,
|
||||||
|
SpaceProductAllocationRepository,
|
||||||
|
SpaceRepository,
|
||||||
|
} from '@app/common/modules/space';
|
||||||
|
import {
|
||||||
|
SpaceModelProductAllocationRepoitory,
|
||||||
|
SpaceModelRepository,
|
||||||
|
SubspaceModelProductAllocationRepoitory,
|
||||||
|
SubspaceModelRepository,
|
||||||
|
} from '@app/common/modules/space-model';
|
||||||
|
import {
|
||||||
|
SubspaceProductAllocationRepository,
|
||||||
|
SubspaceRepository,
|
||||||
|
} from '@app/common/modules/space/repositories/subspace.repository';
|
||||||
|
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
|
||||||
|
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
|
||||||
|
import {
|
||||||
|
UserRepository,
|
||||||
|
UserSpaceRepository,
|
||||||
|
} from '@app/common/modules/user/repositories';
|
||||||
import { EmailService } from '@app/common/util/email.service';
|
import { EmailService } from '@app/common/util/email.service';
|
||||||
|
import { CommunityModule } from 'src/community/community.module';
|
||||||
|
import { CommunityService } from 'src/community/services';
|
||||||
|
import { DeviceService } from 'src/device/services';
|
||||||
|
import { ProjectUserService } from 'src/project/services/project-user.service';
|
||||||
|
import { SceneService } from 'src/scene/services';
|
||||||
|
import {
|
||||||
|
SpaceModelService,
|
||||||
|
SubSpaceModelService,
|
||||||
|
} from 'src/space-model/services';
|
||||||
|
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 {
|
import {
|
||||||
SpaceLinkService,
|
SpaceLinkService,
|
||||||
SpaceService,
|
SpaceService,
|
||||||
@ -21,68 +78,11 @@ import {
|
|||||||
SubSpaceService,
|
SubSpaceService,
|
||||||
ValidationService,
|
ValidationService,
|
||||||
} from 'src/space/services';
|
} from 'src/space/services';
|
||||||
import { CommunityService } from 'src/community/services';
|
|
||||||
import {
|
|
||||||
InviteSpaceRepository,
|
|
||||||
SpaceLinkRepository,
|
|
||||||
SpaceProductAllocationRepository,
|
|
||||||
SpaceRepository,
|
|
||||||
TagRepository,
|
|
||||||
} from '@app/common/modules/space';
|
|
||||||
import { SpaceModelRepository } from '@app/common/modules/space-model';
|
|
||||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
|
||||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
|
||||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
|
||||||
import { UserService, UserSpaceService } from 'src/users/services';
|
|
||||||
import { UserDevicePermissionService } from 'src/user-device-permission/services';
|
|
||||||
import {
|
|
||||||
DeviceRepository,
|
|
||||||
DeviceUserPermissionRepository,
|
|
||||||
} from '@app/common/modules/device/repositories';
|
|
||||||
import { PermissionTypeRepository } from '@app/common/modules/permission/repositories';
|
|
||||||
import { ProjectUserService } from 'src/project/services/project-user.service';
|
|
||||||
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
|
||||||
import { RegionRepository } from '@app/common/modules/region/repositories';
|
|
||||||
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
|
|
||||||
import { CommunityModule } from 'src/community/community.module';
|
|
||||||
import { TagService as NewTagService } from 'src/tags/services';
|
|
||||||
import { TagService } from 'src/space/services/tag';
|
|
||||||
import {
|
|
||||||
SpaceModelService,
|
|
||||||
SubSpaceModelService,
|
|
||||||
} from 'src/space-model/services';
|
|
||||||
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
|
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
|
||||||
import {
|
|
||||||
SubspaceProductAllocationRepository,
|
|
||||||
SubspaceRepository,
|
|
||||||
} from '@app/common/modules/space/repositories/subspace.repository';
|
|
||||||
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
|
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
|
||||||
import {
|
import { TagService as NewTagService } from 'src/tags/services';
|
||||||
SpaceModelProductAllocationRepoitory,
|
import { UserDevicePermissionService } from 'src/user-device-permission/services';
|
||||||
SubspaceModelProductAllocationRepoitory,
|
import { UserService, UserSpaceService } from 'src/users/services';
|
||||||
SubspaceModelRepository,
|
|
||||||
} from '@app/common/modules/space-model';
|
|
||||||
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
|
|
||||||
import { ProductRepository } from '@app/common/modules/product/repositories';
|
|
||||||
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 { DeviceService } from 'src/device/services';
|
|
||||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
|
||||||
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
|
||||||
import { SceneService } from 'src/scene/services';
|
|
||||||
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 {
|
|
||||||
PowerClampHourlyRepository,
|
|
||||||
PowerClampDailyRepository,
|
|
||||||
PowerClampMonthlyRepository,
|
|
||||||
} from '@app/common/modules/power-clamp/repositories';
|
|
||||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
|
||||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
|
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
|
||||||
@ -123,7 +123,6 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
SpaceProductAllocationService,
|
SpaceProductAllocationService,
|
||||||
SpaceLinkRepository,
|
SpaceLinkRepository,
|
||||||
SubspaceRepository,
|
SubspaceRepository,
|
||||||
TagService,
|
|
||||||
SubspaceDeviceService,
|
SubspaceDeviceService,
|
||||||
SubspaceProductAllocationService,
|
SubspaceProductAllocationService,
|
||||||
SpaceModelRepository,
|
SpaceModelRepository,
|
||||||
@ -134,7 +133,6 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
SpaceModelProductAllocationService,
|
SpaceModelProductAllocationService,
|
||||||
SpaceProductAllocationRepository,
|
SpaceProductAllocationRepository,
|
||||||
SubspaceProductAllocationRepository,
|
SubspaceProductAllocationRepository,
|
||||||
TagRepository,
|
|
||||||
SubspaceModelRepository,
|
SubspaceModelRepository,
|
||||||
SubspaceModelProductAllocationService,
|
SubspaceModelProductAllocationService,
|
||||||
SpaceModelProductAllocationRepoitory,
|
SpaceModelProductAllocationRepoitory,
|
||||||
@ -152,6 +150,8 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
|
|||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampMonthlyRepository,
|
PowerClampMonthlyRepository,
|
||||||
SqlLoaderService,
|
SqlLoaderService,
|
||||||
|
OccupancyService,
|
||||||
|
AqiDataService,
|
||||||
],
|
],
|
||||||
exports: [InviteUserService],
|
exports: [InviteUserService],
|
||||||
})
|
})
|
||||||
|
@ -1,36 +1,42 @@
|
|||||||
import {
|
import { RoleType } from '@app/common/constants/role.type.enum';
|
||||||
Injectable,
|
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
|
||||||
BadRequestException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { AddUserInvitationDto } from '../dtos';
|
|
||||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
|
||||||
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
|
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
|
||||||
|
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';
|
||||||
import { generateRandomString } from '@app/common/helper/randomString';
|
import { generateRandomString } from '@app/common/helper/randomString';
|
||||||
import { EntityManager, In, IsNull, Not, QueryRunner } from 'typeorm';
|
import { InviteUserEntity } from '@app/common/modules/Invite-user/entities';
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
import { UserEntity } from '@app/common/modules/user/entities';
|
|
||||||
import { RoleType } from '@app/common/constants/role.type.enum';
|
|
||||||
import {
|
import {
|
||||||
InviteUserRepository,
|
InviteUserRepository,
|
||||||
InviteUserSpaceRepository,
|
InviteUserSpaceRepository,
|
||||||
} from '@app/common/modules/Invite-user/repositiories';
|
} from '@app/common/modules/Invite-user/repositiories';
|
||||||
import { CheckEmailDto } from '../dtos/check-email.dto';
|
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
||||||
|
import { SpaceRepository } from '@app/common/modules/space';
|
||||||
|
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||||
|
import { UserEntity } from '@app/common/modules/user/entities';
|
||||||
import { UserRepository } from '@app/common/modules/user/repositories';
|
import { UserRepository } from '@app/common/modules/user/repositories';
|
||||||
import { EmailService } from '@app/common/util/email.service';
|
import { EmailService } from '@app/common/util/email.service';
|
||||||
import { SpaceRepository } from '@app/common/modules/space';
|
import {
|
||||||
import { ActivateCodeDto } from '../dtos/active-code.dto';
|
BadRequestException,
|
||||||
import { UserSpaceService } from 'src/users/services';
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { SpaceUserService } from 'src/space/services';
|
import { SpaceUserService } from 'src/space/services';
|
||||||
|
import { UserSpaceService } from 'src/users/services';
|
||||||
|
import {
|
||||||
|
DataSource,
|
||||||
|
EntityManager,
|
||||||
|
In,
|
||||||
|
IsNull,
|
||||||
|
Not,
|
||||||
|
QueryRunner,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { AddUserInvitationDto } from '../dtos';
|
||||||
|
import { ActivateCodeDto } from '../dtos/active-code.dto';
|
||||||
|
import { CheckEmailDto } from '../dtos/check-email.dto';
|
||||||
import {
|
import {
|
||||||
DisableUserInvitationDto,
|
DisableUserInvitationDto,
|
||||||
UpdateUserInvitationDto,
|
UpdateUserInvitationDto,
|
||||||
} from '../dtos/update.invite-user.dto';
|
} from '../dtos/update.invite-user.dto';
|
||||||
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
|
||||||
import { InviteUserEntity } from '@app/common/modules/Invite-user/entities';
|
|
||||||
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InviteUserService {
|
export class InviteUserService {
|
||||||
@ -658,12 +664,12 @@ export class InviteUserService {
|
|||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await this.emailService.sendEmailWithTemplate(
|
await this.emailService.sendEmailWithTemplate({
|
||||||
userData.email,
|
email: userData.email,
|
||||||
userData.firstName,
|
name: userData.firstName,
|
||||||
disable,
|
isEnable: !disable,
|
||||||
false,
|
isDelete: false,
|
||||||
);
|
});
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
return new SuccessResponseDto({
|
return new SuccessResponseDto({
|
||||||
@ -797,12 +803,12 @@ export class InviteUserService {
|
|||||||
{ isActive: false },
|
{ isActive: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await this.emailService.sendEmailWithTemplate(
|
await this.emailService.sendEmailWithTemplate({
|
||||||
userData.email,
|
email: userData.email,
|
||||||
userData.firstName,
|
name: userData.firstName,
|
||||||
false,
|
isEnable: false,
|
||||||
true,
|
isDelete: true,
|
||||||
);
|
});
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
return new SuccessResponseDto({
|
return new SuccessResponseDto({
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -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,13 +1,66 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
|
||||||
import { PowerClampService } from './services/power-clamp.service';
|
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||||
import { PowerClampController } from './controllers';
|
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||||
|
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||||
|
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||||
|
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||||
|
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||||
|
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||||
import {
|
import {
|
||||||
PowerClampDailyRepository,
|
PowerClampDailyRepository,
|
||||||
PowerClampHourlyRepository,
|
PowerClampHourlyRepository,
|
||||||
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 { ProductRepository } from '@app/common/modules/product/repositories';
|
||||||
|
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||||
|
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||||
|
import {
|
||||||
|
SceneIconRepository,
|
||||||
|
SceneRepository,
|
||||||
|
} from '@app/common/modules/scene/repositories';
|
||||||
|
import {
|
||||||
|
InviteSpaceRepository,
|
||||||
|
SpaceLinkRepository,
|
||||||
|
SpaceProductAllocationRepository,
|
||||||
|
SpaceRepository,
|
||||||
|
} from '@app/common/modules/space';
|
||||||
|
import {
|
||||||
|
SpaceModelProductAllocationRepoitory,
|
||||||
|
SpaceModelRepository,
|
||||||
|
SubspaceModelProductAllocationRepoitory,
|
||||||
|
SubspaceModelRepository,
|
||||||
|
} from '@app/common/modules/space-model';
|
||||||
|
import {
|
||||||
|
SubspaceProductAllocationRepository,
|
||||||
|
SubspaceRepository,
|
||||||
|
} from '@app/common/modules/space/repositories/subspace.repository';
|
||||||
|
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { CommunityService } from 'src/community/services';
|
||||||
|
import { DeviceService } from 'src/device/services';
|
||||||
|
import { SceneService } from 'src/scene/services';
|
||||||
|
import {
|
||||||
|
SpaceModelService,
|
||||||
|
SubSpaceModelService,
|
||||||
|
} from 'src/space-model/services';
|
||||||
|
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 {
|
||||||
|
SpaceDeviceService,
|
||||||
|
SpaceLinkService,
|
||||||
|
SpaceService,
|
||||||
|
SubspaceDeviceService,
|
||||||
|
SubSpaceService,
|
||||||
|
ValidationService,
|
||||||
|
} from 'src/space/services';
|
||||||
|
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
|
||||||
|
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
|
||||||
|
import { TagService } from 'src/tags/services';
|
||||||
|
import { PowerClampController } from './controllers';
|
||||||
|
import { PowerClampService as PowerClamp } from './services/power-clamp.service';
|
||||||
|
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
controllers: [PowerClampController],
|
controllers: [PowerClampController],
|
||||||
@ -17,7 +70,48 @@ import { DeviceRepository } from '@app/common/modules/device/repositories';
|
|||||||
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,
|
||||||
|
SubspaceModelRepository,
|
||||||
|
SubspaceModelProductAllocationService,
|
||||||
|
SpaceModelProductAllocationRepoitory,
|
||||||
|
SubspaceModelProductAllocationRepoitory,
|
||||||
|
OccupancyService,
|
||||||
|
AqiDataService,
|
||||||
],
|
],
|
||||||
exports: [PowerClampService],
|
exports: [PowerClamp],
|
||||||
})
|
})
|
||||||
export class PowerClampModule {}
|
export class PowerClampModule {}
|
||||||
|
@ -16,8 +16,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,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user