Compare commits

..

1 Commits

Author SHA1 Message Date
6c15ce77fe bug fix 2025-05-22 16:09:12 +03:00
137 changed files with 5185 additions and 5343 deletions

View File

@ -21,7 +21,6 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
"@typescript-eslint/no-unused-vars": 'warn',
}, },
settings: { settings: {
'import/resolver': { 'import/resolver': {

View File

@ -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 { OAuth2Client } from 'google-auth-library';
import { UserSessionEntity } from '../../../../common/src/modules/session/entities';
import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository';
import { UserRepository } from '../../../../common/src/modules/user/repositories';
import { HelperHashService } from '../../helper/services'; 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 { PlatformType } from '@app/common/constants/platform-type.enum';
import { RoleType } from '@app/common/constants/role.type.enum';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@ -40,17 +40,16 @@ export class AuthService {
}, },
relations: ['roleType', 'project'], relations: ['roleType', 'project'],
}); });
if (!user) {
throw new BadRequestException('Invalid credentials');
}
if ( if (
platform === PlatformType.WEB && platform === PlatformType.WEB &&
[RoleType.SPACE_OWNER, RoleType.SPACE_MEMBER].includes( (user.roleType.type === RoleType.SPACE_OWNER ||
user.roleType.type as RoleType, user.roleType.type === RoleType.SPACE_MEMBER)
)
) { ) {
throw new UnauthorizedException('Access denied for web platform'); throw new UnauthorizedException('Access denied for web platform');
} }
if (!user) {
throw new BadRequestException('Invalid credentials');
}
if (!user.isUserVerified) { if (!user.isUserVerified) {
throw new BadRequestException('User is not verified'); throw new BadRequestException('User is not verified');

View File

@ -465,16 +465,7 @@ 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';
@ -524,20 +515,6 @@ export class ControllerRoute {
'This endpoint retrieves the occupancy heat map data based on the provided parameters.'; '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';

View File

@ -1,8 +0,0 @@
export enum PollutantType {
AQI = 'aqi',
PM25 = 'pm25',
PM10 = 'pm10',
VOC = 'voc',
CO2 = 'co2',
CH2O = 'ch2o',
}

View File

@ -19,5 +19,4 @@ export enum ProductType {
FOUR_S = '4S', FOUR_S = '4S',
SIX_S = '6S', SIX_S = '6S',
SOS = 'SOS', SOS = 'SOS',
AQI = 'AQI',
} }

View File

@ -1,31 +1,51 @@
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 { DeviceEntity } from '../modules/device/entities'; import { SnakeNamingStrategy } from './strategies';
import { PermissionTypeEntity } from '../modules/permission/entities'; import { UserEntity } from '../modules/user/entities/user.entity';
import { ProductEntity } from '../modules/product/entities';
import { UserSessionEntity } from '../modules/session/entities/session.entity'; import { UserSessionEntity } from '../modules/session/entities/session.entity';
import { UserOtpEntity } from '../modules/user/entities'; import { UserOtpEntity } from '../modules/user/entities';
import { UserEntity } from '../modules/user/entities/user.entity'; import { ProductEntity } from '../modules/product/entities';
import { SnakeNamingStrategy } from './strategies'; import { DeviceEntity } from '../modules/device/entities';
import { PermissionTypeEntity } from '../modules/permission/entities';
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger'; import { UserSpaceEntity } from '../modules/user/entities';
import { createLogger } from 'winston'; import { DeviceUserPermissionEntity } from '../modules/device/entities';
import { winstonLoggerOptions } from '../logger/services/winston.logger'; import { RoleTypeEntity } from '../modules/role-type/entities';
import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities'; import { UserNotificationEntity } from '../modules/user/entities';
import { AutomationEntity } from '../modules/automation/entities'; import { DeviceNotificationEntity } from '../modules/device/entities';
import { ClientEntity } from '../modules/client/entities'; import { RegionEntity } from '../modules/region/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 {
DeviceNotificationEntity, SpaceModelEntity,
DeviceUserPermissionEntity, SubspaceModelEntity,
} from '../modules/device/entities'; TagModel,
SpaceModelProductAllocationEntity,
SubspaceModelProductAllocationEntity,
} from '../modules/space-model/entities';
import { import {
InviteUserEntity, InviteUserEntity,
InviteUserSpaceEntity, InviteUserSpaceEntity,
} from '../modules/Invite-user/entities'; } from '../modules/Invite-user/entities';
import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/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,
@ -35,29 +55,6 @@ import {
PresenceSensorDailyDeviceEntity, PresenceSensorDailyDeviceEntity,
PresenceSensorDailySpaceEntity, PresenceSensorDailySpaceEntity,
} from '../modules/presence-sensor/entities'; } 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 { 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';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
@ -86,7 +83,9 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
PermissionTypeEntity, PermissionTypeEntity,
CommunityEntity, CommunityEntity,
SpaceEntity, SpaceEntity,
SpaceLinkEntity,
SubspaceEntity, SubspaceEntity,
TagEntity,
UserSpaceEntity, UserSpaceEntity,
DeviceUserPermissionEntity, DeviceUserPermissionEntity,
RoleTypeEntity, RoleTypeEntity,
@ -101,6 +100,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
SceneDeviceEntity, SceneDeviceEntity,
SpaceModelEntity, SpaceModelEntity,
SubspaceModelEntity, SubspaceModelEntity,
TagModel,
InviteUserEntity, InviteUserEntity,
InviteUserSpaceEntity, InviteUserSpaceEntity,
InviteSpaceEntity, InviteSpaceEntity,
@ -115,8 +115,6 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
PowerClampMonthlyEntity, PowerClampMonthlyEntity,
PresenceSensorDailyDeviceEntity, PresenceSensorDailyDeviceEntity,
PresenceSensorDailySpaceEntity, 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'))),
@ -125,7 +123,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
logger: typeOrmLogger, logger: typeOrmLogger,
extra: { extra: {
charset: 'utf8mb4', charset: 'utf8mb4',
max: 100, // set pool max size max: 20, // set pool max size
idleTimeoutMillis: 5000, // close idle clients after 5 second idleTimeoutMillis: 5000, // close idle clients after 5 second
connectionTimeoutMillis: 12_000, // return an error after 11 second if connection could not be established connectionTimeoutMillis: 12_000, // return an error after 11 second if connection could not be established
maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion) maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)

View File

@ -1,9 +1,10 @@
import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsDate, IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
import { BooleanValues } from '../constants/boolean-values.enum';
import { IsPageRequestParam } from '../validators/is-page-request-param.validator'; import { IsPageRequestParam } from '../validators/is-page-request-param.validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsSizeRequestParam } from '../validators/is-size-request-param.validator'; import { IsSizeRequestParam } from '../validators/is-size-request-param.validator';
import { Transform } from 'class-transformer';
import { parseToDate } from '../util/parseToDate';
import { BooleanValues } from '../constants/boolean-values.enum';
export class PaginationRequestGetListDto { export class PaginationRequestGetListDto {
@ApiProperty({ @ApiProperty({
@ -18,7 +19,6 @@ export class PaginationRequestGetListDto {
return value.obj.includeSpaces === BooleanValues.TRUE; return value.obj.includeSpaces === BooleanValues.TRUE;
}) })
public includeSpaces?: boolean = false; public includeSpaces?: boolean = false;
@IsOptional() @IsOptional()
@IsPageRequestParam({ @IsPageRequestParam({
message: 'Page must be bigger than 0', message: 'Page must be bigger than 0',
@ -40,4 +40,40 @@ export class PaginationRequestGetListDto {
description: 'Size request', description: 'Size request',
}) })
size?: number; size?: number;
@IsOptional()
@ApiProperty({
name: 'name',
required: false,
description: 'Name to be filtered',
})
name?: string;
@ApiProperty({
name: 'from',
required: false,
type: Number,
description: `Start time in UNIX timestamp format to filter`,
example: 1674172800000,
})
@IsOptional()
@Transform(({ value }) => parseToDate(value))
@IsDate({
message: `From must be in UNIX timestamp format in order to parse to Date instance`,
})
from?: Date;
@ApiProperty({
name: 'to',
required: false,
type: Number,
description: `End time in UNIX timestamp format to filter`,
example: 1674259200000,
})
@IsOptional()
@Transform(({ value }) => parseToDate(value))
@IsDate({
message: `To must be in UNIX timestamp format in order to parse to Date instance`,
})
to?: Date;
} }

View File

@ -11,7 +11,6 @@ import {
} 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 { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
@Module({ @Module({
providers: [ providers: [
@ -24,7 +23,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
AqiDataService,
], ],
controllers: [DeviceStatusFirebaseController], controllers: [DeviceStatusFirebaseController],
exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository], exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository],

View File

@ -23,7 +23,6 @@ import { PowerClampService } from '@app/common/helper/services/power.clamp.servi
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 { PresenceSensorEnum } from '@app/common/constants/presence.sensor.enum';
import { OccupancyService } from '@app/common/helper/services/occupancy.service'; 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;
@ -33,7 +32,6 @@ export class DeviceStatusFirebaseService {
private readonly deviceRepository: DeviceRepository, private readonly deviceRepository: DeviceRepository,
private readonly powerClampService: PowerClampService, private readonly powerClampService: PowerClampService,
private readonly occupancyService: OccupancyService, 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');
@ -76,28 +74,6 @@ export class DeviceStatusFirebaseService {
); );
} }
} }
async addDeviceStatusToOurDb(
addDeviceStatusDto: AddDeviceStatusDto,
): Promise<AddDeviceStatusDto | null> {
try {
const device = await this.getDeviceByDeviceTuyaUuid(
addDeviceStatusDto.deviceTuyaUuid,
);
if (device?.uuid) {
return await this.createDeviceStatusInOurDb({
deviceUuid: device.uuid,
...addDeviceStatusDto,
productType: device.productDevice.prodType,
});
}
// Return null if device not found or no UUID
return null;
} catch (error) {
// Handle the error silently, perhaps log it internally or ignore it
return null;
}
}
async addDeviceStatusToFirebase( async addDeviceStatusToFirebase(
addDeviceStatusDto: AddDeviceStatusDto, addDeviceStatusDto: AddDeviceStatusDto,
): Promise<AddDeviceStatusDto | null> { ): Promise<AddDeviceStatusDto | null> {
@ -233,13 +209,6 @@ export class DeviceStatusFirebaseService {
return existingData; return existingData;
}); });
// Return the updated data
const snapshot: DataSnapshot = await get(dataRef);
return snapshot.val();
}
async createDeviceStatusInOurDb(
addDeviceStatusDto: AddDeviceStatusDto,
): Promise<any> {
// Save logs to your repository // Save logs to your repository
const newLogs = addDeviceStatusDto.log.properties.map((property) => { const newLogs = addDeviceStatusDto.log.properties.map((property) => {
return this.deviceStatusLogRepository.create({ return this.deviceStatusLogRepository.create({
@ -293,10 +262,9 @@ export class DeviceStatusFirebaseService {
); );
} }
} }
if (addDeviceStatusDto.productType === ProductType.AQI) {
await this.aqiDataService.updateAQISensorHistoricalData( // Return the updated data
addDeviceStatusDto.deviceUuid, const snapshot: DataSnapshot = await get(dataRef);
); return snapshot.val();
}
} }
} }

View File

@ -1,47 +0,0 @@
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);
}
}

View File

@ -16,7 +16,7 @@ export class SosHandlerService {
); );
} }
async handleSosEventFirebase(devId: string, logData: any): Promise<void> { async handleSosEvent(devId: string, logData: any): Promise<void> {
try { try {
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: devId, deviceTuyaUuid: devId,
@ -39,28 +39,4 @@ export class SosHandlerService {
this.logger.error('Failed to send SOS true value', err); this.logger.error('Failed to send SOS true value', err);
} }
} }
async handleSosEventOurDb(devId: string, logData: any): Promise<void> {
try {
await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({
deviceTuyaUuid: devId,
status: [{ code: 'sos', value: true }],
log: logData,
});
setTimeout(async () => {
try {
await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({
deviceTuyaUuid: devId,
status: [{ code: 'sos', value: false }],
log: logData,
});
} catch (err) {
this.logger.error('Failed to send SOS false value', err);
}
}, 2000);
} catch (err) {
this.logger.error('Failed to send SOS true value', err);
}
}
} }

View File

@ -9,14 +9,6 @@ export class TuyaWebSocketService {
private client: any; private client: any;
private readonly isDevEnv: boolean; private readonly isDevEnv: boolean;
private messageQueue: {
devId: string;
status: any;
logData: any;
}[] = [];
private isProcessing = false;
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
@ -34,12 +26,12 @@ export class TuyaWebSocketService {
}); });
if (this.configService.get<string>('tuya-config.TRUN_ON_TUYA_SOCKET')) { if (this.configService.get<string>('tuya-config.TRUN_ON_TUYA_SOCKET')) {
// Set up event handlers
this.setupEventHandlers(); this.setupEventHandlers();
// Start receiving messages
this.client.start(); this.client.start();
} }
// Trigger the queue processor every 2 seconds
setInterval(() => this.processQueue(), 10000);
} }
private setupEventHandlers() { private setupEventHandlers() {
@ -51,10 +43,10 @@ export class TuyaWebSocketService {
this.client.message(async (ws: WebSocket, message: any) => { this.client.message(async (ws: WebSocket, message: any) => {
try { try {
const { devId, status, logData } = this.extractMessageData(message); const { devId, status, logData } = this.extractMessageData(message);
if (this.sosHandlerService.isSosTriggered(status)) { if (this.sosHandlerService.isSosTriggered(status)) {
await this.sosHandlerService.handleSosEventFirebase(devId, logData); await this.sosHandlerService.handleSosEvent(devId, logData);
} else { } else {
// Firebase real-time update
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: devId, deviceTuyaUuid: devId,
status: status, status: status,
@ -62,13 +54,9 @@ export class TuyaWebSocketService {
}); });
} }
// Push to internal queue
this.messageQueue.push({ devId, status, logData });
// Acknowledge the message
this.client.ackMessage(message.messageId); this.client.ackMessage(message.messageId);
} catch (error) { } catch (error) {
console.error('Error receiving message:', error); console.error('Error processing message:', error);
} }
}); });
@ -92,38 +80,6 @@ export class TuyaWebSocketService {
console.error('WebSocket error:', error); console.error('WebSocket error:', error);
}); });
} }
private async processQueue() {
if (this.isProcessing || this.messageQueue.length === 0) return;
this.isProcessing = true;
const batch = [...this.messageQueue];
this.messageQueue = [];
try {
for (const item of batch) {
if (this.sosHandlerService.isSosTriggered(item.status)) {
await this.sosHandlerService.handleSosEventOurDb(
item.devId,
item.logData,
);
} else {
await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({
deviceTuyaUuid: item.devId,
status: item.status,
log: item.logData,
});
}
}
} catch (error) {
console.error('Error processing batch:', error);
// Re-add the batch to the queue for retry
this.messageQueue.unshift(...batch);
} finally {
this.isProcessing = false;
}
}
private extractMessageData(message: any): { private extractMessageData(message: any): {
devId: string; devId: string;
status: any; status: any;

View File

@ -1,11 +0,0 @@
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 {}

View File

@ -1,82 +0,0 @@
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;
}

View File

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

View File

@ -1,184 +0,0 @@
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);
}
}

