diff --git a/libs/common/src/constants/product-type.enum.ts b/libs/common/src/constants/product-type.enum.ts index 1bb1394..182f43c 100644 --- a/libs/common/src/constants/product-type.enum.ts +++ b/libs/common/src/constants/product-type.enum.ts @@ -15,6 +15,7 @@ export enum ProductType { WL = 'WL', GD = 'GD', CUR = 'CUR', + CUR_2 = 'CUR_2', PC = 'PC', FOUR_S = '4S', SIX_S = '6S', diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index ebfdb2a..3038c35 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -127,8 +127,8 @@ import { BookableSpaceEntity } from '../modules/booking/entities'; 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) }, diff --git a/libs/common/src/firebase/devices-status/devices-status.module.ts b/libs/common/src/firebase/devices-status/devices-status.module.ts index 52f6123..54d5cfa 100644 --- a/libs/common/src/firebase/devices-status/devices-status.module.ts +++ b/libs/common/src/firebase/devices-status/devices-status.module.ts @@ -3,28 +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'; -import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ providers: [ DeviceStatusFirebaseService, DeviceRepository, DeviceStatusLogRepository, - PowerClampService, - PowerClampHourlyRepository, - PowerClampDailyRepository, - PowerClampMonthlyRepository, - SqlLoaderService, - OccupancyService, - AqiDataService, ], controllers: [DeviceStatusFirebaseController], exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository], diff --git a/libs/common/src/firebase/devices-status/services/devices-status.service.ts b/libs/common/src/firebase/devices-status/services/devices-status.service.ts index 4b0b0f7..b3ef843 100644 --- a/libs/common/src/firebase/devices-status/services/devices-status.service.ts +++ b/libs/common/src/firebase/devices-status/services/devices-status.service.ts @@ -18,12 +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'; -import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Injectable() export class DeviceStatusFirebaseService { private tuya: TuyaContext; @@ -31,9 +25,6 @@ export class DeviceStatusFirebaseService { constructor( private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, - private readonly powerClampService: PowerClampService, - private readonly occupancyService: OccupancyService, - private readonly aqiDataService: AqiDataService, private deviceStatusLogRepository: DeviceStatusLogRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -76,25 +67,94 @@ export class DeviceStatusFirebaseService { ); } } + async addBatchDeviceStatusToOurDb( + batch: { + deviceTuyaUuid: string; + status: any; + log: any; + device: any; + }[], + ): Promise { + 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 { 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; } } @@ -108,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); @@ -211,64 +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, - ); - } - } - if (addDeviceStatusDto.productType === ProductType.AQI) { - await this.aqiDataService.updateAQISensorHistoricalData( - addDeviceStatusDto.deviceUuid, - ); - } // Return the updated data const snapshot: DataSnapshot = await get(dataRef); return snapshot.val(); diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts index df152e2..41992d3 100644 --- a/libs/common/src/helper/helper.module.ts +++ b/libs/common/src/helper/helper.module.ts @@ -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, diff --git a/libs/common/src/helper/services/aqi.data.service.ts b/libs/common/src/helper/services/aqi.data.service.ts index 3e19b6c..9a18274 100644 --- a/libs/common/src/helper/services/aqi.data.service.ts +++ b/libs/common/src/helper/services/aqi.data.service.ts @@ -1,44 +1,63 @@ -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 AqiDataService { constructor( private readonly sqlLoader: SqlLoaderService, private readonly dataSource: DataSource, - private readonly deviceRepository: DeviceRepository, ) {} - async updateAQISensorHistoricalData(deviceUuid: string): Promise { - try { - const now = new Date(); - const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD - const device = await this.deviceRepository.findOne({ - where: { uuid: deviceUuid }, - relations: ['spaceDevice'], - }); - await this.executeProcedure( - 'fact_daily_space_aqi', - 'proceduce_update_daily_space_aqi', - [dateStr, device.spaceDevice?.uuid], - ); + async updateAQISensorHistoricalData(): Promise { + 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 insert or update aqi data:', err); + console.error('Failed to update AQI sensor historical data:', err); throw err; } } - - private async executeProcedure( - procedureFolderName: string, + 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 { - 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 { diff --git a/libs/common/src/helper/services/occupancy.service.ts b/libs/common/src/helper/services/occupancy.service.ts index ea99b7c..cec1560 100644 --- a/libs/common/src/helper/services/occupancy.service.ts +++ b/libs/common/src/helper/services/occupancy.service.ts @@ -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 { - 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 { + 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 { - 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 { - 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 { diff --git a/libs/common/src/helper/services/power.clamp.service.ts b/libs/common/src/helper/services/power.clamp.service.ts index 7c83208..7805dd5 100644 --- a/libs/common/src/helper/services/power.clamp.service.ts +++ b/libs/common/src/helper/services/power.clamp.service.ts @@ -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 { + async updateEnergyConsumedHistoricalData(): Promise { 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 { - 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 { diff --git a/libs/common/src/helper/services/sos.handler.service.ts b/libs/common/src/helper/services/sos.handler.service.ts index 4e957dc..55a0daf 100644 --- a/libs/common/src/helper/services/sos.handler.service.ts +++ b/libs/common/src/helper/services/sos.handler.service.ts @@ -16,21 +16,46 @@ export class SosHandlerService { ); } - async handleSosEvent(devId: string, logData: any): Promise { + async handleSosEventFirebase(device: any, logData: any): Promise { + 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); } diff --git a/libs/common/src/helper/services/tuya.web.socket.service.ts b/libs/common/src/helper/services/tuya.web.socket.service.ts index 5a810ab..d30200f 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -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('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; diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql index 04fb661..aa2fa2a 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql @@ -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 ), -- Query Pipeline Starts Here @@ -277,7 +276,10 @@ SELECT 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 + 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) diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql index e669864..f2bd3da 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql @@ -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 ), presence_logs AS ( @@ -86,8 +85,7 @@ final_data AS ( ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage FROM summed_intervals s JOIN params p - ON p.space_id = s.space_id - AND p.event_date = s.event_date + ON p.event_date = s.event_date ) INSERT INTO public."space-daily-occupancy-duration" ( diff --git a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_daily_device_energy_consumed_procedure.sql b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_daily_device_energy_consumed_procedure.sql index c549891..233b24d 100644 --- a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_daily_device_energy_consumed_procedure.sql +++ b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_daily_device_energy_consumed_procedure.sql @@ -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 ( diff --git a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_hourly_device_energy_consumed_procedure.sql b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_hourly_device_energy_consumed_procedure.sql index ffefc4f..afe6e4d 100644 --- a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_hourly_device_energy_consumed_procedure.sql +++ b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_hourly_device_energy_consumed_procedure.sql @@ -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 ( diff --git a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_monthly_device_energy_consumed_procedure.sql b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_monthly_device_energy_consumed_procedure.sql index 0e69d60..8deddda 100644 --- a/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_monthly_device_energy_consumed_procedure.sql +++ b/libs/common/src/sql/procedures/fact_device_energy_consumed/fact_monthly_device_energy_consumed_procedure.sql @@ -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 ), diff --git a/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql index cc727c0..ecf5ffc 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql @@ -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) diff --git a/package-lock.json b/package-lock.json index eaf972a..e8718b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index eaec865..6e16079 100644 --- a/package.json +++ b/package.json @@ -30,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", @@ -50,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", diff --git a/src/app.module.ts b/src/app.module.ts index 6bc32bd..f88b556 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,7 +1,7 @@ import { SeederModule } from '@app/common/seed/seeder.module'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { APP_INTERCEPTOR } from '@nestjs/core'; +import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { WinstonModule } from 'nest-winston'; import { AuthenticationModule } from './auth/auth.module'; import { AutomationModule } from './automation/automation.module'; @@ -35,19 +35,33 @@ import { UserNotificationModule } from './user-notification/user-notification.mo 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'; import { BookingModule } from './booking'; @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, @@ -83,6 +97,8 @@ import { BookingModule } from './booking'; OccupancyModule, WeatherModule, AqiModule, + SchedulerModule, + NestScheduleModule.forRoot(), BookingModule, ], providers: [ @@ -90,10 +106,10 @@ import { BookingModule } from './booking'; provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, }, - /* { + { provide: APP_GUARD, useClass: ThrottlerGuard, - }, */ + }, ], }) export class AppModule {} diff --git a/src/commission-device/commission-device.module.ts b/src/commission-device/commission-device.module.ts index 3306705..a189f71 100644 --- a/src/commission-device/commission-device.module.ts +++ b/src/commission-device/commission-device.module.ts @@ -30,6 +30,8 @@ import { PowerClampService } from '@app/common/helper/services/power.clamp.servi 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], @@ -59,6 +61,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; SqlLoaderService, OccupancyService, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [], }) diff --git a/src/commission-device/services/commission-device.service.ts b/src/commission-device/services/commission-device.service.ts index ac2aae5..88d6a03 100644 --- a/src/commission-device/services/commission-device.service.ts +++ b/src/commission-device/services/commission-device.service.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import { ProjectParam } from '@app/common/dto/project-param.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { CommunityRepository } from '@app/common/modules/community/repositories'; import { DeviceRepository } from '@app/common/modules/device/repositories'; @@ -20,6 +21,7 @@ export class DeviceCommissionService { constructor( private readonly tuyaService: TuyaService, private readonly deviceService: DeviceService, + private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly communityRepository: CommunityRepository, private readonly spaceRepository: SpaceRepository, private readonly subspaceRepository: SubspaceRepository, @@ -209,6 +211,10 @@ export class DeviceCommissionService { rawDeviceId, tuyaSpaceId, ); + + await this.deviceStatusFirebaseService.addDeviceStatusByDeviceUuid( + rawDeviceId, + ); successCount.value++; console.log( `Device ${rawDeviceId} successfully processed and transferred to Tuya space ${tuyaSpaceId}`, diff --git a/src/community/community.module.ts b/src/community/community.module.ts index 0567ffb..e41f78d 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -64,6 +64,8 @@ import { 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], @@ -118,6 +120,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; SqlLoaderService, OccupancyService, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [CommunityService, SpacePermissionService], }) diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 5de34fa..d4ff99c 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -190,24 +190,26 @@ export class CommunityService { .distinct(true); if (includeSpaces) { - qb.leftJoinAndSelect('c.spaces', 'space', 'space.disabled = false') + 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') - .andWhere('space.spaceName != :orphanSpaceName', { - orphanSpaceName: ORPHAN_SPACE_NAME, - }) - .andWhere('space.disabled = :disabled', { disabled: false }); + ); + // .leftJoinAndSelect('space.spaceModel', 'spaceModel') } if (search) { qb.andWhere( - `c.name ILIKE '%${search}%' ${includeSpaces ? "OR space.space_name ILIKE '%" + search + "%'" : ''}`, + `c.name ILIKE :search ${includeSpaces ? 'OR space.space_name ILIKE :search' : ''}`, + { search }, ); } @@ -215,12 +217,21 @@ export class CommunityService { 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( 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, diff --git a/src/device/commands/cur2-commands.ts b/src/device/commands/cur2-commands.ts new file mode 100644 index 0000000..22301f8 --- /dev/null +++ b/src/device/commands/cur2-commands.ts @@ -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'; +} diff --git a/src/device/controllers/device-project.controller.ts b/src/device/controllers/device-project.controller.ts index e5181dd..1585415 100644 --- a/src/device/controllers/device-project.controller.ts +++ b/src/device/controllers/device-project.controller.ts @@ -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); } diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 84c9d64..e34a8b6 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -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[]; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index d2ac4e7..793d854 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -53,7 +53,7 @@ import { DeviceSceneParamDto } from '../dtos/device.param.dto'; import { GetDeviceLogsDto, GetDevicesBySpaceOrCommunityDto, - GetDoorLockDevices, + GetDevicesFilterDto, } from '../dtos/get.device.dto'; import { controlDeviceInterface, @@ -955,19 +955,20 @@ export class DeviceService { async getAllDevices( param: ProjectParam, - query: GetDoorLockDevices, + { deviceType, spaces }: GetDevicesFilterDto, ): Promise { 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: [ @@ -1563,7 +1564,7 @@ export class DeviceService { } } - async getDoorLockDevices(projectUuid: string) { + async getDoorLockDevices(projectUuid: string, spaces?: string[]) { await this.validateProject(projectUuid); const devices = await this.deviceRepository.find({ @@ -1573,6 +1574,7 @@ export class DeviceService { }, spaceDevice: { spaceName: Not(ORPHAN_SPACE_NAME), + uuid: spaces && spaces.length ? In(spaces) : undefined, community: { project: { uuid: projectUuid, diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index c2eaad1..407fced 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -30,6 +30,8 @@ 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], @@ -58,6 +60,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; OccupancyService, CommunityRepository, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [DoorLockService], }) diff --git a/src/group/group.module.ts b/src/group/group.module.ts index 443ac31..7f9f6ab 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -28,6 +28,8 @@ 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], @@ -55,6 +57,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; OccupancyService, CommunityRepository, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [GroupService], }) diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index 31e3a9b..d8655c5 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -82,6 +82,8 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su 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], @@ -150,6 +152,8 @@ import { UserService, UserSpaceService } from 'src/users/services'; SqlLoaderService, OccupancyService, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [InviteUserService], }) diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index 0cf8e42..4042663 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -111,6 +111,7 @@ export class InviteUserService { }); const invitedUser = await queryRunner.manager.save(inviteUser); + const invitedRoleType = await this.getRoleTypeByUuid(roleUuid); // Link user to spaces const spacePromises = validSpaces.map(async (space) => { @@ -128,7 +129,7 @@ export class InviteUserService { await this.emailService.sendEmailWithInvitationTemplate(email, { name: firstName, invitationCode, - role: roleType, + role: invitedRoleType.replace(/_/g, ' '), spacesList: spaceNames, }); diff --git a/src/main.ts b/src/main.ts index d337a66..28b546f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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, diff --git a/src/power-clamp/power-clamp.module.ts b/src/power-clamp/power-clamp.module.ts index 120e368..bb8317b 100644 --- a/src/power-clamp/power-clamp.module.ts +++ b/src/power-clamp/power-clamp.module.ts @@ -60,6 +60,8 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su 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], @@ -109,6 +111,8 @@ import { PowerClampService as PowerClamp } from './services/power-clamp.service' SubspaceModelProductAllocationRepoitory, OccupancyService, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [PowerClamp], }) diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 7c7f7d3..8860e11 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -67,6 +67,8 @@ 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]; @@ -124,6 +126,8 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; SqlLoaderService, OccupancyService, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [ProjectService, CqrsModule], }) diff --git a/src/schedule/constants/device-function-map.ts b/src/schedule/constants/device-function-map.ts new file mode 100644 index 0000000..7258f45 --- /dev/null +++ b/src/schedule/constants/device-function-map.ts @@ -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; +}; diff --git a/src/schedule/services/schedule.service.ts b/src/schedule/services/schedule.service.ts index ee029cf..1296900 100644 --- a/src/schedule/services/schedule.service.ts +++ b/src/schedule/services/schedule.service.ts @@ -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,7 +64,261 @@ export class ScheduleService { ); } } - async enableScheduleDeviceInTuya( + async deleteDeviceSchedule(deviceUuid: string, scheduleId: string) { + 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, + ); + + return await this.deleteScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + scheduleId, + ); + } catch (error) { + throw new HttpException( + error.message || 'Error While Deleting Schedule', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addDeviceSchedule(deviceUuid: string, addScheduleDto: AddScheduleDto) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + if ( + deviceDetails.productDevice.prodType == ProductType.CUR_2 && + addScheduleDto.category != 'Timer' + ) { + throw new HttpException( + 'Invalid category for CUR_2 devices', + HttpStatus.BAD_REQUEST, + ); + } + + this.ensureProductTypeSupportedForSchedule( + deviceDetails.productDevice.prodType as ProductType, + ); + + await this.addScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + addScheduleDto, + deviceDetails.productDevice.prodType as ProductType, + ); + } catch (error) { + throw new HttpException( + error.message || 'Error While Adding Schedule', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDeviceScheduleByCategory(deviceUuid: string, category: string) { + 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, + ); + const schedules = await this.getScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + category, + deviceDetails.productDevice.prodType as ProductType, + ); + const result = schedules.result.map((schedule: any) => { + return { + category: + deviceDetails.productDevice.prodType == ProductType.CUR_2 + ? schedule.category + : schedule.category.replace('category_', ''), + enable: schedule.enable, + function: { + code: schedule.functions[0].code, + value: schedule.functions[0].value, + }, + time: schedule.time, + schedule_id: schedule.timer_id, + timezone_id: schedule.timezone_id, + days: getEnabledDays(schedule.loops), + }; + }); + return convertKeysToCamelCase(result); + } catch (error) { + throw new HttpException( + error.message || 'Error While Adding Schedule', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + 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); + } + + if ( + deviceDetails.productDevice.prodType == ProductType.CUR_2 && + updateScheduleDto.category != 'Timer' + ) { + throw new HttpException( + 'Invalid category for CUR_2 devices', + HttpStatus.BAD_REQUEST, + ); + } + + // Corrected condition for supported device types + this.ensureProductTypeSupportedForSchedule( + deviceDetails.productDevice.prodType as ProductType, + ); + + await this.updateScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + updateScheduleDto, + deviceDetails.productDevice.prodType as ProductType, + ); + } 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, + deviceType: ProductType, + ): Promise { + 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: + deviceType == ProductType.CUR_2 + ? addScheduleDto.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, + deviceType: ProductType, + ): Promise { + try { + const categoryToSent = + deviceType == ProductType.CUR_2 ? category : `category_${category}`; + const path = `/v2.0/cloud/timer/device/${deviceId}?category=${categoryToSent}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + return response as getDeviceScheduleInterface; + } catch (error) { + console.error('Error fetching device schedule from Tuya:', error); + + throw new HttpException( + 'Error fetching device schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async updateScheduleDeviceInTuya( + deviceId: string, + updateScheduleDto: UpdateScheduleDto, + deviceType: ProductType, + ): Promise { + try { + const convertedTime = convertTimestampToDubaiTime(updateScheduleDto.time); + const loops = getScheduleStatus(updateScheduleDto.days); + + const path = `/v2.0/cloud/timer/device/${deviceId}`; + const response = await this.tuya.request({ + method: 'PUT', + path, + body: { + timer_id: updateScheduleDto.scheduleId, + time: convertedTime.time, + timezone_id: 'Asia/Dubai', + loops: `${loops}`, + functions: [ + { + code: updateScheduleDto.function.code, + value: updateScheduleDto.function.value, + }, + ], + category: + deviceType == ProductType.CUR_2 + ? updateScheduleDto.category + : `category_${updateScheduleDto.category}`, + }, + }); + + return response as addScheduleDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error updating schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async enableScheduleDeviceInTuya( deviceId: string, enableScheduleDto: EnableScheduleDto, ): Promise { @@ -98,42 +341,8 @@ export class ScheduleService { ); } } - async deleteDeviceSchedule(deviceUuid: string, scheduleId: string) { - 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, - ); - } - return await this.deleteScheduleDeviceInTuya( - deviceDetails.deviceTuyaUuid, - scheduleId, - ); - } catch (error) { - throw new HttpException( - error.message || 'Error While Deleting Schedule', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async deleteScheduleDeviceInTuya( + private async deleteScheduleDeviceInTuya( deviceId: string, scheduleId: string, ): Promise { @@ -152,227 +361,24 @@ export class ScheduleService { ); } } - async addDeviceSchedule(deviceUuid: string, addScheduleDto: AddScheduleDto) { - 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.addScheduleDeviceInTuya( - deviceDetails.deviceTuyaUuid, - addScheduleDto, - ); - } catch (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( - error.message || 'Error While Adding Schedule', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async addScheduleDeviceInTuya( - deviceId: string, - addScheduleDto: AddScheduleDto, - ): Promise { - 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); - - 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, - ); - } - const schedules = await this.getScheduleDeviceInTuya( - deviceDetails.deviceTuyaUuid, - category, - ); - const result = schedules.result.map((schedule: any) => { - return { - category: schedule.category.replace('category_', ''), - enable: schedule.enable, - function: { - code: schedule.functions[0].code, - value: schedule.functions[0].value, - }, - time: schedule.time, - schedule_id: schedule.timer_id, - timezone_id: schedule.timezone_id, - days: getEnabledDays(schedule.loops), - }; - }); - return convertKeysToCamelCase(result); - } catch (error) { - throw new HttpException( - error.message || 'Error While Adding Schedule', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getScheduleDeviceInTuya( - deviceId: string, - category: string, - ): Promise { - try { - const path = `/v2.0/cloud/timer/device/${deviceId}?category=category_${category}`; - const response = await this.tuya.request({ - method: 'GET', - path, - }); - - return response as getDeviceScheduleInterface; - } catch (error) { - console.error('Error fetching device schedule from Tuya:', error); - - throw new HttpException( - 'Error fetching device schedule from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - 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( - deviceId: string, - updateScheduleDto: UpdateScheduleDto, - ): Promise { - try { - const convertedTime = convertTimestampToDubaiTime(updateScheduleDto.time); - const loops = getScheduleStatus(updateScheduleDto.days); - - const path = `/v2.0/cloud/timer/device/${deviceId}`; - const response = await this.tuya.request({ - method: 'PUT', - path, - body: { - timer_id: updateScheduleDto.scheduleId, - time: convertedTime.time, - timezone_id: 'Asia/Dubai', - loops: `${loops}`, - functions: [ - { - code: updateScheduleDto.function.code, - value: updateScheduleDto.function.value, - }, - ], - category: `category_${updateScheduleDto.category}`, - }, - }); - - return response as addScheduleDeviceInterface; - } catch (error) { - throw new HttpException( - 'Error updating schedule from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, + 'This device is not supported for schedule', + HttpStatus.BAD_REQUEST, ); } } diff --git a/src/scheduler/scheduler.module.ts b/src/scheduler/scheduler.module.ts new file mode 100644 index 0000000..28cf164 --- /dev/null +++ b/src/scheduler/scheduler.module.ts @@ -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 {} diff --git a/src/scheduler/scheduler.service.ts b/src/scheduler/scheduler.service.ts new file mode 100644 index 0000000..e8e4337 --- /dev/null +++ b/src/scheduler/scheduler.service.ts @@ -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, + 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[]) { + 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'); + } +} diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index 4cd6435..1b0a576 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -63,6 +63,8 @@ import { import { SpaceModelService, SubSpaceModelService } from './services'; import { SpaceModelProductAllocationService } from './services/space-model-product-allocation.service'; import { SubspaceModelProductAllocationService } from './services/subspace/subspace-model-product-allocation.service'; +import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories'; +import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories'; const CommandHandlers = [ PropogateUpdateSpaceModelHandler, @@ -120,6 +122,8 @@ const CommandHandlers = [ SqlLoaderService, OccupancyService, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [CqrsModule, SpaceModelService], }) diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index fb08b59..a4e3af7 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -333,7 +333,12 @@ export class SpaceService { .andWhere('space.disabled = :disabled', { disabled: false }); const space = await queryBuilder.getOne(); - + if (!space) { + throw new HttpException( + `Space with ID ${spaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } return new SuccessResponseDto({ message: `Space with ID ${spaceUuid} successfully fetched`, data: space, @@ -343,7 +348,7 @@ export class SpaceService { throw error; // If it's an HttpException, rethrow it } else { throw new HttpException( - 'An error occurred while deleting the community', + 'An error occurred while fetching the space', HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -681,7 +686,7 @@ export class SpaceService { } } - private buildSpaceHierarchy(spaces: SpaceEntity[]): SpaceEntity[] { + buildSpaceHierarchy(spaces: SpaceEntity[]): SpaceEntity[] { const map = new Map(); // Step 1: Create a map of spaces by UUID diff --git a/src/space/space.module.ts b/src/space/space.module.ts index 8229879..288706e 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -88,6 +88,8 @@ import { } from './services'; import { SpaceProductAllocationService } from './services/space-product-allocation.service'; import { SubspaceProductAllocationService } from './services/subspace/subspace-product-allocation.service'; +import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories'; +import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories'; export const CommandHandlers = [DisableSpaceHandler]; @@ -161,6 +163,8 @@ export const CommandHandlers = [DisableSpaceHandler]; SqlLoaderService, OccupancyService, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [SpaceService], }) diff --git a/src/vistor-password/services/visitor-password.service.ts b/src/vistor-password/services/visitor-password.service.ts index 0bc1b02..d7ce937 100644 --- a/src/vistor-password/services/visitor-password.service.ts +++ b/src/vistor-password/services/visitor-password.service.ts @@ -1,39 +1,39 @@ -import { VisitorPasswordRepository } from './../../../libs/common/src/modules/visitor-password/repositories/visitor-password.repository'; +import { ProductType } from '@app/common/constants/product-type.enum'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; import { - Injectable, + BadRequestException, HttpException, HttpStatus, - BadRequestException, + Injectable, } from '@nestjs/common'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { addDeviceObjectInterface, createTickInterface, } from '../interfaces/visitor-password.interface'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { ProductType } from '@app/common/constants/product-type.enum'; +import { VisitorPasswordRepository } from './../../../libs/common/src/modules/visitor-password/repositories/visitor-password.repository'; -import { AddDoorLockTemporaryPasswordDto } from '../dtos'; -import { EmailService } from '@app/common/util/email.service'; -import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services'; -import { DoorLockService } from 'src/door-lock/services'; -import { DeviceService } from 'src/device/services'; -import { DeviceStatuses } from '@app/common/constants/device-status.enum'; import { DaysEnum, EnableDisableStatusEnum, } from '@app/common/constants/days.enum'; -import { PasswordType } from '@app/common/constants/password-type.enum'; +import { DeviceStatuses } from '@app/common/constants/device-status.enum'; import { CommonHourMinutes, CommonHours, } from '@app/common/constants/hours-minutes.enum'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant'; +import { PasswordType } from '@app/common/constants/password-type.enum'; import { VisitorPasswordEnum } from '@app/common/constants/visitor-password.enum'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { EmailService } from '@app/common/util/email.service'; +import { DeviceService } from 'src/device/services'; +import { DoorLockService } from 'src/door-lock/services'; +import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services'; import { Not } from 'typeorm'; -import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant'; +import { AddDoorLockTemporaryPasswordDto } from '../dtos'; @Injectable() export class VisitorPasswordService { @@ -57,6 +57,67 @@ export class VisitorPasswordService { secretKey, }); } + + async getPasswords(projectUuid: string) { + await this.validateProject(projectUuid); + + const deviceIds = await this.deviceRepository.find({ + where: { + productDevice: { + prodType: ProductType.DL, + }, + spaceDevice: { + spaceName: Not(ORPHAN_SPACE_NAME), + community: { + project: { + uuid: projectUuid, + }, + }, + }, + isActive: true, + }, + }); + + const data = []; + deviceIds.forEach((deviceId) => { + data.push( + this.doorLockService + .getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true, false) + .catch(() => {}), + this.doorLockService + .getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true, true) + .catch(() => {}), + this.doorLockService + .getOnlineTemporaryPasswordsMultiple(deviceId.uuid, true, false) + .catch(() => {}), + this.doorLockService + .getOnlineTemporaryPasswordsMultiple(deviceId.uuid, true, true) + .catch(() => {}), + this.doorLockService + .getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true, false) + .catch(() => {}), + this.doorLockService + .getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true, true) + .catch(() => {}), + this.doorLockService + .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true, false) + .catch(() => {}), + this.doorLockService + .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true, true) + .catch(() => {}), + ); + }); + const result = (await Promise.all(data)).flat().filter((datum) => { + return datum != null; + }); + + return new SuccessResponseDto({ + message: 'Successfully retrieved temporary passwords', + data: result, + statusCode: HttpStatus.OK, + }); + } + async handleTemporaryPassword( addDoorLockTemporaryPasswordDto: AddDoorLockTemporaryPasswordDto, userUuid: string, @@ -105,7 +166,7 @@ export class VisitorPasswordService { statusCode: HttpStatus.CREATED, }); } - async addOfflineMultipleTimeTemporaryPassword( + private async addOfflineMultipleTimeTemporaryPassword( addDoorLockOfflineMultipleDto: AddDoorLockTemporaryPasswordDto, userUuid: string, projectUuid: string, @@ -169,6 +230,7 @@ export class VisitorPasswordService { success: true, result: createMultipleOfflinePass.result, deviceUuid, + deviceName: deviceDetails.name, }; } catch (error) { return { @@ -231,7 +293,7 @@ export class VisitorPasswordService { } } - async addOfflineOneTimeTemporaryPassword( + private async addOfflineOneTimeTemporaryPassword( addDoorLockOfflineOneTimeDto: AddDoorLockTemporaryPasswordDto, userUuid: string, projectUuid: string, @@ -295,6 +357,7 @@ export class VisitorPasswordService { success: true, result: createOnceOfflinePass.result, deviceUuid, + deviceName: deviceDetails.name, }; } catch (error) { return { @@ -357,7 +420,7 @@ export class VisitorPasswordService { } } - async addOfflineTemporaryPasswordTuya( + private async addOfflineTemporaryPasswordTuya( doorLockUuid: string, type: string, addDoorLockOfflineMultipleDto: AddDoorLockTemporaryPasswordDto, @@ -387,7 +450,7 @@ export class VisitorPasswordService { ); } } - async addOnlineTemporaryPasswordMultipleTime( + private async addOnlineTemporaryPasswordMultipleTime( addDoorLockOnlineMultipleDto: AddDoorLockTemporaryPasswordDto, userUuid: string, projectUuid: string, @@ -448,6 +511,7 @@ export class VisitorPasswordService { success: true, id: createPass.result.id, deviceUuid, + deviceName: passwordData.deviceName, }; } catch (error) { return { @@ -508,67 +572,8 @@ export class VisitorPasswordService { ); } } - async getPasswords(projectUuid: string) { - await this.validateProject(projectUuid); - const deviceIds = await this.deviceRepository.find({ - where: { - productDevice: { - prodType: ProductType.DL, - }, - spaceDevice: { - spaceName: Not(ORPHAN_SPACE_NAME), - community: { - project: { - uuid: projectUuid, - }, - }, - }, - isActive: true, - }, - }); - - const data = []; - deviceIds.forEach((deviceId) => { - data.push( - this.doorLockService - .getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true, false) - .catch(() => {}), - this.doorLockService - .getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true, true) - .catch(() => {}), - this.doorLockService - .getOnlineTemporaryPasswordsMultiple(deviceId.uuid, true, false) - .catch(() => {}), - this.doorLockService - .getOnlineTemporaryPasswordsMultiple(deviceId.uuid, true, true) - .catch(() => {}), - this.doorLockService - .getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true, false) - .catch(() => {}), - this.doorLockService - .getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true, true) - .catch(() => {}), - this.doorLockService - .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true, false) - .catch(() => {}), - this.doorLockService - .getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true, true) - .catch(() => {}), - ); - }); - const result = (await Promise.all(data)).flat().filter((datum) => { - return datum != null; - }); - - return new SuccessResponseDto({ - message: 'Successfully retrieved temporary passwords', - data: result, - statusCode: HttpStatus.OK, - }); - } - - async addOnlineTemporaryPasswordOneTime( + private async addOnlineTemporaryPasswordOneTime( addDoorLockOnlineOneTimeDto: AddDoorLockTemporaryPasswordDto, userUuid: string, projectUuid: string, @@ -627,6 +632,7 @@ export class VisitorPasswordService { return { success: true, id: createPass.result.id, + deviceName: passwordData.deviceName, deviceUuid, }; } catch (error) { @@ -688,7 +694,7 @@ export class VisitorPasswordService { ); } } - async getTicketAndEncryptedPassword( + private async getTicketAndEncryptedPassword( doorLockUuid: string, passwordPlan: string, projectUuid: string, @@ -725,6 +731,7 @@ export class VisitorPasswordService { ticketKey: ticketDetails.result.ticket_key, encryptedPassword: decrypted, deviceTuyaUuid: deviceDetails.deviceTuyaUuid, + deviceName: deviceDetails.name, }; } catch (error) { throw new HttpException( @@ -734,7 +741,7 @@ export class VisitorPasswordService { } } - async createDoorLockTicketTuya( + private async createDoorLockTicketTuya( deviceUuid: string, ): Promise { try { @@ -753,7 +760,7 @@ export class VisitorPasswordService { } } - async addOnlineTemporaryPasswordMultipleTuya( + private async addOnlineTemporaryPasswordMultipleTuya( addDeviceObj: addDeviceObjectInterface, doorLockUuid: string, ): Promise { @@ -795,7 +802,7 @@ export class VisitorPasswordService { } } - getWorkingDayValue(days) { + private getWorkingDayValue(days) { // Array representing the days of the week const weekDays = [ DaysEnum.SAT, @@ -827,36 +834,7 @@ export class VisitorPasswordService { return workingDayValue; } - getDaysFromWorkingDayValue(workingDayValue) { - // Array representing the days of the week - const weekDays = [ - DaysEnum.SAT, - DaysEnum.FRI, - DaysEnum.THU, - DaysEnum.WED, - DaysEnum.TUE, - DaysEnum.MON, - DaysEnum.SUN, - ]; - - // Convert the integer to a binary string and pad with leading zeros to ensure 7 bits - const binaryString = workingDayValue - .toString(2) - .padStart(7, EnableDisableStatusEnum.DISABLED); - - // Initialize an array to hold the days of the week - const days = []; - - // Iterate through the binary string and weekDays array - for (let i = 0; i < binaryString.length; i++) { - if (binaryString[i] === EnableDisableStatusEnum.ENABLED) { - days.push(weekDays[i]); - } - } - - return days; - } - timeToMinutes(timeStr) { + private timeToMinutes(timeStr) { try { // Special case for "24:00" if (timeStr === CommonHours.TWENTY_FOUR) { @@ -883,38 +861,7 @@ export class VisitorPasswordService { throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); } } - minutesToTime(totalMinutes) { - try { - if ( - typeof totalMinutes !== 'number' || - totalMinutes < 0 || - totalMinutes > CommonHourMinutes.TWENTY_FOUR - ) { - throw new Error('Invalid minutes value'); - } - - if (totalMinutes === CommonHourMinutes.TWENTY_FOUR) { - return CommonHours.TWENTY_FOUR; - } - - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - - const formattedHours = String(hours).padStart( - 2, - EnableDisableStatusEnum.DISABLED, - ); - const formattedMinutes = String(minutes).padStart( - 2, - EnableDisableStatusEnum.DISABLED, - ); - - return `${formattedHours}:${formattedMinutes}`; - } catch (error) { - throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - async getDeviceByDeviceUuid( + private async getDeviceByDeviceUuid( deviceUuid: string, withProductDevice: boolean = true, projectUuid: string, @@ -939,7 +886,7 @@ export class VisitorPasswordService { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } - async addOnlineTemporaryPasswordOneTimeTuya( + private async addOnlineTemporaryPasswordOneTimeTuya( addDeviceObj: addDeviceObjectInterface, doorLockUuid: string, ): Promise { diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index c66ba39..b1d927c 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -32,6 +32,8 @@ 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, DoorLockModule], controllers: [VisitorPasswordController], @@ -61,6 +63,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; OccupancyService, CommunityRepository, AqiDataService, + PresenceSensorDailySpaceRepository, + AqiSpaceDailyPollutantStatsRepository, ], exports: [VisitorPasswordService], })