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) 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 5a810ab..0c32c04 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() { @@ -43,10 +51,10 @@ 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.handleSosEvent(devId, logData); + await this.sosHandlerService.handleSosEventFirebase(devId, logData); } else { + // Firebase real-time update await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ deviceTuyaUuid: devId, status: status, @@ -54,9 +62,13 @@ export class TuyaWebSocketService { }); } + // 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 +92,38 @@ 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.handleSosEventOurDb( + item.devId, + item.logData, + ); + } else { + await this.deviceStatusFirebaseService.addDeviceStatusToOurDb({ + deviceTuyaUuid: item.devId, + status: item.status, + log: item.logData, + }); + } + } + } catch (error) { + console.error('Error processing batch:', error); + // Re-add the batch to the queue for retry + this.messageQueue.unshift(...batch); + } finally { + this.isProcessing = false; + } + } + private extractMessageData(message: any): { devId: string; status: any; diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 5de34fa..d4ff99c 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -190,24 +190,26 @@ export class CommunityService { .distinct(true); if (includeSpaces) { - qb.leftJoinAndSelect('c.spaces', 'space', 'space.disabled = false') + qb.leftJoinAndSelect( + 'c.spaces', + 'space', + 'space.disabled = :disabled AND space.spaceName != :orphanSpaceName', + { disabled: false, orphanSpaceName: ORPHAN_SPACE_NAME }, + ) .leftJoinAndSelect('space.parent', 'parent') .leftJoinAndSelect( 'space.children', 'children', 'children.disabled = :disabled', { disabled: false }, - ) - // .leftJoinAndSelect('space.spaceModel', 'spaceModel') - .andWhere('space.spaceName != :orphanSpaceName', { - orphanSpaceName: ORPHAN_SPACE_NAME, - }) - .andWhere('space.disabled = :disabled', { disabled: false }); + ); + // .leftJoinAndSelect('space.spaceModel', 'spaceModel') } if (search) { qb.andWhere( - `c.name ILIKE '%${search}%' ${includeSpaces ? "OR space.space_name ILIKE '%" + search + "%'" : ''}`, + `c.name ILIKE :search ${includeSpaces ? 'OR space.space_name ILIKE :search' : ''}`, + { search }, ); } @@ -215,12 +217,21 @@ export class CommunityService { const { baseResponseDto, paginationResponseDto } = await customModel.findAll({ ...pageable, modelName: 'community' }, qb); + if (includeSpaces) { + baseResponseDto.data = baseResponseDto.data.map((community) => ({ + ...community, + spaces: this.spaceService.buildSpaceHierarchy(community.spaces || []), + })); + } return new PageResponse( baseResponseDto, paginationResponseDto, ); } catch (error) { // Generic error handling + if (error instanceof HttpException) { + throw error; + } throw new HttpException( error.message || 'An error occurred while fetching communities.', HttpStatus.INTERNAL_SERVER_ERROR, diff --git a/src/device/controllers/device-project.controller.ts b/src/device/controllers/device-project.controller.ts index e5181dd..1585415 100644 --- a/src/device/controllers/device-project.controller.ts +++ b/src/device/controllers/device-project.controller.ts @@ -1,11 +1,11 @@ -import { DeviceService } from '../services/device.service'; -import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import { ControllerRoute } from '@app/common/constants/controller-route'; -import { PermissionsGuard } from 'src/guards/permissions.guard'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { Permissions } from 'src/decorators/permissions.decorator'; -import { GetDoorLockDevices, ProjectParam } from '../dtos'; +import { PermissionsGuard } from 'src/guards/permissions.guard'; +import { GetDevicesFilterDto, ProjectParam } from '../dtos'; +import { DeviceService } from '../services/device.service'; @ApiTags('Device Module') @Controller({ @@ -25,7 +25,7 @@ export class DeviceProjectController { }) async getAllDevices( @Param() param: ProjectParam, - @Query() query: GetDoorLockDevices, + @Query() query: GetDevicesFilterDto, ) { return await this.deviceService.getAllDevices(param, query); } diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 84c9d64..e34a8b6 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -1,6 +1,7 @@ import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; import { ApiProperty } from '@nestjs/swagger'; import { + IsArray, IsEnum, IsNotEmpty, IsOptional, @@ -41,16 +42,7 @@ export class GetDeviceLogsDto { @IsOptional() public endTime: string; } -export class GetDoorLockDevices { - @ApiProperty({ - description: 'Device Type', - enum: DeviceTypeEnum, - required: false, - }) - @IsEnum(DeviceTypeEnum) - @IsOptional() - public deviceType: DeviceTypeEnum; -} + export class GetDevicesBySpaceOrCommunityDto { @ApiProperty({ description: 'Device Product Type', @@ -72,3 +64,23 @@ export class GetDevicesBySpaceOrCommunityDto { @IsNotEmpty({ message: 'Either spaceUuid or communityUuid must be provided' }) requireEither?: never; // This ensures at least one of them is provided } + +export class GetDevicesFilterDto { + @ApiProperty({ + description: 'Device Type', + enum: DeviceTypeEnum, + required: false, + }) + @IsEnum(DeviceTypeEnum) + @IsOptional() + public deviceType: DeviceTypeEnum; + @ApiProperty({ + description: 'List of Space IDs to filter devices', + required: false, + example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'], + }) + @IsOptional() + @IsArray() + @IsUUID('4', { each: true }) + public spaces?: string[]; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index d2ac4e7..793d854 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -53,7 +53,7 @@ import { DeviceSceneParamDto } from '../dtos/device.param.dto'; import { GetDeviceLogsDto, GetDevicesBySpaceOrCommunityDto, - GetDoorLockDevices, + GetDevicesFilterDto, } from '../dtos/get.device.dto'; import { controlDeviceInterface, @@ -955,19 +955,20 @@ export class DeviceService { async getAllDevices( param: ProjectParam, - query: GetDoorLockDevices, + { deviceType, spaces }: GetDevicesFilterDto, ): Promise { try { await this.validateProject(param.projectUuid); - if (query.deviceType === DeviceTypeEnum.DOOR_LOCK) { - return await this.getDoorLockDevices(param.projectUuid); - } else if (!query.deviceType) { + if (deviceType === DeviceTypeEnum.DOOR_LOCK) { + return await this.getDoorLockDevices(param.projectUuid, spaces); + } else if (!deviceType) { const devices = await this.deviceRepository.find({ where: { isActive: true, spaceDevice: { - community: { project: { uuid: param.projectUuid } }, + uuid: spaces && spaces.length ? In(spaces) : undefined, spaceName: Not(ORPHAN_SPACE_NAME), + community: { project: { uuid: param.projectUuid } }, }, }, relations: [ @@ -1563,7 +1564,7 @@ export class DeviceService { } } - async getDoorLockDevices(projectUuid: string) { + async getDoorLockDevices(projectUuid: string, spaces?: string[]) { await this.validateProject(projectUuid); const devices = await this.deviceRepository.find({ @@ -1573,6 +1574,7 @@ export class DeviceService { }, spaceDevice: { spaceName: Not(ORPHAN_SPACE_NAME), + uuid: spaces && spaces.length ? In(spaces) : undefined, community: { project: { uuid: projectUuid, diff --git a/src/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