View File

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

View File

@ -1,10 +0,0 @@
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());
}
}

View File

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

View File

@ -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', 'deviceId', 'code', 'value']) @Unique('event_time_idx', ['eventTime'])
export class DeviceStatusLogEntity { export class DeviceStatusLogEntity {
@PrimaryColumn({ type: 'int', generated: true, unsigned: true }) @Column({ type: 'int', generated: true, unsigned: true })
id: number; id: number;
@Column({ type: 'text' }) @Column({ type: 'text' })
eventId: string; eventId: string;
@Column({ type: 'timestamptz' }) @PrimaryColumn({ type: 'timestamptz' })
eventTime: Date; eventTime: Date;
@Column({ @Column({

View File

@ -78,8 +78,8 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
@OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {}) @OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {})
sceneDevices: SceneDeviceEntity[]; sceneDevices: SceneDeviceEntity[];
@ManyToOne(() => NewTagEntity, (tag) => tag.devices) @OneToMany(() => NewTagEntity, (tag) => tag.devices)
@JoinColumn({ name: 'tag_uuid' }) // @JoinTable({ name: 'device_tags' })
public tag: NewTagEntity; public tag: NewTagEntity;
@OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device) @OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device)
powerClampHourly: PowerClampHourlyEntity[]; powerClampHourly: PowerClampHourlyEntity[];

View File

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

View File

@ -1,23 +0,0 @@
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;
}

View File

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

View File

@ -1,32 +0,0 @@
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);
}
}

View File

@ -1,11 +0,0 @@
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 {}

View File

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

View File

@ -1,10 +0,0 @@
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());
}
}

View File

