Compare commits

..

100 Commits

Author SHA1 Message Date
7ea53feddc add deviceName to handle password API 2025-06-30 08:54:25 +03:00
c7a4ff1194 fix: schedule device types (#441) 2025-06-29 15:27:55 +03:00
8a4633b158 Merge pull request #439 from SyncrowIOT/add-check-log-to-trace-the-map-issue
feat: enhance device status handling with caching and batch processin…
2025-06-25 18:59:37 -06:00
f80d097ff8 refactor: optimize log insertion and clean up device cache handling in TuyaWebSocketService 2025-06-25 18:57:56 -06:00
04bd156df1 Merge branch 'dev' into add-check-log-to-trace-the-map-issue 2025-06-25 18:42:43 -06:00
731819aeaa feat: enhance device status handling with caching and batch processing improvements 2025-06-25 18:37:46 -06:00
68d2d3b53d fix: improve device retrieval logic in addDeviceStatusToFirebase method 2025-06-25 08:13:02 -06:00
3fcfe2d92f Merge pull request #438 from SyncrowIOT/temp-fix-to-check
fix: enhance device status handling by integrating device cache for improved performance
2025-06-25 08:06:29 -06:00
c0a069b460 fix: enhance device status handling by integrating device cache for improved performance 2025-06-25 08:03:23 -06:00
30724d7d37 Merge pull request #436 from SyncrowIOT/add-check-log-to-trace-the-map-issue
fix: add validation for missing properties in device status logs
2025-06-25 05:32:50 -06:00
324661e1ee fix: add missing check for device UUID in batch processing logs 2025-06-25 05:30:15 -06:00
a83424f45b fix: remove unnecessary validation for missing properties in device status logs 2025-06-25 05:29:28 -06:00
71f6ccb4db fix: add validation for missing properties in device status logs 2025-06-25 05:20:26 -06:00
68692b7c8b increase rate limit to 100 per minute for each IP (#435) 2025-06-25 13:50:38 +03:00
4d60c1ed54 Merge pull request #434 from SyncrowIOT/fix-time-out-connections-db
Fix-time-out-connections-db
2025-06-25 04:47:59 -06:00
27dbe04299 fix: remove unnecessary comment from ScheduleModule import in scheduler module 2025-06-25 04:47:38 -06:00
9bebcb2f3e feat: implement scheduler for periodic data updates and optimize database procedures
- Added SchedulerModule and SchedulerService to handle hourly data updates for AQI, occupancy, and energy consumption.
- Refactored existing services to remove unused device repository dependencies and streamline procedure execution.
- Updated SQL procedures to use correct parameter indexing.
- Enhanced error handling and logging for scheduled tasks.
- Integrated new repositories for presence sensor and AQI pollutant stats across multiple modules.
- Added NestJS schedule package for task scheduling capabilities.
2025-06-25 03:20:25 -06:00
43ab0030f0 refactor: clean up unused services and optimize batch processing in DeviceStatusFirebaseService 2025-06-25 03:20:12 -06:00
c48adb73b5 Merge pull request #433 from SyncrowIOT/DATA-adjust-remaining-procedures
DATA-adjust-remaining-procedures
2025-06-25 01:55:12 -06:00
d255e6811e update procedures 2025-06-25 10:47:37 +03:00
e58d2d4831 Test/prevent server block on rate limit (#432) 2025-06-24 14:56:02 +03:00
147cf0b582 Merge pull request #431 from SyncrowIOT/DATA-adjust-procedures
DATA-adjust-procedures
2025-06-24 04:58:09 -06:00
4e6b6f6ac5 adjusted procedures 2025-06-24 13:04:21 +03:00
932a3efd1c Sp 1780 be configure the curtain module device (#424)
* task: add Cur new device configuration
2025-06-24 12:18:46 +03:00
0a1ccad120 add check if not space not found (#430) 2025-06-24 12:18:15 +03:00
f337e6c681 Test/prevent server block on rate limit (#421) 2025-06-24 10:55:38 +03:00
f5bf857071 Merge pull request #429 from SyncrowIOT/add-queue-event-handler
Add queue event handler
2025-06-23 08:13:36 -06:00
d1d4d529a8 Add methods to handle SOS events and device status updates in Firebase and our DB 2025-06-23 08:10:33 -06:00
37b582f521 Merge pull request #428 from SyncrowIOT/add-queue-event-handler
Implement message queue for TuyaWebSocketService and batch processing
2025-06-23 07:35:22 -06:00
cf19f08dca turn on all the updates data points 2025-06-23 07:33:01 -06:00
ff370b2baa Implement message queue for TuyaWebSocketService and batch processing 2025-06-23 07:31:58 -06:00
04f64407e1 turn off some update data points 2025-06-23 07:10:47 -06:00
d7eef5d03e Merge pull request #427 from SyncrowIOT/revert-426-SP-1778-be-fix-time-out-connections-in-the-db
Revert "SP-1778-be-fix-time-out-connections-in-the-db"
2025-06-23 07:09:20 -06:00
c8d691b380 tern off data procedure 2025-06-23 07:02:23 -06:00
75d03366c2 Revert "SP-1778-be-fix-time-out-connections-in-the-db" 2025-06-23 06:58:57 -06:00
52cb69cc84 Merge pull request #426 from SyncrowIOT/SP-1778-be-fix-time-out-connections-in-the-db
SP-1778-be-fix-time-out-connections-in-the-db
2025-06-23 06:38:58 -06:00
a6053b3971 refactor: implement query runners for database operations in multiple services 2025-06-23 06:34:53 -06:00
60d2c8330b fix: increase DB max pool size (#425) 2025-06-23 15:23:53 +03:00
fddd06e06d fix: add space condition to the join operator instead of general query (#423) 2025-06-23 12:44:19 +03:00
3160773c2a fix: spaces structure in communities (#420) 2025-06-23 10:21:55 +03:00
110ed4157a task: add spaces filter to get devices by project (#422) 2025-06-23 09:34:59 +03:00
aa9e90bf08 Test/prevent server block on rate limit (#419)
* increase DB max connection to 50
2025-06-19 14:34:23 +03:00
c5dd5e28fd Test/prevent server block on rate limit (#418) 2025-06-19 13:54:22 +03:00
603e74af09 Test/prevent server block on rate limit (#417)
* task: add trust proxy header

* add logging

* task: test rate limits on sever

* task: increase rate limit timeout

* fix: merge conflicts
2025-06-19 12:54:59 +03:00
0e36f32ed6 Test/prevent server block on rate limit (#415)
* task: increase rate limit timeout
2025-06-19 10:15:29 +03:00
705ceeba29 Test/prevent server block on rate limit (#414)
* task: test rate limits on sever
2025-06-19 09:45:09 +03:00
a37d5bb299 task: add trust proxy header (#411)
* task: add trust proxy header

* add logging
2025-06-18 12:05:53 +03:00
689a38ee0c Revamp/space management (#409)
* task: add getCommunitiesV2

* task: update getOneSpace API to match revamp structure

* refactor: implement modifications to pace management APIs

* refactor: remove space link
2025-06-18 10:34:29 +03:00
a91d0f22a4 fix: send correct enable status to email sender function (#407) 2025-06-13 09:46:41 +03:00
0db060ae3f Merge pull request #406 from SyncrowIOT/add-space-daily-occupancy-duration-entity
feat: add SpaceDailyOccupancyDuration entity, DTO, and repository for occupancy tracking
2025-06-12 04:17:18 -06:00
f2ed04f206 feat: add SpaceDailyOccupancyDuration entity, DTO, and repository for occupancy tracking 2025-06-12 02:49:59 -06:00
ea9a65178d fix: add space filter to "join" operation instead of "and" operation (#405) 2025-06-11 16:28:33 +03:00
8503ee728d Refactor/space management (#404)
* refactor: reducing used queries on get communities (#385)

* refactor: fix create space logic (#394)

* Remove unique constraint on subspace and product in SubspaceProductAllocationEntity; update product relation to nullable in NewTagEntity

* refactor: fix create space logic

* device model updated to include the fixes and final columns

* updated space models to include suggested fixes, update final logic and column names

* task: removing old references of the old tag-product relation

* task: remove old use of tags

* task: remove old tag & tag model usage

* refactor: delete space

* task: remove unused functions

* fix lint rule
2025-06-11 13:15:21 +03:00
4f5e1b23f6 Merge pull request #403 from SyncrowIOT/SP-1629-AND-SP-1630-AQI-APIs
Sp 1629 and sp 1630 aqi ap is
2025-06-11 00:28:14 -06:00
2cb77504ca Add PollutantType enum and update AQI-related entities and services to use it 2025-06-11 00:28:00 -06:00
c86be27576 Add AQI module and related services, controllers, and DTOs
- Introduced AqiModule with AqiService and AqiController for handling AQI data.
- Added DTOs for AQI requests: GetAqiDailyBySpaceDto and GetAqiPollutantBySpaceDto.
- Implemented AqiDataService for managing AQI sensor historical data.
- Updated existing modules to include AqiDataService where necessary.
- Defined new routes for AQI data retrieval in ControllerRoute.
2025-06-10 18:19:34 -06:00
3a08f9f258 Merge branch 'dev' into aqi-test 2025-06-10 17:08:06 -06:00
5c96a3b117 Merge pull request #402 from SyncrowIOT/add-code-and-value-as-uniqe-key
Refactor DeviceStatusLogEntity: expand unique constraint to include c…
2025-06-10 01:43:09 -06:00
97e14e70f7 Refactor DeviceStatusLogEntity: expand unique constraint to include code and value 2025-06-10 01:41:41 -06:00
03d44cb14f Merge pull request #401 from SyncrowIOT/fix-log-insert-error
Refactor DeviceStatusLogEntity: update unique constraint to include d…
2025-06-10 01:27:15 -06:00
0793441e06 Refactor DeviceStatusLogEntity: correct unique constraint name for event time and device ID 2025-06-10 01:24:33 -06:00
b6321c2530 Refactor DeviceStatusLogEntity: update unique constraint to include deviceId 2025-06-10 01:18:58 -06:00
b8d34b0d9f Merge pull request #400 from SyncrowIOT/revert-398-fix-log-duplication-issue
Revert "Refactor DeviceStatusLogEntity: update unique constraint and primary …"
2025-06-10 01:04:29 -06:00
c1065126aa Revert "Refactor DeviceStatusLogEntity: update unique constraint and primary …" 2025-06-10 01:03:45 -06:00
1742454984 Merge pull request #398 from SyncrowIOT/fix-log-duplication-issue
Refactor DeviceStatusLogEntity: update unique constraint and primary …
2025-06-10 00:26:43 -06:00
7eb13088ac Refactor DeviceStatusLogEntity: update unique constraint and primary key definition 2025-06-09 04:50:58 -06:00
7b97e50d2e Merge pull request #391 from SyncrowIOT/DATA-space-model-aqi-update-logic
AQI space model updated with new hourly to daily logic for calculatio…
2025-06-04 17:40:39 -04:00
4fb26fc131 Merge pull request #397 from SyncrowIOT/DATA-daily-procedure-aqi
Procedures insert-all, update, and select for daily space air quality…
2025-06-04 17:39:34 -04:00
ee0261d102 Fix typos procedure select and update 2025-06-04 17:32:50 -04:00
0d6de2df43 Refactor AqiSpaceDailyPollutantStatsEntity: update unique constraint and rename event fields for clarity 2025-06-04 15:24:13 -06:00
80e89dd035 fix name of snapshot table 2025-06-04 17:20:25 -04:00
466863e71f Procedures insert-all, update, and select for daily space air quality stats 2025-06-04 16:33:55 -04:00
30aafdede6 Merge pull request #390 from SyncrowIOT/DATA-device-model-aqi-update-logic
device model for aqi updated with hourly to daily logic getting max, …
2025-06-04 12:04:10 +03:00
01ce4d4b29 Merge pull request #396 from SyncrowIOT/fix-some-issue-in-get-commuinty-and-weather-apis
Fix some issue in get commuinty and weather apis
2025-06-04 01:32:21 -06:00
43dfaaa90d fix: utilize WEATHER_API_URL in WeatherService for dynamic API endpoint 2025-06-04 01:32:01 -06:00
ea021ad228 fix: update error message for invalid latitude and longitude in fetchWeatherDetails method 2025-06-04 01:09:49 -06:00
cd3e9016f2 fix: improve error handling in fetchWeatherDetails method 2025-06-04 01:08:33 -06:00
ef2245eae1 Add AQI space daily pollutant stats module and related entities, DTOs, and repositories 2025-06-03 23:37:52 -06:00
3ad81864d1 updated space models to include suggested fixes, update final logic and column names 2025-06-03 21:05:34 -04:00
ab3efedc35 device model updated to include the fixes and final columns 2025-06-03 20:52:27 -04:00
4a984ae5dd Merge pull request #395 from SyncrowIOT/SP-1678-be-implement-weather-apis-by-location
Add Weather module with controller, service, and DTO for fetching weather details
2025-06-03 03:05:01 -06:00
c39129f75b Add Weather module with controller, service, and DTO for fetching weather details 2025-06-03 02:38:44 -06:00
35ce13a67f fix: return proper error on login API (#386) 2025-06-03 09:47:24 +03:00
12a9272b8b Merge pull request #393 from SyncrowIOT/SP-1675-be-return-space-uuid-in-get-devices-api
SP-1675-be-return-space-uuid-in-get-devices-api
2025-06-02 01:29:04 -06:00
0fe6c80731 Add utility function to associate space UUID with devices in community and device services 2025-06-01 21:56:08 -06:00
81e017430e Merge pull request #392 from SyncrowIOT/temp-product-relation-fixes
Remove unique constraint on subspace and product in SubspaceProductAllocationEntity; update product relation to nullable in NewTagEntity
2025-06-02 06:20:03 +03:00
191d0dfaf6 Remove unique constraint on subspace and product in SubspaceProductAllocationEntity; update product relation to nullable in NewTagEntity 2025-06-01 21:16:51 -06:00
5b0135ba80 AQI space model updated with new hourly to daily logic for calculations and categorization of aqi brackets 2025-06-01 16:09:17 -04:00
2fee8c055e device model for aqi updated with hourly to daily logic getting max, min, avergae and percentage of categorical values for each aqi bracket 2025-06-01 16:03:07 -04:00
59161d4049 Merge pull request #389 from SyncrowIOT/DATA-daily-space-aqi-model
Air quality and AQI for space model
2025-06-01 14:31:13 -04:00
b989338790 Merge pull request #388 from SyncrowIOT/DATA-device-aqi-fix-update
fixed pm25 code + date format. Added average AQI level from device
2025-06-01 14:31:00 -04:00
f5ed9d4fce Merge pull request #387 from SyncrowIOT/DATA-occupancy-duration-fix
DATA-occupancy-duration-fix
2025-05-30 11:59:31 +03:00
3ac48183bd procedure fix test 2025-05-30 11:27:12 +03:00
684205053d testing fix 2025-05-30 10:51:26 +03:00
bfd92fdd87 fix 2025-05-29 15:05:06 +03:00
dd54af5f46 fix 2025-05-29 13:38:50 +03:00
90fc44ab53 Air quality and AQI for space model 2025-05-28 21:14:01 -04:00
efdf918159 fixed pm25 code + date format. Added average AQI level from device 2025-05-28 20:42:56 -04:00
25967d02f9 model adjustments 2025-05-28 13:55:56 +03:00
f44dc793a6 Merge pull request #383 from SyncrowIOT/daily-aqi-score
AQI score calculations and air quality model
2025-05-22 14:57:12 +03:00
151 changed files with 6045 additions and 5794 deletions

View File

@ -21,6 +21,7 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
"@typescript-eslint/no-unused-vars": 'warn',
},
settings: {
'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 {
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as argon2 from 'argon2';
import { HelperHashService } from '../../helper/services';
import { UserRepository } from '../../../../common/src/modules/user/repositories';
import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository';
import { UserSessionEntity } from '../../../../common/src/modules/session/entities';
import { ConfigService } from '@nestjs/config';
import { OAuth2Client } from 'google-auth-library';
import { PlatformType } from '@app/common/constants/platform-type.enum';
import { RoleType } from '@app/common/constants/role.type.enum';
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';
@Injectable()
export class AuthService {
@ -40,16 +40,17 @@ export class AuthService {
},
relations: ['roleType', 'project'],
});
if (
platform === PlatformType.WEB &&
(user.roleType.type === RoleType.SPACE_OWNER ||
user.roleType.type === RoleType.SPACE_MEMBER)
) {
throw new UnauthorizedException('Access denied for web platform');
}
if (!user) {
throw new BadRequestException('Invalid credentials');
}
if (
platform === PlatformType.WEB &&
[RoleType.SPACE_OWNER, RoleType.SPACE_MEMBER].includes(
user.roleType.type as RoleType,
)
) {
throw new UnauthorizedException('Access denied for web platform');
}
if (!user.isUserVerified) {
throw new BadRequestException('User is not verified');

View File

@ -465,7 +465,16 @@ export class ControllerRoute {
'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 {
public static readonly ROUTE = 'policy';
@ -515,6 +524,20 @@ export class ControllerRoute {
'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 {
public static readonly ROUTE = 'devices';

View File

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

View File

@ -15,8 +15,10 @@ export enum ProductType {
WL = 'WL',
GD = 'GD',
CUR = 'CUR',
CUR_2 = 'CUR_2',
PC = 'PC',
FOUR_S = '4S',
SIX_S = '6S',
SOS = 'SOS',
AQI = 'AQI',
}

View File

@ -1,51 +1,31 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SnakeNamingStrategy } from './strategies';
import { UserEntity } from '../modules/user/entities/user.entity';
import { UserSessionEntity } from '../modules/session/entities/session.entity';
import { UserOtpEntity } from '../modules/user/entities';
import { ProductEntity } from '../modules/product/entities';
import { DeviceEntity } from '../modules/device/entities';
import { PermissionTypeEntity } from '../modules/permission/entities';
import { ProductEntity } from '../modules/product/entities';
import { UserSessionEntity } from '../modules/session/entities/session.entity';
import { UserOtpEntity } from '../modules/user/entities';
import { UserEntity } from '../modules/user/entities/user.entity';
import { SnakeNamingStrategy } from './strategies';
import { UserSpaceEntity } from '../modules/user/entities';
import { DeviceUserPermissionEntity } from '../modules/device/entities';
import { RoleTypeEntity } from '../modules/role-type/entities';
import { UserNotificationEntity } from '../modules/user/entities';
import { DeviceNotificationEntity } from '../modules/device/entities';
import { RegionEntity } from '../modules/region/entities';
import { TimeZoneEntity } from '../modules/timezone/entities';
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger';
import { createLogger } from 'winston';
import { winstonLoggerOptions } from '../logger/services/winston.logger';
import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities';
import { AutomationEntity } from '../modules/automation/entities';
import { ClientEntity } from '../modules/client/entities';
import { CommunityEntity } from '../modules/community/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 {
SpaceModelEntity,
SubspaceModelEntity,
TagModel,
SpaceModelProductAllocationEntity,
SubspaceModelProductAllocationEntity,
} from '../modules/space-model/entities';
DeviceNotificationEntity,
DeviceUserPermissionEntity,
} from '../modules/device/entities';
import {
InviteUserEntity,
InviteUserSpaceEntity,
} from '../modules/Invite-user/entities';
import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity';
import { AutomationEntity } from '../modules/automation/entities';
import { SpaceProductAllocationEntity } from '../modules/space/entities/space-product-allocation.entity';
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
import { SpaceEntity } from '../modules/space/entities/space.entity';
import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity';
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
import { TagEntity } from '../modules/space/entities/tag.entity';
import { ClientEntity } from '../modules/client/entities';
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger';
import { createLogger } from 'winston';
import { winstonLoggerOptions } from '../logger/services/winston.logger';
import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities';
import {
PowerClampDailyEntity,
PowerClampHourlyEntity,
@ -55,6 +35,29 @@ import {
PresenceSensorDailyDeviceEntity,
PresenceSensorDailySpaceEntity,
} from '../modules/presence-sensor/entities';
import { ProjectEntity } from '../modules/project/entities';
import { RegionEntity } from '../modules/region/entities';
import { RoleTypeEntity } from '../modules/role-type/entities';
import { SceneDeviceEntity } from '../modules/scene-device/entities';
import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
} from '../modules/space-model/entities';
import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity';
import { 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({
imports: [
TypeOrmModule.forRootAsync({
@ -83,9 +86,7 @@ import {
PermissionTypeEntity,
CommunityEntity,
SpaceEntity,
SpaceLinkEntity,
SubspaceEntity,
TagEntity,
UserSpaceEntity,
DeviceUserPermissionEntity,
RoleTypeEntity,
@ -100,7 +101,6 @@ import {
SceneDeviceEntity,
SpaceModelEntity,
SubspaceModelEntity,
TagModel,
InviteUserEntity,
InviteUserSpaceEntity,
InviteSpaceEntity,
@ -115,6 +115,8 @@ import {
PowerClampMonthlyEntity,
PresenceSensorDailyDeviceEntity,
PresenceSensorDailySpaceEntity,
AqiSpaceDailyPollutantStatsEntity,
SpaceDailyOccupancyDurationEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
@ -123,8 +125,8 @@ import {
logger: typeOrmLogger,
extra: {
charset: 'utf8mb4',
max: 20, // set pool max size
idleTimeoutMillis: 5000, // close idle clients after 5 second
max: 100, // set pool max size
idleTimeoutMillis: 3000, // close idle clients after 5 second
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)
},

View File

@ -1,10 +1,9 @@
import { IsBoolean, IsDate, IsOptional } from 'class-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 { Transform } from 'class-transformer';
import { parseToDate } from '../util/parseToDate';
import { IsBoolean, IsOptional } from 'class-validator';
import { BooleanValues } from '../constants/boolean-values.enum';
import { IsPageRequestParam } from '../validators/is-page-request-param.validator';
import { IsSizeRequestParam } from '../validators/is-size-request-param.validator';
export class PaginationRequestGetListDto {
@ApiProperty({
@ -19,6 +18,7 @@ export class PaginationRequestGetListDto {
return value.obj.includeSpaces === BooleanValues.TRUE;
})
public includeSpaces?: boolean = false;
@IsOptional()
@IsPageRequestParam({
message: 'Page must be bigger than 0',
@ -40,40 +40,4 @@ export class PaginationRequestGetListDto {
description: 'Size request',
})
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

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

View File

@ -18,11 +18,6 @@ import {
runTransaction,
} from 'firebase/database';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { ProductType } from '@app/common/constants/product-type.enum';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum';
import { PresenceSensorEnum } from '@app/common/constants/presence.sensor.enum';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Injectable()
export class DeviceStatusFirebaseService {
private tuya: TuyaContext;
@ -30,8 +25,6 @@ export class DeviceStatusFirebaseService {
constructor(
private readonly configService: ConfigService,
private readonly deviceRepository: DeviceRepository,
private readonly powerClampService: PowerClampService,
private readonly occupancyService: OccupancyService,
private deviceStatusLogRepository: DeviceStatusLogRepository,
) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
@ -74,25 +67,94 @@ export class DeviceStatusFirebaseService {
);
}
}
async addBatchDeviceStatusToOurDb(
batch: {
deviceTuyaUuid: string;
status: any;
log: any;
device: any;
}[],
): Promise<void> {
const allLogs = [];
console.log(`🔁 Preparing logs from batch of ${batch.length} items...`);
for (const item of batch) {
const device = item.device;
if (!device?.uuid) {
console.log(`⛔ Skipped unknown device: ${item.deviceTuyaUuid}`);
continue;
}
const logs = item.log.properties.map((property) =>
this.deviceStatusLogRepository.create({
deviceId: device.uuid,
deviceTuyaId: item.deviceTuyaUuid,
productId: item.log.productId,
log: item.log,
code: property.code,
value: property.value,
eventId: item.log.dataId,
eventTime: new Date(property.time).toISOString(),
}),
);
allLogs.push(...logs);
}
console.log(`📝 Total logs to insert: ${allLogs.length}`);
const insertLogsPromise = (async () => {
const chunkSize = 300;
let insertedCount = 0;
for (let i = 0; i < allLogs.length; i += chunkSize) {
const chunk = allLogs.slice(i, i + chunkSize);
try {
const result = await this.deviceStatusLogRepository
.createQueryBuilder()
.insert()
.into('device-status-log') // or use DeviceStatusLogEntity
.values(chunk)
.orIgnore() // skip duplicates
.execute();
insertedCount += result.identifiers.length;
console.log(
`✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`,
);
} catch (error) {
console.error('❌ Insert error (skipped chunk):', error.message);
}
}
console.log(
`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`,
);
})();
await insertLogsPromise;
}
async addDeviceStatusToFirebase(
addDeviceStatusDto: AddDeviceStatusDto,
addDeviceStatusDto: AddDeviceStatusDto & { device?: any },
): Promise<AddDeviceStatusDto | null> {
try {
const device = await this.getDeviceByDeviceTuyaUuid(
addDeviceStatusDto.deviceTuyaUuid,
);
let device = addDeviceStatusDto.device;
if (!device) {
device = await this.getDeviceByDeviceTuyaUuid(
addDeviceStatusDto.deviceTuyaUuid,
);
}
if (device?.uuid) {
return await this.createDeviceStatusFirebase({
deviceUuid: device.uuid,
...addDeviceStatusDto,
productType: device.productDevice.prodType,
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;
}
}
@ -106,6 +168,15 @@ export class DeviceStatusFirebaseService {
relations: ['productDevice'],
});
}
async getAllDevices() {
return await this.deviceRepository.find({
where: {
isActive: true,
},
relations: ['productDevice'],
});
}
async getDevicesInstructionStatus(deviceUuid: string) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
@ -209,60 +280,6 @@ export class DeviceStatusFirebaseService {
return existingData;
});
// Save logs to your repository
const newLogs = addDeviceStatusDto.log.properties.map((property) => {
return this.deviceStatusLogRepository.create({
deviceId: addDeviceStatusDto.deviceUuid,
deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid,
productId: addDeviceStatusDto.log.productId,
log: addDeviceStatusDto.log,
code: property.code,
value: property.value,
eventId: addDeviceStatusDto.log.dataId,
eventTime: new Date(property.time).toISOString(),
});
});
await this.deviceStatusLogRepository.save(newLogs);
if (addDeviceStatusDto.productType === ProductType.PC) {
const energyCodes = new Set([
PowerClampEnergyEnum.ENERGY_CONSUMED,
PowerClampEnergyEnum.ENERGY_CONSUMED_A,
PowerClampEnergyEnum.ENERGY_CONSUMED_B,
PowerClampEnergyEnum.ENERGY_CONSUMED_C,
]);
const energyStatus = addDeviceStatusDto?.log?.properties?.find((status) =>
energyCodes.has(status.code),
);
if (energyStatus) {
await this.powerClampService.updateEnergyConsumedHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
}
if (
addDeviceStatusDto.productType === ProductType.CPS ||
addDeviceStatusDto.productType === ProductType.WPS
) {
const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]);
const occupancyStatus = addDeviceStatusDto?.log?.properties?.find(
(status) => occupancyCodes.has(status.code),
);
if (occupancyStatus) {
await this.occupancyService.updateOccupancySensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
await this.occupancyService.updateOccupancySensorHistoricalDurationData(
addDeviceStatusDto.deviceUuid,
);
}
}
// Return the updated data
const snapshot: DataSnapshot = await get(dataRef);
return snapshot.val();

View File

@ -8,7 +8,10 @@ import { TuyaWebSocketService } from './services/tuya.web.socket.service';
import { OneSignalService } from './services/onesignal.service';
import { DeviceMessagesService } from './services/device.messages.service';
import { DeviceRepositoryModule } from '../modules/device/device.repository.module';
import { DeviceNotificationRepository } from '../modules/device/repositories';
import {
DeviceNotificationRepository,
DeviceRepository,
} from '../modules/device/repositories';
import { DeviceStatusFirebaseModule } from '../firebase/devices-status/devices-status.module';
import { CommunityPermissionService } from './services/community.permission.service';
import { CommunityRepository } from '../modules/community/repositories';
@ -27,6 +30,7 @@ import { SosHandlerService } from './services/sos.handler.service';
DeviceNotificationRepository,
CommunityRepository,
SosHandlerService,
DeviceRepository,
],
exports: [
HelperHashService,

View File

@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { SqlLoaderService } from './sql-loader.service';
@Injectable()
export class AqiDataService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async updateAQISensorHistoricalData(): Promise<void> {
try {
const { dateStr } = this.getFormattedDates();
// Execute all procedures in parallel
await Promise.all([
this.executeProcedureWithRetry(
'proceduce_update_daily_space_aqi',
[dateStr],
'fact_daily_space_aqi',
),
]);
} catch (err) {
console.error('Failed to update AQI sensor historical data:', err);
throw err;
}
}
private getFormattedDates(): { dateStr: string } {
const now = new Date();
return {
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
};
}
private async executeProcedureWithRetry(
procedureFileName: string,
params: (string | number | null)[],
folderName: string,
retries = 3,
): Promise<void> {
try {
const query = this.loadQuery(folderName, procedureFileName);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
} catch (err) {
if (retries > 0) {
const delayMs = 1000 * (4 - retries); // Exponential backoff
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return this.executeProcedureWithRetry(
procedureFileName,
params,
folderName,
retries - 1,
);
}
console.error(`Failed to execute ${procedureFileName}:`, err);
throw err;
}
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

View File

@ -1,65 +1,68 @@
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { Injectable } from '@nestjs/common';
import { SqlLoaderService } from './sql-loader.service';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { SqlLoaderService } from './sql-loader.service';
@Injectable()
export class OccupancyService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
private readonly deviceRepository: DeviceRepository,
) {}
async updateOccupancySensorHistoricalDurationData(
deviceUuid: string,
): Promise<void> {
try {
const now = new Date();
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
const device = await this.deviceRepository.findOne({
where: { uuid: deviceUuid },
relations: ['spaceDevice'],
});
await this.executeProcedure(
'fact_daily_space_occupancy_duration',
'procedure_update_daily_space_occupancy_duration',
[dateStr, device.spaceDevice?.uuid],
);
async updateOccupancyDataProcedures(): Promise<void> {
try {
const { dateStr } = this.getFormattedDates();
// Execute all procedures in parallel
await Promise.all([
this.executeProcedureWithRetry(
'procedure_update_fact_space_occupancy',
[dateStr],
'fact_space_occupancy_count',
),
this.executeProcedureWithRetry(
'procedure_update_daily_space_occupancy_duration',
[dateStr],
'fact_daily_space_occupancy_duration',
),
]);
} catch (err) {
console.error('Failed to insert or update occupancy duration data:', err);
console.error('Failed to update occupancy data:', err);
throw err;
}
}
async updateOccupancySensorHistoricalData(deviceUuid: string): Promise<void> {
try {
const now = new Date();
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
const device = await this.deviceRepository.findOne({
where: { uuid: deviceUuid },
relations: ['spaceDevice'],
});
await this.executeProcedure(
'fact_space_occupancy_count',
'procedure_update_fact_space_occupancy',
[dateStr, device.spaceDevice?.uuid],
);
} catch (err) {
console.error('Failed to insert or update occupancy data:', err);
throw err;
}
private getFormattedDates(): { dateStr: string } {
const now = new Date();
return {
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
};
}
private async executeProcedure(
procedureFolderName: string,
private async executeProcedureWithRetry(
procedureFileName: string,
params: (string | number | null)[],
folderName: string,
retries = 3,
): Promise<void> {
const query = this.loadQuery(procedureFolderName, procedureFileName);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
try {
const query = this.loadQuery(folderName, procedureFileName);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
} catch (err) {
if (retries > 0) {
const delayMs = 1000 * (4 - retries); // Exponential backoff
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return this.executeProcedureWithRetry(
procedureFileName,
params,
folderName,
retries - 1,
);
}
console.error(`Failed to execute ${procedureFileName}:`, err);
throw err;
}
}
private loadQuery(folderName: string, fileName: string): string {

View File

@ -1,7 +1,7 @@
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';
import { SqlLoaderService } from './sql-loader.service';
@Injectable()
export class PowerClampService {
@ -10,48 +10,72 @@ export class PowerClampService {
private readonly dataSource: DataSource,
) {}
async updateEnergyConsumedHistoricalData(deviceUuid: string): Promise<void> {
async updateEnergyConsumedHistoricalData(): Promise<void> {
try {
const now = new Date();
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
const hour = now.getHours();
const monthYear = now
.toLocaleDateString('en-US', {
month: '2-digit',
year: 'numeric',
})
.replace('/', '-'); // MM-YYYY
const { dateStr, monthYear } = this.getFormattedDates();
await this.executeProcedure(
'fact_hourly_device_energy_consumed_procedure',
[deviceUuid, dateStr, hour],
);
await this.executeProcedure(
'fact_daily_device_energy_consumed_procedure',
[deviceUuid, dateStr],
);
await this.executeProcedure(
'fact_monthly_device_energy_consumed_procedure',
[deviceUuid, monthYear],
);
// Execute all procedures in parallel
await Promise.all([
this.executeProcedureWithRetry(
'fact_hourly_device_energy_consumed_procedure',
[dateStr],
'fact_device_energy_consumed',
),
this.executeProcedureWithRetry(
'fact_daily_device_energy_consumed_procedure',
[dateStr],
'fact_device_energy_consumed',
),
this.executeProcedureWithRetry(
'fact_monthly_device_energy_consumed_procedure',
[monthYear],
'fact_device_energy_consumed',
),
]);
} catch (err) {
console.error('Failed to insert or update energy data:', err);
console.error('Failed to update energy consumption data:', err);
throw err;
}
}
private async executeProcedure(
private getFormattedDates(): { dateStr: string; monthYear: string } {
const now = new Date();
return {
dateStr: now.toLocaleDateString('en-CA'), // YYYY-MM-DD
monthYear: now
.toLocaleDateString('en-US', {
month: '2-digit',
year: 'numeric',
})
.replace('/', '-'), // MM-YYYY
};
}
private async executeProcedureWithRetry(
procedureFileName: string,
params: (string | number | null)[],
folderName: string,
retries = 3,
): Promise<void> {
const query = this.loadQuery(
'fact_device_energy_consumed',
procedureFileName,
);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
try {
const query = this.loadQuery(folderName, procedureFileName);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
} catch (err) {
if (retries > 0) {
const delayMs = 1000 * (4 - retries); // Exponential backoff
console.warn(`Retrying ${procedureFileName} (${retries} retries left)`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return this.executeProcedureWithRetry(
procedureFileName,
params,
folderName,
retries - 1,
);
}
console.error(`Failed to execute ${procedureFileName}:`, err);
throw err;
}
}
private loadQuery(folderName: string, fileName: string): string {

View File

@ -16,21 +16,46 @@ export class SosHandlerService {
);
}
async handleSosEvent(devId: string, logData: any): Promise<void> {
async handleSosEventFirebase(device: any, logData: any): Promise<void> {
const sosTrueStatus = [{ code: 'sos', value: true }];
const sosFalseStatus = [{ code: 'sos', value: false }];
try {
// ✅ Send true status
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: devId,
status: [{ code: 'sos', value: true }],
deviceTuyaUuid: device.deviceTuyaUuid,
status: sosTrueStatus,
log: logData,
device,
});
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([
{
deviceTuyaUuid: device.deviceTuyaUuid,
status: sosTrueStatus,
log: logData,
device,
},
]);
// ✅ Schedule false status
setTimeout(async () => {
try {
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: devId,
status: [{ code: 'sos', value: false }],
deviceTuyaUuid: device.deviceTuyaUuid,
status: sosFalseStatus,
log: logData,
device,
});
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([
{
deviceTuyaUuid: device.deviceTuyaUuid,
status: sosFalseStatus,
log: logData,
device,
},
]);
} catch (err) {
this.logger.error('Failed to send SOS false value', err);
}

View File

@ -1,13 +1,24 @@
import { Injectable } from '@nestjs/common';
import { Injectable, OnModuleInit } from '@nestjs/common';
import TuyaWebsocket from '../../config/tuya-web-socket-config';
import { ConfigService } from '@nestjs/config';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { SosHandlerService } from './sos.handler.service';
import * as NodeCache from 'node-cache';
@Injectable()
export class TuyaWebSocketService {
export class TuyaWebSocketService implements OnModuleInit {
private client: any;
private readonly isDevEnv: boolean;
private readonly deviceCache = new NodeCache({ stdTTL: 7200 }); // TTL = 2 hour
private messageQueue: {
devId: string;
status: any;
logData: any;
device: any;
}[] = [];
private isProcessing = false;
constructor(
private readonly configService: ConfigService,
@ -26,16 +37,36 @@ export class TuyaWebSocketService {
});
if (this.configService.get<string>('tuya-config.TRUN_ON_TUYA_SOCKET')) {
// Set up event handlers
this.setupEventHandlers();
// Start receiving messages
this.client.start();
}
// Run the queue processor every 15 seconds
setInterval(() => this.processQueue(), 15000);
// Refresh the cache every 1 hour
setInterval(() => this.initializeDeviceCache(), 30 * 60 * 1000); // 30 minutes
}
async onModuleInit() {
await this.initializeDeviceCache();
}
private async initializeDeviceCache() {
try {
const allDevices = await this.deviceStatusFirebaseService.getAllDevices();
allDevices.forEach((device) => {
if (device.deviceTuyaUuid) {
this.deviceCache.set(device.deviceTuyaUuid, device);
}
});
console.log(`✅ Refreshed cache with ${allDevices.length} devices.`);
} catch (error) {
console.error('❌ Failed to initialize device cache:', error);
}
}
private setupEventHandlers() {
// Event handlers
this.client.open(() => {
console.log('open');
});
@ -43,23 +74,38 @@ export class TuyaWebSocketService {
this.client.message(async (ws: WebSocket, message: any) => {
try {
const { devId, status, logData } = this.extractMessageData(message);
if (!Array.isArray(logData?.properties)) {
this.client.ackMessage(message.messageId);
return;
}
const device = this.deviceCache.get(devId);
if (!device) {
// console.log(⛔ Unknown device: ${devId}, message ignored.);
this.client.ackMessage(message.messageId);
return;
}
if (this.sosHandlerService.isSosTriggered(status)) {
await this.sosHandlerService.handleSosEvent(devId, logData);
await this.sosHandlerService.handleSosEventFirebase(devId, logData);
} else {
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
deviceTuyaUuid: devId,
status: status,
status,
log: logData,
device,
});
}
// Push to internal queue
this.messageQueue.push({ devId, status, logData, device });
// Acknowledge the message
this.client.ackMessage(message.messageId);
} catch (error) {
console.error('Error processing message:', error);
console.error('Error receiving message:', error);
}
});
this.client.reconnect(() => {
console.log('reconnect');
});
@ -80,6 +126,37 @@ export class TuyaWebSocketService {
console.error('WebSocket error:', error);
});
}
private async processQueue() {
if (this.isProcessing) {
console.log('⏳ Skipping: still processing previous batch');
return;
}
if (this.messageQueue.length === 0) return;
this.isProcessing = true;
const batch = [...this.messageQueue];
this.messageQueue = [];
console.log(`🔁 Processing batch of size: ${batch.length}`);
try {
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb(
batch.map((item) => ({
deviceTuyaUuid: item.devId,
status: item.status,
log: item.logData,
device: item.device,
})),
);
} catch (error) {
console.error('❌ Error processing batch:', error);
this.messageQueue.unshift(...batch); // retry
} finally {
this.isProcessing = false;
}
}
private extractMessageData(message: any): {
devId: string;
status: any;

View File

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

View File

@ -0,0 +1,82 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class AqiSpaceDailyPollutantStatsDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsNotEmpty()
@IsString()
spaceUuid: string;
@IsNotEmpty()
@IsString()
eventDay: string;
@IsNotEmpty()
@IsNumber()
eventHour: number;
@IsNumber()
pm1Min: number;
@IsNumber()
pm1Avg: number;
@IsNumber()
pm1Max: number;
@IsNumber()
pm10Min: number;
@IsNumber()
pm10Avg: number;
@IsNumber()
pm10Max: number;
@IsNumber()
pm25Min: number;
@IsNumber()
pm25Avg: number;
@IsNumber()
pm25Max: number;
@IsNumber()
ch2oMin: number;
@IsNumber()
ch2oAvg: number;
@IsNumber()
ch2oMax: number;
@IsNumber()
vocMin: number;
@IsNumber()
vocAvg: number;
@IsNumber()
vocMax: number;
@IsNumber()
co2Min: number;
@IsNumber()
co2Avg: number;
@IsNumber()
co2Max: number;
@IsNumber()
aqiMin: number;
@IsNumber()
aqiAvg: number;
@IsNumber()
aqiMax: number;
}

View File

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

View File

@ -0,0 +1,184 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
import { AqiSpaceDailyPollutantStatsDto } from '../dtos';
@Entity({ name: 'space-daily-pollutant-stats' })
@Unique(['spaceUuid', 'eventDate'])
export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity<AqiSpaceDailyPollutantStatsDto> {
@Column({ nullable: false })
public spaceUuid: string;
@ManyToOne(() => SpaceEntity, (space) => space.aqiSensorDaily)
space: SpaceEntity;
@Column({ type: 'date', nullable: false })
public eventDate: Date;
@Column('float', { nullable: true })
public goodAqiPercentage?: number;
@Column('float', { nullable: true })
public moderateAqiPercentage?: number;
@Column('float', { nullable: true })
public unhealthySensitiveAqiPercentage?: number;
@Column('float', { nullable: true })
public unhealthyAqiPercentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyAqiPercentage?: number;
@Column('float', { nullable: true })
public hazardousAqiPercentage?: number;
@Column('float', { nullable: true })
public dailyAvgAqi?: number;
@Column('float', { nullable: true })
public dailyMaxAqi?: number;
@Column('float', { nullable: true })
public dailyMinAqi?: number;
@Column('float', { nullable: true })
public goodPm25Percentage?: number;
@Column('float', { nullable: true })
public moderatePm25Percentage?: number;
@Column('float', { nullable: true })
public unhealthySensitivePm25Percentage?: number;
@Column('float', { nullable: true })
public unhealthyPm25Percentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyPm25Percentage?: number;
@Column('float', { nullable: true })
public hazardousPm25Percentage?: number;
@Column('float', { nullable: true })
public dailyAvgPm25?: number;
@Column('float', { nullable: true })
public dailyMaxPm25?: number;
@Column('float', { nullable: true })
public dailyMinPm25?: number;
@Column('float', { nullable: true })
public goodPm10Percentage?: number;
@Column('float', { nullable: true })
public moderatePm10Percentage?: number;
@Column('float', { nullable: true })
public unhealthySensitivePm10Percentage?: number;
@Column('float', { nullable: true })
public unhealthyPm10Percentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyPm10Percentage?: number;
@Column('float', { nullable: true })
public hazardousPm10Percentage?: number;
@Column('float', { nullable: true })
public dailyAvgPm10?: number;
@Column('float', { nullable: true })
public dailyMaxPm10?: number;
@Column('float', { nullable: true })
public dailyMinPm10?: number;
@Column('float', { nullable: true })
public goodVocPercentage?: number;
@Column('float', { nullable: true })
public moderateVocPercentage?: number;
@Column('float', { nullable: true })
public unhealthySensitiveVocPercentage?: number;
@Column('float', { nullable: true })
public unhealthyVocPercentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyVocPercentage?: number;
@Column('float', { nullable: true })
public hazardousVocPercentage?: number;
@Column('float', { nullable: true })
public dailyAvgVoc?: number;
@Column('float', { nullable: true })
public dailyMaxVoc?: number;
@Column('float', { nullable: true })
public dailyMinVoc?: number;
@Column('float', { nullable: true })
public goodCo2Percentage?: number;
@Column('float', { nullable: true })
public moderateCo2Percentage?: number;
@Column('float', { nullable: true })
public unhealthySensitiveCo2Percentage?: number;
@Column('float', { nullable: true })
public unhealthyCo2Percentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyCo2Percentage?: number;
@Column('float', { nullable: true })
public hazardousCo2Percentage?: number;
@Column('float', { nullable: true })
public dailyAvgCo2?: number;
@Column('float', { nullable: true })
public dailyMaxCo2?: number;
@Column('float', { nullable: true })
public dailyMinCo2?: number;
@Column('float', { nullable: true })
public goodCh2oPercentage?: number;
@Column('float', { nullable: true })
public moderateCh2oPercentage?: number;
@Column('float', { nullable: true })
public unhealthySensitiveCh2oPercentage?: number;
@Column('float', { nullable: true })
public unhealthyCh2oPercentage?: number;
@Column('float', { nullable: true })
public veryUnhealthyCh2oPercentage?: number;
@Column('float', { nullable: true })
public hazardousCh2oPercentage?: number;
@Column('float', { nullable: true })
public dailyAvgCh2o?: number;
@Column('float', { nullable: true })
public dailyMaxCh2o?: number;
@Column('float', { nullable: true })
public dailyMinCh2o?: number;
constructor(partial: Partial<AqiSpaceDailyPollutantStatsEntity>) {
super();
Object.assign(this, partial);
}
}

View File

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

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { AqiSpaceDailyPollutantStatsEntity } from '../entities';
@Injectable()
export class AqiSpaceDailyPollutantStatsRepository extends Repository<AqiSpaceDailyPollutantStatsEntity> {
constructor(private dataSource: DataSource) {
super(AqiSpaceDailyPollutantStatsEntity, dataSource.createEntityManager());
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class SpaceDailyOccupancyDurationDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public spaceUuid: string;
@IsString()
@IsNotEmpty()
public eventDate: string;
@IsNumber()
@IsNotEmpty()
public occupancyPercentage: number;
@IsNumber()
@IsNotEmpty()
public occupiedSeconds: number;
}

View File

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

View File

@ -0,0 +1,32 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
import { SpaceDailyOccupancyDurationDto } from '../dtos';
@Entity({ name: 'space-daily-occupancy-duration' })
@Unique(['spaceUuid', 'eventDate'])
export class SpaceDailyOccupancyDurationEntity extends AbstractEntity<SpaceDailyOccupancyDurationDto> {
@Column({ nullable: false })
public spaceUuid: string;
@Column({ nullable: false, type: 'date' })
public eventDate: string;
public CountTotalPresenceDetected: number;
@ManyToOne(() => SpaceEntity, (space) => space.presenceSensorDaily)
space: SpaceEntity;
@Column({ type: 'int' })
occupancyPercentage: number;
@Column({ type: 'int', nullable: true })
occupiedSeconds?: number;
@Column({ type: 'int', nullable: true })
deviceCount?: number;
constructor(partial: Partial<SpaceDailyOccupancyDurationEntity>) {
super();
Object.assign(this, partial);
}
}

View File

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

View File

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

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { SpaceDailyOccupancyDurationEntity } from '../entities/occupancy.entity';
@Injectable()
export class SpaceDailyOccupancyDurationEntityRepository extends Repository<SpaceDailyOccupancyDurationEntity> {
constructor(private dataSource: DataSource) {
super(SpaceDailyOccupancyDurationEntity, dataSource.createEntityManager());
}
}

View File

@ -1,10 +1,7 @@
import { Column, Entity, OneToMany } from 'typeorm';
import { ProductDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceEntity } from '../../device/entities';
import { TagModel } from '../../space-model';
import { TagEntity } from '../../space/entities/tag.entity';
import { NewTagEntity } from '../../tag/entities';
import { ProductDto } from '../dtos';
@Entity({ name: 'product' })
export class ProductEntity extends AbstractEntity<ProductDto> {
@Column({
@ -28,15 +25,6 @@ export class ProductEntity extends AbstractEntity<ProductDto> {
})
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(
() => DeviceEntity,
(devicesProductEntity) => devicesProductEntity.productDevice,

View File

@ -12,6 +12,7 @@ export class RoleTypeEntity extends AbstractEntity<RoleTypeDto> {
nullable: false,
enum: Object.values(RoleType),
})
// why is this ts-type string not enum?
type: string;
@OneToMany(() => UserEntity, (inviteUser) => inviteUser.roleType, {
nullable: true,

View File

@ -1,21 +0,0 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class TagModelDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public name: string;
@IsString()
@IsNotEmpty()
public productUuid: string;
@IsString()
spaceModelUuid: string;
@IsString()
subspaceModelUuid: string;
}

View File

@ -1,4 +1,3 @@
export * from './space-model-product-allocation.entity';
export * from './space-model.entity';
export * from './subspace-model';
export * from './tag-model.entity';
export * from './space-model-product-allocation.entity';

View File

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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
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 { SubSpaceModelDto } from '../../dtos';
import { SpaceModelEntity } from '../space-model.entity';
import { TagModel } from '../tag-model.entity';
import { SubspaceModelProductAllocationEntity } from './subspace-model-product-allocation.entity';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
@Entity({ name: 'subspace-model' })
export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
@ -41,9 +40,6 @@ export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
})
public disabled: boolean;
@OneToMany(() => TagModel, (tag) => tag.subspaceModel)
tags: TagModel[];
@OneToMany(
() => SubspaceModelProductAllocationEntity,
(allocation) => allocation.subspaceModel,

View File

@ -1,38 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { TagModelDto } from '../dtos/tag-model.dto';
import { SpaceModelEntity } from './space-model.entity';
import { SubspaceModelEntity } from './subspace-model';
import { ProductEntity } from '../../product/entities';
import { TagEntity } from '../../space/entities/tag.entity';
@Entity({ name: 'tag_model' })
export class TagModel extends AbstractEntity<TagModelDto> {
@Column({ type: 'varchar', length: 255 })
tag: string;
@ManyToOne(() => ProductEntity, (product) => product.tagModels, {
nullable: false,
})
@JoinColumn({ name: 'product_id' })
product: ProductEntity;
@ManyToOne(() => SpaceModelEntity, (space) => space.tags, { nullable: true })
@JoinColumn({ name: 'space_model_id' })
spaceModel: SpaceModelEntity;
@ManyToOne(() => SubspaceModelEntity, (subspace) => subspace.tags, {
nullable: true,
})
@JoinColumn({ name: 'subspace_model_id' })
subspaceModel: SubspaceModelEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@OneToMany(() => TagEntity, (tag) => tag.model)
tags: TagEntity[];
}

View File

@ -1,11 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
TagModel,
} from '../entities';
@Injectable()
@ -21,13 +20,6 @@ export class SubspaceModelRepository extends Repository<SubspaceModelEntity> {
}
}
@Injectable()
export class TagModelRepository extends Repository<TagModel> {
constructor(private dataSource: DataSource) {
super(TagModel, dataSource.createEntityManager());
}
}
@Injectable()
export class SpaceModelProductAllocationRepoitory extends Repository<SpaceModelProductAllocationEntity> {
constructor(private dataSource: DataSource) {

View File

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

View File

@ -1,32 +1,3 @@
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from './space.entity';
import { Direction } from '@app/common/constants/direction.enum';
@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);
}
}
export class SpaceLinkEntity extends AbstractEntity {}

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import { SubspaceModelEntity } from '@app/common/modules/space-model';
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { SubspaceDto } from '../../dtos';
import { SpaceEntity } from '../space.entity';
import { TagEntity } from '../tag.entity';
import { SubspaceProductAllocationEntity } from './subspace-product-allocation.entity';
@Entity({ name: 'subspace' })
@ -43,9 +42,6 @@ export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
})
subSpaceModel?: SubspaceModelEntity;
@OneToMany(() => TagEntity, (tag) => tag.subspace)
tags: TagEntity[];
@OneToMany(
() => SubspaceProductAllocationEntity,
(allocation) => allocation.subspace,

View File

@ -1,41 +0,0 @@
import { Entity, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProductEntity } from '../../product/entities';
import { TagDto } from '../dtos';
import { TagModel } from '../../space-model/entities/tag-model.entity';
import { DeviceEntity } from '../../device/entities';
import { SubspaceEntity } from './subspace/subspace.entity';
@Entity({ name: 'tag' })
export class TagEntity extends AbstractEntity<TagDto> {
@Column({ type: 'varchar', length: 255, nullable: true })
tag: string;
@ManyToOne(() => TagModel, (model) => model.tags, {
nullable: true,
})
model: TagModel;
@ManyToOne(() => ProductEntity, (product) => product.tags, {
nullable: false,
})
product: ProductEntity;
@ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, {
nullable: true,
})
@JoinColumn({ name: 'subspace_id' })
subspace: SubspaceEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@OneToOne(() => DeviceEntity, (device) => device.tag, {
nullable: true,
})
@JoinColumn({ name: 'device_id' })
device: DeviceEntity;
}

View File

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

View File

@ -6,7 +6,6 @@ import { SpaceProductAllocationEntity } from './entities/space-product-allocatio
import { SpaceEntity } from './entities/space.entity';
import { SubspaceProductAllocationEntity } from './entities/subspace/subspace-product-allocation.entity';
import { SubspaceEntity } from './entities/subspace/subspace.entity';
import { TagEntity } from './entities/tag.entity';
@Module({
providers: [],
@ -16,7 +15,6 @@ import { TagEntity } from './entities/tag.entity';
TypeOrmModule.forFeature([
SpaceEntity,
SubspaceEntity,
TagEntity,
InviteSpaceEntity,
SpaceProductAllocationEntity,
SubspaceProductAllocationEntity,

View File

@ -1,11 +1,10 @@
import { Entity, Column, ManyToOne, Unique, ManyToMany } from 'typeorm';
import { ProductEntity } from '../../product/entities';
import { ProjectEntity } from '../../project/entities';
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { NewTagDto } from '../dtos/tag.dto';
import { DeviceEntity } from '../../device/entities/device.entity';
import { ProjectEntity } from '../../project/entities';
import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity';
import { SubspaceModelProductAllocationEntity } from '../../space-model/entities/subspace-model/subspace-model-product-allocation.entity';
import { DeviceEntity } from '../../device/entities/device.entity';
import { NewTagDto } from '../dtos/tag.dto';
@Entity({ name: 'new_tag' })
@Unique(['name', 'project'])
@ -24,31 +23,25 @@ export class NewTagEntity extends AbstractEntity<NewTagDto> {
})
name: string;
@ManyToOne(() => ProductEntity, (product) => product.newTags, {
nullable: false,
onDelete: 'CASCADE',
})
public product: ProductEntity;
@ManyToOne(() => ProjectEntity, (project) => project.tags, {
nullable: false,
onDelete: 'CASCADE',
})
public project: ProjectEntity;
@ManyToMany(
@OneToMany(
() => SpaceModelProductAllocationEntity,
(allocation) => allocation.tags,
(allocation) => allocation.tag,
)
public spaceModelAllocations: SpaceModelProductAllocationEntity[];
@ManyToMany(
@OneToMany(
() => SubspaceModelProductAllocationEntity,
(allocation) => allocation.tags,
(allocation) => allocation.tag,
)
public subspaceModelAllocations: SubspaceModelProductAllocationEntity[];
@ManyToOne(() => DeviceEntity, (device) => device.tag)
@OneToMany(() => DeviceEntity, (device) => device.tag)
public devices: DeviceEntity[];
constructor(partial: Partial<NewTagEntity>) {

View File

@ -0,0 +1,39 @@
WITH params AS (
SELECT
$1::uuid AS space_uuid,
TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month
)
SELECT
sdp.space_uuid,
sdp.event_date,
sdp.good_aqi_percentage, sdp.moderate_aqi_percentage, sdp.unhealthy_sensitive_aqi_percentage, sdp.unhealthy_aqi_percentage,
sdp.very_unhealthy_aqi_percentage, sdp.hazardous_aqi_percentage,
sdp.daily_avg_aqi, sdp.daily_max_aqi, sdp.daily_min_aqi,
sdp.good_pm25_percentage, sdp.moderate_pm25_percentage, sdp.unhealthy_sensitive_pm25_percentage, sdp.unhealthy_pm25_percentage,
sdp.very_unhealthy_pm25_percentage, sdp.hazardous_pm25_percentage,
sdp.daily_avg_pm25, sdp.daily_max_pm25, sdp.daily_min_pm25,
sdp.good_pm10_percentage, sdp.moderate_pm10_percentage, sdp.unhealthy_sensitive_pm10_percentage, sdp.unhealthy_pm10_percentage,
sdp.very_unhealthy_pm10_percentage, sdp.hazardous_pm10_percentage,
sdp.daily_avg_pm10, sdp.daily_max_pm10, sdp.daily_min_pm10,
sdp.good_voc_percentage, sdp.moderate_voc_percentage, sdp.unhealthy_sensitive_voc_percentage, sdp.unhealthy_voc_percentage,
sdp.very_unhealthy_voc_percentage, sdp.hazardous_voc_percentage,
sdp.daily_avg_voc, sdp.daily_max_voc, sdp.daily_min_voc,
sdp.good_co2_percentage, sdp.moderate_co2_percentage, sdp.unhealthy_sensitive_co2_percentage, sdp.unhealthy_co2_percentage,
sdp.very_unhealthy_co2_percentage, sdp.hazardous_co2_percentage,
sdp.daily_avg_co2, sdp.daily_max_co2, sdp.daily_min_co2,
sdp.good_ch2o_percentage, sdp.moderate_ch2o_percentage, sdp.unhealthy_sensitive_ch2o_percentage, sdp.unhealthy_ch2o_percentage,
sdp.very_unhealthy_ch2o_percentage, sdp.hazardous_ch2o_percentage,
sdp.daily_avg_ch2o, sdp.daily_max_ch2o, sdp.daily_min_ch2o
FROM public."space-daily-pollutant-stats" AS sdp
CROSS JOIN params p
WHERE
(p.space_uuid IS NULL OR sdp.space_uuid = p.space_uuid)
AND (p.event_month IS NULL OR TO_CHAR(sdp.event_date, 'YYYY-MM') = TO_CHAR(p.event_month, 'YYYY-MM'))
ORDER BY sdp.space_uuid, sdp.event_date;

View File

@ -0,0 +1,376 @@
WITH params AS (
SELECT
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date
),
-- 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
JOIN params
ON params.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

@ -0,0 +1,367 @@
-- Query Pipeline Starts Here
WITH device_space AS (
SELECT
device.uuid AS device_id,
device.space_device_uuid AS space_id,
"device-status-log".event_time::timestamp AS event_time,
"device-status-log".code,
"device-status-log".value
FROM device
LEFT JOIN "device-status-log"
ON device.uuid = "device-status-log".device_id
LEFT JOIN product
ON product.uuid = device.product_device_uuid
WHERE product.cat_name = 'hjjcy'
),
average_pollutants AS (
SELECT
event_time::date AS event_date,
date_trunc('hour', event_time) AS event_hour,
space_id,
-- PM1
MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min,
AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg,
MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max,
-- PM25
MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min,
AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg,
MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max,
-- PM10
MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min,
AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg,
MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max,
-- VOC
MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min,
AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg,
MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max,
-- CH2O
MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min,
AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg,
MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max,
-- CO2
MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min,
AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg,
MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max
FROM device_space
GROUP BY space_id, event_hour, event_date
),
filled_pollutants AS (
SELECT
*,
-- AVG
COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f,
COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f,
COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f,
COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f,
COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f,
-- MIN
COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f,
COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f,
COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f,
COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f,
COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f,
-- MAX
COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f,
COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f,
COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f,
COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f,
COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f
FROM average_pollutants
),
hourly_results AS (
SELECT
space_id,
event_date,
event_hour,
pm1_min, pm1_avg, pm1_max,
pm25_min_f, pm25_avg_f, pm25_max_f,
pm10_min_f, pm10_avg_f, pm10_max_f,
voc_min_f, voc_avg_f, voc_max_f,
co2_min_f, co2_avg_f, co2_max_f,
ch2o_min_f, ch2o_avg_f, ch2o_max_f,
GREATEST(
calculate_aqi('pm25', pm25_min_f),
calculate_aqi('pm10', pm10_min_f)
) AS hourly_min_aqi,
GREATEST(
calculate_aqi('pm25', pm25_avg_f),
calculate_aqi('pm10', pm10_avg_f)
) AS hourly_avg_aqi,
GREATEST(
calculate_aqi('pm25', pm25_max_f),
calculate_aqi('pm10', pm10_max_f)
) AS hourly_max_aqi,
classify_aqi(GREATEST(
calculate_aqi('pm25', pm25_avg_f),
calculate_aqi('pm10', pm10_avg_f)
)) AS aqi_category,
classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category,
classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category,
classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category,
classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category,
classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category
FROM filled_pollutants
),
daily_category_counts AS (
SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, aqi_category
UNION ALL
SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, pm25_category
UNION ALL
SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, pm10_category
UNION ALL
SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, voc_category
UNION ALL
SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, co2_category
UNION ALL
SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, ch2o_category
),
daily_totals AS (
SELECT
space_id,
event_date,
SUM(category_count) AS total_count
FROM daily_category_counts
where pollutant = 'aqi'
GROUP BY space_id, event_date
),
-- Pivot Categories into Columns
daily_percentages AS (
select
dt.space_id,
dt.event_date,
-- AQI CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage,
-- PM25 CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage,
-- PM10 CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage,
-- VOC CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage,
-- CO2 CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage,
-- CH20 CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage
FROM daily_totals dt
LEFT JOIN daily_category_counts dcc
ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date
GROUP BY dt.space_id, dt.event_date, dt.total_count
),
daily_averages AS (
SELECT
space_id,
event_date,
-- AQI
ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi,
ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi,
ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi,
-- PM25
ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25,
ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25,
ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25,
-- PM10
ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10,
ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10,
ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10,
-- VOC
ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc,
ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc,
ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc,
-- CO2
ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2,
ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2,
ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2,
-- CH2O
ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o,
ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o,
ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o
FROM hourly_results
GROUP BY space_id, event_date
),
final_data as(
SELECT
p.space_id,
p.event_date,
p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage,
a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi,
p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage,
a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25,
p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage,
a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10,
p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage,
a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc,
p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage,
a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2,
p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage,
a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o
FROM daily_percentages p
LEFT JOIN daily_averages a
ON p.space_id = a.space_id AND p.event_date = a.event_date
ORDER BY p.space_id, p.event_date)
INSERT INTO public."space-daily-pollutant-stats" (
space_uuid,
event_date,
good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage,
daily_avg_aqi, daily_max_aqi, daily_min_aqi,
good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage,
daily_avg_pm25, daily_max_pm25, daily_min_pm25,
good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage,
daily_avg_pm10, daily_max_pm10, daily_min_pm10,
good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage,
daily_avg_voc, daily_max_voc, daily_min_voc,
good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage,
daily_avg_co2, daily_max_co2, daily_min_co2,
good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage,
daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o
)
SELECT
space_id,
event_date,
good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage,
daily_avg_aqi, daily_max_aqi, daily_min_aqi,
good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage,
daily_avg_pm25, daily_max_pm25, daily_min_pm25,
good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage,
daily_avg_pm10, daily_max_pm10, daily_min_pm10,
good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage,
daily_avg_voc, daily_max_voc, daily_min_voc,
good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage,
daily_avg_co2, daily_max_co2, daily_min_co2,
good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage,
daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o
FROM final_data
ON CONFLICT (space_uuid, event_date) DO UPDATE
SET
good_aqi_percentage = EXCLUDED.good_aqi_percentage,
moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage,
unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage,
unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage,
very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage,
hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage,
daily_avg_aqi = EXCLUDED.daily_avg_aqi,
daily_max_aqi = EXCLUDED.daily_max_aqi,
daily_min_aqi = EXCLUDED.daily_min_aqi,
good_pm25_percentage = EXCLUDED.good_pm25_percentage,
moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage,
unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage,
unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage,
very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage,
hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage,
daily_avg_pm25 = EXCLUDED.daily_avg_pm25,
daily_max_pm25 = EXCLUDED.daily_max_pm25,
daily_min_pm25 = EXCLUDED.daily_min_pm25,
good_pm10_percentage = EXCLUDED.good_pm10_percentage,
moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage,
unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage,
unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage,
very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage,
hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage,
daily_avg_pm10 = EXCLUDED.daily_avg_pm10,
daily_max_pm10 = EXCLUDED.daily_max_pm10,
daily_min_pm10 = EXCLUDED.daily_min_pm10,
good_voc_percentage = EXCLUDED.good_voc_percentage,
moderate_voc_percentage = EXCLUDED.moderate_voc_percentage,
unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage,
unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage,
very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage,
hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage,
daily_avg_voc = EXCLUDED.daily_avg_voc,
daily_max_voc = EXCLUDED.daily_max_voc,
daily_min_voc = EXCLUDED.daily_min_voc,
good_co2_percentage = EXCLUDED.good_co2_percentage,
moderate_co2_percentage = EXCLUDED.moderate_co2_percentage,
unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage,
unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage,
very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage,
hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage,
daily_avg_co2 = EXCLUDED.daily_avg_co2,
daily_max_co2 = EXCLUDED.daily_max_co2,
daily_min_co2 = EXCLUDED.daily_min_co2,
good_ch2o_percentage = EXCLUDED.good_ch2o_percentage,
moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage,
unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage,
unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage,
very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage,
hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage,
daily_avg_ch2o = EXCLUDED.daily_avg_ch2o,
daily_max_ch2o = EXCLUDED.daily_max_ch2o,
daily_min_ch2o = EXCLUDED.daily_min_ch2o;

View File

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

View File

@ -1,117 +1,107 @@
WITH params AS (
SELECT
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date,
$2::uuid AS space_id
)
, start_date AS (
SELECT
d.uuid AS device_id,
d.space_device_uuid AS space_id,
l.value,
l.event_time::timestamp AS event_time,
LAG(l.event_time::timestamp) OVER (PARTITION BY d.uuid ORDER BY l.event_time) AS prev_timestamp
FROM device d
LEFT JOIN "device-status-log" l
ON d.uuid = l.device_id
LEFT JOIN product p
ON p.uuid = d.product_device_uuid
WHERE p.cat_name = 'hps'
AND l.code = 'presence_state'
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date
),
-- Step 2: Identify periods when device reports "none"
device_none_periods AS (
SELECT
space_id,
device_id,
event_time AS empty_from,
LEAD(event_time) OVER (PARTITION BY device_id ORDER BY event_time) AS empty_until
FROM start_date
WHERE value = 'none'
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'
),
-- Step 3: Clip the "none" periods to the edges of each day
clipped_device_none_periods AS (
SELECT
space_id,
GREATEST(empty_from, DATE_TRUNC('day', empty_from)) AS clipped_from,
LEAST(empty_until, DATE_TRUNC('day', empty_until) + INTERVAL '1 day') AS clipped_until
FROM device_none_periods
WHERE empty_until IS NOT NULL
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
),
-- Step 4: Break multi-day periods into daily intervals
generated_daily_intervals AS (
SELECT
space_id,
gs::date AS day,
GREATEST(clipped_from, gs) AS interval_start,
LEAST(clipped_until, gs + INTERVAL '1 day') AS interval_end
FROM clipped_device_none_periods,
LATERAL generate_series(DATE_TRUNC('day', clipped_from), DATE_TRUNC('day', clipped_until), INTERVAL '1 day') AS gs
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
),
-- Step 5: Merge overlapping or adjacent intervals per day
merged_intervals AS (
SELECT
space_id,
day,
interval_start,
interval_end
FROM (
SELECT
space_id,
day,
interval_start,
interval_end,
LAG(interval_end) OVER (PARTITION BY space_id, day ORDER BY interval_start) AS prev_end
FROM generated_daily_intervals
) sub
WHERE prev_end IS NULL OR interval_start > prev_end
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
),
-- Step 6: Sum up total missing seconds (device reported "none") per day
missing_seconds_per_day AS (
SELECT
space_id,
day AS missing_date,
SUM(EXTRACT(EPOCH FROM (interval_end - interval_start))) AS total_missing_seconds
FROM merged_intervals
GROUP BY space_id, day
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
),
-- Step 7: Calculate total occupied time per day (86400 - missing)
occupied_seconds_per_day AS (
SELECT
space_id,
missing_date as event_date,
86400 - total_missing_seconds AS total_occupied_seconds,
(86400 - total_missing_seconds)/86400*100 as occupancy_percentage
FROM missing_seconds_per_day
)
-- Final Output
, final_data as (
SELECT occupied_seconds_per_day.space_id,
occupied_seconds_per_day.event_date,
occupied_seconds_per_day.occupancy_percentage
FROM occupied_seconds_per_day
join params p on true
and p.space_id = occupied_seconds_per_day.space_id
and p.event_date = occupied_seconds_per_day.event_date
ORDER BY 1,2
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.event_date = s.event_date
)
INSERT INTO public."space-daily-occupancy-duration" (
space_uuid,
event_date,
occupied_seconds,
occupancy_percentage
)
select space_id,
event_date,
occupancy_percentage
SELECT
space_id,
event_date,
occupied_seconds,
occupancy_percentage
FROM final_data
ON CONFLICT (space_uuid, event_date) DO UPDATE
SET
occupancy_percentage = EXCLUDED.occupancy_percentage;
occupancy_percentage = EXCLUDED.occupancy_percentage,
occupied_seconds = EXCLUDED.occupied_seconds;

View File

@ -1,7 +1,6 @@
WITH params AS (
SELECT
$1::uuid AS device_id,
$2::date AS target_date
$1::date AS target_date
),
total_energy AS (
SELECT
@ -14,8 +13,7 @@ total_energy AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND log.event_time::date = params.target_date
GROUP BY 1,2,3,4,5
),
energy_phase_A AS (
@ -29,8 +27,7 @@ energy_phase_A AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND log.event_time::date = params.target_date
GROUP BY 1,2,3,4,5
),
energy_phase_B AS (
@ -44,8 +41,7 @@ energy_phase_B AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND log.event_time::date = params.target_date
GROUP BY 1,2,3,4,5
),
energy_phase_C AS (
@ -59,8 +55,7 @@ energy_phase_C AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND log.event_time::date = params.target_date
GROUP BY 1,2,3,4,5
),
final_data AS (

View File

@ -1,8 +1,6 @@
WITH params AS (
SELECT
$1::uuid AS device_id,
$2::date AS target_date,
$3::text AS target_hour
$1::date AS target_date
),
total_energy AS (
SELECT
@ -15,9 +13,7 @@ total_energy AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
GROUP BY 1,2,3,4,5
),
energy_phase_A AS (
@ -31,9 +27,7 @@ energy_phase_A AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
GROUP BY 1,2,3,4,5
),
energy_phase_B AS (
@ -47,9 +41,7 @@ energy_phase_B AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
GROUP BY 1,2,3,4,5
),
energy_phase_C AS (
@ -63,9 +55,7 @@ energy_phase_C AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND log.device_id = params.device_id
AND log.event_time::date = params.target_date
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
GROUP BY 1,2,3,4,5
),
final_data AS (

View File

@ -1,7 +1,6 @@
WITH params AS (
SELECT
$1::uuid AS device_id,
$2::text AS target_month -- Format should match 'MM-YYYY'
$1::text AS target_month -- Format should match 'MM-YYYY'
),
total_energy AS (
SELECT
@ -14,7 +13,6 @@ total_energy AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumed'
AND log.device_id = params.device_id
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
GROUP BY 1,2,3,4,5
),
@ -29,7 +27,6 @@ energy_phase_A AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedA'
AND log.device_id = params.device_id
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
GROUP BY 1,2,3,4,5
),
@ -44,7 +41,6 @@ energy_phase_B AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedB'
AND log.device_id = params.device_id
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
GROUP BY 1,2,3,4,5
),
@ -59,7 +55,6 @@ energy_phase_C AS (
MAX(log.value)::integer AS max_value
FROM "device-status-log" log, params
WHERE log.code = 'EnergyConsumedC'
AND log.device_id = params.device_id
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
GROUP BY 1,2,3,4,5
),

View File

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

View File

@ -1,7 +1,6 @@
WITH params AS (
SELECT
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date,
$2::uuid AS space_id
TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date
),
device_logs AS (
@ -87,8 +86,7 @@ SELECT summary.space_id,
count_total_presence_detected
FROM summary
JOIN params P ON true
where summary.space_id = P.space_id
and (P.event_date IS NULL or summary.event_date::date = P.event_date)
where (P.event_date IS NULL or summary.event_date::date = P.event_date)
ORDER BY space_id, event_date)

View File

@ -26,119 +26,337 @@ BEGIN
('pm10', 255, 354, 151, 200),
-- VOC
('voc_value', 0, 200, 0, 50),
('voc_value', 201, 400, 51, 100),
('voc_value', 401, 600, 101, 150),
('voc_value', 601, 1000, 151, 200),
('voc', 0, 200, 0, 50),
('voc', 201, 400, 51, 100),
('voc', 401, 600, 101, 150),
('voc', 601, 1000, 151, 200),
-- CH2O
('ch2o_value', 0, 2, 0, 50),
('ch2o_value', 2.1, 4, 51, 100),
('ch2o_value', 4.1, 6, 101, 150),
('ch2o', 0, 2, 0, 50),
('ch2o', 2.1, 4, 51, 100),
('ch2o', 4.1, 6, 101, 150),
-- CO2
('co2_value', 350, 1000, 0, 50),
('co2_value', 1001, 1250, 51, 100),
('co2_value', 1251, 1500, 101, 150),
('co2_value', 1501, 2000, 151, 200)
('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;
-- Linear interpolation
RETURN ROUND(((i_high - i_low) * (concentration - c_low) / (c_high - c_low)) + i_low);
END;
$$ LANGUAGE plpgsql;
-- CTE for device + status log + space
-- 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.created_at,
device.space_device_uuid AS space_id,
"device-status-log".event_id,
"device-status-log".event_time::date,
"device-status-log".event_time::timestamp AS event_time,
"device-status-log".code,
"device-status-log".value,
"device-status-log".log
"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'
WHERE product.cat_name = 'hjjcy'
),
-- Aggregate air sensor data per device per day
air_data AS (
average_pollutants AS (
SELECT
event_time AS date,
event_time::date AS event_date,
date_trunc('hour', event_time) AS event_hour,
device_id,
space_id,
-- VOC
MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min,
MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max,
AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg,
-- PM1
MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min,
MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max,
AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg,
MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max,
-- PM2.5
-- PM25
MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min,
MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max,
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,
MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max,
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,
MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max,
AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg,
-- Humidity
MIN(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_low,
MAX(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_high,
AVG(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_avg,
-- Temperature
MIN(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_low,
MAX(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_high,
AVG(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_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,
MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max,
AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg
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 date, device_id, space_id
)
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;
-- Final select with AQI calculation
SELECT
date,
device_id,
space_id,
voc_min, voc_max, voc_avg,
pm1_min, pm1_max, pm1_avg,
pm25_min, pm25_max, pm25_avg,
pm10_min, pm10_max, pm10_avg,
ch2o_min, ch2o_max, ch2o_avg,
humidity_low, humidity_high, humidity_avg,
temp_low, temp_high, temp_avg,
co2_min, co2_max, co2_avg,
GREATEST(
calculate_aqi('pm25', pm25_avg),
calculate_aqi('pm10', pm10_avg),
calculate_aqi('voc_value', voc_avg),
calculate_aqi('co2_value', co2_avg),
calculate_aqi('ch2o_value', ch2o_avg)
) AS overall_AQI
FROM air_data;

View File

@ -0,0 +1,275 @@
-- Query Pipeline Starts Here
WITH device_space AS (
SELECT
device.uuid AS device_id,
device.space_device_uuid AS space_id,
"device-status-log".event_time::timestamp AS event_time,
"device-status-log".code,
"device-status-log".value
FROM device
LEFT JOIN "device-status-log"
ON device.uuid = "device-status-log".device_id
LEFT JOIN product
ON product.uuid = device.product_device_uuid
WHERE product.cat_name = 'hjjcy'
),
average_pollutants AS (
SELECT
event_time::date AS event_date,
date_trunc('hour', event_time) AS event_hour,
space_id,
-- PM1
MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min,
AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg,
MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max,
-- PM25
MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min,
AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg,
MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max,
-- PM10
MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min,
AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg,
MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max,
-- VOC
MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min,
AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg,
MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max,
-- CH2O
MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min,
AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg,
MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max,
-- CO2
MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min,
AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg,
MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max
FROM device_space
GROUP BY space_id, event_hour, event_date
),
filled_pollutants AS (
SELECT
*,
-- AVG
COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f,
COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f,
COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f,
COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f,
COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f,
-- MIN
COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f,
COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f,
COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f,
COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f,
COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f,
-- MAX
COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f,
COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f,
COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f,
COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f,
COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f
FROM average_pollutants
),
hourly_results AS (
SELECT
space_id,
event_date,
event_hour,
pm1_min, pm1_avg, pm1_max,
pm25_min_f, pm25_avg_f, pm25_max_f,
pm10_min_f, pm10_avg_f, pm10_max_f,
voc_min_f, voc_avg_f, voc_max_f,
co2_min_f, co2_avg_f, co2_max_f,
ch2o_min_f, ch2o_avg_f, ch2o_max_f,
GREATEST(
calculate_aqi('pm25', pm25_min_f),
calculate_aqi('pm10', pm10_min_f)
) AS hourly_min_aqi,
GREATEST(
calculate_aqi('pm25', pm25_avg_f),
calculate_aqi('pm10', pm10_avg_f)
) AS hourly_avg_aqi,
GREATEST(
calculate_aqi('pm25', pm25_max_f),
calculate_aqi('pm10', pm10_max_f)
) AS hourly_max_aqi,
classify_aqi(GREATEST(
calculate_aqi('pm25', pm25_avg_f),
calculate_aqi('pm10', pm10_avg_f)
)) AS aqi_category,
classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category,
classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category,
classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category,
classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category,
classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category
FROM filled_pollutants
),
daily_category_counts AS (
SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, aqi_category
UNION ALL
SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, pm25_category
UNION ALL
SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, pm10_category
UNION ALL
SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, voc_category
UNION ALL
SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, co2_category
UNION ALL
SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count
FROM hourly_results
GROUP BY space_id, event_date, ch2o_category
),
daily_totals AS (
SELECT
space_id,
event_date,
SUM(category_count) AS total_count
FROM daily_category_counts
where pollutant = 'aqi'
GROUP BY space_id, event_date
),
-- Pivot Categories into Columns
daily_percentages AS (
select
dt.space_id,
dt.event_date,
-- AQI CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage,
-- PM25 CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage,
-- PM10 CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage,
-- VOC CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage,
-- CO2 CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage,
-- CH20 CATEGORIES
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage,
ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage
FROM daily_totals dt
LEFT JOIN daily_category_counts dcc
ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date
GROUP BY dt.space_id, dt.event_date, dt.total_count
),
daily_averages AS (
SELECT
space_id,
event_date,
-- AQI
ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi,
ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi,
ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi,
-- PM25
ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25,
ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25,
ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25,
-- PM10
ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10,
ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10,
ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10,
-- VOC
ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc,
ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc,
ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc,
-- CO2
ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2,
ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2,
ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2,
-- CH2O
ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o,
ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o,
ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o
FROM hourly_results
GROUP BY space_id, event_date
)
SELECT
p.space_id,
p.event_date,
p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage,
a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi,
p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage,
a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25,
p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage,
a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10,
p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage,
a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc,
p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage,
a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2,
p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage,
a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o
FROM daily_percentages p
LEFT JOIN daily_averages a
ON p.space_id = a.space_id AND p.event_date = a.event_date
ORDER BY p.space_id, p.event_date;

View File

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

View File

@ -0,0 +1,18 @@
export function calculateAQI(pm2_5: number): number {
const breakpoints = [
{ pmLow: 0.0, pmHigh: 12.0, aqiLow: 0, aqiHigh: 50 },
{ pmLow: 12.1, pmHigh: 35.4, aqiLow: 51, aqiHigh: 100 },
{ pmLow: 35.5, pmHigh: 55.4, aqiLow: 101, aqiHigh: 150 },
{ pmLow: 55.5, pmHigh: 150.4, aqiLow: 151, aqiHigh: 200 },
{ pmLow: 150.5, pmHigh: 250.4, aqiLow: 201, aqiHigh: 300 },
{ pmLow: 250.5, pmHigh: 500.4, aqiLow: 301, aqiHigh: 500 },
];
const bp = breakpoints.find((b) => pm2_5 >= b.pmLow && pm2_5 <= b.pmHigh);
if (!bp) return pm2_5 > 500.4 ? 500 : 0; // Handle out-of-range values
return Math.round(
((bp.aqiHigh - bp.aqiLow) / (bp.pmHigh - bp.pmLow)) * (pm2_5 - bp.pmLow) +
bp.aqiLow,
);
}

View File

@ -0,0 +1,11 @@
import { DeviceEntity } from '../modules/device/entities';
export function addSpaceUuidToDevices(
devices: DeviceEntity[],
spaceUuid: string,
): DeviceEntity[] {
return devices.map((device) => {
(device as any).spaceUuid = spaceUuid;
return device;
});
}

View File

@ -1,7 +1,7 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
import axios from 'axios';
import * as nodemailer from 'nodemailer';
import {
SEND_EMAIL_API_URL_DEV,
SEND_EMAIL_API_URL_PROD,
@ -83,12 +83,17 @@ export class EmailService {
);
}
}
async sendEmailWithTemplate(
email: string,
name: string,
isEnable: boolean,
isDelete: boolean,
): Promise<void> {
async sendEmailWithTemplate({
email,
name,
isEnable,
isDelete,
}: {
email: string;
name: string;
isEnable: boolean;
isDelete: boolean;
}): Promise<void> {
const isProduction = process.env.NODE_ENV === 'production';
const API_TOKEN = this.configService.get<string>(
'email-config.MAILTRAP_API_TOKEN',

64
package-lock.json generated
View File

@ -18,6 +18,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^7.3.0",
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.4.0",
@ -38,6 +39,7 @@
"ioredis": "^5.3.2",
"morgan": "^1.10.0",
"nest-winston": "^1.10.2",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.10",
"onesignal-node": "^3.4.0",
"passport-jwt": "^4.0.1",
@ -2538,6 +2540,19 @@
"@nestjs/core": "^10.0.0"
}
},
"node_modules/@nestjs/schedule": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz",
"integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==",
"license": "MIT",
"dependencies": {
"cron": "4.3.0"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0"
}
},
"node_modules/@nestjs/schematics": {
"version": "10.2.3",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz",
@ -3215,6 +3230,12 @@
"@types/node": "*"
}
},
"node_modules/@types/luxon": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
"license": "MIT"
},
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -5426,6 +5447,19 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/cron": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz",
"integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==",
"license": "MIT",
"dependencies": {
"@types/luxon": "~3.6.0",
"luxon": "~3.6.0"
},
"engines": {
"node": ">=18.x"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -9777,6 +9811,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/luxon": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
@ -10142,6 +10185,27 @@
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-cache": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
"license": "MIT",
"dependencies": {
"clone": "2.x"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/node-cache/node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",

View File

@ -10,6 +10,7 @@
"format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"",
"start": "npm run test && node dist/main",
"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:prod": "npm run test && node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
@ -29,6 +30,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^7.3.0",
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.4.0",
@ -49,6 +51,7 @@
"ioredis": "^5.3.2",
"morgan": "^1.10.0",
"nest-winston": "^1.10.2",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.10",
"onesignal-node": "^3.4.0",
"passport-jwt": "^4.0.1",

View File

@ -1,51 +1,66 @@
import { SeederModule } from '@app/common/seed/seeder.module';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import config from './config';
import { AuthenticationModule } from './auth/auth.module';
import { UserModule } from './users/user.module';
import { GroupModule } from './group/group.module';
import { DeviceModule } from './device/device.module';
import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module';
import { CommunityModule } from './community/community.module';
import { SeederModule } from '@app/common/seed/seeder.module';
import { UserNotificationModule } from './user-notification/user-notification.module';
import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module';
import { SceneModule } from './scene/scene.module';
import { DoorLockModule } from './door-lock/door.lock.module';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { WinstonModule } from 'nest-winston';
import { AuthenticationModule } from './auth/auth.module';
import { AutomationModule } from './automation/automation.module';
import { RegionModule } from './region/region.module';
import { TimeZoneModule } from './timezone/timezone.module';
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
import { ScheduleModule } from './schedule/schedule.module';
import { SpaceModule } from './space/space.module';
import { ProductModule } from './product';
import { ProjectModule } from './project';
import { SpaceModelModule } from './space-model';
import { InviteUserModule } from './invite-user/invite-user.module';
import { PermissionModule } from './permission/permission.module';
import { RoleModule } from './role/role.module';
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
import { TagModule } from './tags/tags.module';
import { ClientModule } from './client/client.module';
import { DeviceCommissionModule } from './commission-device/commission-device.module';
import { PowerClampModule } from './power-clamp/power-clamp.module';
import { WinstonModule } from 'nest-winston';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { CommunityModule } from './community/community.module';
import config from './config';
import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module';
import { DeviceModule } from './device/device.module';
import { DoorLockModule } from './door-lock/door.lock.module';
import { GroupModule } from './group/group.module';
import { HealthModule } from './health/health.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { InviteUserModule } from './invite-user/invite-user.module';
import { PermissionModule } from './permission/permission.module';
import { PowerClampModule } from './power-clamp/power-clamp.module';
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
import { ProductModule } from './product';
import { ProjectModule } from './project';
import { RegionModule } from './region/region.module';
import { RoleModule } from './role/role.module';
import { SceneModule } from './scene/scene.module';
import { ScheduleModule } from './schedule/schedule.module';
import { SpaceModelModule } from './space-model';
import { SpaceModule } from './space/space.module';
import { TagModule } from './tags/tags.module';
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
import { TimeZoneModule } from './timezone/timezone.module';
import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module';
import { UserNotificationModule } from './user-notification/user-notification.module';
import { UserModule } from './users/user.module';
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
import { ThrottlerGuard } from '@nestjs/throttler';
import { ThrottlerModule } from '@nestjs/throttler/dist/throttler.module';
import { isArray } from 'class-validator';
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
import { AqiModule } from './aqi/aqi.module';
import { OccupancyModule } from './occupancy/occupancy.module';
import { WeatherModule } from './weather/weather.module';
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
import { SchedulerModule } from './scheduler/scheduler.module';
@Module({
imports: [
ConfigModule.forRoot({
load: config,
}),
/* ThrottlerModule.forRoot({
throttlers: [{ ttl: 100000, limit: 30 }],
}), */
ThrottlerModule.forRoot({
throttlers: [{ ttl: 60000, limit: 100 }],
generateKey: (context) => {
const req = context.switchToHttp().getRequest();
console.log('Real IP:', req.headers['x-forwarded-for']);
return req.headers['x-forwarded-for']
? isArray(req.headers['x-forwarded-for'])
? req.headers['x-forwarded-for'][0].split(':')[0]
: req.headers['x-forwarded-for'].split(':')[0]
: req.ip;
},
}),
WinstonModule.forRoot(winstonLoggerOptions),
ClientModule,
AuthenticationModule,
@ -79,16 +94,20 @@ import { OccupancyModule } from './occupancy/occupancy.module';
PowerClampModule,
HealthModule,
OccupancyModule,
WeatherModule,
AqiModule,
SchedulerModule,
NestScheduleModule.forRoot(),
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
/* {
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
}, */
},
],
})
export class AppModule {}

11
src/aqi/aqi.module.ts Normal file
View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { AqiService } from './services';
import { AqiController } from './controllers';
@Module({
imports: [ConfigModule],
controllers: [AqiController],
providers: [AqiService, SqlLoaderService],
})
export class AqiModule {}

View File

@ -0,0 +1,64 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { AqiService } from '../services/aqi.service';
import {
GetAqiDailyBySpaceDto,
GetAqiPollutantBySpaceDto,
} from '../dto/get-aqi.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SpaceParamsDto } from '../dto/aqi-params.dto';
@ApiTags('AQI Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.AQI.ROUTE,
})
export class AqiController {
constructor(private readonly aqiService: AqiService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('range/space/:spaceUuid')
@ApiOperation({
summary: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_SUMMARY,
description: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_DESCRIPTION,
})
@ApiParam({
name: 'spaceUuid',
description: 'UUID of the Space',
required: true,
})
async getAQIRangeDataBySpace(
@Param() params: SpaceParamsDto,
@Query() query: GetAqiDailyBySpaceDto,
): Promise<BaseResponseDto> {
return await this.aqiService.getAQIRangeDataBySpace(params, query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('distribution/space/:spaceUuid')
@ApiOperation({
summary: ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_SUMMARY,
description:
ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_DESCRIPTION,
})
@ApiParam({
name: 'spaceUuid',
description: 'UUID of the Space',
required: true,
})
async getAQIDistributionDataBySpace(
@Param() params: SpaceParamsDto,
@Query() query: GetAqiPollutantBySpaceDto,
): Promise<BaseResponseDto> {
return await this.aqiService.getAQIDistributionDataBySpace(params, query);
}
}

View File

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

View File

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

View File

@ -0,0 +1,37 @@
import { PollutantType } from '@app/common/constants/pollutants.enum';
import { ApiProperty } from '@nestjs/swagger';
import { Matches, IsNotEmpty, IsString } from 'class-validator';
export class GetAqiDailyBySpaceDto {
@ApiProperty({
description: 'Month and year in format YYYY-MM',
example: '2025-03',
required: true,
})
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/, {
message: 'monthDate must be in YYYY-MM format',
})
@IsNotEmpty()
monthDate: string;
}
export class GetAqiPollutantBySpaceDto {
@ApiProperty({
description: 'Pollutant Type',
enum: PollutantType,
example: PollutantType.AQI,
required: true,
})
@IsString()
@IsNotEmpty()
public pollutantType: string;
@ApiProperty({
description: 'Month and year in format YYYY-MM',
example: '2025-03',
required: true,
})
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/, {
message: 'monthDate must be in YYYY-MM format',
})
@IsNotEmpty()
monthDate: string;
}

View File

@ -0,0 +1,138 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
GetAqiDailyBySpaceDto,
GetAqiPollutantBySpaceDto,
} from '../dto/get-aqi.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SpaceParamsDto } from '../dto/aqi-params.dto';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { PollutantType } from '@app/common/constants/pollutants.enum';
@Injectable()
export class AqiService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async getAQIDistributionDataBySpace(
params: SpaceParamsDto,
query: GetAqiPollutantBySpaceDto,
): Promise<BaseResponseDto> {
const { monthDate, pollutantType } = query;
const { spaceUuid } = params;
try {
const data = await this.executeProcedure(
'fact_daily_space_aqi',
'proceduce_select_daily_space_aqi',
[spaceUuid, monthDate],
);
const categories = [
'good',
'moderate',
'unhealthy_sensitive',
'unhealthy',
'very_unhealthy',
'hazardous',
];
const transformedData = data.map((item) => {
const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD
const categoryData = categories.map((category) => {
const key = `${category}_${pollutantType.toLowerCase()}_percentage`;
return {
type: category,
percentage: item[key] ?? 0,
};
});
return { date, data: categoryData };
});
const response = this.buildResponse(
`AQI distribution data fetched successfully for ${spaceUuid} space and pollutant ${pollutantType}`,
transformedData,
);
return response;
} catch (error) {
console.error('Failed to fetch AQI distribution data', {
error,
spaceUuid,
});
throw new HttpException(
error.response?.message || 'Failed to fetch AQI distribution data',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getAQIRangeDataBySpace(
params: SpaceParamsDto,
query: GetAqiDailyBySpaceDto,
): Promise<BaseResponseDto> {
const { monthDate } = query;
const { spaceUuid } = params;
try {
const data = await this.executeProcedure(
'fact_daily_space_aqi',
'proceduce_select_daily_space_aqi',
[spaceUuid, monthDate],
);
// Define pollutants dynamically
const pollutants = Object.values(PollutantType);
const transformedData = data.map((item) => {
const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD
const dailyData = pollutants.map((type) => ({
type,
min: item[`daily_min_${type}`],
max: item[`daily_max_${type}`],
average: item[`daily_avg_${type}`],
}));
return { date, data: dailyData };
});
const response = this.buildResponse(
`AQI data fetched successfully for ${spaceUuid} space`,
transformedData,
);
return convertKeysToCamelCase(response);
} catch (error) {
console.error('Failed to fetch AQI data', {
error,
spaceUuid,
});
throw new HttpException(
error.response?.message || 'Failed to fetch AQI data',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private buildResponse(message: string, data: any[]) {
return new SuccessResponseDto({
message,
data,
statusCode: HttpStatus.OK,
});
}
private async executeProcedure(
procedureFolderName: string,
procedureFileName: string,
params: (string | number | null)[],
): Promise<any[]> {
const query = this.loadQuery(procedureFolderName, procedureFileName);
return await this.dataSource.query(query, params);
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

View File

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

View File

@ -29,6 +29,9 @@ import {
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';
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
@Module({
imports: [ConfigModule, SpaceRepositoryModule],
@ -57,6 +60,9 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
PowerClampMonthlyRepository,
SqlLoaderService,
OccupancyService,
AqiDataService,
PresenceSensorDailySpaceRepository,
AqiSpaceDailyPollutantStatsRepository,
],
exports: [],
})

View File

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

View File

@ -8,7 +8,6 @@ import {
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
TagRepository,
} from '@app/common/modules/space/repositories';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
@ -64,6 +63,9 @@ import {
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
@ -78,6 +80,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
ProjectRepository,
SpaceService,
InviteSpaceRepository,
// Todo: find out why this is needed
SpaceLinkService,
SubSpaceService,
ValidationService,
@ -86,6 +89,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
SpaceProductAllocationService,
SpaceLinkRepository,
SubspaceRepository,
// Todo: find out why this is needed
TagService,
SubspaceDeviceService,
SubspaceProductAllocationService,
@ -97,7 +101,6 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
SpaceModelProductAllocationService,
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
TagRepository,
SubspaceModelRepository,
SubspaceModelProductAllocationService,
SpaceModelProductAllocationRepoitory,
@ -116,6 +119,9 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
PowerClampMonthlyRepository,
SqlLoaderService,
OccupancyService,
AqiDataService,
PresenceSensorDailySpaceRepository,
AqiSpaceDailyPollutantStatsRepository,
],
exports: [CommunityService, SpacePermissionService],
})

View File

@ -1,4 +1,3 @@
import { CommunityService } from '../services/community.service';
import {
Body,
Controller,
@ -10,17 +9,18 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AddCommunityDto } from '../dtos/add.community.dto';
import { GetCommunityParams } from '../dtos/get.community.dto';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
import { CommunityService } from '../services/community.service';
// import { CheckUserCommunityGuard } from 'src/guards/user.community.guard';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
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';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { ProjectParam } from '../dtos';
@ApiTags('Community Module')
@Controller({
@ -45,6 +45,21 @@ export class CommunityController {
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()
@UseGuards(PermissionsGuard)
@Permissions('COMMUNITY_VIEW')

View File

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

View File

@ -1,4 +1,5 @@
import AuthConfig from './auth.config';
import AppConfig from './app.config';
import JwtConfig from './jwt.config';
export default [AuthConfig, AppConfig, JwtConfig];
import WeatherOpenConfig from './weather.open.config';
export default [AuthConfig, AppConfig, JwtConfig, WeatherOpenConfig];

View File

@ -0,0 +1,9 @@
import { registerAs } from '@nestjs/config';
export default registerAs(
'openweather-config',
(): Record<string, any> => ({
OPEN_WEATHER_MAP_API_KEY: process.env.OPEN_WEATHER_MAP_API_KEY,
WEATHER_API_URL: process.env.WEATHER_API_URL,
}),
);

View File

@ -0,0 +1,28 @@
interface BaseCommand {
code: string;
value: any;
}
export interface ControlCur2Command extends BaseCommand {
code: 'control';
value: 'open' | 'close' | 'stop';
}
export interface ControlCur2PercentCommand extends BaseCommand {
code: 'percent_control';
value: 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100;
}
export interface ControlCur2AccurateCalibrationCommand extends BaseCommand {
code: 'accurate_calibration';
value: 'start' | 'end'; // Assuming this is a numeric value for calibration
}
export interface ControlCur2TDirectionConCommand extends BaseCommand {
code: 'control_t_direction_con';
value: 'forward' | 'back';
}
export interface ControlCur2QuickCalibrationCommand extends BaseCommand {
code: 'tr_timecon';
value: number; // between 10 and 120
}
export interface ControlCur2MotorModeCommand extends BaseCommand {
code: 'elec_machinery_mode';
value: 'strong_power' | 'dry_contact';
}

View File

@ -1,11 +1,11 @@
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 { 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 { GetDoorLockDevices, ProjectParam } from '../dtos';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { GetDevicesFilterDto, ProjectParam } from '../dtos';
import { DeviceService } from '../services/device.service';
@ApiTags('Device Module')
@Controller({
@ -25,7 +25,7 @@ export class DeviceProjectController {
})
async getAllDevices(
@Param() param: ProjectParam,
@Query() query: GetDoorLockDevices,
@Query() query: GetDevicesFilterDto,
) {
return await this.deviceService.getAllDevices(param, query);
}

View File

@ -1,6 +1,7 @@
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
import { ApiProperty } from '@nestjs/swagger';
import {
IsArray,
IsEnum,
IsNotEmpty,
IsOptional,
@ -41,16 +42,7 @@ export class GetDeviceLogsDto {
@IsOptional()
public endTime: string;
}
export class GetDoorLockDevices {
@ApiProperty({
description: 'Device Type',
enum: DeviceTypeEnum,
required: false,
})
@IsEnum(DeviceTypeEnum)
@IsOptional()
public deviceType: DeviceTypeEnum;
}
export class GetDevicesBySpaceOrCommunityDto {
@ApiProperty({
description: 'Device Product Type',
@ -72,3 +64,23 @@ export class GetDevicesBySpaceOrCommunityDto {
@IsNotEmpty({ message: 'Either spaceUuid or communityUuid must be 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,39 +1,46 @@
import { ORPHAN_SPACE_NAME } from './../../../libs/common/src/constants/orphan-constant';
import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository';
import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum';
import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum';
import { BatteryStatus } from '@app/common/constants/battery-status.enum';
import { DeviceStatuses } from '@app/common/constants/device-status.enum';
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
import { ProductType } from '@app/common/constants/product-type.enum';
import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { addSpaceUuidToDevices } from '@app/common/util/device-utils';
import {
Injectable,
HttpException,
HttpStatus,
NotFoundException,
BadRequestException,
forwardRef,
HttpException,
HttpStatus,
Inject,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { AddAutomationDto } from 'src/automation/dtos';
import { SceneService } from 'src/scene/services';
import { In, Not, QueryRunner } from 'typeorm';
import { ProjectParam } from '../dtos';
import {
AddDeviceDto,
AddSceneToFourSceneDeviceDto,
UpdateDeviceDto,
AssignDeviceToSpaceDto,
UpdateDeviceDto,
} 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 {
BatchControlDevicesDto,
BatchFactoryResetDevicesDto,
@ -41,32 +48,29 @@ import {
ControlDeviceDto,
GetSceneFourSceneDeviceDto,
} from '../dtos/control.device.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { In, Not, QueryRunner } from 'typeorm';
import { ProductType } from '@app/common/constants/product-type.enum';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { DeviceStatuses } from '@app/common/constants/device-status.enum';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
import { BatteryStatus } from '@app/common/constants/battery-status.enum';
import { SceneService } from 'src/scene/services';
import { AddAutomationDto } from 'src/automation/dtos';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum';
import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum';
import { DeviceSceneParamDto } from '../dtos/device.param.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { ProjectParam } from '../dtos';
import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum';
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceSceneParamDto } from '../dtos/device.param.dto';
import {
GetDeviceLogsDto,
GetDevicesBySpaceOrCommunityDto,
GetDevicesFilterDto,
} from '../dtos/get.device.dto';
import {
controlDeviceInterface,
DeviceInstructionResponse,
GetDeviceDetailsFunctionsInterface,
GetDeviceDetailsFunctionsStatusInterface,
GetDeviceDetailsInterface,
getDeviceLogsInterface,
GetMacAddressInterface,
GetPowerClampFunctionsStatusInterface,
updateDeviceFirmwareInterface,
} from '../interfaces/get.device.interface';
import {
ORPHAN_COMMUNITY_NAME,
ORPHAN_SPACE_NAME,
} from './../../../libs/common/src/constants/orphan-constant';
import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository';
@Injectable()
export class DeviceService {
@ -197,46 +201,6 @@ export class DeviceService {
}
}
async getDevicesBySpaceId(
getDeviceBySpaceUuidDto: GetDeviceBySpaceUuidDto,
): Promise<GetDeviceDetailsInterface[]> {
try {
const devices = await this.deviceRepository.find({
where: {
spaceDevice: { uuid: getDeviceBySpaceUuidDto.spaceUuid },
isActive: true,
},
relations: [
'spaceDevice',
'productDevice',
'permission',
'permission.permissionType',
],
});
const devicesData = await Promise.all(
devices.map(async (device) => {
return {
haveRoom: device.spaceDevice ? true : false,
productUuid: device.productDevice.uuid,
productType: device.productDevice.prodType,
permissionType: device.permission[0].permissionType.type,
...(await this.getDeviceDetailsByDeviceIdTuya(
device.deviceTuyaUuid,
)),
uuid: device.uuid,
} as GetDeviceDetailsInterface;
}),
);
return devicesData;
} catch (error) {
// Handle the error here
throw new HttpException(
'Error fetching devices by space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async transferDeviceInSpaces(
assignDeviceToSpaceDto: AssignDeviceToSpaceDto,
projectUuid: string,
@ -991,19 +955,20 @@ export class DeviceService {
async getAllDevices(
param: ProjectParam,
query: GetDoorLockDevices,
{ deviceType, spaces }: GetDevicesFilterDto,
): Promise<BaseResponseDto> {
try {
await this.validateProject(param.projectUuid);
if (query.deviceType === DeviceTypeEnum.DOOR_LOCK) {
return await this.getDoorLockDevices(param.projectUuid);
} else if (!query.deviceType) {
if (deviceType === DeviceTypeEnum.DOOR_LOCK) {
return await this.getDoorLockDevices(param.projectUuid, spaces);
} else if (!deviceType) {
const devices = await this.deviceRepository.find({
where: {
isActive: true,
spaceDevice: {
community: { project: { uuid: param.projectUuid } },
uuid: spaces && spaces.length ? In(spaces) : undefined,
spaceName: Not(ORPHAN_SPACE_NAME),
community: { project: { uuid: param.projectUuid } },
},
},
relations: [
@ -1198,39 +1163,6 @@ export class DeviceService {
}
}
async getFullSpaceHierarchy(
space: SpaceEntity,
): Promise<{ uuid: string; spaceName: string }[]> {
try {
// Fetch only the relevant spaces, starting with the target space
const targetSpace = await this.spaceRepository.findOne({
where: { uuid: space.uuid },
relations: ['parent', 'children'],
});
// Fetch only the ancestors of the target space
const ancestors = await this.fetchAncestors(targetSpace);
// Optionally, fetch descendants if required
const descendants = await this.fetchDescendants(targetSpace);
const fullHierarchy = [...ancestors, targetSpace, ...descendants].map(
(space) => ({
uuid: space.uuid,
spaceName: space.spaceName,
}),
);
return fullHierarchy;
} catch (error) {
console.error('Error fetching space hierarchy:', error.message);
throw new HttpException(
'Error fetching space hierarchy',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getPowerClampInstructionStatus(deviceDetails: any) {
try {
const deviceStatus = await this.getPowerClampInstructionStatusTuya(
@ -1330,27 +1262,6 @@ export class DeviceService {
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(
deviceUuid: string,
addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto,
@ -1653,23 +1564,7 @@ export class DeviceService {
}
}
async moveDevicesToSpace(
targetSpace: SpaceEntity,
deviceIds: string[],
): Promise<void> {
if (!deviceIds || deviceIds.length === 0) {
throw new HttpException(
'No device IDs provided for transfer',
HttpStatus.BAD_REQUEST,
);
}
await this.deviceRepository.update(
{ uuid: In(deviceIds) },
{ spaceDevice: targetSpace },
);
}
async getDoorLockDevices(projectUuid: string) {
async getDoorLockDevices(projectUuid: string, spaces?: string[]) {
await this.validateProject(projectUuid);
const devices = await this.deviceRepository.find({
@ -1679,6 +1574,7 @@ export class DeviceService {
},
spaceDevice: {
spaceName: Not(ORPHAN_SPACE_NAME),
uuid: spaces && spaces.length ? In(spaces) : undefined,
community: {
project: {
uuid: projectUuid,
@ -1786,7 +1682,8 @@ export class DeviceService {
throw new NotFoundException('Space not found');
}
const allDevices: DeviceEntity[] = [...space.devices];
const allDevices: DeviceEntity[] = [];
allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid));
// Recursive fetch function
const fetchChildren = async (parentSpace: SpaceEntity) => {
@ -1796,7 +1693,7 @@ export class DeviceService {
});
for (const child of children) {
allDevices.push(...child.devices);
allDevices.push(...addSpaceUuidToDevices(child.devices, child.uuid));
if (child.children.length > 0) {
await fetchChildren(child);
@ -1835,7 +1732,7 @@ export class DeviceService {
visitedSpaceUuids.add(space.uuid);
if (space.devices?.length) {
allDevices.push(...space.devices);
allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid));
}
if (space.children?.length) {
@ -1858,4 +1755,39 @@ export class DeviceService {
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,6 +29,9 @@ import {
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
@Module({
imports: [ConfigModule, DeviceRepositoryModule],
controllers: [DoorLockController],
@ -56,6 +59,9 @@ import { CommunityRepository } from '@app/common/modules/community/repositories'
SqlLoaderService,
OccupancyService,
CommunityRepository,
AqiDataService,
PresenceSensorDailySpaceRepository,
AqiSpaceDailyPollutantStatsRepository,
],
exports: [DoorLockService],
})

View File

@ -27,6 +27,9 @@ import {
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
@Module({
imports: [ConfigModule, DeviceRepositoryModule],
controllers: [GroupController],
@ -53,6 +56,9 @@ import { CommunityRepository } from '@app/common/modules/community/repositories'
SqlLoaderService,
OccupancyService,
CommunityRepository,
AqiDataService,
PresenceSensorDailySpaceRepository,
AqiSpaceDailyPollutantStatsRepository,
],
exports: [GroupService],
})

View File

@ -1,89 +1,89 @@
import { Module } from '@nestjs/common';
import { InviteUserService } from './services/invite-user.service';
import { InviteUserController } from './controllers/invite-user.controller';
import { ConfigModule } from '@nestjs/config';
import { InviteUserController } from './controllers/invite-user.controller';
import { InviteUserService } from './services/invite-user.service';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import {
UserRepository,
UserSpaceRepository,
} from '@app/common/modules/user/repositories';
DeviceRepository,
DeviceUserPermissionRepository,
} from '@app/common/modules/device/repositories';
import { InviteUserRepositoryModule } from '@app/common/modules/Invite-user/Invite-user.repository.module';
import {
InviteUserRepository,
InviteUserSpaceRepository,
} from '@app/common/modules/Invite-user/repositiories';
import { EmailService } from '@app/common/util/email.service';
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 { 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 {
SpaceLinkService,
SpaceService,
SpaceUserService,
SubspaceDeviceService,
SubSpaceService,
ValidationService,
} from 'src/space/services';
import { CommunityService } from 'src/community/services';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
TagRepository,
} from '@app/common/modules/space';
import { SpaceModelRepository } from '@app/common/modules/space-model';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { UserService, UserSpaceService } from 'src/users/services';
import { UserDevicePermissionService } from 'src/user-device-permission/services';
import {
DeviceRepository,
DeviceUserPermissionRepository,
} from '@app/common/modules/device/repositories';
import { PermissionTypeRepository } from '@app/common/modules/permission/repositories';
import { ProjectUserService } from 'src/project/services/project-user.service';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { CommunityModule } from 'src/community/community.module';
import { TagService as NewTagService } from 'src/tags/services';
import { TagService } from 'src/space/services/tag';
import {
SpaceModelService,
SubSpaceModelService,
} from 'src/space-model/services';
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
import {
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';
import { TagService as NewTagService } from 'src/tags/services';
import { UserDevicePermissionService } from 'src/user-device-permission/services';
import { UserService, UserSpaceService } from 'src/users/services';
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
@Module({
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
@ -116,7 +116,6 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
TimeZoneRepository,
SpaceService,
InviteSpaceRepository,
SpaceLinkService,
SubSpaceService,
ValidationService,
NewTagService,
@ -124,7 +123,6 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
SpaceProductAllocationService,
SpaceLinkRepository,
SubspaceRepository,
TagService,
SubspaceDeviceService,
SubspaceProductAllocationService,
SpaceModelRepository,
@ -135,7 +133,6 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
SpaceModelProductAllocationService,
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
TagRepository,
SubspaceModelRepository,
SubspaceModelProductAllocationService,
SpaceModelProductAllocationRepoitory,
@ -154,6 +151,9 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
PowerClampMonthlyRepository,
SqlLoaderService,
OccupancyService,
AqiDataService,
PresenceSensorDailySpaceRepository,
AqiSpaceDailyPollutantStatsRepository,
],
exports: [InviteUserService],
})

View File

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

View File

@ -1,15 +1,13 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { setupSwaggerAuthentication } from '../libs/common/src/util/user-auth.swagger.utils';
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 { Logger } from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
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 { json, urlencoded } from 'body-parser';
import helmet from 'helmet';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { setupSwaggerAuthentication } from '../libs/common/src/util/user-auth.swagger.utils';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
@ -23,13 +21,6 @@ async function bootstrap() {
app.use(new RequestContextMiddleware().use);
app.use(
rateLimit({
windowMs: 5 * 60 * 1000,
max: 500,
}),
);
app.use(
helmet({
contentSecurityPolicy: false,

View File

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

View File

@ -1,73 +1,74 @@
import { Global, Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { ProjectController } from './controllers';
import { ProjectService } from './services';
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 { 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 { CreateOrphanSpaceHandler } from './handler';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
TagRepository,
} 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 {
SpaceModelProductAllocationRepoitory,
SpaceModelRepository,
SubspaceModelProductAllocationRepoitory,
SubspaceModelRepository,
} from '@app/common/modules/space-model';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import {
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
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 { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import {
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 { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
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 { 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 { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import {
PowerClampDailyRepository,
PowerClampHourlyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
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 { ProjectController } from './controllers';
import { ProjectUserController } from './controllers/project-user.controller';
import { CreateOrphanSpaceHandler } from './handler';
import { ProjectService } from './services';
import { ProjectUserService } from './services/project-user.service';
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
const CommandHandlers = [CreateOrphanSpaceHandler];
@ -87,7 +88,6 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
UserRepository,
SpaceService,
InviteSpaceRepository,
SpaceLinkService,
SubSpaceService,
ValidationService,
TagService,
@ -111,7 +111,6 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
DeviceStatusFirebaseService,
SceneService,
TuyaService,
TagRepository,
SubspaceModelRepository,
SubspaceModelProductAllocationService,
SpaceModelProductAllocationRepoitory,
@ -126,6 +125,9 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
PowerClampMonthlyRepository,
SqlLoaderService,
OccupancyService,
AqiDataService,
PresenceSensorDailySpaceRepository,
AqiSpaceDailyPollutantStatsRepository,
],
exports: [ProjectService, CqrsModule],
})

View File

@ -1,4 +1,17 @@
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 { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { UserRepository } from '@app/common/modules/user/repositories';
import { format } from '@fast-csv/format';
import {
forwardRef,
HttpException,
@ -6,24 +19,12 @@ import {
Inject,
Injectable,
} from '@nestjs/common';
import { CreateProjectDto, GetProjectParam } from '../dto';
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';
import { PassThrough } from 'stream';
import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command';
import { CreateProjectDto, GetProjectParam } from '../dto';
import { QueryRunner } from 'typeorm';
@Injectable()
export class ProjectService {
@ -212,8 +213,14 @@ export class ProjectService {
}
}
async findOne(uuid: string): Promise<ProjectEntity> {
const project = await this.projectRepository.findOne({ where: { uuid } });
async findOne(
uuid: string,
queryRunner?: QueryRunner,
): Promise<ProjectEntity> {
const projectRepository = queryRunner
? queryRunner.manager.getRepository(ProjectEntity)
: this.projectRepository;
const project = await projectRepository.findOne({ where: { uuid } });
if (!project) {
throw new HttpException(
`Invalid project with uuid ${uuid}`,
@ -236,11 +243,11 @@ export class ProjectService {
'communities.spaces.parent',
'communities.spaces.productAllocations',
'communities.spaces.productAllocations.product',
'communities.spaces.productAllocations.tags',
'communities.spaces.productAllocations.tag',
'communities.spaces.subspaces',
'communities.spaces.subspaces.productAllocations',
'communities.spaces.subspaces.productAllocations.product',
'communities.spaces.subspaces.productAllocations.tags',
'communities.spaces.subspaces.productAllocations.tag',
],
});
@ -303,52 +310,38 @@ export class ProjectService {
if (subspace.disabled) continue;
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({
'Device ID': '',
'Community Name': space.community?.name || '',
'Space Name': space.spaceName,
'Space Location': spaceLocation,
'Subspace Name': '',
Tag: tag.name,
'Subspace Name': subspace.subspaceName || '',
Tag: productAllocation.tag.name,
'Product Name': productAllocation.product.name || '',
'Community UUID': space.community?.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();
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

@ -0,0 +1,32 @@
import {
ControlCur2AccurateCalibrationCommand,
ControlCur2Command,
ControlCur2PercentCommand,
ControlCur2QuickCalibrationCommand,
ControlCur2TDirectionConCommand,
} from 'src/device/commands/cur2-commands';
export enum ScheduleProductType {
CUR_2 = 'CUR_2',
}
export const DeviceFunctionMap: {
[T in ScheduleProductType]: (body: DeviceFunction[T]) => any;
} = {
[ScheduleProductType.CUR_2]: ({ code, value }) => {
return [
{
code,
value,
},
];
},
};
type DeviceFunction = {
[ScheduleProductType.CUR_2]:
| ControlCur2Command
| ControlCur2PercentCommand
| ControlCur2AccurateCalibrationCommand
| ControlCur2TDirectionConCommand
| ControlCur2QuickCalibrationCommand;
};

View File

@ -1,6 +1,6 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import {
AddScheduleDto,
EnableScheduleDto,
@ -11,14 +11,14 @@ import {
getDeviceScheduleInterface,
} from '../interfaces/get.schedule.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProductType } from '@app/common/constants/product-type.enum';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { convertTimestampToDubaiTime } from '@app/common/helper/convertTimestampToDubaiTime';
import {
getEnabledDays,
getScheduleStatus,
} from '@app/common/helper/getScheduleStatus';
import { DeviceRepository } from '@app/common/modules/device/repositories';
@Injectable()
export class ScheduleService {
@ -49,22 +49,11 @@ export class ScheduleService {
}
// Corrected condition for supported device types
if (
deviceDetails.productDevice.prodType !== ProductType.THREE_G &&
deviceDetails.productDevice.prodType !== ProductType.ONE_G &&
deviceDetails.productDevice.prodType !== ProductType.TWO_G &&
deviceDetails.productDevice.prodType !== ProductType.WH &&
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD
) {
throw new HttpException(
'This device is not supported for schedule',
HttpStatus.BAD_REQUEST,
);
}
return await this.enableScheduleDeviceInTuya(
this.ensureProductTypeSupportedForSchedule(
deviceDetails.productDevice.prodType as ProductType,
);
return this.enableScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
enableScheduleDto,
);
@ -75,29 +64,6 @@ export class ScheduleService {
);
}
}
async enableScheduleDeviceInTuya(
deviceId: string,
enableScheduleDto: EnableScheduleDto,
): Promise<addScheduleDeviceInterface> {
try {
const path = `/v2.0/cloud/timer/device/${deviceId}/state`;
const response = await this.tuya.request({
method: 'PUT',
path,
body: {
enable: enableScheduleDto.enable,
timer_id: enableScheduleDto.scheduleId,
},
});
return response as addScheduleDeviceInterface;
} catch (error) {
throw new HttpException(
'Error while updating schedule from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async deleteDeviceSchedule(deviceUuid: string, scheduleId: string) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
@ -107,21 +73,10 @@ export class ScheduleService {
}
// Corrected condition for supported device types
if (
deviceDetails.productDevice.prodType !== ProductType.THREE_G &&
deviceDetails.productDevice.prodType !== ProductType.ONE_G &&
deviceDetails.productDevice.prodType !== ProductType.TWO_G &&
deviceDetails.productDevice.prodType !== ProductType.WH &&
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD
) {
throw new HttpException(
'This device is not supported for schedule',
HttpStatus.BAD_REQUEST,
);
}
this.ensureProductTypeSupportedForSchedule(
deviceDetails.productDevice.prodType as ProductType,
);
return await this.deleteScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
scheduleId,
@ -133,25 +88,6 @@ export class ScheduleService {
);
}
}
async deleteScheduleDeviceInTuya(
deviceId: string,
scheduleId: string,
): Promise<addScheduleDeviceInterface> {
try {
const path = `/v2.0/cloud/timer/device/${deviceId}/batch?timer_ids=${scheduleId}`;
const response = await this.tuya.request({
method: 'DELETE',
path,
});
return response as addScheduleDeviceInterface;
} catch (error) {
throw new HttpException(
'Error while deleting schedule from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async addDeviceSchedule(deviceUuid: string, addScheduleDto: AddScheduleDto) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
@ -160,22 +96,10 @@ export class ScheduleService {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
// Corrected condition for supported device types
if (
deviceDetails.productDevice.prodType !== ProductType.THREE_G &&
deviceDetails.productDevice.prodType !== ProductType.ONE_G &&
deviceDetails.productDevice.prodType !== ProductType.TWO_G &&
deviceDetails.productDevice.prodType !== ProductType.WH &&
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD
) {
throw new HttpException(
'This device is not supported for schedule',
HttpStatus.BAD_REQUEST,
);
}
this.ensureProductTypeSupportedForSchedule(
deviceDetails.productDevice.prodType as ProductType,
);
await this.addScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
addScheduleDto,
@ -187,40 +111,6 @@ export class ScheduleService {
);
}
}
async addScheduleDeviceInTuya(
deviceId: string,
addScheduleDto: AddScheduleDto,
): Promise<addScheduleDeviceInterface> {
try {
const convertedTime = convertTimestampToDubaiTime(addScheduleDto.time);
const loops = getScheduleStatus(addScheduleDto.days);
const path = `/v2.0/cloud/timer/device/${deviceId}`;
const response = await this.tuya.request({
method: 'POST',
path,
body: {
time: convertedTime.time,
timezone_id: 'Asia/Dubai',
loops: `${loops}`,
functions: [
{
code: addScheduleDto.function.code,
value: addScheduleDto.function.value,
},
],
category: `category_${addScheduleDto.category}`,
},
});
return response as addScheduleDeviceInterface;
} catch (error) {
throw new HttpException(
'Error adding schedule from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getDeviceScheduleByCategory(deviceUuid: string, category: string) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
@ -229,21 +119,9 @@ export class ScheduleService {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
// Corrected condition for supported device types
if (
deviceDetails.productDevice.prodType !== ProductType.THREE_G &&
deviceDetails.productDevice.prodType !== ProductType.ONE_G &&
deviceDetails.productDevice.prodType !== ProductType.TWO_G &&
deviceDetails.productDevice.prodType !== ProductType.WH &&
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD
) {
throw new HttpException(
'This device is not supported for schedule',
HttpStatus.BAD_REQUEST,
);
}
this.ensureProductTypeSupportedForSchedule(
deviceDetails.productDevice.prodType as ProductType,
);
const schedules = await this.getScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
category,
@ -270,7 +148,82 @@ export class ScheduleService {
);
}
}
async getScheduleDeviceInTuya(
async updateDeviceSchedule(
deviceUuid: string,
updateScheduleDto: UpdateScheduleDto,
) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
if (!deviceDetails || !deviceDetails.deviceTuyaUuid) {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
// Corrected condition for supported device types
this.ensureProductTypeSupportedForSchedule(
deviceDetails.productDevice.prodType as ProductType,
);
await this.updateScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
updateScheduleDto,
);
} catch (error) {
throw new HttpException(
error.message || 'Error While Updating Schedule',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private getDeviceByDeviceUuid(
deviceUuid: string,
withProductDevice: boolean = true,
) {
return this.deviceRepository.findOne({
where: {
uuid: deviceUuid,
isActive: true,
},
...(withProductDevice && { relations: ['productDevice'] }),
});
}
private async addScheduleDeviceInTuya(
deviceId: string,
addScheduleDto: AddScheduleDto,
): Promise<addScheduleDeviceInterface> {
try {
const convertedTime = convertTimestampToDubaiTime(addScheduleDto.time);
const loops = getScheduleStatus(addScheduleDto.days);
const path = `/v2.0/cloud/timer/device/${deviceId}`;
const response = await this.tuya.request({
method: 'POST',
path,
body: {
time: convertedTime.time,
timezone_id: 'Asia/Dubai',
loops: `${loops}`,
functions: [
{
...addScheduleDto.function,
},
],
category: `category_${addScheduleDto.category}`,
},
});
return response as addScheduleDeviceInterface;
} catch (error) {
throw new HttpException(
'Error adding schedule from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async getScheduleDeviceInTuya(
deviceId: string,
category: string,
): Promise<getDeviceScheduleInterface> {
@ -291,57 +244,8 @@ export class ScheduleService {
);
}
}
async getDeviceByDeviceUuid(
deviceUuid: string,
withProductDevice: boolean = true,
) {
return await this.deviceRepository.findOne({
where: {
uuid: deviceUuid,
isActive: true,
},
...(withProductDevice && { relations: ['productDevice'] }),
});
}
async updateDeviceSchedule(
deviceUuid: string,
updateScheduleDto: UpdateScheduleDto,
) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
if (!deviceDetails || !deviceDetails.deviceTuyaUuid) {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
// Corrected condition for supported device types
if (
deviceDetails.productDevice.prodType !== ProductType.THREE_G &&
deviceDetails.productDevice.prodType !== ProductType.ONE_G &&
deviceDetails.productDevice.prodType !== ProductType.TWO_G &&
deviceDetails.productDevice.prodType !== ProductType.WH &&
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD
) {
throw new HttpException(
'This device is not supported for schedule',
HttpStatus.BAD_REQUEST,
);
}
await this.updateScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
updateScheduleDto,
);
} catch (error) {
throw new HttpException(
error.message || 'Error While Updating Schedule',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateScheduleDeviceInTuya(
private async updateScheduleDeviceInTuya(
deviceId: string,
updateScheduleDto: UpdateScheduleDto,
): Promise<addScheduleDeviceInterface> {
@ -376,4 +280,69 @@ export class ScheduleService {
);
}
}
private async enableScheduleDeviceInTuya(
deviceId: string,
enableScheduleDto: EnableScheduleDto,
): Promise<addScheduleDeviceInterface> {
try {
const path = `/v2.0/cloud/timer/device/${deviceId}/state`;
const response = await this.tuya.request({
method: 'PUT',
path,
body: {
enable: enableScheduleDto.enable,
timer_id: enableScheduleDto.scheduleId,
},
});
return response as addScheduleDeviceInterface;
} catch (error) {
throw new HttpException(
'Error while updating schedule from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async deleteScheduleDeviceInTuya(
deviceId: string,
scheduleId: string,
): Promise<addScheduleDeviceInterface> {
try {
const path = `/v2.0/cloud/timer/device/${deviceId}/batch?timer_ids=${scheduleId}`;
const response = await this.tuya.request({
method: 'DELETE',
path,
});
return response as addScheduleDeviceInterface;
} catch (error) {
throw new HttpException(
'Error while deleting schedule from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private ensureProductTypeSupportedForSchedule(deviceType: ProductType): void {
if (
![
ProductType.THREE_G,
ProductType.ONE_G,
ProductType.TWO_G,
ProductType.WH,
ProductType.ONE_1TG,
ProductType.TWO_2TG,
ProductType.THREE_3TG,
ProductType.GD,
ProductType.CUR_2,
].includes(deviceType)
) {
throw new HttpException(
'This device is not supported for schedule',
HttpStatus.BAD_REQUEST,
);
}
}
}

View File

@ -0,0 +1,25 @@
import { DatabaseModule } from '@app/common/database/database.module';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SchedulerService } from './scheduler.service';
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
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';
@Module({
imports: [
NestScheduleModule.forRoot(),
TypeOrmModule.forFeature([]),
DatabaseModule,
],
providers: [
SchedulerService,
SqlLoaderService,
PowerClampService,
OccupancyService,
AqiDataService,
],
})
export class SchedulerModule {}

View File

@ -0,0 +1,92 @@
import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
@Injectable()
export class SchedulerService {
constructor(
private readonly powerClampService: PowerClampService,
private readonly occupancyService: OccupancyService,
private readonly aqiDataService: AqiDataService,
) {
console.log('SchedulerService initialized!');
}
@Cron(CronExpression.EVERY_HOUR)
async runHourlyProcedures() {
console.log('\n======== Starting Procedures ========');
console.log(new Date().toISOString(), 'Scheduler running...');
try {
const results = await Promise.allSettled([
this.executeTask(
() => this.powerClampService.updateEnergyConsumedHistoricalData(),
'Energy Consumption',
),
this.executeTask(
() => this.occupancyService.updateOccupancyDataProcedures(),
'Occupancy Data',
),
this.executeTask(
() => this.aqiDataService.updateAQISensorHistoricalData(),
'AQI Data',
),
]);
this.logResults(results);
} catch (error) {
console.error('MAIN SCHEDULER ERROR:', error);
if (error.stack) {
console.error('Error stack:', error.stack);
}
}
}
private async executeTask(
task: () => Promise<void>,
name: string,
): Promise<{ name: string; status: string }> {
try {
console.log(`[${new Date().toISOString()}] Starting ${name} task...`);
await task();
console.log(
`[${new Date().toISOString()}] ${name} task completed successfully`,
);
return { name, status: 'success' };
} catch (error) {
console.error(
`[${new Date().toISOString()}] ${name} task failed:`,
error.message,
);
if (error.stack) {
console.error('Task error stack:', error.stack);
}
return { name, status: 'failed' };
}
}
private logResults(results: PromiseSettledResult<any>[]) {
const successCount = results.filter((r) => r.status === 'fulfilled').length;
const failedCount = results.length - successCount;
console.log('\n======== Task Results ========');
console.log(`Successful tasks: ${successCount}`);
console.log(`Failed tasks: ${failedCount}`);
if (failedCount > 0) {
console.log('\n======== Failed Tasks Details ========');
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`Task ${index + 1} failed:`, result.reason);
if (result.reason.stack) {
console.error('Error stack:', result.reason.stack);
}
}
});
}
console.log('\n======== Scheduler Completed ========\n');
}
}

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