From a37d5bb29954abfdb03004357b3f611d5d3d4634 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Wed, 18 Jun 2025 12:05:53 +0300 Subject: [PATCH 01/40] task: add trust proxy header (#411) * task: add trust proxy header * add logging --- src/main.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main.ts b/src/main.ts index d337a66..c9256ef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,14 @@ +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 { AppModule } from './app.module'; +import { json, urlencoded } from 'body-parser'; 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 { 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); @@ -27,9 +26,18 @@ async function bootstrap() { rateLimit({ windowMs: 5 * 60 * 1000, max: 500, + standardHeaders: true, + legacyHeaders: false, }), ); + app.use((req, res, next) => { + console.log('Real IP:', req.ip); + next(); + }); + + app.getHttpAdapter().getInstance().set('trust proxy', 1); + app.use( helmet({ contentSecurityPolicy: false, From 705ceeba293464815951d3d05e83187a3102f151 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 19 Jun 2025 09:45:09 +0300 Subject: [PATCH 02/40] Test/prevent server block on rate limit (#414) * task: test rate limits on sever --- src/main.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index c9256ef..0a01fdd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,9 +9,10 @@ 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'; +import { NestExpressApplication } from '@nestjs/platform-express'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule); app.enableCors(); @@ -21,11 +22,12 @@ async function bootstrap() { app.useGlobalFilters(new HttpExceptionFilter()); app.use(new RequestContextMiddleware().use); + app.set('trust proxy', true); app.use( rateLimit({ - windowMs: 5 * 60 * 1000, - max: 500, + windowMs: 30 * 1000, + max: 50, standardHeaders: true, legacyHeaders: false, }), @@ -36,7 +38,7 @@ async function bootstrap() { next(); }); - app.getHttpAdapter().getInstance().set('trust proxy', 1); + // app.getHttpAdapter().getInstance().set('trust proxy', 1); app.use( helmet({ From 0e36f32ed668e07df1b7861ace2a8a50f3f13beb Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 19 Jun 2025 10:15:29 +0300 Subject: [PATCH 03/40] Test/prevent server block on rate limit (#415) * task: increase rate limit timeout --- src/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 0a01fdd..543767f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import { RequestContextMiddleware } from '@app/common/middleware/request-context import { SeederService } from '@app/common/seed/services/seeder.service'; import { Logger, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; import { json, urlencoded } from 'body-parser'; import rateLimit from 'express-rate-limit'; import helmet from 'helmet'; @@ -26,7 +27,7 @@ async function bootstrap() { app.use( rateLimit({ - windowMs: 30 * 1000, + windowMs: 2 * 60 * 1000, max: 50, standardHeaders: true, legacyHeaders: false, From 603e74af0962553f4a5b43b16094648105219005 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 19 Jun 2025 12:54:59 +0300 Subject: [PATCH 04/40] Test/prevent server block on rate limit (#417) * task: add trust proxy header * add logging * task: test rate limits on sever * task: increase rate limit timeout * fix: merge conflicts --- src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 543767f..33ce70a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,6 @@ 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'; -import { NestExpressApplication } from '@nestjs/platform-express'; async function bootstrap() { const app = await NestFactory.create(AppModule); From c5dd5e28fd88f3026f3c59b01d8c186f118043d9 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 19 Jun 2025 13:54:22 +0300 Subject: [PATCH 05/40] Test/prevent server block on rate limit (#418) --- src/main.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index 33ce70a..e00dca6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,6 @@ import { RequestContextMiddleware } from '@app/common/middleware/request-context import { SeederService } from '@app/common/seed/services/seeder.service'; import { Logger, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { NestExpressApplication } from '@nestjs/platform-express'; import { json, urlencoded } from 'body-parser'; import rateLimit from 'express-rate-limit'; import helmet from 'helmet'; @@ -12,7 +11,7 @@ import { AppModule } from './app.module'; import { HttpExceptionFilter } from './common/filters/http-exception/http-exception.filter'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule); app.enableCors(); @@ -22,14 +21,11 @@ async function bootstrap() { app.useGlobalFilters(new HttpExceptionFilter()); app.use(new RequestContextMiddleware().use); - app.set('trust proxy', true); app.use( rateLimit({ - windowMs: 2 * 60 * 1000, - max: 50, - standardHeaders: true, - legacyHeaders: false, + windowMs: 5 * 60 * 1000, + max: 500, }), ); From aa9e90bf084afabb2788408e2d3716a052df606f Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 19 Jun 2025 14:34:23 +0300 Subject: [PATCH 06/40] Test/prevent server block on rate limit (#419) * increase DB max connection to 50 --- libs/common/src/database/database.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 373041c..f3ec232 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -125,7 +125,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; logger: typeOrmLogger, extra: { charset: 'utf8mb4', - max: 20, // set pool max size + max: 50, // set pool max size idleTimeoutMillis: 5000, // 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) From 110ed4157a2456aae523b15e274fa3f872dd46c8 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Mon, 23 Jun 2025 09:34:59 +0300 Subject: [PATCH 07/40] task: add spaces filter to get devices by project (#422) --- .../controllers/device-project.controller.ts | 14 ++++---- src/device/dtos/get.device.dto.ts | 32 +++++++++++++------ src/device/services/device.service.ts | 16 ++++++---- 3 files changed, 38 insertions(+), 24 deletions(-) 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, From 3160773c2a0906a71242b6d7a26de4ba1b2b5288 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Mon, 23 Jun 2025 10:21:55 +0300 Subject: [PATCH 08/40] fix: spaces structure in communities (#420) --- src/community/services/community.service.ts | 13 ++++++++++++- src/space/services/space.service.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 5de34fa..3b213f9 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -207,7 +207,8 @@ export class CommunityService { 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: `%${search}%` }, ); } @@ -215,12 +216,22 @@ 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/space/services/space.service.ts b/src/space/services/space.service.ts index fb08b59..cbbe953 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -681,7 +681,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 From fddd06e06d2242b22088cf1c359c6299fc179770 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Mon, 23 Jun 2025 12:44:19 +0300 Subject: [PATCH 09/40] fix: add space condition to the join operator instead of general query (#423) --- src/community/services/community.service.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 3b213f9..d4ff99c 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -190,25 +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' : ''}`, - { search: `%${search}%` }, + { search }, ); } @@ -216,7 +217,6 @@ export class CommunityService { const { baseResponseDto, paginationResponseDto } = await customModel.findAll({ ...pageable, modelName: 'community' }, qb); - if (includeSpaces) { baseResponseDto.data = baseResponseDto.data.map((community) => ({ ...community, From 60d2c8330b8245260fb8dead2fc1bc596540cab5 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Mon, 23 Jun 2025 15:23:53 +0300 Subject: [PATCH 10/40] fix: increase DB max pool size (#425) --- libs/common/src/database/database.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index f3ec232..dd25da9 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -125,7 +125,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; logger: typeOrmLogger, extra: { charset: 'utf8mb4', - max: 50, // set pool max size + max: 100, // set pool max size idleTimeoutMillis: 5000, // 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) From a6053b39710776326709fb3608f9537116855d59 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 23 Jun 2025 06:34:53 -0600 Subject: [PATCH 11/40] refactor: implement query runners for database operations in multiple services --- .../services/devices-status.service.ts | 84 +++++++++++++------ .../src/helper/services/aqi.data.service.ts | 15 +++- .../src/helper/services/occupancy.service.ts | 15 +++- .../helper/services/power.clamp.service.ts | 21 +++-- 4 files changed, 97 insertions(+), 38 deletions(-) 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..1d201d3 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 @@ -24,6 +24,7 @@ import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.e 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'; +import { DataSource, QueryRunner } from 'typeorm'; @Injectable() export class DeviceStatusFirebaseService { private tuya: TuyaContext; @@ -35,6 +36,7 @@ export class DeviceStatusFirebaseService { private readonly occupancyService: OccupancyService, private readonly aqiDataService: AqiDataService, private deviceStatusLogRepository: DeviceStatusLogRepository, + private readonly dataSource: DataSource, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -79,28 +81,46 @@ export class DeviceStatusFirebaseService { async addDeviceStatusToFirebase( addDeviceStatusDto: AddDeviceStatusDto, ): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); try { const device = await this.getDeviceByDeviceTuyaUuid( addDeviceStatusDto.deviceTuyaUuid, + queryRunner, ); if (device?.uuid) { - return await this.createDeviceStatusFirebase({ - deviceUuid: device.uuid, - ...addDeviceStatusDto, - productType: device.productDevice.prodType, - }); + const result = await this.createDeviceStatusFirebase( + { + deviceUuid: device.uuid, + ...addDeviceStatusDto, + productType: device.productDevice.prodType, + }, + queryRunner, + ); + await queryRunner.commitTransaction(); + return result; } // Return null if device not found or no UUID + await queryRunner.rollbackTransaction(); return null; } catch (error) { - // Handle the error silently, perhaps log it internally or ignore it + await queryRunner.rollbackTransaction(); return null; + } finally { + await queryRunner.release(); } } + async getDeviceByDeviceTuyaUuid( + deviceTuyaUuid: string, + queryRunner?: QueryRunner, + ) { + const repo = queryRunner + ? queryRunner.manager.getRepository(this.deviceRepository.target) + : this.deviceRepository; - async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) { - return await this.deviceRepository.findOne({ + return await repo.findOne({ where: { deviceTuyaUuid, isActive: true, @@ -108,6 +128,7 @@ export class DeviceStatusFirebaseService { relations: ['productDevice'], }); } + async getDevicesInstructionStatus(deviceUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); @@ -153,9 +174,14 @@ export class DeviceStatusFirebaseService { } async getDeviceByDeviceUuid( deviceUuid: string, - withProductDevice: boolean = true, + withProductDevice = true, + queryRunner?: QueryRunner, ) { - return await this.deviceRepository.findOne({ + const repo = queryRunner + ? queryRunner.manager.getRepository(this.deviceRepository.target) + : this.deviceRepository; + + return await repo.findOne({ where: { uuid: deviceUuid, isActive: true, @@ -163,21 +189,20 @@ export class DeviceStatusFirebaseService { ...(withProductDevice && { relations: ['productDevice'] }), }); } + async createDeviceStatusFirebase( addDeviceStatusDto: AddDeviceStatusDto, + queryRunner?: QueryRunner, ): Promise { const dataRef = ref( this.firebaseDb, `device-status/${addDeviceStatusDto.deviceUuid}`, ); - // Use a transaction to handle concurrent updates + // Step 1: Update Firebase Realtime Database await runTransaction(dataRef, (existingData) => { - if (!existingData) { - existingData = {}; - } + if (!existingData) existingData = {}; - // Assign default values if fields are not present if (!existingData.deviceTuyaUuid) { existingData.deviceTuyaUuid = addDeviceStatusDto.deviceTuyaUuid; } @@ -191,18 +216,15 @@ export class DeviceStatusFirebaseService { existingData.status = []; } - // Create a map to track existing status codes + // Merge incoming status with existing status const statusMap = new Map( existingData.status.map((item) => [item.code, item.value]), ); - // Update or add status codes - for (const statusItem of addDeviceStatusDto.status) { statusMap.set(statusItem.code, statusItem.value); } - // Convert the map back to an array format existingData.status = Array.from(statusMap, ([code, value]) => ({ code, value, @@ -211,9 +233,9 @@ export class DeviceStatusFirebaseService { return existingData; }); - // Save logs to your repository - const newLogs = addDeviceStatusDto.log.properties.map((property) => { - return this.deviceStatusLogRepository.create({ + // Step 2: Save device status log entries + const newLogs = addDeviceStatusDto.log.properties.map((property) => + this.deviceStatusLogRepository.create({ deviceId: addDeviceStatusDto.deviceUuid, deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid, productId: addDeviceStatusDto.log.productId, @@ -222,10 +244,19 @@ export class DeviceStatusFirebaseService { value: property.value, eventId: addDeviceStatusDto.log.dataId, eventTime: new Date(property.time).toISOString(), - }); - }); - await this.deviceStatusLogRepository.save(newLogs); + }), + ); + if (queryRunner) { + const repo = queryRunner.manager.getRepository( + this.deviceStatusLogRepository.target, + ); + await repo.save(newLogs); + } else { + await this.deviceStatusLogRepository.save(newLogs); + } + + // Step 3: Trigger additional data services if (addDeviceStatusDto.productType === ProductType.PC) { const energyCodes = new Set([ PowerClampEnergyEnum.ENERGY_CONSUMED, @@ -269,7 +300,8 @@ export class DeviceStatusFirebaseService { addDeviceStatusDto.deviceUuid, ); } - // Return the updated data + + // Step 4: Return updated Firebase status const snapshot: DataSnapshot = await get(dataRef); return snapshot.val(); } diff --git a/libs/common/src/helper/services/aqi.data.service.ts b/libs/common/src/helper/services/aqi.data.service.ts index 3e19b6c..c8f5c1a 100644 --- a/libs/common/src/helper/services/aqi.data.service.ts +++ b/libs/common/src/helper/services/aqi.data.service.ts @@ -36,9 +36,18 @@ export class AqiDataService { procedureFileName: string, params: (string | number | null)[], ): Promise { - const query = this.loadQuery(procedureFolderName, procedureFileName); - await this.dataSource.query(query, params); - console.log(`Procedure ${procedureFileName} executed successfully.`); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + try { + const query = this.loadQuery(procedureFolderName, procedureFileName); + await queryRunner.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); + } catch (err) { + console.error(`Failed to execute procedure ${procedureFileName}:`, err); + throw err; + } finally { + await queryRunner.release(); + } } 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..b3d50cf 100644 --- a/libs/common/src/helper/services/occupancy.service.ts +++ b/libs/common/src/helper/services/occupancy.service.ts @@ -57,9 +57,18 @@ export class OccupancyService { procedureFileName: string, params: (string | number | null)[], ): Promise { - const query = this.loadQuery(procedureFolderName, procedureFileName); - await this.dataSource.query(query, params); - console.log(`Procedure ${procedureFileName} executed successfully.`); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + try { + const query = this.loadQuery(procedureFolderName, procedureFileName); + await queryRunner.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); + } catch (err) { + console.error(`Failed to execute procedure ${procedureFileName}:`, err); + throw err; + } finally { + await queryRunner.release(); + } } 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..6cb667b 100644 --- a/libs/common/src/helper/services/power.clamp.service.ts +++ b/libs/common/src/helper/services/power.clamp.service.ts @@ -46,12 +46,21 @@ export class PowerClampService { procedureFileName: string, params: (string | number | null)[], ): Promise { - const query = this.loadQuery( - 'fact_device_energy_consumed', - procedureFileName, - ); - await this.dataSource.query(query, params); - console.log(`Procedure ${procedureFileName} executed successfully.`); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + try { + const query = this.loadQuery( + 'fact_device_energy_consumed', + procedureFileName, + ); + await queryRunner.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); + } catch (err) { + console.error(`Failed to execute procedure ${procedureFileName}:`, err); + throw err; + } finally { + await queryRunner.release(); + } } private loadQuery(folderName: string, fileName: string): string { From 75d03366c27ed7c6947035c74bfb6a45382626fb Mon Sep 17 00:00:00 2001 From: faljawhary Date: Mon, 23 Jun 2025 06:58:57 -0600 Subject: [PATCH 12/40] Revert "SP-1778-be-fix-time-out-connections-in-the-db" --- .../services/devices-status.service.ts | 84 ++++++------------- .../src/helper/services/aqi.data.service.ts | 15 +--- .../src/helper/services/occupancy.service.ts | 15 +--- .../helper/services/power.clamp.service.ts | 21 ++--- 4 files changed, 38 insertions(+), 97 deletions(-) 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 1d201d3..4b0b0f7 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 @@ -24,7 +24,6 @@ import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.e 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'; -import { DataSource, QueryRunner } from 'typeorm'; @Injectable() export class DeviceStatusFirebaseService { private tuya: TuyaContext; @@ -36,7 +35,6 @@ export class DeviceStatusFirebaseService { private readonly occupancyService: OccupancyService, private readonly aqiDataService: AqiDataService, private deviceStatusLogRepository: DeviceStatusLogRepository, - private readonly dataSource: DataSource, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -81,46 +79,28 @@ export class DeviceStatusFirebaseService { async addDeviceStatusToFirebase( addDeviceStatusDto: AddDeviceStatusDto, ): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); try { const device = await this.getDeviceByDeviceTuyaUuid( addDeviceStatusDto.deviceTuyaUuid, - queryRunner, ); if (device?.uuid) { - const result = await this.createDeviceStatusFirebase( - { - deviceUuid: device.uuid, - ...addDeviceStatusDto, - productType: device.productDevice.prodType, - }, - queryRunner, - ); - await queryRunner.commitTransaction(); - return result; + return await this.createDeviceStatusFirebase({ + deviceUuid: device.uuid, + ...addDeviceStatusDto, + productType: device.productDevice.prodType, + }); } // Return null if device not found or no UUID - await queryRunner.rollbackTransaction(); return null; } catch (error) { - await queryRunner.rollbackTransaction(); + // Handle the error silently, perhaps log it internally or ignore it return null; - } finally { - await queryRunner.release(); } } - async getDeviceByDeviceTuyaUuid( - deviceTuyaUuid: string, - queryRunner?: QueryRunner, - ) { - const repo = queryRunner - ? queryRunner.manager.getRepository(this.deviceRepository.target) - : this.deviceRepository; - return await repo.findOne({ + async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) { + return await this.deviceRepository.findOne({ where: { deviceTuyaUuid, isActive: true, @@ -128,7 +108,6 @@ export class DeviceStatusFirebaseService { relations: ['productDevice'], }); } - async getDevicesInstructionStatus(deviceUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); @@ -174,14 +153,9 @@ export class DeviceStatusFirebaseService { } async getDeviceByDeviceUuid( deviceUuid: string, - withProductDevice = true, - queryRunner?: QueryRunner, + withProductDevice: boolean = true, ) { - const repo = queryRunner - ? queryRunner.manager.getRepository(this.deviceRepository.target) - : this.deviceRepository; - - return await repo.findOne({ + return await this.deviceRepository.findOne({ where: { uuid: deviceUuid, isActive: true, @@ -189,20 +163,21 @@ export class DeviceStatusFirebaseService { ...(withProductDevice && { relations: ['productDevice'] }), }); } - async createDeviceStatusFirebase( addDeviceStatusDto: AddDeviceStatusDto, - queryRunner?: QueryRunner, ): Promise { const dataRef = ref( this.firebaseDb, `device-status/${addDeviceStatusDto.deviceUuid}`, ); - // Step 1: Update Firebase Realtime Database + // Use a transaction to handle concurrent updates await runTransaction(dataRef, (existingData) => { - if (!existingData) existingData = {}; + if (!existingData) { + existingData = {}; + } + // Assign default values if fields are not present if (!existingData.deviceTuyaUuid) { existingData.deviceTuyaUuid = addDeviceStatusDto.deviceTuyaUuid; } @@ -216,15 +191,18 @@ export class DeviceStatusFirebaseService { existingData.status = []; } - // Merge incoming status with existing status + // Create a map to track existing status codes const statusMap = new Map( existingData.status.map((item) => [item.code, item.value]), ); + // Update or add status codes + for (const statusItem of addDeviceStatusDto.status) { statusMap.set(statusItem.code, statusItem.value); } + // Convert the map back to an array format existingData.status = Array.from(statusMap, ([code, value]) => ({ code, value, @@ -233,9 +211,9 @@ export class DeviceStatusFirebaseService { return existingData; }); - // Step 2: Save device status log entries - const newLogs = addDeviceStatusDto.log.properties.map((property) => - this.deviceStatusLogRepository.create({ + // 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, @@ -244,19 +222,10 @@ export class DeviceStatusFirebaseService { value: property.value, eventId: addDeviceStatusDto.log.dataId, eventTime: new Date(property.time).toISOString(), - }), - ); + }); + }); + await this.deviceStatusLogRepository.save(newLogs); - if (queryRunner) { - const repo = queryRunner.manager.getRepository( - this.deviceStatusLogRepository.target, - ); - await repo.save(newLogs); - } else { - await this.deviceStatusLogRepository.save(newLogs); - } - - // Step 3: Trigger additional data services if (addDeviceStatusDto.productType === ProductType.PC) { const energyCodes = new Set([ PowerClampEnergyEnum.ENERGY_CONSUMED, @@ -300,8 +269,7 @@ export class DeviceStatusFirebaseService { addDeviceStatusDto.deviceUuid, ); } - - // Step 4: Return updated Firebase status + // Return the updated data const snapshot: DataSnapshot = await get(dataRef); return snapshot.val(); } diff --git a/libs/common/src/helper/services/aqi.data.service.ts b/libs/common/src/helper/services/aqi.data.service.ts index c8f5c1a..3e19b6c 100644 --- a/libs/common/src/helper/services/aqi.data.service.ts +++ b/libs/common/src/helper/services/aqi.data.service.ts @@ -36,18 +36,9 @@ export class AqiDataService { procedureFileName: string, params: (string | number | null)[], ): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - try { - const query = this.loadQuery(procedureFolderName, procedureFileName); - await queryRunner.query(query, params); - console.log(`Procedure ${procedureFileName} executed successfully.`); - } catch (err) { - console.error(`Failed to execute procedure ${procedureFileName}:`, err); - throw err; - } finally { - await queryRunner.release(); - } + const query = this.loadQuery(procedureFolderName, procedureFileName); + await this.dataSource.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); } private loadQuery(folderName: string, fileName: string): string { diff --git a/libs/common/src/helper/services/occupancy.service.ts b/libs/common/src/helper/services/occupancy.service.ts index b3d50cf..ea99b7c 100644 --- a/libs/common/src/helper/services/occupancy.service.ts +++ b/libs/common/src/helper/services/occupancy.service.ts @@ -57,18 +57,9 @@ export class OccupancyService { procedureFileName: string, params: (string | number | null)[], ): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - try { - const query = this.loadQuery(procedureFolderName, procedureFileName); - await queryRunner.query(query, params); - console.log(`Procedure ${procedureFileName} executed successfully.`); - } catch (err) { - console.error(`Failed to execute procedure ${procedureFileName}:`, err); - throw err; - } finally { - await queryRunner.release(); - } + const query = this.loadQuery(procedureFolderName, procedureFileName); + await this.dataSource.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); } private loadQuery(folderName: string, fileName: string): string { diff --git a/libs/common/src/helper/services/power.clamp.service.ts b/libs/common/src/helper/services/power.clamp.service.ts index 6cb667b..7c83208 100644 --- a/libs/common/src/helper/services/power.clamp.service.ts +++ b/libs/common/src/helper/services/power.clamp.service.ts @@ -46,21 +46,12 @@ export class PowerClampService { procedureFileName: string, params: (string | number | null)[], ): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - try { - const query = this.loadQuery( - 'fact_device_energy_consumed', - procedureFileName, - ); - await queryRunner.query(query, params); - console.log(`Procedure ${procedureFileName} executed successfully.`); - } catch (err) { - console.error(`Failed to execute procedure ${procedureFileName}:`, err); - throw err; - } finally { - await queryRunner.release(); - } + const query = this.loadQuery( + 'fact_device_energy_consumed', + procedureFileName, + ); + await this.dataSource.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); } private loadQuery(folderName: string, fileName: string): string { From c8d691b3800c836a69d30fc051ff0a0f90ad2509 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 23 Jun 2025 07:02:23 -0600 Subject: [PATCH 13/40] tern off data procedure --- .../services/devices-status.service.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) 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..2138466 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 @@ -245,30 +245,30 @@ export class DeviceStatusFirebaseService { } } - if ( - addDeviceStatusDto.productType === ProductType.CPS || - addDeviceStatusDto.productType === ProductType.WPS - ) { - const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]); + // 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), - ); + // 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, - ); - } + // 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(); From 04f64407e1a77f29b3643615f74cec40063575d2 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 23 Jun 2025 07:10:47 -0600 Subject: [PATCH 14/40] turn off some update data points --- .../services/devices-status.service.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 2138466..477df26 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 @@ -226,24 +226,24 @@ export class DeviceStatusFirebaseService { }); 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, - ]); + // 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), - ); + // const energyStatus = addDeviceStatusDto?.log?.properties?.find((status) => + // energyCodes.has(status.code), + // ); - if (energyStatus) { - await this.powerClampService.updateEnergyConsumedHistoricalData( - addDeviceStatusDto.deviceUuid, - ); - } - } + // if (energyStatus) { + // await this.powerClampService.updateEnergyConsumedHistoricalData( + // addDeviceStatusDto.deviceUuid, + // ); + // } + // } // if ( // addDeviceStatusDto.productType === ProductType.CPS || From ff370b2baa648607d10f7bd5da7394ada5920ff2 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 23 Jun 2025 07:31:58 -0600 Subject: [PATCH 15/40] Implement message queue for TuyaWebSocketService and batch processing --- .../services/tuya.web.socket.service.ts | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) 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..dcf07b9 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -9,6 +9,14 @@ export class TuyaWebSocketService { private client: any; private readonly isDevEnv: boolean; + private messageQueue: { + devId: string; + status: any; + logData: any; + }[] = []; + + private isProcessing = false; + constructor( private readonly configService: ConfigService, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, @@ -26,12 +34,12 @@ 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(); } + + // Trigger the queue processor every 2 seconds + setInterval(() => this.processQueue(), 10000); } private setupEventHandlers() { @@ -44,19 +52,13 @@ export class TuyaWebSocketService { try { const { devId, status, logData } = this.extractMessageData(message); - if (this.sosHandlerService.isSosTriggered(status)) { - await this.sosHandlerService.handleSosEvent(devId, logData); - } else { - await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ - deviceTuyaUuid: devId, - status: status, - log: logData, - }); - } + // Push to internal queue + this.messageQueue.push({ devId, status, logData }); + // Acknowledge the message this.client.ackMessage(message.messageId); } catch (error) { - console.error('Error processing message:', error); + console.error('Error receiving message:', error); } }); @@ -80,6 +82,35 @@ export class TuyaWebSocketService { console.error('WebSocket error:', error); }); } + private async processQueue() { + if (this.isProcessing || this.messageQueue.length === 0) return; + + this.isProcessing = true; + + const batch = [...this.messageQueue]; + this.messageQueue = []; + + try { + for (const item of batch) { + if (this.sosHandlerService.isSosTriggered(item.status)) { + await this.sosHandlerService.handleSosEvent(item.devId, item.logData); + } else { + await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ + deviceTuyaUuid: item.devId, + status: item.status, + log: item.logData, + }); + } + } + } catch (error) { + console.error('Error processing batch:', error); + // Re-add the batch to the queue for retry + this.messageQueue.unshift(...batch); + } finally { + this.isProcessing = false; + } + } + private extractMessageData(message: any): { devId: string; status: any; From cf19f08dcadcf51d419ab19492a453d2dca072c6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 23 Jun 2025 07:33:01 -0600 Subject: [PATCH 16/40] turn on all the updates data points --- .../services/devices-status.service.ts | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) 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 477df26..4b0b0f7 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 @@ -226,49 +226,49 @@ export class DeviceStatusFirebaseService { }); 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, - // ]); + 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), - // ); + const energyStatus = addDeviceStatusDto?.log?.properties?.find((status) => + energyCodes.has(status.code), + ); - // if (energyStatus) { - // await this.powerClampService.updateEnergyConsumedHistoricalData( - // addDeviceStatusDto.deviceUuid, - // ); - // } - // } + if (energyStatus) { + await this.powerClampService.updateEnergyConsumedHistoricalData( + addDeviceStatusDto.deviceUuid, + ); + } + } - // if ( - // addDeviceStatusDto.productType === ProductType.CPS || - // addDeviceStatusDto.productType === ProductType.WPS - // ) { - // const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]); + 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), - // ); + 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, - // ); - // } + 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(); From d1d4d529a82dac6810fa847c7abf72a03c6c9dbd Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 23 Jun 2025 08:10:33 -0600 Subject: [PATCH 17/40] Add methods to handle SOS events and device status updates in Firebase and our DB --- .../services/devices-status.service.ts | 32 +++++++++++++++++-- .../helper/services/sos.handler.service.ts | 26 ++++++++++++++- .../services/tuya.web.socket.service.ts | 17 ++++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) 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..695022b 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 @@ -76,6 +76,28 @@ export class DeviceStatusFirebaseService { ); } } + async addDeviceStatusToOurDb( + addDeviceStatusDto: AddDeviceStatusDto, + ): Promise { + try { + const device = await this.getDeviceByDeviceTuyaUuid( + addDeviceStatusDto.deviceTuyaUuid, + ); + + if (device?.uuid) { + return await this.createDeviceStatusInOurDb({ + deviceUuid: device.uuid, + ...addDeviceStatusDto, + productType: device.productDevice.prodType, + }); + } + // Return null if device not found or no UUID + return null; + } catch (error) { + // Handle the error silently, perhaps log it internally or ignore it + return null; + } + } async addDeviceStatusToFirebase( addDeviceStatusDto: AddDeviceStatusDto, ): Promise { @@ -211,6 +233,13 @@ export class DeviceStatusFirebaseService { return existingData; }); + // Return the updated data + const snapshot: DataSnapshot = await get(dataRef); + return snapshot.val(); + } + async createDeviceStatusInOurDb( + addDeviceStatusDto: AddDeviceStatusDto, + ): Promise { // Save logs to your repository const newLogs = addDeviceStatusDto.log.properties.map((property) => { return this.deviceStatusLogRepository.create({ @@ -269,8 +298,5 @@ export class DeviceStatusFirebaseService { addDeviceStatusDto.deviceUuid, ); } - // Return the updated data - const snapshot: DataSnapshot = await get(dataRef); - return snapshot.val(); } } diff --git a/libs/common/src/helper/services/sos.handler.service.ts b/libs/common/src/helper/services/sos.handler.service.ts index 4e957dc..e5f9df9 100644 --- a/libs/common/src/helper/services/sos.handler.service.ts +++ b/libs/common/src/helper/services/sos.handler.service.ts @@ -16,7 +16,7 @@ export class SosHandlerService { ); } - async handleSosEvent(devId: string, logData: any): Promise { + async handleSosEventFirebase(devId: string, logData: any): Promise { try { await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ deviceTuyaUuid: devId, @@ -39,4 +39,28 @@ export class SosHandlerService { this.logger.error('Failed to send SOS true value', err); } } + + async handleSosEventOurDb(devId: string, logData: any): Promise { + try { + await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({ + deviceTuyaUuid: devId, + status: [{ code: 'sos', value: true }], + log: logData, + }); + + setTimeout(async () => { + try { + await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({ + deviceTuyaUuid: devId, + status: [{ code: 'sos', value: false }], + log: logData, + }); + } catch (err) { + this.logger.error('Failed to send SOS false value', err); + } + }, 2000); + } catch (err) { + this.logger.error('Failed to send SOS true value', err); + } + } } 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 dcf07b9..0c32c04 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -51,6 +51,16 @@ export class TuyaWebSocketService { this.client.message(async (ws: WebSocket, message: any) => { try { const { devId, status, logData } = this.extractMessageData(message); + if (this.sosHandlerService.isSosTriggered(status)) { + await this.sosHandlerService.handleSosEventFirebase(devId, logData); + } else { + // Firebase real-time update + await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ + deviceTuyaUuid: devId, + status: status, + log: logData, + }); + } // Push to internal queue this.messageQueue.push({ devId, status, logData }); @@ -93,9 +103,12 @@ export class TuyaWebSocketService { try { for (const item of batch) { if (this.sosHandlerService.isSosTriggered(item.status)) { - await this.sosHandlerService.handleSosEvent(item.devId, item.logData); + await this.sosHandlerService.handleSosEventOurDb( + item.devId, + item.logData, + ); } else { - await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ + await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({ deviceTuyaUuid: item.devId, status: item.status, log: item.logData, From f337e6c68143543b764cfefbb0d3b28d2cfe0250 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Tue, 24 Jun 2025 10:55:38 +0300 Subject: [PATCH 18/40] Test/prevent server block on rate limit (#421) --- src/app.module.ts | 18 ++++++++++++------ src/main.ts | 15 ++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index ce64932..2401b0c 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,6 +35,8 @@ 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 { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; import { AqiModule } from './aqi/aqi.module'; import { OccupancyModule } from './occupancy/occupancy.module'; @@ -44,9 +46,13 @@ import { WeatherModule } from './weather/weather.module'; ConfigModule.forRoot({ load: config, }), - /* ThrottlerModule.forRoot({ - throttlers: [{ ttl: 100000, limit: 30 }], - }), */ + ThrottlerModule.forRoot({ + throttlers: [{ ttl: 60000, limit: 30 }], + generateKey: (context) => { + const req = context.switchToHttp().getRequest(); + return req.headers['x-forwarded-for'] || req.ip; + }, + }), WinstonModule.forRoot(winstonLoggerOptions), ClientModule, AuthenticationModule, @@ -88,10 +94,10 @@ import { WeatherModule } from './weather/weather.module'; provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, }, - /* { + { provide: APP_GUARD, useClass: ThrottlerGuard, - }, */ + }, ], }) export class AppModule {} diff --git a/src/main.ts b/src/main.ts index e00dca6..67edc11 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,6 @@ 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 rateLimit from 'express-rate-limit'; import helmet from 'helmet'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { setupSwaggerAuthentication } from '../libs/common/src/util/user-auth.swagger.utils'; @@ -22,15 +21,13 @@ async function bootstrap() { app.use(new RequestContextMiddleware().use); - app.use( - rateLimit({ - windowMs: 5 * 60 * 1000, - max: 500, - }), - ); - app.use((req, res, next) => { - console.log('Real IP:', req.ip); + console.log( + 'Real IP:', + req.ip, + req.headers['x-forwarded-for'], + req.connection.remoteAddress, + ); next(); }); From 0a1ccad12010a7adfbbe83afcc26840e597f90f1 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Tue, 24 Jun 2025 12:18:15 +0300 Subject: [PATCH 19/40] add check if not space not found (#430) --- src/space/services/space.service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index cbbe953..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, ); } From 932a3efd1ce933bdb26701ff8ca6ec914c9361ec Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Tue, 24 Jun 2025 12:18:46 +0300 Subject: [PATCH 20/40] Sp 1780 be configure the curtain module device (#424) * task: add Cur new device configuration --- .../common/src/constants/product-type.enum.ts | 1 + src/device/commands/cur2-commands.ts | 28 ++ src/schedule/constants/device-function-map.ts | 32 ++ src/schedule/services/schedule.service.ts | 356 ++++++++---------- 4 files changed, 224 insertions(+), 193 deletions(-) create mode 100644 src/device/commands/cur2-commands.ts create mode 100644 src/schedule/constants/device-function-map.ts 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/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/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..e4bd586 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( + ProductType[deviceDetails.productDevice.prodType], + ); + + return this.enableScheduleDeviceInTuya( deviceDetails.deviceTuyaUuid, enableScheduleDto, ); @@ -75,29 +64,6 @@ export class ScheduleService { ); } } - async enableScheduleDeviceInTuya( - deviceId: string, - enableScheduleDto: EnableScheduleDto, - ): Promise { - try { - const path = `/v2.0/cloud/timer/device/${deviceId}/state`; - const response = await this.tuya.request({ - method: 'PUT', - path, - body: { - enable: enableScheduleDto.enable, - timer_id: enableScheduleDto.scheduleId, - }, - }); - - return response as addScheduleDeviceInterface; - } catch (error) { - throw new HttpException( - 'Error while updating schedule from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } async deleteDeviceSchedule(deviceUuid: string, scheduleId: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); @@ -107,21 +73,10 @@ export class ScheduleService { } // Corrected condition for supported device types - if ( - deviceDetails.productDevice.prodType !== ProductType.THREE_G && - deviceDetails.productDevice.prodType !== ProductType.ONE_G && - deviceDetails.productDevice.prodType !== ProductType.TWO_G && - deviceDetails.productDevice.prodType !== ProductType.WH && - deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && - deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && - deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && - deviceDetails.productDevice.prodType !== ProductType.GD - ) { - throw new HttpException( - 'This device is not supported for schedule', - HttpStatus.BAD_REQUEST, - ); - } + this.ensureProductTypeSupportedForSchedule( + ProductType[deviceDetails.productDevice.prodType], + ); + return await this.deleteScheduleDeviceInTuya( deviceDetails.deviceTuyaUuid, scheduleId, @@ -133,25 +88,6 @@ export class ScheduleService { ); } } - async deleteScheduleDeviceInTuya( - deviceId: string, - scheduleId: string, - ): Promise { - try { - const path = `/v2.0/cloud/timer/device/${deviceId}/batch?timer_ids=${scheduleId}`; - const response = await this.tuya.request({ - method: 'DELETE', - path, - }); - - return response as addScheduleDeviceInterface; - } catch (error) { - throw new HttpException( - 'Error while deleting schedule from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } async addDeviceSchedule(deviceUuid: string, addScheduleDto: AddScheduleDto) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); @@ -160,22 +96,10 @@ export class ScheduleService { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } - // Corrected condition for supported device types - if ( - deviceDetails.productDevice.prodType !== ProductType.THREE_G && - deviceDetails.productDevice.prodType !== ProductType.ONE_G && - deviceDetails.productDevice.prodType !== ProductType.TWO_G && - deviceDetails.productDevice.prodType !== ProductType.WH && - deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && - deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && - deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && - deviceDetails.productDevice.prodType !== ProductType.GD - ) { - throw new HttpException( - 'This device is not supported for schedule', - HttpStatus.BAD_REQUEST, - ); - } + this.ensureProductTypeSupportedForSchedule( + ProductType[deviceDetails.productDevice.prodType], + ); + await this.addScheduleDeviceInTuya( deviceDetails.deviceTuyaUuid, addScheduleDto, @@ -187,40 +111,6 @@ export class ScheduleService { ); } } - 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); @@ -229,21 +119,10 @@ export class ScheduleService { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } // Corrected condition for supported device types - if ( - deviceDetails.productDevice.prodType !== ProductType.THREE_G && - deviceDetails.productDevice.prodType !== ProductType.ONE_G && - deviceDetails.productDevice.prodType !== ProductType.TWO_G && - deviceDetails.productDevice.prodType !== ProductType.WH && - deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && - deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && - deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && - deviceDetails.productDevice.prodType !== ProductType.GD - ) { - throw new HttpException( - 'This device is not supported for schedule', - HttpStatus.BAD_REQUEST, - ); - } + this.ensureProductTypeSupportedForSchedule( + ProductType[deviceDetails.productDevice.prodType], + ); + const schedules = await this.getScheduleDeviceInTuya( deviceDetails.deviceTuyaUuid, category, @@ -270,7 +149,82 @@ export class ScheduleService { ); } } - async getScheduleDeviceInTuya( + async updateDeviceSchedule( + deviceUuid: string, + updateScheduleDto: UpdateScheduleDto, + ) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + // Corrected condition for supported device types + this.ensureProductTypeSupportedForSchedule( + ProductType[deviceDetails.productDevice.prodType], + ); + + await this.updateScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + updateScheduleDto, + ); + } catch (error) { + throw new HttpException( + error.message || 'Error While Updating Schedule', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private getDeviceByDeviceUuid( + deviceUuid: string, + withProductDevice: boolean = true, + ) { + return this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + isActive: true, + }, + ...(withProductDevice && { relations: ['productDevice'] }), + }); + } + + private async addScheduleDeviceInTuya( + deviceId: string, + addScheduleDto: AddScheduleDto, + ): Promise { + try { + const convertedTime = convertTimestampToDubaiTime(addScheduleDto.time); + const loops = getScheduleStatus(addScheduleDto.days); + + const path = `/v2.0/cloud/timer/device/${deviceId}`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + time: convertedTime.time, + timezone_id: 'Asia/Dubai', + loops: `${loops}`, + functions: [ + { + ...addScheduleDto.function, + }, + ], + category: `category_${addScheduleDto.category}`, + }, + }); + + return response as addScheduleDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error adding schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async getScheduleDeviceInTuya( deviceId: string, category: string, ): Promise { @@ -291,57 +245,8 @@ export class ScheduleService { ); } } - async getDeviceByDeviceUuid( - deviceUuid: string, - withProductDevice: boolean = true, - ) { - return await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - isActive: true, - }, - ...(withProductDevice && { relations: ['productDevice'] }), - }); - } - async updateDeviceSchedule( - deviceUuid: string, - updateScheduleDto: UpdateScheduleDto, - ) { - try { - const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); - if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { - throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); - } - - // Corrected condition for supported device types - if ( - deviceDetails.productDevice.prodType !== ProductType.THREE_G && - deviceDetails.productDevice.prodType !== ProductType.ONE_G && - deviceDetails.productDevice.prodType !== ProductType.TWO_G && - deviceDetails.productDevice.prodType !== ProductType.WH && - deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && - deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && - deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && - deviceDetails.productDevice.prodType !== ProductType.GD - ) { - throw new HttpException( - 'This device is not supported for schedule', - HttpStatus.BAD_REQUEST, - ); - } - await this.updateScheduleDeviceInTuya( - deviceDetails.deviceTuyaUuid, - updateScheduleDto, - ); - } catch (error) { - throw new HttpException( - error.message || 'Error While Updating Schedule', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async updateScheduleDeviceInTuya( + private async updateScheduleDeviceInTuya( deviceId: string, updateScheduleDto: UpdateScheduleDto, ): Promise { @@ -376,4 +281,69 @@ export class ScheduleService { ); } } + + private async enableScheduleDeviceInTuya( + deviceId: string, + enableScheduleDto: EnableScheduleDto, + ): Promise { + try { + const path = `/v2.0/cloud/timer/device/${deviceId}/state`; + const response = await this.tuya.request({ + method: 'PUT', + path, + body: { + enable: enableScheduleDto.enable, + timer_id: enableScheduleDto.scheduleId, + }, + }); + + return response as addScheduleDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error while updating schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async deleteScheduleDeviceInTuya( + deviceId: string, + scheduleId: string, + ): Promise { + try { + const path = `/v2.0/cloud/timer/device/${deviceId}/batch?timer_ids=${scheduleId}`; + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + return response as addScheduleDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error while deleting schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private ensureProductTypeSupportedForSchedule(deviceType: ProductType): void { + if ( + ![ + ProductType.THREE_G, + ProductType.ONE_G, + ProductType.TWO_G, + ProductType.WH, + ProductType.ONE_1TG, + ProductType.TWO_2TG, + ProductType.THREE_3TG, + ProductType.GD, + ProductType.CUR_2, + ].includes(deviceType) + ) { + throw new HttpException( + 'This device is not supported for schedule', + HttpStatus.BAD_REQUEST, + ); + } + } } From 4e6b6f6ac574f9a2c6992d5f4bc5eae8f0dd8a74 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:04:21 +0300 Subject: [PATCH 21/40] adjusted procedures --- .../fact_daily_device_energy_consumed_procedure.sql | 13 ++++--------- ...fact_hourly_device_energy_consumed_procedure.sql | 12 +----------- ...act_monthly_device_energy_consumed_procedure.sql | 5 ----- 3 files changed, 5 insertions(+), 25 deletions(-) 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..ab9e7d2 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,6 +1,5 @@ WITH params AS ( SELECT - $1::uuid AS device_id, $2::date AS target_date ), total_energy AS ( @@ -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..c056a0f 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 + $2::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..691de79 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,6 +1,5 @@ WITH params AS ( SELECT - $1::uuid AS device_id, $2::text AS target_month -- Format should match 'MM-YYYY' ), total_energy AS ( @@ -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 ), From e58d2d4831675e551e89f036f59c6f352ef902c2 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Tue, 24 Jun 2025 14:56:02 +0300 Subject: [PATCH 22/40] Test/prevent server block on rate limit (#432) --- src/app.module.ts | 8 +++++++- src/main.ts | 12 ------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 2401b0c..712531f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -37,6 +37,7 @@ 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'; @@ -50,7 +51,12 @@ import { WeatherModule } from './weather/weather.module'; throttlers: [{ ttl: 60000, limit: 30 }], generateKey: (context) => { const req = context.switchToHttp().getRequest(); - return req.headers['x-forwarded-for'] || req.ip; + 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), diff --git a/src/main.ts b/src/main.ts index 67edc11..28b546f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,18 +21,6 @@ async function bootstrap() { app.use(new RequestContextMiddleware().use); - app.use((req, res, next) => { - console.log( - 'Real IP:', - req.ip, - req.headers['x-forwarded-for'], - req.connection.remoteAddress, - ); - next(); - }); - - // app.getHttpAdapter().getInstance().set('trust proxy', 1); - app.use( helmet({ contentSecurityPolicy: false, From d255e6811e89905491b6e02d83ab787408fa061a Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:47:37 +0300 Subject: [PATCH 23/40] update procedures --- .../proceduce_update_daily_space_aqi.sql | 8 +++++--- .../procedure_update_daily_space_occupancy_duration.sql | 6 ++---- .../procedure_update_fact_space_occupancy.sql | 6 ++---- 3 files changed, 9 insertions(+), 11 deletions(-) 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_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) From 43ab0030f0f70338363edfd01d5dacbbf76239c1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 03:20:12 -0600 Subject: [PATCH 24/40] refactor: clean up unused services and optimize batch processing in DeviceStatusFirebaseService --- .../devices-status/devices-status.module.ts | 16 -- .../services/devices-status.service.ts | 166 ++++++++---------- .../services/tuya.web.socket.service.ts | 40 ++--- 3 files changed, 95 insertions(+), 127 deletions(-) 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 695022b..3fae855 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,28 +67,85 @@ export class DeviceStatusFirebaseService { ); } } - async addDeviceStatusToOurDb( - addDeviceStatusDto: AddDeviceStatusDto, - ): Promise { - try { - const device = await this.getDeviceByDeviceTuyaUuid( - addDeviceStatusDto.deviceTuyaUuid, - ); + async addBatchDeviceStatusToOurDb( + batch: { deviceTuyaUuid: string; status: any; log: any }[], + ): Promise { + const allLogs = []; + const deviceMap = new Map(); - if (device?.uuid) { - return await this.createDeviceStatusInOurDb({ - deviceUuid: device.uuid, - ...addDeviceStatusDto, - productType: device.productDevice.prodType, - }); - } - // Return null if device not found or no UUID - return null; - } catch (error) { - // Handle the error silently, perhaps log it internally or ignore it - return null; + console.log( + `🧠 Starting device lookups for batch of ${batch.length} items...`, + ); + + // Step 1: Parallel device fetching + await Promise.all( + batch.map(async (item) => { + if (!deviceMap.has(item.deviceTuyaUuid)) { + const device = await this.getDeviceByDeviceTuyaUuid( + item.deviceTuyaUuid, + ); + device?.uuid && deviceMap.set(item.deviceTuyaUuid, device); + } + }), + ); + + console.log(`🔍 Found ${deviceMap.size} devices from batch`); + + // Step 2: Prepare logs and updates + for (const item of batch) { + const device = deviceMap.get(item.deviceTuyaUuid); + if (!device?.uuid) 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}`); + // Step 3: Insert logs in chunks with ON CONFLICT DO NOTHING + 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}`, + ); + })(); + + // Step 5: Wait for both insert and post-processing to finish + await Promise.all([insertLogsPromise]); } + async addDeviceStatusToFirebase( addDeviceStatusDto: AddDeviceStatusDto, ): Promise { @@ -237,66 +285,4 @@ export class DeviceStatusFirebaseService { const snapshot: DataSnapshot = await get(dataRef); return snapshot.val(); } - async createDeviceStatusInOurDb( - addDeviceStatusDto: AddDeviceStatusDto, - ): Promise { - // 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, - ); - } - } } 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 0c32c04..3f56a63 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -38,8 +38,8 @@ export class TuyaWebSocketService { this.client.start(); } - // Trigger the queue processor every 2 seconds - setInterval(() => this.processQueue(), 10000); + // Trigger the queue processor every 15 seconds + setInterval(() => this.processQueue(), 15000); } private setupEventHandlers() { @@ -93,32 +93,30 @@ export class TuyaWebSocketService { }); } private async processQueue() { - if (this.isProcessing || this.messageQueue.length === 0) return; + 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 { - for (const item of batch) { - if (this.sosHandlerService.isSosTriggered(item.status)) { - await this.sosHandlerService.handleSosEventOurDb( - item.devId, - item.logData, - ); - } else { - await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({ - deviceTuyaUuid: item.devId, - status: item.status, - log: item.logData, - }); - } - } + await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( + batch.map((item) => ({ + deviceTuyaUuid: item.devId, + status: item.status, + log: item.logData, + })), + ); } catch (error) { - console.error('Error processing batch:', error); - // Re-add the batch to the queue for retry - this.messageQueue.unshift(...batch); + console.error('❌ Error processing batch:', error); + this.messageQueue.unshift(...batch); // retry } finally { this.isProcessing = false; } From 9bebcb2f3e1d306124ffac7eb70bcdeef4890414 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 03:20:25 -0600 Subject: [PATCH 25/40] feat: implement scheduler for periodic data updates and optimize database procedures - Added SchedulerModule and SchedulerService to handle hourly data updates for AQI, occupancy, and energy consumption. - Refactored existing services to remove unused device repository dependencies and streamline procedure execution. - Updated SQL procedures to use correct parameter indexing. - Enhanced error handling and logging for scheduled tasks. - Integrated new repositories for presence sensor and AQI pollutant stats across multiple modules. - Added NestJS schedule package for task scheduling capabilities. --- libs/common/src/database/database.module.ts | 2 +- .../src/helper/services/aqi.data.service.ts | 65 ++++++++----- .../src/helper/services/occupancy.service.ts | 89 +++++++++--------- .../helper/services/power.clamp.service.ts | 90 +++++++++++------- .../helper/services/sos.handler.service.ts | 38 +++----- .../services/tuya.web.socket.service.ts | 2 +- ...daily_device_energy_consumed_procedure.sql | 2 +- ...ourly_device_energy_consumed_procedure.sql | 2 +- ...nthly_device_energy_consumed_procedure.sql | 2 +- package-lock.json | 42 +++++++++ package.json | 1 + src/app.module.ts | 4 + .../commission-device.module.ts | 4 + src/community/community.module.ts | 4 + src/door-lock/door.lock.module.ts | 4 + src/group/group.module.ts | 4 + src/invite-user/invite-user.module.ts | 4 + src/power-clamp/power-clamp.module.ts | 4 + src/project/project.module.ts | 4 + src/scheduler/scheduler.module.ts | 25 +++++ src/scheduler/scheduler.service.ts | 92 +++++++++++++++++++ src/space-model/space-model.module.ts | 4 + src/space/space.module.ts | 4 + .../visitor-password.module.ts | 4 + 24 files changed, 368 insertions(+), 128 deletions(-) create mode 100644 src/scheduler/scheduler.module.ts create mode 100644 src/scheduler/scheduler.service.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index dd25da9..e86ac6e 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -126,7 +126,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; extra: { charset: 'utf8mb4', max: 100, // set pool max size - idleTimeoutMillis: 5000, // close idle clients after 5 second + 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/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 e5f9df9..dd69f33 100644 --- a/libs/common/src/helper/services/sos.handler.service.ts +++ b/libs/common/src/helper/services/sos.handler.service.ts @@ -23,6 +23,13 @@ export class SosHandlerService { status: [{ code: 'sos', value: true }], log: logData, }); + await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([ + { + deviceTuyaUuid: devId, + status: [{ code: 'sos', value: true }], + log: logData, + }, + ]); setTimeout(async () => { try { @@ -31,30 +38,13 @@ export class SosHandlerService { status: [{ code: 'sos', value: false }], log: logData, }); - } catch (err) { - this.logger.error('Failed to send SOS false value', err); - } - }, 2000); - } catch (err) { - this.logger.error('Failed to send SOS true value', err); - } - } - - async handleSosEventOurDb(devId: string, logData: any): Promise { - try { - await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({ - deviceTuyaUuid: devId, - status: [{ code: 'sos', value: true }], - log: logData, - }); - - setTimeout(async () => { - try { - await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({ - deviceTuyaUuid: devId, - status: [{ code: 'sos', value: false }], - log: logData, - }); + await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([ + { + deviceTuyaUuid: devId, + status: [{ code: 'sos', value: false }], + log: logData, + }, + ]); } 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 3f56a63..1db1991 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -108,7 +108,7 @@ export class TuyaWebSocketService { try { await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( - batch.map((item) => ({ + batch?.map((item) => ({ deviceTuyaUuid: item.devId, status: item.status, log: item.logData, 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 ab9e7d2..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,6 +1,6 @@ WITH params AS ( SELECT - $2::date AS target_date + $1::date AS target_date ), total_energy AS ( SELECT 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 c056a0f..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,6 +1,6 @@ WITH params AS ( SELECT - $2::date AS target_date + $1::date AS target_date ), total_energy AS ( SELECT 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 691de79..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,6 +1,6 @@ WITH params AS ( SELECT - $2::text AS target_month -- Format should match 'MM-YYYY' + $1::text AS target_month -- Format should match 'MM-YYYY' ), total_energy AS ( SELECT diff --git a/package-lock.json b/package-lock.json index eaf972a..e3305e5 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", @@ -2538,6 +2539,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 +3229,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 +5446,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 +9810,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", diff --git a/package.json b/package.json index eaec865..55d546b 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", diff --git a/src/app.module.ts b/src/app.module.ts index 712531f..a7ae475 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -42,6 +42,8 @@ import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston import { AqiModule } from './aqi/aqi.module'; import { OccupancyModule } from './occupancy/occupancy.module'; import { WeatherModule } from './weather/weather.module'; +import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule'; // ✅ الباكيج الرسمي +import { SchedulerModule } from './scheduler/scheduler.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -94,6 +96,8 @@ import { WeatherModule } from './weather/weather.module'; OccupancyModule, WeatherModule, AqiModule, + SchedulerModule, + NestScheduleModule.forRoot(), ], providers: [ { 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/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/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/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/scheduler/scheduler.module.ts b/src/scheduler/scheduler.module.ts new file mode 100644 index 0000000..f5bbb09 --- /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/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/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], }) From 27dbe0429940a8a1831e814aae19d9cc1cc9eb75 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 04:47:38 -0600 Subject: [PATCH 26/40] fix: remove unnecessary comment from ScheduleModule import in scheduler module --- src/app.module.ts | 2 +- src/scheduler/scheduler.module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index a7ae475..f0ab9fc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -42,7 +42,7 @@ import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston 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 { ScheduleModule as NestScheduleModule } from '@nestjs/schedule'; import { SchedulerModule } from './scheduler/scheduler.module'; @Module({ imports: [ diff --git a/src/scheduler/scheduler.module.ts b/src/scheduler/scheduler.module.ts index f5bbb09..28cf164 100644 --- a/src/scheduler/scheduler.module.ts +++ b/src/scheduler/scheduler.module.ts @@ -3,7 +3,7 @@ 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 { 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'; From 68692b7c8b4feb58452b95b030d11b6eb27f2a2e Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Wed, 25 Jun 2025 13:50:38 +0300 Subject: [PATCH 27/40] increase rate limit to 100 per minute for each IP (#435) --- src/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.module.ts b/src/app.module.ts index f0ab9fc..3eee546 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -50,7 +50,7 @@ import { SchedulerModule } from './scheduler/scheduler.module'; load: config, }), ThrottlerModule.forRoot({ - throttlers: [{ ttl: 60000, limit: 30 }], + throttlers: [{ ttl: 60000, limit: 100 }], generateKey: (context) => { const req = context.switchToHttp().getRequest(); console.log('Real IP:', req.headers['x-forwarded-for']); From 71f6ccb4db7f17edcd11eea4d7222d9f2f837aaa Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:20:26 -0600 Subject: [PATCH 28/40] fix: add validation for missing properties in device status logs --- .../devices-status/services/devices-status.service.ts | 8 +++++++- .../common/src/helper/services/tuya.web.socket.service.ts | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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 3fae855..7bbdd87 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 @@ -95,7 +95,13 @@ export class DeviceStatusFirebaseService { for (const item of batch) { const device = deviceMap.get(item.deviceTuyaUuid); if (!device?.uuid) continue; - + if (!Array.isArray(item.log?.properties)) { + console.warn( + `🚨 Missing properties for device: ${item.deviceTuyaUuid}`, + ); + console.log('🔍 Problematic item:', JSON.stringify(item, null, 2)); + continue; + } const logs = item.log.properties.map((property) => this.deviceStatusLogRepository.create({ deviceId: device.uuid, 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 1db1991..3bc3bc8 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -107,6 +107,14 @@ export class TuyaWebSocketService { console.log(`🔁 Processing batch of size: ${batch.length}`); try { + batch.forEach((item, index) => { + if (!Array.isArray(item?.logData?.properties)) { + console.warn( + `❌ Item #${index + 1} has invalid or missing 'properties':`, + ); + console.log(JSON.stringify(item, null, 2)); + } + }); await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( batch?.map((item) => ({ deviceTuyaUuid: item.devId, From a83424f45b0b3517171dde2d1018e07c83a80fe6 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:29:28 -0600 Subject: [PATCH 29/40] fix: remove unnecessary validation for missing properties in device status logs --- .../devices-status/services/devices-status.service.ts | 7 ------- .../src/helper/services/tuya.web.socket.service.ts | 11 +++-------- 2 files changed, 3 insertions(+), 15 deletions(-) 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 7bbdd87..a4f7aad 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 @@ -95,13 +95,6 @@ export class DeviceStatusFirebaseService { for (const item of batch) { const device = deviceMap.get(item.deviceTuyaUuid); if (!device?.uuid) continue; - if (!Array.isArray(item.log?.properties)) { - console.warn( - `🚨 Missing properties for device: ${item.deviceTuyaUuid}`, - ); - console.log('🔍 Problematic item:', JSON.stringify(item, null, 2)); - continue; - } const logs = item.log.properties.map((property) => this.deviceStatusLogRepository.create({ deviceId: device.uuid, 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 3bc3bc8..9d56240 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -51,6 +51,9 @@ export class TuyaWebSocketService { this.client.message(async (ws: WebSocket, message: any) => { try { const { devId, status, logData } = this.extractMessageData(message); + if (!Array.isArray(logData?.properties)) { + return; + } if (this.sosHandlerService.isSosTriggered(status)) { await this.sosHandlerService.handleSosEventFirebase(devId, logData); } else { @@ -107,14 +110,6 @@ export class TuyaWebSocketService { console.log(`🔁 Processing batch of size: ${batch.length}`); try { - batch.forEach((item, index) => { - if (!Array.isArray(item?.logData?.properties)) { - console.warn( - `❌ Item #${index + 1} has invalid or missing 'properties':`, - ); - console.log(JSON.stringify(item, null, 2)); - } - }); await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( batch?.map((item) => ({ deviceTuyaUuid: item.devId, From 324661e1eee6aea203139afde2f4cac040848620 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:30:15 -0600 Subject: [PATCH 30/40] fix: add missing check for device UUID in batch processing logs --- .../firebase/devices-status/services/devices-status.service.ts | 1 + 1 file changed, 1 insertion(+) 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 a4f7aad..3fae855 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 @@ -95,6 +95,7 @@ export class DeviceStatusFirebaseService { for (const item of batch) { const device = deviceMap.get(item.deviceTuyaUuid); if (!device?.uuid) continue; + const logs = item.log.properties.map((property) => this.deviceStatusLogRepository.create({ deviceId: device.uuid, From c0a069b4607e91459c07fb07e9d59f7b1a415b8c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 08:03:23 -0600 Subject: [PATCH 31/40] fix: enhance device status handling by integrating device cache for improved performance --- .../services/devices-status.service.ts | 105 ++++++++---------- libs/common/src/helper/helper.module.ts | 6 +- .../helper/services/sos.handler.service.ts | 48 +++++--- .../services/tuya.web.socket.service.ts | 53 +++++++-- 4 files changed, 128 insertions(+), 84 deletions(-) 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 3fae855..3c431e3 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 @@ -69,32 +69,23 @@ export class DeviceStatusFirebaseService { } async addBatchDeviceStatusToOurDb( batch: { deviceTuyaUuid: string; status: any; log: any }[], + deviceCache: Map, ): Promise { const allLogs = []; - const deviceMap = new Map(); console.log( - `🧠 Starting device lookups for batch of ${batch.length} items...`, + `🧠 Preparing logs from batch of ${batch.length} items using cached devices only...`, ); - // Step 1: Parallel device fetching - await Promise.all( - batch.map(async (item) => { - if (!deviceMap.has(item.deviceTuyaUuid)) { - const device = await this.getDeviceByDeviceTuyaUuid( - item.deviceTuyaUuid, - ); - device?.uuid && deviceMap.set(item.deviceTuyaUuid, device); - } - }), - ); - - console.log(`🔍 Found ${deviceMap.size} devices from batch`); - - // Step 2: Prepare logs and updates for (const item of batch) { - const device = deviceMap.get(item.deviceTuyaUuid); - if (!device?.uuid) continue; + const device = deviceCache.get(item.deviceTuyaUuid); + + if (!device?.uuid) { + console.log( + `⛔ Ignored unknown device in batch: ${item.deviceTuyaUuid}`, + ); + continue; + } const logs = item.log.properties.map((property) => this.deviceStatusLogRepository.create({ @@ -112,59 +103,53 @@ export class DeviceStatusFirebaseService { } console.log(`📝 Total logs to insert: ${allLogs.length}`); - // Step 3: Insert logs in chunks with ON CONFLICT DO NOTHING - const insertLogsPromise = (async () => { - const chunkSize = 300; - let insertedCount = 0; + 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(); + 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); - } + 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}`, - ); - })(); - - // Step 5: Wait for both insert and post-processing to finish - await Promise.all([insertLogsPromise]); + console.log(`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`); } async addDeviceStatusToFirebase( addDeviceStatusDto: AddDeviceStatusDto, + deviceCache: Map, ): Promise { try { - const device = await this.getDeviceByDeviceTuyaUuid( - addDeviceStatusDto.deviceTuyaUuid, - ); - - if (device?.uuid) { - return await this.createDeviceStatusFirebase({ - deviceUuid: device.uuid, - ...addDeviceStatusDto, - productType: device.productDevice.prodType, - }); + const device = deviceCache.get(addDeviceStatusDto.deviceTuyaUuid); + if (!device?.uuid) { + console.log( + `⛔ Skipping Firebase update for unknown device: ${addDeviceStatusDto.deviceTuyaUuid}`, + ); + return null; } - // Return null if device not found or no UUID - return null; + + // Ensure product info and uuid are attached + addDeviceStatusDto.deviceUuid = device.uuid; + addDeviceStatusDto.productUuid = device.productDevice?.uuid; + addDeviceStatusDto.productType = device.productDevice?.prodType; + + return await this.createDeviceStatusFirebase(addDeviceStatusDto); } catch (error) { - // Handle the error silently, perhaps log it internally or ignore it + console.error('❌ Error in addDeviceStatusToFirebase:', error); return null; } } 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/sos.handler.service.ts b/libs/common/src/helper/services/sos.handler.service.ts index dd69f33..e883b62 100644 --- a/libs/common/src/helper/services/sos.handler.service.ts +++ b/libs/common/src/helper/services/sos.handler.service.ts @@ -16,35 +16,53 @@ export class SosHandlerService { ); } - async handleSosEventFirebase(devId: string, logData: any): Promise { + async handleSosEventFirebase( + devId: string, + logData: any, + deviceCache: Map, + ): Promise { try { - await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ - deviceTuyaUuid: devId, - status: [{ code: 'sos', value: true }], - log: logData, - }); - await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([ + await this.deviceStatusFirebaseService.addDeviceStatusToFirebase( { deviceTuyaUuid: devId, status: [{ code: 'sos', value: true }], log: logData, }, - ]); + deviceCache, + ); + + await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( + [ + { + deviceTuyaUuid: devId, + status: [{ code: 'sos', value: true }], + log: logData, + }, + ], + deviceCache, + ); setTimeout(async () => { try { - await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ - deviceTuyaUuid: devId, - status: [{ code: 'sos', value: false }], - log: logData, - }); - await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([ + await this.deviceStatusFirebaseService.addDeviceStatusToFirebase( { deviceTuyaUuid: devId, status: [{ code: 'sos', value: false }], log: logData, }, - ]); + deviceCache, + ); + + await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( + [ + { + deviceTuyaUuid: devId, + status: [{ code: 'sos', value: false }], + log: logData, + }, + ], + deviceCache, + ); } 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 9d56240..e0850be 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -3,6 +3,7 @@ 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 { DeviceRepository } from '@app/common/modules/device/repositories'; @Injectable() export class TuyaWebSocketService { @@ -16,11 +17,13 @@ export class TuyaWebSocketService { }[] = []; private isProcessing = false; + private deviceCache: Map = new Map(); constructor( private readonly configService: ConfigService, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly sosHandlerService: SosHandlerService, + private readonly deviceRepository: DeviceRepository, ) { this.isDevEnv = this.configService.get('NODE_ENV') === 'development'; @@ -33,6 +36,11 @@ export class TuyaWebSocketService { maxRetryTimes: 100, }); + this.loadAllActiveDevices(); + + // Reload device cache every 1 hour + setInterval(() => this.loadAllActiveDevices(), 60 * 60 * 1000); + if (this.configService.get('tuya-config.TRUN_ON_TUYA_SOCKET')) { this.setupEventHandlers(); this.client.start(); @@ -42,6 +50,22 @@ export class TuyaWebSocketService { setInterval(() => this.processQueue(), 15000); } + private async loadAllActiveDevices(): Promise { + const devices = await this.deviceRepository.find({ + where: { isActive: true }, + relations: ['productDevice'], + }); + + this.deviceCache.clear(); + devices.forEach((device) => { + this.deviceCache.set(device.deviceTuyaUuid, device); + }); + + console.log( + `🔄 Device cache reloaded: ${this.deviceCache.size} active devices at ${new Date().toISOString()}`, + ); + } + private setupEventHandlers() { // Event handlers this.client.open(() => { @@ -51,18 +75,30 @@ export class TuyaWebSocketService { this.client.message(async (ws: WebSocket, message: any) => { try { const { devId, status, logData } = this.extractMessageData(message); - if (!Array.isArray(logData?.properties)) { + if (!Array.isArray(logData?.properties)) return; + + const device = this.deviceCache.get(devId); + if (!device) { + // console.log(`⛔ Ignored unknown device: ${devId}`); return; } + if (this.sosHandlerService.isSosTriggered(status)) { - await this.sosHandlerService.handleSosEventFirebase(devId, logData); + await this.sosHandlerService.handleSosEventFirebase( + devId, + logData, + this.deviceCache, + ); } else { // Firebase real-time update - await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ - deviceTuyaUuid: devId, - status: status, - log: logData, - }); + await this.deviceStatusFirebaseService.addDeviceStatusToFirebase( + { + deviceTuyaUuid: devId, + status, + log: logData, + }, + this.deviceCache, + ); } // Push to internal queue @@ -111,11 +147,12 @@ export class TuyaWebSocketService { try { await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( - batch?.map((item) => ({ + batch.map((item) => ({ deviceTuyaUuid: item.devId, status: item.status, log: item.logData, })), + this.deviceCache, ); } catch (error) { console.error('❌ Error processing batch:', error); From 68d2d3b53dc9dc1afb227213a17d60c06632839f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 08:13:02 -0600 Subject: [PATCH 32/40] fix: improve device retrieval logic in addDeviceStatusToFirebase method --- .../services/devices-status.service.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 3c431e3..6f83d99 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 @@ -131,10 +131,19 @@ export class DeviceStatusFirebaseService { async addDeviceStatusToFirebase( addDeviceStatusDto: AddDeviceStatusDto, - deviceCache: Map, + deviceCache?: Map, ): Promise { try { - const device = deviceCache.get(addDeviceStatusDto.deviceTuyaUuid); + let device; + + if (deviceCache) { + device = deviceCache.get(addDeviceStatusDto.deviceTuyaUuid); + } else { + device = await this.getDeviceByDeviceTuyaUuid( + addDeviceStatusDto.deviceTuyaUuid, + ); + } + if (!device?.uuid) { console.log( `⛔ Skipping Firebase update for unknown device: ${addDeviceStatusDto.deviceTuyaUuid}`, @@ -142,7 +151,6 @@ export class DeviceStatusFirebaseService { return null; } - // Ensure product info and uuid are attached addDeviceStatusDto.deviceUuid = device.uuid; addDeviceStatusDto.productUuid = device.productDevice?.uuid; addDeviceStatusDto.productType = device.productDevice?.prodType; From 731819aeaa7dbaccfaaf2879e3480616746384f9 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 18:37:46 -0600 Subject: [PATCH 33/40] feat: enhance device status handling with caching and batch processing improvements --- .../services/devices-status.service.ts | 60 +++++++++---------- .../helper/services/sos.handler.service.ts | 29 ++++++--- .../services/tuya.web.socket.service.ts | 46 ++++++++++++-- package-lock.json | 22 +++++++ package.json | 1 + 5 files changed, 113 insertions(+), 45 deletions(-) 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 3fae855..6560276 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 @@ -68,33 +68,23 @@ export class DeviceStatusFirebaseService { } } async addBatchDeviceStatusToOurDb( - batch: { deviceTuyaUuid: string; status: any; log: any }[], + batch: { + deviceTuyaUuid: string; + status: any; + log: any; + device: any; + }[], ): Promise { const allLogs = []; - const deviceMap = new Map(); - console.log( - `🧠 Starting device lookups for batch of ${batch.length} items...`, - ); + console.log(`🔁 Preparing logs from batch of ${batch.length} items...`); - // Step 1: Parallel device fetching - await Promise.all( - batch.map(async (item) => { - if (!deviceMap.has(item.deviceTuyaUuid)) { - const device = await this.getDeviceByDeviceTuyaUuid( - item.deviceTuyaUuid, - ); - device?.uuid && deviceMap.set(item.deviceTuyaUuid, device); - } - }), - ); - - console.log(`🔍 Found ${deviceMap.size} devices from batch`); - - // Step 2: Prepare logs and updates for (const item of batch) { - const device = deviceMap.get(item.deviceTuyaUuid); - if (!device?.uuid) continue; + 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({ @@ -142,23 +132,24 @@ export class DeviceStatusFirebaseService { ); })(); - // Step 5: Wait for both insert and post-processing to finish - await Promise.all([insertLogsPromise]); + 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 @@ -178,6 +169,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); diff --git a/libs/common/src/helper/services/sos.handler.service.ts b/libs/common/src/helper/services/sos.handler.service.ts index dd69f33..55a0daf 100644 --- a/libs/common/src/helper/services/sos.handler.service.ts +++ b/libs/common/src/helper/services/sos.handler.service.ts @@ -16,33 +16,44 @@ export class SosHandlerService { ); } - async handleSosEventFirebase(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: devId, - status: [{ code: 'sos', value: true }], + 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: devId, - status: [{ code: 'sos', value: false }], + deviceTuyaUuid: device.deviceTuyaUuid, + status: sosFalseStatus, log: logData, + device, }, ]); } catch (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 9d56240..be32b1d 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -1,18 +1,21 @@ -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; @@ -38,8 +41,29 @@ export class TuyaWebSocketService { this.client.start(); } - // Trigger the queue processor every 15 seconds + // 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() { @@ -52,6 +76,14 @@ export class TuyaWebSocketService { 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.warn(`⚠️ Device not found in cache: ${devId}`); + this.client.ackMessage(message.messageId); return; } if (this.sosHandlerService.isSosTriggered(status)) { @@ -60,13 +92,14 @@ export class TuyaWebSocketService { // Firebase real-time update await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ deviceTuyaUuid: devId, - status: status, + status, log: logData, + device, }); } // Push to internal queue - this.messageQueue.push({ devId, status, logData }); + this.messageQueue.push({ devId, status, logData, device }); // Acknowledge the message this.client.ackMessage(message.messageId); @@ -111,10 +144,11 @@ export class TuyaWebSocketService { try { await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( - batch?.map((item) => ({ + batch.map((item) => ({ deviceTuyaUuid: item.devId, status: item.status, log: item.logData, + device: item.device, })), ); } catch (error) { diff --git a/package-lock.json b/package-lock.json index e3305e5..e8718b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,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", @@ -10184,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 55d546b..6e16079 100644 --- a/package.json +++ b/package.json @@ -51,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", From f80d097ff88b882e1b82b9dd5603289b75357f7e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 25 Jun 2025 18:57:56 -0600 Subject: [PATCH 34/40] refactor: optimize log insertion and clean up device cache handling in TuyaWebSocketService --- .../services/devices-status.service.ts | 41 ++++++++++--------- .../services/tuya.web.socket.service.ts | 22 ++-------- 2 files changed, 24 insertions(+), 39 deletions(-) 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 3972b2a..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 @@ -102,28 +102,30 @@ export class DeviceStatusFirebaseService { } console.log(`📝 Total logs to insert: ${allLogs.length}`); - 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(); + const insertLogsPromise = (async () => { + const chunkSize = 300; + let insertedCount = 0; - insertedCount += result.identifiers.length; - console.log( - `✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`, - ); - } catch (error) { - console.error('❌ Insert error (skipped chunk):', error.message); + 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}`, @@ -153,7 +155,6 @@ export class DeviceStatusFirebaseService { // Return null if device not found or no UUID return null; } catch (error) { - console.error('❌ Error in addDeviceStatusToFirebase:', error); return null; } } 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 63de80b..d30200f 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -19,13 +19,11 @@ export class TuyaWebSocketService implements OnModuleInit { }[] = []; private isProcessing = false; - private deviceCache: Map = new Map(); constructor( private readonly configService: ConfigService, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly sosHandlerService: SosHandlerService, - private readonly deviceRepository: DeviceRepository, ) { this.isDevEnv = this.configService.get('NODE_ENV') === 'development'; @@ -38,11 +36,6 @@ export class TuyaWebSocketService implements OnModuleInit { maxRetryTimes: 100, }); - this.loadAllActiveDevices(); - - // Reload device cache every 1 hour - setInterval(() => this.loadAllActiveDevices(), 60 * 60 * 1000); - if (this.configService.get('tuya-config.TRUN_ON_TUYA_SOCKET')) { this.setupEventHandlers(); this.client.start(); @@ -74,7 +67,6 @@ export class TuyaWebSocketService implements OnModuleInit { } private setupEventHandlers() { - // Event handlers this.client.open(() => { console.log('open'); }); @@ -89,19 +81,14 @@ export class TuyaWebSocketService implements OnModuleInit { const device = this.deviceCache.get(devId); if (!device) { - // console.warn(`⚠️ Device not found in cache: ${devId}`); + // console.log(⛔ Unknown device: ${devId}, message ignored.); this.client.ackMessage(message.messageId); return; } if (this.sosHandlerService.isSosTriggered(status)) { - await this.sosHandlerService.handleSosEventFirebase( - devId, - logData, - this.deviceCache, - ); + await this.sosHandlerService.handleSosEventFirebase(devId, logData); } else { - // Firebase real-time update await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ deviceTuyaUuid: devId, status, @@ -116,10 +103,9 @@ export class TuyaWebSocketService implements OnModuleInit { // Acknowledge the message this.client.ackMessage(message.messageId); } catch (error) { - console.error('Error receiving message:', error); + console.error('❌ Error receiving message:', error); } }); - this.client.reconnect(() => { console.log('reconnect'); }); @@ -156,14 +142,12 @@ export class TuyaWebSocketService implements OnModuleInit { try { await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( - batch.map((item) => ({ batch.map((item) => ({ deviceTuyaUuid: item.devId, status: item.status, log: item.logData, device: item.device, })), - this.deviceCache, ); } catch (error) { console.error('❌ Error processing batch:', error); From c7a4ff119457d4bd749c7d62650dd46528da838a Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Sun, 29 Jun 2025 15:27:55 +0300 Subject: [PATCH 35/40] fix: schedule device types (#441) --- src/schedule/services/schedule.service.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/schedule/services/schedule.service.ts b/src/schedule/services/schedule.service.ts index e4bd586..d88ee20 100644 --- a/src/schedule/services/schedule.service.ts +++ b/src/schedule/services/schedule.service.ts @@ -50,7 +50,7 @@ export class ScheduleService { // Corrected condition for supported device types this.ensureProductTypeSupportedForSchedule( - ProductType[deviceDetails.productDevice.prodType], + deviceDetails.productDevice.prodType as ProductType, ); return this.enableScheduleDeviceInTuya( @@ -74,7 +74,7 @@ export class ScheduleService { // Corrected condition for supported device types this.ensureProductTypeSupportedForSchedule( - ProductType[deviceDetails.productDevice.prodType], + deviceDetails.productDevice.prodType as ProductType, ); return await this.deleteScheduleDeviceInTuya( @@ -97,7 +97,7 @@ export class ScheduleService { } this.ensureProductTypeSupportedForSchedule( - ProductType[deviceDetails.productDevice.prodType], + deviceDetails.productDevice.prodType as ProductType, ); await this.addScheduleDeviceInTuya( @@ -120,9 +120,8 @@ export class ScheduleService { } // Corrected condition for supported device types this.ensureProductTypeSupportedForSchedule( - ProductType[deviceDetails.productDevice.prodType], + deviceDetails.productDevice.prodType as ProductType, ); - const schedules = await this.getScheduleDeviceInTuya( deviceDetails.deviceTuyaUuid, category, @@ -162,7 +161,7 @@ export class ScheduleService { // Corrected condition for supported device types this.ensureProductTypeSupportedForSchedule( - ProductType[deviceDetails.productDevice.prodType], + deviceDetails.productDevice.prodType as ProductType, ); await this.updateScheduleDeviceInTuya( From 82c82d521c88648db3220ba29a99c6de7e50ec31 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Mon, 30 Jun 2025 08:57:43 +0300 Subject: [PATCH 36/40] add deviceName to handle password API (#442) --- .../services/visitor-password.service.ts | 239 +++++++----------- 1 file changed, 93 insertions(+), 146 deletions(-) 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 { From f4f7999ae0a1d2a4c278e2600e23e896ed36c7e3 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Mon, 30 Jun 2025 09:48:16 +0300 Subject: [PATCH 37/40] add device to firebase & stop moving it from the OEM space (#443) --- src/commission-device/services/commission-device.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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}`, From f4e748d73561b65592bf0431e40de9be8315a060 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 30 Jun 2025 00:58:30 -0600 Subject: [PATCH 38/40] fix: update role type formatting in user invitation email --- src/invite-user/services/invite-user.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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, }); From b3f8b928262381aa470f483fb77def98f596571d Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Mon, 30 Jun 2025 15:35:23 +0300 Subject: [PATCH 39/40] ensure Timer is the category value for CUR2 type (#446) --- src/schedule/services/schedule.service.ts | 39 +++++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/schedule/services/schedule.service.ts b/src/schedule/services/schedule.service.ts index d88ee20..e89c6d8 100644 --- a/src/schedule/services/schedule.service.ts +++ b/src/schedule/services/schedule.service.ts @@ -96,6 +96,16 @@ export class ScheduleService { 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, ); @@ -103,6 +113,7 @@ export class ScheduleService { await this.addScheduleDeviceInTuya( deviceDetails.deviceTuyaUuid, addScheduleDto, + deviceDetails.productDevice.prodType as ProductType, ); } catch (error) { throw new HttpException( @@ -128,7 +139,10 @@ export class ScheduleService { ); const result = schedules.result.map((schedule: any) => { return { - category: schedule.category.replace('category_', ''), + category: + deviceDetails.productDevice.prodType == ProductType.CUR_2 + ? schedule.category + : schedule.category.replace('category_', ''), enable: schedule.enable, function: { code: schedule.functions[0].code, @@ -159,6 +173,16 @@ export class ScheduleService { 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, @@ -167,6 +191,7 @@ export class ScheduleService { await this.updateScheduleDeviceInTuya( deviceDetails.deviceTuyaUuid, updateScheduleDto, + deviceDetails.productDevice.prodType as ProductType, ); } catch (error) { throw new HttpException( @@ -192,6 +217,7 @@ export class ScheduleService { private async addScheduleDeviceInTuya( deviceId: string, addScheduleDto: AddScheduleDto, + deviceType: ProductType, ): Promise { try { const convertedTime = convertTimestampToDubaiTime(addScheduleDto.time); @@ -210,7 +236,10 @@ export class ScheduleService { ...addScheduleDto.function, }, ], - category: `category_${addScheduleDto.category}`, + category: + deviceType == ProductType.CUR_2 + ? addScheduleDto.category + : `category_${addScheduleDto.category}`, }, }); @@ -248,6 +277,7 @@ export class ScheduleService { private async updateScheduleDeviceInTuya( deviceId: string, updateScheduleDto: UpdateScheduleDto, + deviceType: ProductType, ): Promise { try { const convertedTime = convertTimestampToDubaiTime(updateScheduleDto.time); @@ -268,7 +298,10 @@ export class ScheduleService { value: updateScheduleDto.function.value, }, ], - category: `category_${updateScheduleDto.category}`, + category: + deviceType == ProductType.CUR_2 + ? updateScheduleDto.category + : `category_${updateScheduleDto.category}`, }, }); From 0b9eef276e0a8eb1335174b52cee8cfdc8a83e5c Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Mon, 30 Jun 2025 15:52:01 +0300 Subject: [PATCH 40/40] ensure Timer is the category value for CUR2 type (#448) --- src/schedule/services/schedule.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/schedule/services/schedule.service.ts b/src/schedule/services/schedule.service.ts index e89c6d8..1296900 100644 --- a/src/schedule/services/schedule.service.ts +++ b/src/schedule/services/schedule.service.ts @@ -136,6 +136,7 @@ export class ScheduleService { const schedules = await this.getScheduleDeviceInTuya( deviceDetails.deviceTuyaUuid, category, + deviceDetails.productDevice.prodType as ProductType, ); const result = schedules.result.map((schedule: any) => { return { @@ -255,9 +256,12 @@ export class ScheduleService { private async getScheduleDeviceInTuya( deviceId: string, category: string, + deviceType: ProductType, ): Promise { try { - const path = `/v2.0/cloud/timer/device/${deviceId}?category=category_${category}`; + 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,