@ -1,7 +1,10 @@
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 { ProductDto } from '../dtos'; import { TagModel } from '../../space-model';
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({
@ -25,6 +28,15 @@ 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,

View File

@ -12,7 +12,6 @@ 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,

View File

@ -0,0 +1,21 @@
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;
}

View File

@ -1,3 +1,4 @@
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';

View File

@ -1,12 +1,18 @@
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import {
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; 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 { NewTagEntity } from '../../tag/entities/tag.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.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',
@ -25,8 +31,9 @@ export class SpaceModelProductAllocationEntity extends AbstractEntity<SpaceModel
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' }) @ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity; public product: ProductEntity;
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' }) @ManyToMany(() => NewTagEntity, { cascade: true, onDelete: 'CASCADE' })
public tag: NewTagEntity; @JoinTable({ name: 'space_model_product_tags' })
public tags: NewTagEntity[];
@OneToMany( @OneToMany(
() => SpaceProductAllocationEntity, () => SpaceProductAllocationEntity,

View File

@ -1,10 +1,11 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { Entity, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProjectEntity } from '../../project/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
import { SpaceModelDto } from '../dtos'; import { SpaceModelDto } from '../dtos';
import { SpaceModelProductAllocationEntity } from './space-model-product-allocation.entity';
import { SubspaceModelEntity } from './subspace-model'; import { SubspaceModelEntity } from './subspace-model';
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';
@Entity({ name: 'space-model' }) @Entity({ name: 'space-model' })
export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> { export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> {
@ -48,6 +49,9 @@ 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,

View File

@ -1,12 +1,11 @@
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
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 { SubspaceModelEntity } from './subspace-model.entity'; import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.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',
@ -28,8 +27,12 @@ export class SubspaceModelProductAllocationEntity extends AbstractEntity<Subspac
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' }) @ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity; public product: ProductEntity;
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' }) @ManyToMany(() => NewTagEntity, (tag) => tag.subspaceModelAllocations, {
public tag: NewTagEntity; cascade: true,
onDelete: 'CASCADE',
})
@JoinTable({ name: 'subspace_model_product_tags' })
public tags: NewTagEntity[];
constructor(partial: Partial<SubspaceModelProductAllocationEntity>) { constructor(partial: Partial<SubspaceModelProductAllocationEntity>) {
super(); super();

View File

@ -1,9 +1,10 @@
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> {
@ -40,6 +41,9 @@ 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,

View File

@ -0,0 +1,38 @@
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[];
}

View File

@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm'; import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { import {
SpaceModelEntity, SpaceModelEntity,
SpaceModelProductAllocationEntity, SpaceModelProductAllocationEntity,
SubspaceModelEntity, SubspaceModelEntity,
SubspaceModelProductAllocationEntity, SubspaceModelProductAllocationEntity,
TagModel,
} from '../entities'; } from '../entities';
@Injectable() @Injectable()
@ -20,6 +21,13 @@ 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) {

View File

@ -1,11 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { SpaceModelEntity, SubspaceModelEntity } from './entities'; import { SpaceModelEntity, SubspaceModelEntity, TagModel } from './entities';
import { Module } from '@nestjs/common';
@Module({ @Module({
providers: [], providers: [],
exports: [], exports: [],
controllers: [], controllers: [],
imports: [TypeOrmModule.forFeature([SpaceModelEntity, SubspaceModelEntity])], imports: [
TypeOrmModule.forFeature([SpaceModelEntity, SubspaceModelEntity, TagModel]),
],
}) })
export class SpaceModelRepositoryModule {} export class SpaceModelRepositoryModule {}

View File

@ -1,3 +1,32 @@
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from './space.entity';
import { Direction } from '@app/common/constants/direction.enum';
export class SpaceLinkEntity extends AbstractEntity {} @Entity({ name: 'space-link' })
export class SpaceLinkEntity extends AbstractEntity {
@ManyToOne(() => SpaceEntity, { nullable: false, onDelete: 'CASCADE' })
@JoinColumn({ name: 'start_space_id' })
public startSpace: SpaceEntity;
@ManyToOne(() => SpaceEntity, { nullable: false, onDelete: 'CASCADE' })
@JoinColumn({ name: 'end_space_id' })
public endSpace: SpaceEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@Column({
nullable: false,
enum: Object.values(Direction),
})
direction: string;
constructor(partial: Partial<SpaceLinkEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -1,13 +1,12 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
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 { SpaceEntity } from './space.entity'; 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 { SpaceProductAllocationDto } from '../dtos/space-product-allocation.dto';
@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',
@ -31,8 +30,9 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' }) @ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity; public product: ProductEntity;
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' }) @ManyToMany(() => NewTagEntity)
public tag: NewTagEntity; @JoinTable({ name: 'space_product_tags' })
public tags: NewTagEntity[];
constructor(partial: Partial<SpaceProductAllocationEntity>) { constructor(partial: Partial<SpaceProductAllocationEntity>) {
super(); super();

View File

@ -1,17 +1,16 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { SpaceDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities'; import { UserSpaceEntity } from '../../user/entities';
import { CommunityEntity } from '../../community/entities';
import { DeviceEntity } from '../../device/entities'; import { DeviceEntity } from '../../device/entities';
import { InviteUserSpaceEntity } from '../../Invite-user/entities'; import { CommunityEntity } from '../../community/entities';
import { SpaceDailyOccupancyDurationEntity } from '../../occupancy/entities'; import { SpaceLinkEntity } from './space-link.entity';
import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities';
import { SceneEntity } from '../../scene/entities'; import { SceneEntity } from '../../scene/entities';
import { SpaceModelEntity } from '../../space-model'; import { SpaceModelEntity } from '../../space-model';
import { UserSpaceEntity } from '../../user/entities'; import { InviteUserSpaceEntity } from '../../Invite-user/entities';
import { SpaceDto } from '../dtos';
import { SpaceProductAllocationEntity } from './space-product-allocation.entity'; import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
import { SubspaceEntity } from './subspace/subspace.entity'; import { SubspaceEntity } from './subspace/subspace.entity';
import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities';
@Entity({ name: 'space' }) @Entity({ name: 'space' })
export class SpaceEntity extends AbstractEntity<SpaceDto> { export class SpaceEntity extends AbstractEntity<SpaceDto> {
@ -74,6 +73,16 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
) )
devices: DeviceEntity[]; devices: DeviceEntity[];
@OneToMany(() => SpaceLinkEntity, (connection) => connection.startSpace, {
nullable: true,
})
public outgoingConnections: SpaceLinkEntity[];
@OneToMany(() => SpaceLinkEntity, (connection) => connection.endSpace, {
nullable: true,
})
public incomingConnections: SpaceLinkEntity[];
@Column({ @Column({
nullable: true, nullable: true,
type: 'text', type: 'text',
@ -106,15 +115,6 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
@OneToMany(() => PresenceSensorDailySpaceEntity, (sensor) => sensor.space) @OneToMany(() => PresenceSensorDailySpaceEntity, (sensor) => sensor.space)
presenceSensorDaily: PresenceSensorDailySpaceEntity[]; 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);

View File

@ -1,13 +1,20 @@
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; import {
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 { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
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', 'tag']) @Unique(['subspace', 'product'])
export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProductAllocationDto> { export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProductAllocationDto> {
@Column({ @Column({
type: 'uuid', type: 'uuid',
@ -31,8 +38,9 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' }) @ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity; public product: ProductEntity;
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' }) @ManyToMany(() => NewTagEntity)
public tag: NewTagEntity; @JoinTable({ name: 'subspace_product_tags' })
public tags: NewTagEntity[];
constructor(partial: Partial<SubspaceProductAllocationEntity>) { constructor(partial: Partial<SubspaceProductAllocationEntity>) {
super(); super();

View File

@ -4,6 +4,7 @@ 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' })
@ -42,6 +43,9 @@ 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,

View File

@ -0,0 +1,41 @@
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;
}

View File

@ -1,8 +1,10 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm'; import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InviteSpaceEntity } from '../entities/invite-space.entity'; import { InviteSpaceEntity } from '../entities/invite-space.entity';
import { SpaceProductAllocationEntity } from '../entities/space-product-allocation.entity'; import { SpaceLinkEntity } from '../entities/space-link.entity';
import { SpaceEntity } from '../entities/space.entity'; import { SpaceEntity } from '../entities/space.entity';
import { TagEntity } from '../entities/tag.entity';
import { SpaceProductAllocationEntity } from '../entities/space-product-allocation.entity';
@Injectable() @Injectable()
export class SpaceRepository extends Repository<SpaceEntity> { export class SpaceRepository extends Repository<SpaceEntity> {
@ -12,7 +14,18 @@ export class SpaceRepository extends Repository<SpaceEntity> {
} }
@Injectable() @Injectable()
export class SpaceLinkRepository {} export class SpaceLinkRepository extends Repository<SpaceLinkEntity> {
constructor(private dataSource: DataSource) {
super(SpaceLinkEntity, dataSource.createEntityManager());
}
}
@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> {

View File

@ -6,6 +6,7 @@ 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: [],
@ -15,6 +16,7 @@ import { SubspaceEntity } from './entities/subspace/subspace.entity';
TypeOrmModule.forFeature([ TypeOrmModule.forFeature([
SpaceEntity, SpaceEntity,
SubspaceEntity, SubspaceEntity,
TagEntity,
InviteSpaceEntity, InviteSpaceEntity,
SpaceProductAllocationEntity, SpaceProductAllocationEntity,
SubspaceProductAllocationEntity, SubspaceProductAllocationEntity,

View File

@ -1,10 +1,11 @@
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import { Entity, Column, ManyToOne, Unique, ManyToMany } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { ProductEntity } from '../../product/entities';
import { DeviceEntity } from '../../device/entities/device.entity';
import { ProjectEntity } from '../../project/entities'; import { ProjectEntity } from '../../project/entities';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { NewTagDto } from '../dtos/tag.dto';
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 { NewTagDto } from '../dtos/tag.dto'; import { DeviceEntity } from '../../device/entities/device.entity';
@Entity({ name: 'new_tag' }) @Entity({ name: 'new_tag' })
@Unique(['name', 'project']) @Unique(['name', 'project'])
@ -23,25 +24,31 @@ 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;
@OneToMany( @ManyToMany(
() => SpaceModelProductAllocationEntity, () => SpaceModelProductAllocationEntity,
(allocation) => allocation.tag, (allocation) => allocation.tags,
) )
public spaceModelAllocations: SpaceModelProductAllocationEntity[]; public spaceModelAllocations: SpaceModelProductAllocationEntity[];
@OneToMany( @ManyToMany(
() => SubspaceModelProductAllocationEntity, () => SubspaceModelProductAllocationEntity,
(allocation) => allocation.tag, (allocation) => allocation.tags,
) )
public subspaceModelAllocations: SubspaceModelProductAllocationEntity[]; public subspaceModelAllocations: SubspaceModelProductAllocationEntity[];
@OneToMany(() => DeviceEntity, (device) => device.tag) @ManyToOne(() => DeviceEntity, (device) => device.tag)
public devices: DeviceEntity[]; public devices: DeviceEntity[];
constructor(partial: Partial<NewTagEntity>) { constructor(partial: Partial<NewTagEntity>) {

View File

@ -1,39 +0,0 @@
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;

View File

@ -1,374 +0,0 @@
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;

View File

@ -1,367 +0,0 @@
-- 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;

View File

@ -1,94 +1,100 @@
WITH presence_logs AS ( -- Step 1: Get device presence events with previous timestamps
SELECT WITH start_date AS (
d.space_device_uuid AS space_id, SELECT
l.device_id, d.uuid AS device_id,
l.event_time, d.space_device_uuid AS space_id,
l.value, l.value,
LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time, l.event_time::timestamp AS event_time,
LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value LAG(l.event_time::timestamp) OVER (PARTITION BY d.uuid ORDER BY l.event_time) AS prev_timestamp
FROM device d FROM device d
JOIN "device-status-log" l ON d.uuid = l.device_id LEFT JOIN "device-status-log" l
JOIN product p ON p.uuid = d.product_device_uuid ON d.uuid = l.device_id
WHERE l.code = 'presence_state' LEFT JOIN product p
AND p.cat_name = 'hps' ON p.uuid = d.product_device_uuid
WHERE p.cat_name = 'hps'
AND l.code = 'presence_state'
), ),
-- Intervals when device was in 'presence' (between prev_time and event_time when value='none') -- Step 2: Identify periods when device reports "none"
presence_intervals AS ( device_none_periods AS (
SELECT SELECT
space_id, space_id,
prev_time AS start_time, device_id,
event_time AS end_time event_time AS empty_from,
FROM presence_logs LEAD(event_time) OVER (PARTITION BY device_id ORDER BY event_time) AS empty_until
WHERE value = 'none' FROM start_date
AND prev_value = 'presence' WHERE value = 'none'
AND prev_time IS NOT NULL
), ),
-- Split intervals across days -- Step 3: Clip the "none" periods to the edges of each day
split_intervals AS ( clipped_device_none_periods AS (
SELECT SELECT
space_id, space_id,
generate_series( GREATEST(empty_from, DATE_TRUNC('day', empty_from)) AS clipped_from,
date_trunc('day', start_time), LEAST(empty_until, DATE_TRUNC('day', empty_until) + INTERVAL '1 day') AS clipped_until
date_trunc('day', end_time), FROM device_none_periods
interval '1 day' WHERE empty_until IS NOT NULL
)::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 -- Step 4: Break multi-day periods into daily intervals
ordered_intervals AS ( generated_daily_intervals AS (
SELECT SELECT
space_id, space_id,
event_date, gs::date AS day,
interval_start, GREATEST(clipped_from, gs) AS interval_start,
interval_end, LEAST(clipped_until, gs + INTERVAL '1 day') AS interval_end
LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end FROM clipped_device_none_periods,
FROM split_intervals LATERAL generate_series(DATE_TRUNC('day', clipped_from), DATE_TRUNC('day', clipped_until), INTERVAL '1 day') AS gs
), ),
grouped_intervals AS ( -- Step 5: Merge overlapping or adjacent intervals per day
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,
event_date, day,
MIN(interval_start) AS merged_start, interval_start,
MAX(interval_end) AS merged_end interval_end
FROM grouped_intervals FROM (
GROUP BY space_id, event_date, grp SELECT
space_id,
day,
interval_start,
interval_end,
LAG(interval_end) OVER (PARTITION BY space_id, day ORDER BY interval_start) AS prev_end
FROM generated_daily_intervals
) sub
WHERE prev_end IS NULL OR interval_start > prev_end
), ),
-- Sum durations of merged intervals -- Step 6: Sum up total missing seconds (device reported "none") per day
summed_intervals AS ( missing_seconds_per_day AS (
SELECT SELECT
space_id, space_id,
event_date, day AS missing_date,
SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds SUM(EXTRACT(EPOCH FROM (interval_end - interval_start))) AS total_missing_seconds
FROM merged_intervals FROM merged_intervals
GROUP BY space_id, event_date GROUP BY space_id, day
), ),
final_data AS ( -- Step 7: Calculate total occupied time per day (86400 - missing)
SELECT occupied_seconds_per_day AS (
space_id, SELECT
event_date, space_id,
LEAST(raw_occupied_seconds, 86400) AS occupied_seconds, missing_date as event_date,
ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage 86400 - total_missing_seconds AS total_occupied_seconds,
FROM summed_intervals (86400 - total_missing_seconds)/86400*100 as occupancy_prct
ORDER BY space_id, event_date) FROM missing_seconds_per_day
)
-- Final Output
, final_data as (
SELECT space_id,
event_date,
total_occupied_seconds,
occupancy_prct
FROM occupied_seconds_per_day
ORDER BY 1,2
)
INSERT INTO public."space-daily-occupancy-duration" ( INSERT INTO public."space-daily-occupancy-duration" (
space_uuid, space_uuid,
@ -98,13 +104,13 @@ INSERT INTO public."space-daily-occupancy-duration" (
) )
select space_id, select space_id,
event_date, event_date,
occupied_seconds, total_occupied_seconds,
occupancy_percentage occupancy_prct
FROM final_data FROM final_data
ON CONFLICT (space_uuid, event_date) DO UPDATE ON CONFLICT (space_uuid, event_date) DO UPDATE
SET SET
occupancy_percentage = EXCLUDED.occupancy_percentage, occupancy_percentage = EXCLUDED.occupancy_percentage,
occupied_seconds = EXCLUDED.occupied_seconds; occupied_seconds = EXCLUDED.occupied_seconds;

View File

@ -2,94 +2,102 @@ WITH params AS (
SELECT SELECT
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date,
$2::uuid AS space_id $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
) )
, start_date AS (
SELECT
d.uuid AS device_id,
d.space_device_uuid AS space_id,
l.value,
l.event_time::timestamp AS event_time,
LAG(l.event_time::timestamp) OVER (PARTITION BY d.uuid ORDER BY l.event_time) AS prev_timestamp
FROM device d
LEFT JOIN "device-status-log" l
ON d.uuid = l.device_id
LEFT JOIN product p
ON p.uuid = d.product_device_uuid
WHERE p.cat_name = 'hps'
AND l.code = 'presence_state'
)
, device_none_periods AS (
SELECT
space_id,
device_id,
event_time AS empty_from,
LEAD(event_time) OVER (PARTITION BY device_id ORDER BY event_time) AS empty_until
FROM start_date
WHERE value = 'none'
)
, clipped_device_none_periods AS (
SELECT
space_id,
GREATEST(empty_from, DATE_TRUNC('day', empty_from)) AS clipped_from,
LEAST(empty_until, DATE_TRUNC('day', empty_until) + INTERVAL '1 day') AS clipped_until
FROM device_none_periods
WHERE empty_until IS NOT NULL
)
, generated_daily_intervals AS (
SELECT
space_id,
gs::date AS day,
GREATEST(clipped_from, gs) AS interval_start,
LEAST(clipped_until, gs + INTERVAL '1 day') AS interval_end
FROM clipped_device_none_periods,
LATERAL generate_series(DATE_TRUNC('day', clipped_from), DATE_TRUNC('day', clipped_until), INTERVAL '1 day') AS gs
)
, merged_intervals AS (
SELECT
space_id,
day,
interval_start,
interval_end
FROM (
SELECT
space_id,
day,
interval_start,
interval_end,
LAG(interval_end) OVER (PARTITION BY space_id, day ORDER BY interval_start) AS prev_end
FROM generated_daily_intervals
) sub
WHERE prev_end IS NULL OR interval_start > prev_end
)
, missing_seconds_per_day AS (
SELECT
space_id,
day AS missing_date,
SUM(EXTRACT(EPOCH FROM (interval_end - interval_start))) AS total_missing_seconds
FROM merged_intervals
GROUP BY space_id, day
)
, occupied_seconds_per_day AS (
SELECT
space_id,
missing_date as event_date,
86400 - total_missing_seconds AS total_occupied_seconds,
(86400 - total_missing_seconds)/86400*100 as occupancy_percentage
FROM missing_seconds_per_day
)
, final_data as (
SELECT
occupied_seconds_per_day.space_id,
occupied_seconds_per_day.event_date,
total_occupied_seconds,
occupancy_percentage
FROM occupied_seconds_per_day
JOIN params p
ON p.space_id = occupied_seconds_per_day.space_id
AND p.event_date = occupied_seconds_per_day.event_date
)
INSERT INTO public."space-daily-occupancy-duration" ( INSERT INTO public."space-daily-occupancy-duration" (
space_uuid, space_uuid,
event_date, event_date,
@ -97,13 +105,12 @@ INSERT INTO public."space-daily-occupancy-duration" (
occupancy_percentage occupancy_percentage
) )
SELECT SELECT
space_id, space_id,
event_date, event_date,
occupied_seconds, total_occupied_seconds,
occupancy_percentage occupancy_percentage
FROM final_data FROM final_data
ON CONFLICT (space_uuid, event_date) DO UPDATE ON CONFLICT (space_uuid, event_date) DO UPDATE
SET SET
occupancy_percentage = EXCLUDED.occupancy_percentage, occupied_seconds = EXCLUDED.occupied_seconds,
occupied_seconds = EXCLUDED.occupied_seconds; occupancy_percentage = EXCLUDED.occupancy_percentage;

View File

@ -16,5 +16,4 @@ WITH params AS (
WHERE A.device_uuid::text = ANY(P.device_ids) WHERE A.device_uuid::text = ANY(P.device_ids)
AND (P.month IS NULL AND (P.month IS NULL
OR date_trunc('month', A.event_date) = P.month OR date_trunc('month', A.event_date) = P.month
); )

View File

@ -1,362 +0,0 @@
-- 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;

View File

@ -1,275 +0,0 @@
-- 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;

View File

@ -1,90 +1,91 @@
WITH presence_logs AS ( -- Step 1: Get device presence events with previous timestamps
SELECT WITH start_date AS (
d.space_device_uuid AS space_id, SELECT
l.device_id, d.uuid AS device_id,
l.event_time, d.space_device_uuid AS space_id,
l.value, l.value,
LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time, l.event_time::timestamp AS event_time,
LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value LAG(l.event_time::timestamp) OVER (PARTITION BY d.uuid ORDER BY l.event_time) AS prev_timestamp
FROM device d FROM device d
JOIN "device-status-log" l ON d.uuid = l.device_id LEFT JOIN "device-status-log" l
JOIN product p ON p.uuid = d.product_device_uuid ON d.uuid = l.device_id
WHERE l.code = 'presence_state' LEFT JOIN product p
AND p.cat_name = 'hps' ON p.uuid = d.product_device_uuid
WHERE p.cat_name = 'hps'
AND l.code = 'presence_state'
), ),
-- Intervals when device was in 'presence' (between prev_time and event_time when value='none') -- Step 2: Identify periods when device reports "none"
presence_intervals AS ( device_none_periods AS (
SELECT SELECT
space_id, space_id,
prev_time AS start_time, device_id,
event_time AS end_time event_time AS empty_from,
FROM presence_logs LEAD(event_time) OVER (PARTITION BY device_id ORDER BY event_time) AS empty_until
WHERE value = 'none' FROM start_date
AND prev_value = 'presence' WHERE value = 'none'
AND prev_time IS NOT NULL
), ),
-- Split intervals across days -- Step 3: Clip the "none" periods to the edges of each day
split_intervals AS ( clipped_device_none_periods AS (
SELECT SELECT
space_id, space_id,
generate_series( GREATEST(empty_from, DATE_TRUNC('day', empty_from)) AS clipped_from,
date_trunc('day', start_time), LEAST(empty_until, DATE_TRUNC('day', empty_until) + INTERVAL '1 day') AS clipped_until
date_trunc('day', end_time), FROM device_none_periods
interval '1 day' WHERE empty_until IS NOT NULL
)::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 -- Step 4: Break multi-day periods into daily intervals
ordered_intervals AS ( generated_daily_intervals AS (
SELECT SELECT
space_id, space_id,
event_date, gs::date AS day,
interval_start, GREATEST(clipped_from, gs) AS interval_start,
interval_end, LEAST(clipped_until, gs + INTERVAL '1 day') AS interval_end
LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end FROM clipped_device_none_periods,
FROM split_intervals LATERAL generate_series(DATE_TRUNC('day', clipped_from), DATE_TRUNC('day', clipped_until), INTERVAL '1 day') AS gs
), ),
grouped_intervals AS ( -- Step 5: Merge overlapping or adjacent intervals per day
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,
event_date, day,
MIN(interval_start) AS merged_start, interval_start,
MAX(interval_end) AS merged_end interval_end
FROM grouped_intervals FROM (
GROUP BY space_id, event_date, grp SELECT
space_id,
day,
interval_start,
interval_end,
LAG(interval_end) OVER (PARTITION BY space_id, day ORDER BY interval_start) AS prev_end
FROM generated_daily_intervals
) sub
WHERE prev_end IS NULL OR interval_start > prev_end
), ),
-- Sum durations of merged intervals -- Step 6: Sum up total missing seconds (device reported "none") per day
summed_intervals AS ( missing_seconds_per_day AS (
SELECT SELECT
space_id, space_id,
event_date, day AS missing_date,
SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds SUM(EXTRACT(EPOCH FROM (interval_end - interval_start))) AS total_missing_seconds
FROM merged_intervals FROM merged_intervals
GROUP BY space_id, event_date GROUP BY space_id, day
),
-- Step 7: Calculate total occupied time per day (86400 - missing)
occupied_seconds_per_day AS (
SELECT
space_id,
missing_date as date,
86400 - total_missing_seconds AS total_occupied_seconds
FROM missing_seconds_per_day
) )
-- Final output with capped seconds and percentage -- Final Output
SELECT SELECT *
space_id, FROM occupied_seconds_per_day
event_date, ORDER BY 1,2;
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;

View File

@ -1,18 +0,0 @@
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,
);
}

View File

@ -1,11 +0,0 @@
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;
});
}

View File

@ -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 axios from 'axios';
import * as nodemailer from 'nodemailer'; import * as nodemailer from 'nodemailer';
import axios from 'axios';
import { import {
SEND_EMAIL_API_URL_DEV, SEND_EMAIL_API_URL_DEV,
SEND_EMAIL_API_URL_PROD, SEND_EMAIL_API_URL_PROD,
@ -83,17 +83,12 @@ export class EmailService {
); );
} }
} }
async sendEmailWithTemplate({ async sendEmailWithTemplate(
email, email: string,
name, name: string,
isEnable, isEnable: boolean,
isDelete, isDelete: boolean,
}: { ): 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',

View File

@ -10,7 +10,6 @@
"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",

View File

@ -1,44 +1,43 @@
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 { APP_INTERCEPTOR } from '@nestjs/core';
import { WinstonModule } from 'nest-winston';
import { AuthenticationModule } from './auth/auth.module';
import { AutomationModule } from './automation/automation.module';
import { ClientModule } from './client/client.module';
import { DeviceCommissionModule } from './commission-device/commission-device.module';
import { CommunityModule } from './community/community.module';
import config from './config'; import config from './config';
import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; import { AuthenticationModule } from './auth/auth.module';
import { DeviceModule } from './device/device.module'; import { UserModule } from './users/user.module';
import { DoorLockModule } from './door-lock/door.lock.module';
import { GroupModule } from './group/group.module'; import { GroupModule } from './group/group.module';
import { HealthModule } from './health/health.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 { LoggingInterceptor } from './interceptors/logging.interceptor';
import { InviteUserModule } from './invite-user/invite-user.module'; import { AutomationModule } from './automation/automation.module';
import { PermissionModule } from './permission/permission.module'; import { RegionModule } from './region/region.module';
import { PowerClampModule } from './power-clamp/power-clamp.module'; import { TimeZoneModule } from './timezone/timezone.module';
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.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 { ProductModule } from './product';
import { ProjectModule } from './project'; 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 { SpaceModelModule } from './space-model';
import { SpaceModule } from './space/space.module'; import { InviteUserModule } from './invite-user/invite-user.module';
import { TagModule } from './tags/tags.module'; import { PermissionModule } from './permission/permission.module';
import { RoleModule } from './role/role.module';
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module'; import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
import { TimeZoneModule } from './timezone/timezone.module'; import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module'; import { TagModule } from './tags/tags.module';
import { UserNotificationModule } from './user-notification/user-notification.module'; import { ClientModule } from './client/client.module';
import { UserModule } from './users/user.module'; import { DeviceCommissionModule } from './commission-device/commission-device.module';
import { VisitorPasswordModule } from './vistor-password/visitor-password.module'; import { PowerClampModule } from './power-clamp/power-clamp.module';
import { WinstonModule } from 'nest-winston';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { HealthModule } from './health/health.module';
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
import { AqiModule } from './aqi/aqi.module';
import { OccupancyModule } from './occupancy/occupancy.module'; import { OccupancyModule } from './occupancy/occupancy.module';
import { WeatherModule } from './weather/weather.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
@ -80,8 +79,6 @@ import { WeatherModule } from './weather/weather.module';
PowerClampModule, PowerClampModule,
HealthModule, HealthModule,
OccupancyModule, OccupancyModule,
WeatherModule,
AqiModule,
], ],
providers: [ providers: [
{ {

View File

@ -1,11 +0,0 @@
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 {}

View File

@ -1,64 +0,0 @@
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);
}
}

View File

@ -1 +0,0 @@
export * from './aqi.controller';

View File

@ -1,7 +0,0 @@
import { IsNotEmpty, IsUUID } from 'class-validator';
export class SpaceParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' })
@IsNotEmpty()
spaceUuid: string;
}

View File

@ -1,37 +0,0 @@
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;
}

View File

@ -1,138 +0,0 @@
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);
}
}

View File

@ -1 +0,0 @@
export * from './aqi.service';

View File

@ -29,7 +29,6 @@ import {
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 { 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],
@ -58,7 +57,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
AqiDataService,
], ],
exports: [], exports: [],
}) })

View File

@ -1,19 +1,17 @@
import * as csv from 'csv-parser';
import * as fs from 'fs'; import * as fs from 'fs';
import * as csv from 'csv-parser';
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 {
@ -120,7 +118,7 @@ export class DeviceCommissionService {
where: { uuid: spaceId }, where: { uuid: spaceId },
relations: [ relations: [
'productAllocations', 'productAllocations',
'productAllocations.tag', 'productAllocations.tags',
'productAllocations.product', 'productAllocations.product',
], ],
}); });
@ -137,7 +135,7 @@ export class DeviceCommissionService {
where: { uuid: subspaceId }, where: { uuid: subspaceId },
relations: [ relations: [
'productAllocations', 'productAllocations',
'productAllocations.tag', 'productAllocations.tags',
'productAllocations.product', 'productAllocations.product',
], ],
}); });
@ -153,23 +151,19 @@ export class DeviceCommissionService {
subspace?.productAllocations || space.productAllocations; subspace?.productAllocations || space.productAllocations;
const match = allocations const match = allocations
.map( .flatMap((pa) =>
({ (pa.tags || []).map((tag) => ({ product: pa.product, tag })),
product,
tag,
}:
| SpaceProductAllocationEntity
| SubspaceProductAllocationEntity) => ({ product, tag }),
) )
.find( .find(({ tag }) => tag.name === tagName);
({ tag, product }) =>
tag.name === tagName && product.name === productName,
);
if (!match) { if (!match) {
console.error( console.error(`No matching tag found for Device ID: ${rawDeviceId}`);
`No matching tag-product combination found for Device ID: ${rawDeviceId}`, failureCount.value++;
); return;
}
if (match.product.name !== productName) {
console.error(`Product name mismatch for Device ID: ${rawDeviceId}`);
failureCount.value++; failureCount.value++;
return; return;
} }

View File

@ -8,6 +8,7 @@ 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,7 +64,6 @@ import {
} 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 { 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],
@ -78,7 +78,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
ProjectRepository, ProjectRepository,
SpaceService, SpaceService,
InviteSpaceRepository, InviteSpaceRepository,
// Todo: find out why this is needed
SpaceLinkService, SpaceLinkService,
SubSpaceService, SubSpaceService,
ValidationService, ValidationService,
@ -87,7 +86,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
SpaceProductAllocationService, SpaceProductAllocationService,
SpaceLinkRepository, SpaceLinkRepository,
SubspaceRepository, SubspaceRepository,
// Todo: find out why this is needed
TagService, TagService,
SubspaceDeviceService, SubspaceDeviceService,
SubspaceProductAllocationService, SubspaceProductAllocationService,
@ -99,6 +97,7 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
SpaceModelProductAllocationService, SpaceModelProductAllocationService,
SpaceProductAllocationRepository, SpaceProductAllocationRepository,
SubspaceProductAllocationRepository, SubspaceProductAllocationRepository,
TagRepository,
SubspaceModelRepository, SubspaceModelRepository,
SubspaceModelProductAllocationService, SubspaceModelProductAllocationService,
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
@ -117,7 +116,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
AqiDataService,
], ],
exports: [CommunityService, SpacePermissionService], exports: [CommunityService, SpacePermissionService],
}) })

View File

@ -1,3 +1,4 @@
import { CommunityService } from '../services/community.service';
import { import {
Body, Body,
Controller, Controller,
@ -9,18 +10,17 @@ import {
Query, Query,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { AddCommunityDto } from '../dtos/add.community.dto'; import { AddCommunityDto } from '../dtos/add.community.dto';
import { GetCommunityParams } from '../dtos/get.community.dto'; import { GetCommunityParams } from '../dtos/get.community.dto';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
import { CommunityService } from '../services/community.service';
// import { CheckUserCommunityGuard } from 'src/guards/user.community.guard'; // import { CheckUserCommunityGuard } from 'src/guards/user.community.guard';
import { ControllerRoute } from '@app/common/constants/controller-route'; import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { PaginationRequestWithSearchGetListDto } from '@app/common/dto/pagination-with-search.request.dto';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { ProjectParam } from '../dtos'; import { ProjectParam } from '../dtos';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PaginationRequestWithSearchGetListDto } from '@app/common/dto/pagination-with-search.request.dto';
@ApiTags('Community Module') @ApiTags('Community Module')
@Controller({ @Controller({
@ -45,21 +45,6 @@ export class CommunityController {
return await this.communityService.createCommunity(param, addCommunityDto); return await this.communityService.createCommunity(param, addCommunityDto);
} }
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('COMMUNITY_VIEW')
@ApiOperation({
summary: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_SUMMARY,
description: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_DESCRIPTION,
})
@Get('v2')
async getCommunitiesV2(
@Param() param: ProjectParam,
@Query() query: PaginationRequestWithSearchGetListDto,
): Promise<any> {
return this.communityService.getCommunitiesV2(param, query);
}
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(PermissionsGuard) @UseGuards(PermissionsGuard)
@Permissions('COMMUNITY_VIEW') @Permissions('COMMUNITY_VIEW')

View File

@ -1,33 +1,28 @@
import { import {
ORPHAN_COMMUNITY_NAME, Injectable,
ORPHAN_SPACE_NAME, HttpException,
} from '@app/common/constants/orphan-constant'; HttpStatus,
NotFoundException,
} from '@nestjs/common';
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto'; 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 { CommunityDto } from '@app/common/modules/community/dtos'; import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { CommunityEntity } from '@app/common/modules/community/entities';
import { CommunityRepository } from '@app/common/modules/community/repositories'; import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceEntity } from '@app/common/modules/device/entities'; import { CommunityDto } from '@app/common/modules/community/dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { SpaceRepository } from '@app/common/modules/space'; import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { ILike, In, Not } from 'typeorm';
import { addSpaceUuidToDevices } from '@app/common/util/device-utils';
import {
HttpException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { SpaceService } from 'src/space/services'; import { SpaceService } from 'src/space/services';
import { QueryRunner, SelectQueryBuilder } from 'typeorm'; import { SpaceRepository } from '@app/common/modules/space';
import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos'; import { DeviceEntity } from '@app/common/modules/device/entities';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
@Injectable() @Injectable()
export class CommunityService { export class CommunityService {
@ -72,18 +67,12 @@ export class CommunityService {
} }
} }
async getCommunityById( async getCommunityById(params: GetCommunityParams): Promise<BaseResponseDto> {
params: GetCommunityParams,
queryRunner?: QueryRunner,
): Promise<BaseResponseDto> {
const { communityUuid, projectUuid } = params; const { communityUuid, projectUuid } = params;
await this.validateProject(projectUuid); await this.validateProject(projectUuid);
const communityRepository = const community = await this.communityRepository.findOneBy({
queryRunner?.manager.getRepository(CommunityEntity) ||
this.communityRepository;
const community = await communityRepository.findOneBy({
uuid: communityUuid, uuid: communityUuid,
}); });
@ -103,36 +92,56 @@ export class CommunityService {
} }
async getCommunities( async getCommunities(
{ projectUuid }: ProjectParam, param: ProjectParam,
pageable: Partial<ExtendedTypeORMCustomModelFindAllQuery>, pageable: Partial<ExtendedTypeORMCustomModelFindAllQuery>,
): Promise<BaseResponseDto> { ): Promise<BaseResponseDto> {
try { try {
const project = await this.validateProject(projectUuid); const project = await this.validateProject(param.projectUuid);
/** pageable.modelName = 'community';
* TODO: removing this breaks the code (should be fixed when refactoring @see TypeORMCustomModel pageable.where = {
*/ project: { uuid: param.projectUuid },
pageable.where = {}; name: Not(`${ORPHAN_COMMUNITY_NAME}-${project.name}`),
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) {
qb.andWhere( const matchingCommunities = await this.communityRepository.find({
`c.name ILIKE '%${pageable.search}%' OR s.space_name ILIKE '%${pageable.search}%'`, where: {
); 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, modelName: 'community' }, qb); await customModel.findAll(pageable);
// 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) => {
@ -140,7 +149,7 @@ export class CommunityService {
await this.spaceService.getSpacesHierarchyForCommunity( await this.spaceService.getSpacesHierarchyForCommunity(
{ {
communityUuid: community.uuid, communityUuid: community.uuid,
projectUuid: projectUuid, projectUuid: param.projectUuid,
}, },
{ {
onlyWithDevices: false, onlyWithDevices: false,
@ -170,75 +179,6 @@ export class CommunityService {
} }
} }
async getCommunitiesV2(
{ projectUuid }: ProjectParam,
{
search,
includeSpaces,
...pageable
}: Partial<ExtendedTypeORMCustomModelFindAllQuery>,
) {
try {
const project = await this.validateProject(projectUuid);
let qb: undefined | SelectQueryBuilder<CommunityEntity> = undefined;
qb = this.communityRepository
.createQueryBuilder('c')
.where('c.project = :projectUuid', { projectUuid })
.andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`)
.distinct(true);
if (includeSpaces) {
qb.leftJoinAndSelect(
'c.spaces',
'space',
'space.disabled = :disabled AND space.spaceName != :orphanSpaceName',
{ disabled: false, orphanSpaceName: ORPHAN_SPACE_NAME },
)
.leftJoinAndSelect('space.parent', 'parent')
.leftJoinAndSelect(
'space.children',
'children',
'children.disabled = :disabled',
{ disabled: false },
);
// .leftJoinAndSelect('space.spaceModel', 'spaceModel')
}
if (search) {
qb.andWhere(
`c.name ILIKE :search ${includeSpaces ? 'OR space.space_name ILIKE :search' : ''}`,
{ search },
);
}
const customModel = TypeORMCustomModel(this.communityRepository);
const { baseResponseDto, paginationResponseDto } =
await customModel.findAll({ ...pageable, modelName: 'community' }, qb);
if (includeSpaces) {
baseResponseDto.data = baseResponseDto.data.map((community) => ({
...community,
spaces: this.spaceService.buildSpaceHierarchy(community.spaces || []),
}));
}
return new PageResponse<CommunityDto>(
baseResponseDto,
paginationResponseDto,
);
} catch (error) {
// Generic error handling
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message || 'An error occurred while fetching communities.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateCommunity( async updateCommunity(
params: GetCommunityParams, params: GetCommunityParams,
updateCommunityDto: UpdateCommunityNameDto, updateCommunityDto: UpdateCommunityNameDto,
@ -396,7 +336,7 @@ export class CommunityService {
visitedSpaceUuids.add(space.uuid); visitedSpaceUuids.add(space.uuid);
if (space.devices?.length) { if (space.devices?.length) {
allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid)); allDevices.push(...space.devices);
} }
if (space.children?.length) { if (space.children?.length) {

View File

@ -1,5 +1,4 @@
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';
import WeatherOpenConfig from './weather.open.config'; export default [AuthConfig, AppConfig, JwtConfig];
export default [AuthConfig, AppConfig, JwtConfig, WeatherOpenConfig];

View File

@ -1,9 +0,0 @@
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,
}),
);

View File

@ -1,11 +1,11 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { GetDevicesFilterDto, ProjectParam } from '../dtos';
import { DeviceService } from '../services/device.service'; import { DeviceService } from '../services/device.service';
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { Permissions } from 'src/decorators/permissions.decorator';
import { GetDoorLockDevices, ProjectParam } from '../dtos';
@ApiTags('Device Module') @ApiTags('Device Module')
@Controller({ @Controller({
@ -25,7 +25,7 @@ export class DeviceProjectController {
}) })
async getAllDevices( async getAllDevices(
@Param() param: ProjectParam, @Param() param: ProjectParam,
@Query() query: GetDevicesFilterDto, @Query() query: GetDoorLockDevices,
) { ) {
return await this.deviceService.getAllDevices(param, query); return await this.deviceService.getAllDevices(param, query);
} }

View File

@ -1,7 +1,6 @@
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 { import {
IsArray,
IsEnum, IsEnum,
IsNotEmpty, IsNotEmpty,
IsOptional, IsOptional,
@ -42,7 +41,16 @@ export class GetDeviceLogsDto {
@IsOptional() @IsOptional()
public endTime: string; public endTime: string;
} }
export class GetDoorLockDevices {
@ApiProperty({
description: 'Device Type',
enum: DeviceTypeEnum,
required: false,
})
@IsEnum(DeviceTypeEnum)
@IsOptional()
public deviceType: DeviceTypeEnum;
}
export class GetDevicesBySpaceOrCommunityDto { export class GetDevicesBySpaceOrCommunityDto {
@ApiProperty({ @ApiProperty({
description: 'Device Product Type', description: 'Device Product Type',
@ -64,23 +72,3 @@ export class GetDevicesBySpaceOrCommunityDto {
@IsNotEmpty({ message: 'Either spaceUuid or communityUuid must be provided' }) @IsNotEmpty({ message: 'Either spaceUuid or communityUuid must be provided' })
requireEither?: never; // This ensures at least one of them is provided requireEither?: never; // This ensures at least one of them is provided
} }
export class GetDevicesFilterDto {
@ApiProperty({
description: 'Device Type',
enum: DeviceTypeEnum,
required: false,
})
@IsEnum(DeviceTypeEnum)
@IsOptional()
public deviceType: DeviceTypeEnum;
@ApiProperty({
description: 'List of Space IDs to filter devices',
required: false,
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
})
@IsOptional()
@IsArray()
@IsUUID('4', { each: true })
public spaces?: string[];
}

View File

@ -1,46 +1,39 @@
import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum'; import { ORPHAN_SPACE_NAME } from './../../../libs/common/src/constants/orphan-constant';
import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum'; import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository';
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 {
BadRequestException, Injectable,
forwardRef,
HttpException, HttpException,
HttpStatus, HttpStatus,
Inject,
Injectable,
NotFoundException, NotFoundException,
BadRequestException,
forwardRef,
Inject,
} from '@nestjs/common'; } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { AddAutomationDto } from 'src/automation/dtos'; import { ConfigService } from '@nestjs/config';
import { SceneService } from 'src/scene/services';
import { In, Not, QueryRunner } from 'typeorm';
import { ProjectParam } from '../dtos';
import { import {
AddDeviceDto, AddDeviceDto,
AddSceneToFourSceneDeviceDto, AddSceneToFourSceneDeviceDto,
AssignDeviceToSpaceDto,
UpdateDeviceDto, UpdateDeviceDto,
AssignDeviceToSpaceDto,
} 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,
GetDevicesBySpaceOrCommunityDto,
GetDoorLockDevices,
} from '../dtos/get.device.dto';
import { import {
BatchControlDevicesDto, BatchControlDevicesDto,
BatchFactoryResetDevicesDto, BatchFactoryResetDevicesDto,
@ -48,29 +41,32 @@ import {
ControlDeviceDto, ControlDeviceDto,
GetSceneFourSceneDeviceDto, GetSceneFourSceneDeviceDto,
} from '../dtos/control.device.dto'; } from '../dtos/control.device.dto';
import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.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 { DeviceSceneParamDto } from '../dtos/device.param.dto';
import { import { BaseResponseDto } from '@app/common/dto/base.response.dto';
GetDeviceLogsDto, import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
GetDevicesBySpaceOrCommunityDto, import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto';
GetDevicesFilterDto, import { DeviceEntity } from '@app/common/modules/device/entities';
} from '../dtos/get.device.dto'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { import { ProjectRepository } from '@app/common/modules/project/repositiories';
controlDeviceInterface, import { ProjectParam } from '../dtos';
DeviceInstructionResponse, import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum';
GetDeviceDetailsFunctionsInterface, import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
GetDeviceDetailsFunctionsStatusInterface, import { CommunityRepository } from '@app/common/modules/community/repositories';
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 {
@ -201,6 +197,46 @@ 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,
@ -955,20 +991,19 @@ export class DeviceService {
async getAllDevices( async getAllDevices(
param: ProjectParam, param: ProjectParam,
{ deviceType, spaces }: GetDevicesFilterDto, query: GetDoorLockDevices,
): Promise<BaseResponseDto> { ): Promise<BaseResponseDto> {
try { try {
await this.validateProject(param.projectUuid); await this.validateProject(param.projectUuid);
if (deviceType === DeviceTypeEnum.DOOR_LOCK) { if (query.deviceType === DeviceTypeEnum.DOOR_LOCK) {
return await this.getDoorLockDevices(param.projectUuid, spaces); return await this.getDoorLockDevices(param.projectUuid);
} else if (!deviceType) { } else if (!query.deviceType) {
const devices = await this.deviceRepository.find({ const devices = await this.deviceRepository.find({
where: { where: {
isActive: true, isActive: true,
spaceDevice: { spaceDevice: {
uuid: spaces && spaces.length ? In(spaces) : undefined,
spaceName: Not(ORPHAN_SPACE_NAME),
community: { project: { uuid: param.projectUuid } }, community: { project: { uuid: param.projectUuid } },
spaceName: Not(ORPHAN_SPACE_NAME),
}, },
}, },
relations: [ relations: [
@ -1163,6 +1198,39 @@ 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(
@ -1262,6 +1330,27 @@ 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,
@ -1564,7 +1653,23 @@ export class DeviceService {
} }
} }
async getDoorLockDevices(projectUuid: string, spaces?: string[]) { 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) {
await this.validateProject(projectUuid); await this.validateProject(projectUuid);
const devices = await this.deviceRepository.find({ const devices = await this.deviceRepository.find({
@ -1574,7 +1679,6 @@ export class DeviceService {
}, },
spaceDevice: { spaceDevice: {
spaceName: Not(ORPHAN_SPACE_NAME), spaceName: Not(ORPHAN_SPACE_NAME),
uuid: spaces && spaces.length ? In(spaces) : undefined,
community: { community: {
project: { project: {
uuid: projectUuid, uuid: projectUuid,
@ -1682,8 +1786,7 @@ export class DeviceService {
throw new NotFoundException('Space not found'); throw new NotFoundException('Space not found');
} }
const allDevices: DeviceEntity[] = []; const allDevices: DeviceEntity[] = [...space.devices];
allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid));
// Recursive fetch function // Recursive fetch function
const fetchChildren = async (parentSpace: SpaceEntity) => { const fetchChildren = async (parentSpace: SpaceEntity) => {
@ -1693,7 +1796,7 @@ export class DeviceService {
}); });
for (const child of children) { for (const child of children) {
allDevices.push(...addSpaceUuidToDevices(child.devices, child.uuid)); allDevices.push(...child.devices);
if (child.children.length > 0) { if (child.children.length > 0) {
await fetchChildren(child); await fetchChildren(child);
@ -1732,7 +1835,7 @@ export class DeviceService {
visitedSpaceUuids.add(space.uuid); visitedSpaceUuids.add(space.uuid);
if (space.devices?.length) { if (space.devices?.length) {
allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid)); allDevices.push(...space.devices);
} }
if (space.children?.length) { if (space.children?.length) {
@ -1755,39 +1858,4 @@ export class DeviceService {
return allDevices; 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}`,
);
}
}
} }

View File

@ -29,7 +29,6 @@ import {
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 { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { CommunityRepository } from '@app/common/modules/community/repositories'; 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],
@ -57,7 +56,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
CommunityRepository, CommunityRepository,
AqiDataService,
], ],
exports: [DoorLockService], exports: [DoorLockService],
}) })

View File

@ -27,7 +27,6 @@ import {
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 { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { CommunityRepository } from '@app/common/modules/community/repositories'; 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],
@ -54,7 +53,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
CommunityRepository, CommunityRepository,
AqiDataService,
], ],
exports: [GroupService], exports: [GroupService],
}) })

View File

@ -1,87 +1,89 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { InviteUserController } from './controllers/invite-user.controller';
import { InviteUserService } from './services/invite-user.service'; import { InviteUserService } from './services/invite-user.service';
import { InviteUserController } from './controllers/invite-user.controller';
import { ConfigModule } from '@nestjs/config';
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 {
DeviceRepository, UserRepository,
DeviceUserPermissionRepository, UserSpaceRepository,
} from '@app/common/modules/device/repositories'; } from '@app/common/modules/user/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,
SpaceService, SpaceService,
SpaceUserService, SpaceUserService,
SubspaceDeviceService, SubspaceDeviceService,
SubSpaceService, SubSpaceService,
ValidationService, ValidationService,
} from 'src/space/services'; } from 'src/space/services';
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service'; import { CommunityService } from 'src/community/services';
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service'; import {
import { TagService as NewTagService } from 'src/tags/services'; InviteSpaceRepository,
import { UserDevicePermissionService } from 'src/user-device-permission/services'; 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 { 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 {
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
import {
SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationRepoitory,
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';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule], imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
@ -114,6 +116,7 @@ import { UserService, UserSpaceService } from 'src/users/services';
TimeZoneRepository, TimeZoneRepository,
SpaceService, SpaceService,
InviteSpaceRepository, InviteSpaceRepository,
SpaceLinkService,
SubSpaceService, SubSpaceService,
ValidationService, ValidationService,
NewTagService, NewTagService,
@ -121,6 +124,7 @@ import { UserService, UserSpaceService } from 'src/users/services';
SpaceProductAllocationService, SpaceProductAllocationService,
SpaceLinkRepository, SpaceLinkRepository,
SubspaceRepository, SubspaceRepository,
TagService,
SubspaceDeviceService, SubspaceDeviceService,
SubspaceProductAllocationService, SubspaceProductAllocationService,
SpaceModelRepository, SpaceModelRepository,
@ -131,6 +135,7 @@ import { UserService, UserSpaceService } from 'src/users/services';
SpaceModelProductAllocationService, SpaceModelProductAllocationService,
SpaceProductAllocationRepository, SpaceProductAllocationRepository,
SubspaceProductAllocationRepository, SubspaceProductAllocationRepository,
TagRepository,
SubspaceModelRepository, SubspaceModelRepository,
SubspaceModelProductAllocationService, SubspaceModelProductAllocationService,
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
@ -149,7 +154,6 @@ import { UserService, UserSpaceService } from 'src/users/services';
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
AqiDataService,
], ],
exports: [InviteUserService], exports: [InviteUserService],
}) })

View File

@ -1,42 +1,36 @@
import { RoleType } from '@app/common/constants/role.type.enum'; import {
import { UserStatusEnum } from '@app/common/constants/user-status.enum'; Injectable,
HttpException,
HttpStatus,
BadRequestException,
} from '@nestjs/common';
import { AddUserInvitationDto } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
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 { InviteUserEntity } from '@app/common/modules/Invite-user/entities'; import { EntityManager, In, IsNull, Not, QueryRunner } from 'typeorm';
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 { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; import { CheckEmailDto } from '../dtos/check-email.dto';
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 { import { SpaceRepository } from '@app/common/modules/space';
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
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 { ActivateCodeDto } from '../dtos/active-code.dto';
import { CheckEmailDto } from '../dtos/check-email.dto'; import { UserSpaceService } from 'src/users/services';
import { SpaceUserService } from 'src/space/services';
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 {
@ -664,12 +658,12 @@ export class InviteUserService {
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
} }
await this.emailService.sendEmailWithTemplate({ await this.emailService.sendEmailWithTemplate(
email: userData.email, userData.email,
name: userData.firstName, userData.firstName,
isEnable: !disable, disable,
isDelete: false, false,
}); );
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return new SuccessResponseDto({ return new SuccessResponseDto({
@ -803,12 +797,12 @@ export class InviteUserService {
{ isActive: false }, { isActive: false },
); );
} }
await this.emailService.sendEmailWithTemplate({ await this.emailService.sendEmailWithTemplate(
email: userData.email, userData.email,
name: userData.firstName, userData.firstName,
isEnable: false, false,
isDelete: true, true,
}); );
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return new SuccessResponseDto({ return new SuccessResponseDto({

View File

@ -1,14 +1,15 @@
import { RequestContextMiddleware } from '@app/common/middleware/request-context.middleware';
import { SeederService } from '@app/common/seed/services/seeder.service';
import { Logger, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { json, urlencoded } from 'body-parser'; import { AppModule } from './app.module';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import helmet from 'helmet'; import helmet from 'helmet';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { setupSwaggerAuthentication } from '../libs/common/src/util/user-auth.swagger.utils'; import { setupSwaggerAuthentication } from '../libs/common/src/util/user-auth.swagger.utils';
import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common';
import { json, urlencoded } from 'body-parser';
import { SeederService } from '@app/common/seed/services/seeder.service';
import { HttpExceptionFilter } from './common/filters/http-exception/http-exception.filter'; import { HttpExceptionFilter } from './common/filters/http-exception/http-exception.filter';
import { Logger } from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { RequestContextMiddleware } from '@app/common/middleware/request-context.middleware';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
@ -29,13 +30,6 @@ async function bootstrap() {
}), }),
); );
app.use((req, res, next) => {
console.log('Real IP:', req.ip);
next();
});
// app.getHttpAdapter().getInstance().set('trust proxy', 1);
app.use( app.use(
helmet({ helmet({
contentSecurityPolicy: false, contentSecurityPolicy: false,

View File

@ -1,65 +1,66 @@
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { Module } from '@nestjs/common';
import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; import { ConfigModule } from '@nestjs/config';
import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { PowerClampService as PowerClamp } 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 { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { import {
SceneIconRepository, SpaceDeviceService,
SceneRepository, SpaceLinkService,
} from '@app/common/modules/scene/repositories'; SpaceService,
SubspaceDeviceService,
SubSpaceService,
ValidationService,
} from 'src/space/services';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { DeviceService } from 'src/device/services';
import { import {
InviteSpaceRepository, InviteSpaceRepository,
SpaceLinkRepository, SpaceLinkRepository,
SpaceProductAllocationRepository, SpaceProductAllocationRepository,
SpaceRepository, SpaceRepository,
TagRepository,
} from '@app/common/modules/space'; } from '@app/common/modules/space';
import { CommunityService } from 'src/community/services';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { import {
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
SpaceModelRepository, SpaceModelRepository,
SubspaceModelProductAllocationRepoitory, SubspaceModelProductAllocationRepoitory,
SubspaceModelRepository, SubspaceModelRepository,
} from '@app/common/modules/space-model'; } from '@app/common/modules/space-model';
import { import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
SubspaceProductAllocationRepository, import { ProductRepository } from '@app/common/modules/product/repositories';
SubspaceRepository, import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
} 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 { SceneService } from 'src/scene/services';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { TagService } from 'src/tags/services';
import { import {
SpaceModelService, SpaceModelService,
SubSpaceModelService, SubSpaceModelService,
} from 'src/space-model/services'; } from 'src/space-model/services';
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import {
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
import { SpaceModelProductAllocationService } from 'src/space-model/services/space-model-product-allocation.service'; import { 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 { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service';
import { import { OccupancyService } from '@app/common/helper/services/occupancy.service';
SpaceDeviceService,
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';
@Module({ @Module({
imports: [ConfigModule], imports: [ConfigModule],
controllers: [PowerClampController], controllers: [PowerClampController],
@ -89,6 +90,7 @@ import { PowerClampService as PowerClamp } from './services/power-clamp.service'
SceneRepository, SceneRepository,
AutomationRepository, AutomationRepository,
InviteSpaceRepository, InviteSpaceRepository,
SpaceLinkService,
SubSpaceService, SubSpaceService,
TagService, TagService,
SpaceModelService, SpaceModelService,
@ -103,12 +105,12 @@ import { PowerClampService as PowerClamp } from './services/power-clamp.service'
SpaceModelProductAllocationService, SpaceModelProductAllocationService,
SpaceProductAllocationRepository, SpaceProductAllocationRepository,
SubspaceProductAllocationRepository, SubspaceProductAllocationRepository,
TagRepository,
SubspaceModelRepository, SubspaceModelRepository,
SubspaceModelProductAllocationService, SubspaceModelProductAllocationService,
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationRepoitory, SubspaceModelProductAllocationRepoitory,
OccupancyService, OccupancyService,
AqiDataService,
], ],
exports: [PowerClamp], exports: [PowerClamp],
}) })

View File

@ -1,72 +1,73 @@
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { Global, Module } from '@nestjs/common';
import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; import { CqrsModule } from '@nestjs/cqrs';
import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { ProjectController } from './controllers';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; import { ProjectService } from './services';
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 { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories';
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 { ProjectRepository } from '@app/common/modules/project/repositiories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; import { CreateOrphanSpaceHandler } from './handler';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { import {
InviteSpaceRepository, InviteSpaceRepository,
SpaceLinkRepository, SpaceLinkRepository,
SpaceProductAllocationRepository, SpaceProductAllocationRepository,
SpaceRepository, SpaceRepository,
TagRepository,
} from '@app/common/modules/space'; } from '@app/common/modules/space';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories';
import { ProjectUserController } from './controllers/project-user.controller';
import { ProjectUserService } from './services/project-user.service';
import {
UserRepository,
UserSpaceRepository,
} from '@app/common/modules/user/repositories';
import {
SpaceLinkService,
SpaceService,
SubspaceDeviceService,
SubSpaceService,
ValidationService,
} from 'src/space/services';
import { TagService } from 'src/tags/services';
import {
SpaceModelService,
SubSpaceModelService,
} from 'src/space-model/services';
import { DeviceService } from 'src/device/services';
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 { CommunityService } from 'src/community/services';
import { import {
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
SpaceModelRepository, SpaceModelRepository,
SubspaceModelProductAllocationRepoitory, SubspaceModelProductAllocationRepoitory,
SubspaceModelRepository, SubspaceModelRepository,
} from '@app/common/modules/space-model'; } from '@app/common/modules/space-model';
import { import { DeviceRepository } from '@app/common/modules/device/repositories';
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
import { import { ProductRepository } from '@app/common/modules/product/repositories';
UserRepository,
UserSpaceRepository,
} from '@app/common/modules/user/repositories';
import { Global, Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
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 { SpaceModelProductAllocationService } from 'src/space-model/services/space-model-product-allocation.service';
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 { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service'; import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { import {
SpaceService, SceneIconRepository,
SubspaceDeviceService, SceneRepository,
SubSpaceService, } from '@app/common/modules/scene/repositories';
ValidationService, import { AutomationRepository } from '@app/common/modules/automation/repositories';
} from 'src/space/services'; import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service'; import {
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service'; PowerClampDailyRepository,
import { TagService } from 'src/tags/services'; PowerClampHourlyRepository,
import { ProjectController } from './controllers'; PowerClampMonthlyRepository,
import { ProjectUserController } from './controllers/project-user.controller'; } from '@app/common/modules/power-clamp/repositories';
import { CreateOrphanSpaceHandler } from './handler'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { ProjectService } from './services'; import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { ProjectUserService } from './services/project-user.service';
const CommandHandlers = [CreateOrphanSpaceHandler]; const CommandHandlers = [CreateOrphanSpaceHandler];
@ -86,6 +87,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
UserRepository, UserRepository,
SpaceService, SpaceService,
InviteSpaceRepository, InviteSpaceRepository,
SpaceLinkService,
SubSpaceService, SubSpaceService,
ValidationService, ValidationService,
TagService, TagService,
@ -109,6 +111,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
DeviceStatusFirebaseService, DeviceStatusFirebaseService,
SceneService, SceneService,
TuyaService, TuyaService,
TagRepository,
SubspaceModelRepository, SubspaceModelRepository,
SubspaceModelProductAllocationService, SubspaceModelProductAllocationService,
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
@ -123,7 +126,6 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
AqiDataService,
], ],
exports: [ProjectService, CqrsModule], exports: [ProjectService, CqrsModule],
}) })

View File

@ -1,17 +1,4 @@
import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
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 {
TypeORMCustomModel,
TypeORMCustomModelFindAllQuery,
} from '@app/common/models/typeOrmCustom.model';
import { ProjectDto } from '@app/common/modules/project/dtos';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { UserRepository } from '@app/common/modules/user/repositories';
import { format } from '@fast-csv/format';
import { import {
forwardRef, forwardRef,
HttpException, HttpException,
@ -19,12 +6,24 @@ import {
Inject, Inject,
Injectable, Injectable,
} from '@nestjs/common'; } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { SpaceService } from 'src/space/services';
import { PassThrough } from 'stream';
import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command';
import { CreateProjectDto, GetProjectParam } from '../dto'; import { CreateProjectDto, GetProjectParam } from '../dto';
import { QueryRunner } from 'typeorm'; import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { ProjectEntity } from '@app/common/modules/project/entities';
import {
TypeORMCustomModel,
TypeORMCustomModelFindAllQuery,
} from '@app/common/models/typeOrmCustom.model';
import { ProjectDto } from '@app/common/modules/project/dtos';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { CommandBus } from '@nestjs/cqrs';
import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command';
import { UserRepository } from '@app/common/modules/user/repositories';
import { format } from '@fast-csv/format';
import { PassThrough } from 'stream';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { SpaceService } from 'src/space/services';
import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
@Injectable() @Injectable()
export class ProjectService { export class ProjectService {
@ -213,14 +212,8 @@ export class ProjectService {
} }
} }
async findOne( async findOne(uuid: string): Promise<ProjectEntity> {
uuid: string, const project = await this.projectRepository.findOne({ where: { uuid } });
queryRunner?: QueryRunner,
): Promise<ProjectEntity> {
const projectRepository = queryRunner
? queryRunner.manager.getRepository(ProjectEntity)
: this.projectRepository;
const project = await projectRepository.findOne({ where: { uuid } });
if (!project) { if (!project) {
throw new HttpException( throw new HttpException(
`Invalid project with uuid ${uuid}`, `Invalid project with uuid ${uuid}`,
@ -243,11 +236,11 @@ export class ProjectService {
'communities.spaces.parent', 'communities.spaces.parent',
'communities.spaces.productAllocations', 'communities.spaces.productAllocations',
'communities.spaces.productAllocations.product', 'communities.spaces.productAllocations.product',
'communities.spaces.productAllocations.tag', 'communities.spaces.productAllocations.tags',
'communities.spaces.subspaces', 'communities.spaces.subspaces',
'communities.spaces.subspaces.productAllocations', 'communities.spaces.subspaces.productAllocations',
'communities.spaces.subspaces.productAllocations.product', 'communities.spaces.subspaces.productAllocations.product',
'communities.spaces.subspaces.productAllocations.tag', 'communities.spaces.subspaces.productAllocations.tags',
], ],
}); });
@ -310,38 +303,52 @@ export class ProjectService {
if (subspace.disabled) continue; if (subspace.disabled) continue;
for (const productAllocation of subspace.productAllocations || []) { for (const productAllocation of subspace.productAllocations || []) {
for (const tag of productAllocation.tags || []) {
csvStream.write({
'Device ID': '',
'Community Name': space.community?.name || '',
'Space Name': space.spaceName,
'Space Location': spaceLocation,
'Subspace Name': subspace.subspaceName || '',
Tag: tag.name,
'Product Name': productAllocation.product.name || '',
'Community UUID': space.community?.uuid || '',
'Space UUID': space.uuid,
'Subspace UUID': subspace.uuid,
});
}
}
}
for (const productAllocation of space.productAllocations || []) {
for (const tag of productAllocation.tags || []) {
csvStream.write({ csvStream.write({
'Device ID': '', 'Device ID': '',
'Community Name': space.community?.name || '', 'Community Name': space.community?.name || '',
'Space Name': space.spaceName, 'Space Name': space.spaceName,
'Space Location': spaceLocation, 'Space Location': spaceLocation,
'Subspace Name': subspace.subspaceName || '', 'Subspace Name': '',
Tag: productAllocation.tag.name, Tag: tag.name,
'Product Name': productAllocation.product.name || '', 'Product Name': productAllocation.product.name || '',
'Community UUID': space.community?.uuid || '', 'Community UUID': space.community?.uuid || '',
'Space UUID': space.uuid, 'Space UUID': space.uuid,
'Subspace UUID': subspace.uuid, 'Subspace UUID': '',
}); });
} }
} }
for (const productAllocation of space.productAllocations || []) {
csvStream.write({
'Device ID': '',
'Community Name': space.community?.name || '',
'Space Name': space.spaceName,
'Space Location': spaceLocation,
'Subspace Name': '',
Tag: productAllocation.tag.name,
'Product Name': productAllocation.product.name || '',
'Community UUID': space.community?.uuid || '',
'Space UUID': space.uuid,
'Subspace UUID': '',
});
}
} }
csvStream.end(); csvStream.end();
return stream; return stream;
} }
getSpaceLocation(space: SpaceEntity): string {
const names = [];
let current = space.parent;
while (current) {
names.unshift(current.spaceName);
current = current.parent;
}
return names.join(' > ');
}
} }

View File

@ -1,6 +1,7 @@
export * from './create-space-model.dto'; export * from './create-space-model.dto';
export * from './link-space-model.dto';
export * from './project-param.dto'; export * from './project-param.dto';
export * from './update-space-model.dto';
export * from './space-model-param'; export * from './space-model-param';
export * from './subspaces-model-dtos'; export * from './subspaces-model-dtos';
export * from './update-space-model.dto'; export * from './tag-model-dtos';
export * from './link-space-model.dto';

View File

@ -1,14 +1,14 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
IsArray,
IsEnum,
IsOptional,
IsString, IsString,
IsOptional,
IsArray,
ValidateNested, ValidateNested,
IsEnum,
} from 'class-validator'; } from 'class-validator';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; import { ModifyTagModelDto } from '../tag-model-dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
export class ModifySubspaceModelDto { export class ModifySubspaceModelDto {
@ApiProperty({ @ApiProperty({
@ -37,11 +37,11 @@ export class ModifySubspaceModelDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
'List of tag modifications (add/update/delete) for the subspace', 'List of tag modifications (add/update/delete) for the subspace',
type: [ModifyTagDto], type: [ModifyTagModelDto],
}) })
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => ModifyTagDto) @Type(() => ModifyTagModelDto)
tags?: ModifyTagDto[]; tags?: ModifyTagModelDto[];
} }

View File

@ -0,0 +1,28 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateTagModelDto {
@ApiProperty({
description: 'Tag associated with the space or subspace models',
example: 'Temperature Control',
})
@IsNotEmpty()
@IsString()
tag: string;
@ApiPropertyOptional({
description: 'UUID of the tag model (required for update/delete)',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@ApiProperty({
description: 'ID of the product associated with the tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsNotEmpty()
@IsString()
productUuid: string;
}

View File

@ -0,0 +1,3 @@
export * from './create-tag-model.dto';
export * from './update-tag-model.dto';
export * from './modify-tag-model.dto';

View File

@ -0,0 +1,46 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum, IsUUID } from 'class-validator';
export class ModifyTagModelDto {
@ApiProperty({
description: 'Action to perform: add, update, or delete',
example: ModifyAction.ADD,
})
@IsEnum(ModifyAction)
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the new tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsUUID()
newTagUuid: string;
@ApiPropertyOptional({
description:
'UUID of an existing tag (required for update/delete, optional for add)',
example: 'a1b2c3d4-5678-90ef-abcd-1234567890ef',
})
@IsOptional()
@IsUUID()
tagUuid?: string;
@ApiPropertyOptional({
description: 'Name of the tag (required for add/update)',
example: 'Temperature Sensor',
})
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({
description:
'UUID of the product associated with the tag (required for add)',
example: 'c789a91e-549a-4753-9006-02f89e8170e0',
})
@IsOptional()
@IsUUID()
productUuid?: string;
}

View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
export class UpdateTagModelDto {
@ApiProperty({
description: 'UUID of the tag to be updated',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsNotEmpty()
@IsUUID()
uuid: string;
@ApiProperty({
description: 'Updated name of the tag',
example: 'Updated Tag Name',
required: false,
})
@IsOptional()
@IsString()
tag?: string;
}

View File

@ -1,13 +1,48 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator';
import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto';
import { Type } from 'class-transformer';
import { import {
DeleteSubspaceModelDto, DeleteSubspaceModelDto,
ModifySubspaceModelDto, ModifySubspaceModelDto,
UpdateSubspaceModelDto, UpdateSubspaceModelDto,
} from './subspaces-model-dtos'; } from './subspaces-model-dtos';
import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto'; import { ModifyTagModelDto } from './tag-model-dtos';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
export class ModifySubspacesModelDto {
@ApiProperty({
description: 'List of subspaces to add',
type: [CreateSubspaceModelDto],
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateSubspaceModelDto)
add?: CreateSubspaceModelDto[];
@ApiProperty({
description: 'List of subspaces to add',
type: [CreateSubspaceModelDto],
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => UpdateSubspaceModelDto)
update?: UpdateSubspaceModelDto[];
@ApiProperty({
description: 'List of subspaces to delete',
type: [DeleteSubspaceModelDto],
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => DeleteSubspaceModelDto)
delete?: DeleteSubspaceModelDto[];
}
export class UpdateSpaceModelDto { export class UpdateSpaceModelDto {
@ApiProperty({ @ApiProperty({
@ -31,11 +66,11 @@ export class UpdateSpaceModelDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
'List of tag modifications (add/update/delete) for the space model', 'List of tag modifications (add/update/delete) for the space model',
type: [ModifyTagDto], type: [ModifyTagModelDto],
}) })
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => ModifyTagDto) @Type(() => ModifyTagModelDto)
tags?: ModifyTagDto[]; tags?: ModifyTagModelDto[];
} }

View File

@ -1,11 +1,11 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { PropogateUpdateSpaceModelProductAllocationCommand } from '../commands';
import { SpaceProductAllocationRepository } from '@app/common/modules/space'; import { SpaceProductAllocationRepository } from '@app/common/modules/space';
import { SubspaceModelProductAllocationRepoitory } from '@app/common/modules/space-model'; import { SubspaceModelProductAllocationRepoitory } from '@app/common/modules/space-model';
import { import {
SubspaceProductAllocationRepository,
SubspaceRepository, SubspaceRepository,
SubspaceProductAllocationRepository,
} from '@app/common/modules/space/repositories/subspace.repository'; } from '@app/common/modules/space/repositories/subspace.repository';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { PropogateUpdateSpaceModelProductAllocationCommand } from '../commands';
@CommandHandler(PropogateUpdateSpaceModelProductAllocationCommand) @CommandHandler(PropogateUpdateSpaceModelProductAllocationCommand)
export class PropogateUpdateSpaceModelProductAllocationHandler export class PropogateUpdateSpaceModelProductAllocationHandler
@ -31,89 +31,89 @@ export class PropogateUpdateSpaceModelProductAllocationHandler
console.log(`Processing ${updatedAllocations.length} allocations...`); console.log(`Processing ${updatedAllocations.length} allocations...`);
// for (const allocation of updatedAllocations) { for (const allocation of updatedAllocations) {
// try { try {
// if (allocation.allocation) { if (allocation.allocation) {
// const spaceAllocations = await this.spaceProductRepository.find({ const spaceAllocations = await this.spaceProductRepository.find({
// where: { uuid: allocation.allocation.uuid }, where: { uuid: allocation.allocation.uuid },
// relations: ['tags'], relations: ['tags'],
// }); });
// if (!spaceAllocations || spaceAllocations.length === 0) { if (!spaceAllocations || spaceAllocations.length === 0) {
// console.warn( console.warn(
// `No space allocations found for UUID: ${allocation.allocation.uuid}`, `No space allocations found for UUID: ${allocation.allocation.uuid}`,
// ); );
// continue; continue;
// } }
// if (allocation.tagsAdded?.length) { if (allocation.tagsAdded?.length) {
// for (const spaceAllocation of spaceAllocations) { for (const spaceAllocation of spaceAllocations) {
// spaceAllocation.tags.push(...allocation.tagsAdded); spaceAllocation.tags.push(...allocation.tagsAdded);
// } }
// await this.spaceProductRepository.save(spaceAllocations); await this.spaceProductRepository.save(spaceAllocations);
// console.log( console.log(
// `Added tags to ${spaceAllocations.length} space allocations.`, `Added tags to ${spaceAllocations.length} space allocations.`,
// ); );
// } }
// if (allocation.tagsRemoved?.length) { if (allocation.tagsRemoved?.length) {
// const tagsToRemoveUUIDs = new Set( const tagsToRemoveUUIDs = new Set(
// allocation.tagsRemoved.map((tag) => tag.uuid), allocation.tagsRemoved.map((tag) => tag.uuid),
// ); );
// for (const spaceAllocation of spaceAllocations) { for (const spaceAllocation of spaceAllocations) {
// spaceAllocation.tags = spaceAllocation.tags.filter( spaceAllocation.tags = spaceAllocation.tags.filter(
// (tag) => !tagsToRemoveUUIDs.has(tag.uuid), (tag) => !tagsToRemoveUUIDs.has(tag.uuid),
// ); );
// } }
// await this.spaceProductRepository.save(spaceAllocations); await this.spaceProductRepository.save(spaceAllocations);
// console.log( console.log(
// `Removed tags from ${spaceAllocations.length} space allocations.`, `Removed tags from ${spaceAllocations.length} space allocations.`,
// ); );
// } }
// } }
// if (allocation.deletedAllocation) { if (allocation.deletedAllocation) {
// const spaceAllocations = await this.spaceProductRepository.find({ const spaceAllocations = await this.spaceProductRepository.find({
// where: { uuid: allocation.deletedAllocation.uuid }, where: { uuid: allocation.deletedAllocation.uuid },
// relations: ['tags'], relations: ['tags'],
// }); });
// if (!spaceAllocations || spaceAllocations.length === 0) { if (!spaceAllocations || spaceAllocations.length === 0) {
// console.warn( console.warn(
// `No space allocations found to delete for UUID: ${allocation.deletedAllocation.uuid}`, `No space allocations found to delete for UUID: ${allocation.deletedAllocation.uuid}`,
// ); );
// continue; continue;
// } }
// await this.spaceProductRepository.remove(spaceAllocations); await this.spaceProductRepository.remove(spaceAllocations);
// console.log( console.log(
// `Deleted ${spaceAllocations.length} space allocations.`, `Deleted ${spaceAllocations.length} space allocations.`,
// ); );
// } }
// if (allocation.newAllocation) { if (allocation.newAllocation) {
// const newAllocations = spaces.map((space) => const newAllocations = spaces.map((space) =>
// this.spaceProductRepository.create({ this.spaceProductRepository.create({
// space, space,
// product: allocation.newAllocation.product, product: allocation.newAllocation.product,
// tag: allocation.newAllocation.tag, tags: allocation.newAllocation.tags,
// inheritedFromModel: allocation.newAllocation, inheritedFromModel: allocation.newAllocation,
// }), }),
// ); );
// await this.spaceProductRepository.save(newAllocations); await this.spaceProductRepository.save(newAllocations);
// console.log( console.log(
// `Created ${newAllocations.length} new space allocations.`, `Created ${newAllocations.length} new space allocations.`,
// ); );
// } }
// } catch (error) { } catch (error) {
// console.error( console.error(
// `Error processing allocation update: ${JSON.stringify(allocation)}`, `Error processing allocation update: ${JSON.stringify(allocation)}`,
// error, error,
// ); );
// } }
// } }
console.log('Finished processing all allocations.'); console.log('Finished processing all allocations.');
} catch (error) { } catch (error) {

View File

@ -1,14 +1,15 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { PropogateUpdateSpaceModelCommand } from '../commands';
import { SpaceProductAllocationRepository } from '@app/common/modules/space'; import { SpaceProductAllocationRepository } from '@app/common/modules/space';
import { SubspaceModelProductAllocationRepoitory } from '@app/common/modules/space-model';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { import {
SubspaceProductAllocationRepository, SubspaceProductAllocationRepository,
SubspaceRepository, SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository'; } from '@app/common/modules/space/repositories/subspace.repository';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { SubspaceModelProductAllocationRepoitory } from '@app/common/modules/space-model';
import { PropogateUpdateSpaceModelCommand } from '../commands'; import { In } from 'typeorm';
import { ISingleSubspaceModel } from '../interfaces'; import { ISingleSubspaceModel } from '../interfaces';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { IUpdatedAllocations } from '../interfaces/subspace-product-allocation-update-result.interface'; import { IUpdatedAllocations } from '../interfaces/subspace-product-allocation-update-result.interface';
@CommandHandler(PropogateUpdateSpaceModelCommand) @CommandHandler(PropogateUpdateSpaceModelCommand)
@ -71,48 +72,48 @@ export class PropogateUpdateSpaceModelHandler
for (const allocation of allocations) { for (const allocation of allocations) {
if (!allocation) continue; if (!allocation) continue;
// if (allocation.allocation) { if (allocation.allocation) {
// try { try {
// const subspaceAllocations = const subspaceAllocations =
// await this.subspaceProductRepository.find({ await this.subspaceProductRepository.find({
// where: { where: {
// inheritedFromModel: { uuid: allocation.allocation.uuid }, inheritedFromModel: { uuid: allocation.allocation.uuid },
// }, },
// relations: ['tags'], relations: ['tags'],
// }); });
// if (!subspaceAllocations || subspaceAllocations.length === 0) if (!subspaceAllocations || subspaceAllocations.length === 0)
// continue; continue;
// if (allocation.tagsAdded?.length) { if (allocation.tagsAdded?.length) {
// for (const subspaceAllocation of subspaceAllocations) { for (const subspaceAllocation of subspaceAllocations) {
// subspaceAllocation.tags.push(...allocation.tagsAdded); subspaceAllocation.tags.push(...allocation.tagsAdded);
// } }
// await this.subspaceProductRepository.save(subspaceAllocations); await this.subspaceProductRepository.save(subspaceAllocations);
// console.log( console.log(
// `Added tags to ${subspaceAllocations.length} subspace allocations.`, `Added tags to ${subspaceAllocations.length} subspace allocations.`,
// ); );
// } }
// if (allocation.tagsRemoved?.length) { if (allocation.tagsRemoved?.length) {
// const tagsToRemoveUUIDs = allocation.tagsRemoved.map( const tagsToRemoveUUIDs = allocation.tagsRemoved.map(
// (tag) => tag.uuid, (tag) => tag.uuid,
// ); );
// for (const subspaceAllocation of subspaceAllocations) { for (const subspaceAllocation of subspaceAllocations) {
// subspaceAllocation.tags = subspaceAllocation.tags.filter( subspaceAllocation.tags = subspaceAllocation.tags.filter(
// (tag) => !tagsToRemoveUUIDs.includes(tag.uuid), (tag) => !tagsToRemoveUUIDs.includes(tag.uuid),
// ); );
// } }
// await this.subspaceProductRepository.save(subspaceAllocations); await this.subspaceProductRepository.save(subspaceAllocations);
// console.log( console.log(
// `Removed tags from ${subspaceAllocations.length} subspace allocations.`, `Removed tags from ${subspaceAllocations.length} subspace allocations.`,
// ); );
// } }
// } catch (error) { } catch (error) {
// console.error('Error processing allocation update:', error); console.error('Error processing allocation update:', error);
// } }
// } }
if (allocation.newAllocation) { if (allocation.newAllocation) {
try { try {
@ -126,7 +127,7 @@ export class PropogateUpdateSpaceModelHandler
const newAllocations = subspaces.map((subspace) => const newAllocations = subspaces.map((subspace) =>
this.subspaceProductRepository.create({ this.subspaceProductRepository.create({
product: allocation.newAllocation.product, product: allocation.newAllocation.product,
tag: allocation.newAllocation.tag, tags: allocation.newAllocation.tags,
subspace, subspace,
inheritedFromModel: allocation.newAllocation, inheritedFromModel: allocation.newAllocation,
}), }),
@ -197,7 +198,7 @@ export class PropogateUpdateSpaceModelHandler
const subspaceAllocation = this.subspaceProductRepository.create({ const subspaceAllocation = this.subspaceProductRepository.create({
subspace: subspace, subspace: subspace,
product: allocation.product, product: allocation.product,
tag: allocation.tag, tags: allocation.tags,
inheritedFromModel: allocation, inheritedFromModel: allocation,
}); });
await this.subspaceProductRepository.save(subspaceAllocation); await this.subspaceProductRepository.save(subspaceAllocation);
@ -210,59 +211,67 @@ export class PropogateUpdateSpaceModelHandler
subspaceModel: ISingleSubspaceModel, subspaceModel: ISingleSubspaceModel,
spaces: SpaceEntity[], spaces: SpaceEntity[],
) { ) {
// const subspaces = await this.subspaceRepository.find({ const subspaces = await this.subspaceRepository.find({
// where: { where: {
// subSpaceModel: { uuid: subspaceModel.subspaceModel.uuid }, subSpaceModel: { uuid: subspaceModel.subspaceModel.uuid },
// disabled: false, disabled: false,
// }, },
// relations: [ relations: [
// 'productAllocations', 'productAllocations',
// 'productAllocations.product', 'productAllocations.product',
// 'productAllocations.tags', 'productAllocations.tags',
// ], ],
// }); });
// if (!subspaces.length) {
// return; if (!subspaces.length) {
// } return;
// const allocationUuidsToRemove = subspaces.flatMap((subspace) => }
// subspace.productAllocations.map((allocation) => allocation.uuid),
// ); const allocationUuidsToRemove = subspaces.flatMap((subspace) =>
// if (allocationUuidsToRemove.length) { subspace.productAllocations.map((allocation) => allocation.uuid),
// await this.subspaceProductRepository.delete(allocationUuidsToRemove); );
// }
// await this.subspaceRepository.update( if (allocationUuidsToRemove.length) {
// { uuid: In(subspaces.map((s) => s.uuid)) }, await this.subspaceProductRepository.delete(allocationUuidsToRemove);
// { disabled: true }, }
// );
// const relocatedAllocations = subspaceModel.relocatedAllocations || []; await this.subspaceRepository.update(
// if (!relocatedAllocations.length) { { uuid: In(subspaces.map((s) => s.uuid)) },
// return; { disabled: true },
// } );
// for (const space of spaces) {
// for (const { allocation, tags = [] } of relocatedAllocations) { const relocatedAllocations = subspaceModel.relocatedAllocations || [];
// const spaceAllocation = await this.spaceProductRepository.findOne({
// where: { if (!relocatedAllocations.length) {
// inheritedFromModel: { uuid: allocation.uuid }, return;
// space: { uuid: space.uuid }, }
// },
// relations: ['tags'], for (const space of spaces) {
// }); for (const { allocation, tags = [] } of relocatedAllocations) {
// if (spaceAllocation) { const spaceAllocation = await this.spaceProductRepository.findOne({
// if (tags.length) { where: {
// spaceAllocation.tags.push(...tags); inheritedFromModel: { uuid: allocation.uuid },
// await this.spaceProductRepository.save(spaceAllocation); space: { uuid: space.uuid },
// } },
// } else { relations: ['tags'],
// const newSpaceAllocation = this.spaceProductRepository.create({ });
// space,
// inheritedFromModel: allocation, if (spaceAllocation) {
// tag: allocation.tag, if (tags.length) {
// product: allocation.product, spaceAllocation.tags.push(...tags);
// }); await this.spaceProductRepository.save(spaceAllocation);
// await this.spaceProductRepository.save(newSpaceAllocation); }
// } } else {
// } const newSpaceAllocation = this.spaceProductRepository.create({
// } space,
inheritedFromModel: allocation,
tags: allocation.tags,
product: allocation.product,
});
await this.spaceProductRepository.save(newSpaceAllocation);
}
}
}
} }
async updateSubspaceModel(subspaceModel: ISingleSubspaceModel) { async updateSubspaceModel(subspaceModel: ISingleSubspaceModel) {

View File

@ -1,3 +1,4 @@
export * from './update-subspace.interface';
export * from './modify-subspace.interface';
export * from './single-subspace.interface'; export * from './single-subspace.interface';
export * from './space-product-allocation.interface'; export * from './space-product-allocation.interface';
export * from './update-subspace.interface';

View File

@ -0,0 +1,24 @@
import { SubspaceModelEntity, TagModel } from '@app/common/modules/space-model';
export interface ModifyspaceModelPayload {
modifiedSubspaceModels?: ModifySubspaceModelPayload;
modifiedTags?: ModifiedTagsModelPayload;
}
export interface ModifySubspaceModelPayload {
addedSubspaceModels?: SubspaceModelEntity[];
updatedSubspaceModels?: UpdatedSubspaceModelPayload[];
deletedSubspaceModels?: string[];
}
export interface UpdatedSubspaceModelPayload {
subspaceName?: string;
modifiedTags?: ModifiedTagsModelPayload;
subspaceModelUuid: string;
}
export interface ModifiedTagsModelPayload {
added?: TagModel[];
updated?: TagModel[];
deleted?: string[];
}

View File

@ -1,11 +1,11 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { import {
SpaceModelProductAllocationEntity, SpaceModelProductAllocationEntity,
SubspaceModelEntity, SubspaceModelEntity,
} from '@app/common/modules/space-model'; } from '@app/common/modules/space-model';
import { ModifyTagModelDto } from '../dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { NewTagEntity } from '@app/common/modules/tag'; import { NewTagEntity } from '@app/common/modules/tag';
import { IUpdatedAllocations } from './subspace-product-allocation-update-result.interface'; import { IUpdatedAllocations } from './subspace-product-allocation-update-result.interface';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
export interface IRelocatedAllocation { export interface IRelocatedAllocation {
allocation: SpaceModelProductAllocationEntity; allocation: SpaceModelProductAllocationEntity;
@ -14,7 +14,7 @@ export interface IRelocatedAllocation {
export interface ISingleSubspaceModel { export interface ISingleSubspaceModel {
subspaceModel: SubspaceModelEntity; subspaceModel: SubspaceModelEntity;
action: ModifyAction; action: ModifyAction;
tags?: ModifyTagDto[]; tags?: ModifyTagModelDto[];
relocatedAllocations?: IRelocatedAllocation[]; relocatedAllocations?: IRelocatedAllocation[];
} }

View File

@ -1,19 +1,20 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { In, QueryRunner } from 'typeorm'; import { In, QueryRunner } from 'typeorm';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ProductEntity } from '@app/common/modules/product/entities';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { SpaceRepository } from '@app/common/modules/space';
import { import {
SpaceModelEntity, SpaceModelEntity,
SpaceModelProductAllocationEntity, SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationEntity,
} from '@app/common/modules/space-model'; } from '@app/common/modules/space-model';
import { NewTagEntity } from '@app/common/modules/tag';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService as NewTagService } from 'src/tags/services'; import { TagService as NewTagService } from 'src/tags/services';
import { ModifySubspaceModelDto } from '../dtos'; import { ProcessTagDto } from 'src/tags/dtos';
import { ModifySubspaceModelDto, ModifyTagModelDto } from '../dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { NewTagEntity } from '@app/common/modules/tag';
import { ProductEntity } from '@app/common/modules/product/entities';
import { SpaceRepository } from '@app/common/modules/space';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { IUpdatedSpaceAllocations } from '../interfaces'; import { IUpdatedSpaceAllocations } from '../interfaces';
@Injectable() @Injectable()
@ -31,220 +32,225 @@ export class SpaceModelProductAllocationService {
queryRunner?: QueryRunner, queryRunner?: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[], modifySubspaceModels?: ModifySubspaceModelDto[],
): Promise<IUpdatedSpaceAllocations[]> { ): Promise<IUpdatedSpaceAllocations[]> {
// try { try {
if (!tags.length) return []; if (!tags.length) return [];
const allocationUpdates: IUpdatedSpaceAllocations[] = []; const allocationUpdates: IUpdatedSpaceAllocations[] = [];
return allocationUpdates;
// const processedTags = await this.tagService.processTags(
// tags,
// projectUuid,
// queryRunner,
// );
// const productAllocations: SpaceModelProductAllocationEntity[] = []; const processedTags = await this.tagService.processTags(
// const existingAllocations = new Map< tags,
// string, projectUuid,
// SpaceModelProductAllocationEntity queryRunner,
// >(); );
// for (const tag of processedTags) { const productAllocations: SpaceModelProductAllocationEntity[] = [];
// let isTagNeeded = true; const existingAllocations = new Map<
string,
SpaceModelProductAllocationEntity
>();
// if (modifySubspaceModels) { for (const tag of processedTags) {
// const relatedSubspaces = await queryRunner.manager.find( let isTagNeeded = true;
// SubspaceModelProductAllocationEntity,
// {
// where: {
// product: { uuid: tag.product.uuid },
// subspaceModel: { spaceModel: { uuid: spaceModel.uuid } },
// tags: { uuid: tag.uuid },
// },
// relations: ['subspaceModel', 'tags'],
// },
// );
// for (const subspaceWithTag of relatedSubspaces) { if (modifySubspaceModels) {
// const modifyingSubspace = modifySubspaceModels.find( const relatedSubspaces = await queryRunner.manager.find(
// (subspace) => SubspaceModelProductAllocationEntity,
// subspace.action === ModifyAction.UPDATE && {
// subspace.uuid === subspaceWithTag.subspaceModel.uuid, where: {
// ); product: { uuid: tag.product.uuid },
subspaceModel: { spaceModel: { uuid: spaceModel.uuid } },
tags: { uuid: tag.uuid },
},
relations: ['subspaceModel', 'tags'],
},
);
// if ( for (const subspaceWithTag of relatedSubspaces) {
// modifyingSubspace && const modifyingSubspace = modifySubspaceModels.find(
// modifyingSubspace.tags && (subspace) =>
// modifyingSubspace.tags.some( subspace.action === ModifyAction.UPDATE &&
// (subspaceTag) => subspace.uuid === subspaceWithTag.subspaceModel.uuid,
// subspaceTag.action === ModifyAction.DELETE && );
// subspaceTag.tagUuid === tag.uuid,
// )
// ) {
// isTagNeeded = true;
// break;
// }
// }
// }
// if (isTagNeeded) { if (
// const hasTags = await this.validateTagWithinSpaceModel( modifyingSubspace &&
// queryRunner, modifyingSubspace.tags &&
// tag, modifyingSubspace.tags.some(
// spaceModel, (subspaceTag) =>
// ); subspaceTag.action === ModifyAction.DELETE &&
subspaceTag.tagUuid === tag.uuid,
)
) {
isTagNeeded = true;
break;
}
}
}
// if (hasTags) continue; if (isTagNeeded) {
const hasTags = await this.validateTagWithinSpaceModel(
queryRunner,
tag,
spaceModel,
);
// let allocation = existingAllocations.get(tag.product.uuid); if (hasTags) continue;
// if (!allocation) {
// allocation = await this.getAllocationByProduct(
// tag.product,
// spaceModel,
// queryRunner,
// );
// if (allocation) {
// existingAllocations.set(tag.product.uuid, allocation);
// }
// }
// if (!allocation) { let allocation = existingAllocations.get(tag.product.uuid);
// allocation = this.createNewAllocation(spaceModel, tag, queryRunner); if (!allocation) {
// productAllocations.push(allocation); allocation = await this.getAllocationByProduct(
// allocationUpdates.push({ tag.product,
// newAllocation: allocation, spaceModel,
// }); queryRunner,
// } else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) { );
// allocation.tags.push(tag); if (allocation) {
// allocationUpdates.push({ existingAllocations.set(tag.product.uuid, allocation);
// allocation: allocation, }
// tagsAdded: [tag], }
// });
// await this.saveAllocation(allocation, queryRunner);
// }
// }
// }
// if (productAllocations.length > 0) { if (!allocation) {
// await this.saveAllocations(productAllocations, queryRunner); allocation = this.createNewAllocation(spaceModel, tag, queryRunner);
// } productAllocations.push(allocation);
allocationUpdates.push({
newAllocation: allocation,
});
} else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) {
allocation.tags.push(tag);
allocationUpdates.push({
allocation: allocation,
tagsAdded: [tag],
});
await this.saveAllocation(allocation, queryRunner);
}
}
}
// return allocationUpdates; if (productAllocations.length > 0) {
// } catch (error) { await this.saveAllocations(productAllocations, queryRunner);
// throw this.handleError(error, 'Failed to create product allocations'); }
// }
return allocationUpdates;
} catch (error) {
throw this.handleError(error, 'Failed to create product allocations');
}
} }
async updateProductAllocations( async updateProductAllocations(
dtos: ModifyTagDto[], dtos: ModifyTagModelDto[],
project: ProjectEntity, project: ProjectEntity,
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
queryRunner: QueryRunner, queryRunner: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[], modifySubspaceModels?: ModifySubspaceModelDto[],
): Promise<IUpdatedSpaceAllocations[]> { ): Promise<IUpdatedSpaceAllocations[]> {
const allocationUpdates: IUpdatedSpaceAllocations[] = []; try {
return allocationUpdates; const addDtos = dtos.filter((dto) => dto.action === ModifyAction.ADD);
// try { const deleteDtos = dtos.filter(
// const addDtos = dtos.filter((dto) => dto.action === ModifyAction.ADD); (dto) => dto.action === ModifyAction.DELETE,
// const deleteDtos = dtos.filter( );
// (dto) => dto.action === ModifyAction.DELETE,
// ); const addTagDtos: ProcessTagDto[] = addDtos.map((dto) => ({
// const addTagDtos: ProcessTagDto[] = addDtos.map((dto) => ({ name: dto.name,
// name: dto.name, productUuid: dto.productUuid,
// productUuid: dto.productUuid, uuid: dto.newTagUuid,
// uuid: dto.newTagUuid, }));
// }));
// // Process added tags // Process added tags
// const processedTags = await this.tagService.processTags( const processedTags = await this.tagService.processTags(
// addTagDtos, addTagDtos,
// project.uuid, project.uuid,
// queryRunner, queryRunner,
// ); );
// const addTagUuidMap = new Map<string, ModifyTagModelDto>();
// processedTags.forEach((tag, index) => { const addTagUuidMap = new Map<string, ModifyTagModelDto>();
// addTagUuidMap.set(tag.uuid, addDtos[index]); processedTags.forEach((tag, index) => {
// }); addTagUuidMap.set(tag.uuid, addDtos[index]);
// const addTagUuids = new Set(processedTags.map((tag) => tag.uuid)); });
// const deleteTagUuids = new Set(deleteDtos.map((dto) => dto.tagUuid));
// const tagsToIgnore = new Set( const addTagUuids = new Set(processedTags.map((tag) => tag.uuid));
// [...addTagUuids].filter((uuid) => deleteTagUuids.has(uuid)), const deleteTagUuids = new Set(deleteDtos.map((dto) => dto.tagUuid));
// );
// // Filter out tags that are added and deleted in the same request const tagsToIgnore = new Set(
// const filteredDtos = dtos.filter( [...addTagUuids].filter((uuid) => deleteTagUuids.has(uuid)),
// (dto) => );
// !(
// tagsToIgnore.has(dto.tagUuid) || // Filter out tags that are added and deleted in the same request
// (dto.action === ModifyAction.ADD && const filteredDtos = dtos.filter(
// tagsToIgnore.has( (dto) =>
// [...addTagUuidMap.keys()].find( !(
// (uuid) => addTagUuidMap.get(uuid) === dto, tagsToIgnore.has(dto.tagUuid) ||
// ), (dto.action === ModifyAction.ADD &&
// )) tagsToIgnore.has(
// ), [...addTagUuidMap.keys()].find(
// ); (uuid) => addTagUuidMap.get(uuid) === dto,
// // Process add and delete actions concurrently ),
// const [updatedAllocations, deletedAllocations] = await Promise.all([ ))
// this.processAddActions( ),
// filteredDtos, );
// project.uuid,
// spaceModel, // Process add and delete actions concurrently
// queryRunner, const [updatedAllocations, deletedAllocations] = await Promise.all([
// modifySubspaceModels, this.processAddActions(
// ), filteredDtos,
// this.processDeleteActions(filteredDtos, queryRunner, spaceModel), project.uuid,
// ]); spaceModel,
// // Combine results and return queryRunner,
// return [...updatedAllocations, ...deletedAllocations]; modifySubspaceModels,
// } catch (error) { ),
// throw this.handleError(error, 'Error while updating product allocations'); this.processDeleteActions(filteredDtos, queryRunner, spaceModel),
// } ]);
// Combine results and return
return [...updatedAllocations, ...deletedAllocations];
} catch (error) {
throw this.handleError(error, 'Error while updating product allocations');
}
} }
private async processAddActions( private async processAddActions(
dtos: ModifyTagDto[], dtos: ModifyTagModelDto[],
projectUuid: string, projectUuid: string,
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
queryRunner: QueryRunner, queryRunner: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[], modifySubspaceModels?: ModifySubspaceModelDto[],
): Promise<IUpdatedSpaceAllocations[]> { ): Promise<IUpdatedSpaceAllocations[]> {
const allocationUpdates: IUpdatedSpaceAllocations[] = []; let allocationUpdates: IUpdatedSpaceAllocations[] = [];
return allocationUpdates;
// const addDtos: ProcessTagDto[] = dtos
// .filter((dto) => dto.action === ModifyAction.ADD)
// .map((dto) => ({
// name: dto.name,
// productUuid: dto.productUuid,
// uuid: dto.newTagUuid,
// }));
// if (addDtos.length > 0) { const addDtos: ProcessTagDto[] = dtos
// allocationUpdates = await this.createProductAllocations( .filter((dto) => dto.action === ModifyAction.ADD)
// projectUuid, .map((dto) => ({
// spaceModel, name: dto.name,
// addDtos, productUuid: dto.productUuid,
// queryRunner, uuid: dto.newTagUuid,
// modifySubspaceModels, }));
// );
// } if (addDtos.length > 0) {
// return allocationUpdates; allocationUpdates = await this.createProductAllocations(
projectUuid,
spaceModel,
addDtos,
queryRunner,
modifySubspaceModels,
);
}
return allocationUpdates;
} }
private createNewAllocation( private createNewAllocation(
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
tag: NewTagEntity, tag: NewTagEntity,
queryRunner?: QueryRunner, queryRunner?: QueryRunner,
) { ): SpaceModelProductAllocationEntity {
// : SpaceModelProductAllocationEntity return queryRunner
// return queryRunner ? queryRunner.manager.create(SpaceModelProductAllocationEntity, {
// ? queryRunner.manager.create(SpaceModelProductAllocationEntity, { spaceModel,
// spaceModel, product: tag.product,
// product: tag.product, tags: [tag],
// tags: [tag], })
// }) : this.spaceModelProductAllocationRepository.create({
// : this.spaceModelProductAllocationRepository.create({ spaceModel,
// spaceModel, product: tag.product,
// product: tag.product, tags: [tag],
// tags: [tag], });
// });
} }
private async getAllocationByProduct( private async getAllocationByProduct(
@ -303,7 +309,7 @@ export class SpaceModelProductAllocationService {
} }
private async processDeleteActions( private async processDeleteActions(
dtos: ModifyTagDto[], dtos: ModifyTagModelDto[],
queryRunner: QueryRunner, queryRunner: QueryRunner,
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
): Promise<IUpdatedSpaceAllocations[]> { ): Promise<IUpdatedSpaceAllocations[]> {
@ -311,10 +317,10 @@ export class SpaceModelProductAllocationService {
if (!dtos || dtos.length === 0) { if (!dtos || dtos.length === 0) {
return; return;
} }
const allocationUpdateToPropagate: IUpdatedSpaceAllocations[] = []; let allocationUpdateToPropagate: IUpdatedSpaceAllocations[] = [];
const tagUuidsToDelete = dtos const tagUuidsToDelete = dtos
// .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid) .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid)
.map((dto) => dto.tagUuid); .map((dto) => dto.tagUuid);
if (tagUuidsToDelete.length === 0) return []; if (tagUuidsToDelete.length === 0) return [];
@ -323,7 +329,7 @@ export class SpaceModelProductAllocationService {
SpaceModelProductAllocationEntity, SpaceModelProductAllocationEntity,
{ {
where: { where: {
tag: In(tagUuidsToDelete), tags: { uuid: In(tagUuidsToDelete) },
spaceModel: { spaceModel: {
uuid: spaceModel.uuid, uuid: spaceModel.uuid,
}, },
@ -342,28 +348,31 @@ export class SpaceModelProductAllocationService {
const allocationUpdates: SpaceModelProductAllocationEntity[] = []; const allocationUpdates: SpaceModelProductAllocationEntity[] = [];
for (const allocation of allocationsToUpdate) { for (const allocation of allocationsToUpdate) {
// const updatedTags = allocation.tags.filter( const updatedTags = allocation.tags.filter(
// (tag) => !tagUuidsToDelete.includes(tag.uuid), (tag) => !tagUuidsToDelete.includes(tag.uuid),
// ); );
// const deletedTags = allocation.tags.filter((tag) =>
// tagUuidsToDelete.includes(tag.uuid), const deletedTags = allocation.tags.filter((tag) =>
// ); tagUuidsToDelete.includes(tag.uuid),
// if (updatedTags.length === allocation.tags.length) { );
// continue;
// } if (updatedTags.length === allocation.tags.length) {
// if (updatedTags.length === 0) { continue;
// deletedAllocations.push(allocation); }
// allocationUpdateToPropagate.push({
// deletedAllocation: allocation, if (updatedTags.length === 0) {
// }); deletedAllocations.push(allocation);
// } else { allocationUpdateToPropagate.push({
// allocation.tags = updatedTags; deletedAllocation: allocation,
// allocationUpdates.push(allocation); });
// allocationUpdateToPropagate.push({ } else {
// allocation: allocation, allocation.tags = updatedTags;
// tagsRemoved: deletedTags, allocationUpdates.push(allocation);
// }); allocationUpdateToPropagate.push({
// } allocation: allocation,
tagsRemoved: deletedTags,
});
}
} }
if (allocationUpdates.length > 0) { if (allocationUpdates.length > 0) {
@ -406,34 +415,33 @@ export class SpaceModelProductAllocationService {
tag: NewTagEntity, tag: NewTagEntity,
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
): Promise<boolean> { ): Promise<boolean> {
return true; const existingAllocationsForProduct = await queryRunner.manager.find(
// const existingAllocationsForProduct = await queryRunner.manager.find( SpaceModelProductAllocationEntity,
// SpaceModelProductAllocationEntity, {
// { where: {
// where: { spaceModel: {
// spaceModel: { uuid: spaceModel.uuid,
// uuid: spaceModel.uuid, },
// }, product: {
// product: { uuid: tag.product.uuid,
// uuid: tag.product.uuid, },
// }, },
// }, relations: ['tags'],
// relations: ['tags'], },
// }, );
// );
// const existingTagsForProduct = existingAllocationsForProduct.flatMap( const existingTagsForProduct = existingAllocationsForProduct.flatMap(
// (allocation) => allocation.tags, (allocation) => allocation.tags,
// ); );
// const isDuplicateTag = existingTagsForProduct.some( const isDuplicateTag = existingTagsForProduct.some(
// (existingTag) => existingTag.uuid === tag.uuid, (existingTag) => existingTag.uuid === tag.uuid,
// ); );
// if (isDuplicateTag) { if (isDuplicateTag) {
// return true; return true;
// } }
// return false; return false;
} }
async clearAllAllocations(spaceModelUuid: string, queryRunner: QueryRunner) { async clearAllAllocations(spaceModelUuid: string, queryRunner: QueryRunner) {

Some files were not shown because too many files have changed in this diff Show More