diff --git a/libs/common/src/constants/product-type.enum.ts b/libs/common/src/constants/product-type.enum.ts index d368865..fd0cc6f 100644 --- a/libs/common/src/constants/product-type.enum.ts +++ b/libs/common/src/constants/product-type.enum.ts @@ -18,4 +18,5 @@ export enum ProductType { PC = 'PC', FOUR_S = '4S', SIX_S = '6S', + SOS = 'SOS', } diff --git a/src/space/services/space-device.service.ts b/src/space/services/space-device.service.ts index aaa7d12..490cca7 100644 --- a/src/space/services/space-device.service.ts +++ b/src/space/services/space-device.service.ts @@ -8,6 +8,8 @@ import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { ValidationService } from './space-validation.service'; +import { ProductType } from '@app/common/constants/product-type.enum'; +import { BatteryStatus } from '@app/common/constants/battery-status.enum'; @Injectable() export class SpaceDeviceService { @@ -18,54 +20,35 @@ export class SpaceDeviceService { async listDevicesInSpace(params: GetSpaceParam): Promise { const { spaceUuid, communityUuid, projectUuid } = params; - try { - const space = - await this.validationService.validateSpaceWithinCommunityAndProject( - communityUuid, - projectUuid, - spaceUuid, - ); - if (!space.devices || space.devices.length === 0) { + try { + // Validate community, project, and fetch space including devices in a single query + const space = await this.validationService.fetchSpaceDevices(spaceUuid); + + if (!space || !space.devices?.length) { throw new HttpException( 'The space does not contain any devices.', HttpStatus.BAD_REQUEST, ); } - const safeFetch = async (device: any) => { - try { - const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya( - device.deviceTuyaUuid, - ); - const tuyaDeviceStatus = - await this.tuyaService.getDevicesInstructionStatusTuya( - device.deviceTuyaUuid, - ); - return { - uuid: device.uuid, - deviceTuyaUuid: device.deviceTuyaUuid, - productUuid: device.productDevice.uuid, - productType: device.productDevice.prodType, - isActive: device.isActive, - updatedAt: device.updatedAt, - deviceTag: device.tag, - subspace: device.subspace, - ...tuyaDetails, - status: tuyaDeviceStatus.result[0].status, - }; - } catch (error) { - console.warn( - `Skipping device with deviceTuyaUuid: ${device.deviceTuyaUuid} due to error. ${error}`, - ); - return null; - } - }; - const detailedDevices = await Promise.all(space.devices.map(safeFetch)); + // Fetch space hierarchy **once** and reverse it to get an ordered hierarchy + const spaceHierarchy = + await this.validationService.getFullSpaceHierarchy(space); + const orderedHierarchy = spaceHierarchy.reverse(); + + // Fetch Tuya details for each device in parallel using Promise.allSettled + const deviceDetailsPromises = space.devices.map((device) => + this.fetchDeviceDetails(device, orderedHierarchy), + ); + + const detailedDevices = (await Promise.allSettled(deviceDetailsPromises)) + .filter((result) => result.status === 'fulfilled' && result.value) + .map((result) => (result as PromiseFulfilledResult).value); return new SuccessResponseDto({ - data: detailedDevices.filter(Boolean), - message: 'Successfully retrieved list of devices', + data: detailedDevices, + message: 'Successfully retrieved list of devices.', }); } catch (error) { console.error('Error listing devices in space:', error); @@ -76,6 +59,70 @@ export class SpaceDeviceService { } } + private async fetchDeviceDetails(device: any, orderedHierarchy: any[]) { + try { + // Fetch Tuya details in parallel + const [tuyaDetails, tuyaDeviceStatusResponse] = await Promise.all([ + this.getDeviceDetailsByDeviceIdTuya(device.deviceTuyaUuid), + this.tuyaService.getDevicesInstructionStatusTuya(device.deviceTuyaUuid), + ]); + + const tuyaStatusList = + tuyaDeviceStatusResponse?.result?.[0]?.status || []; + + return { + spaces: orderedHierarchy.map((space) => ({ + uuid: space.uuid, + spaceName: space.spaceName, + })), + uuid: device.uuid, + deviceTuyaUuid: device.deviceTuyaUuid, + productUuid: device.productDevice?.uuid || null, + productType: device.productDevice?.prodType || null, + isActive: device.isActive, + updatedAt: device.updatedAt, + deviceTag: device.tag, + subspace: device.subspace, + ...tuyaDetails, + ...(this.extractBatteryStatus( + device.productDevice?.prodType, + tuyaStatusList, + ) !== null && { + battery: this.extractBatteryStatus( + device.productDevice?.prodType, + tuyaStatusList, + ), + }), + status: tuyaStatusList, + }; + } catch (error) { + console.warn( + `Skipping device ${device.deviceTuyaUuid} due to error: ${error.message}`, + ); + return null; + } + } + + private extractBatteryStatus( + deviceType: string, + tuyaStatus: any[], + ): number | null { + const batteryCodes = { + [ProductType.DL]: BatteryStatus.RESIDUAL_ELECTRICITY, + [ProductType.DS]: BatteryStatus.BATTERY_PERCENTAGE, + [ProductType.WL]: BatteryStatus.BATTERY_PERCENTAGE, + [ProductType.SOS]: BatteryStatus.BATTERY_PERCENTAGE, + }; + + const batteryCode = batteryCodes[deviceType]; + if (!batteryCode) return null; + + const batteryStatus = tuyaStatus.find( + (status) => status.code === batteryCode, + ); + return batteryStatus ? batteryStatus.value : null; + } + private async getDeviceDetailsByDeviceIdTuya( deviceId: string, ): Promise { diff --git a/src/space/services/space-validation.service.ts b/src/space/services/space-validation.service.ts index 76750dd..9dc614b 100644 --- a/src/space/services/space-validation.service.ts +++ b/src/space/services/space-validation.service.ts @@ -104,6 +104,26 @@ export class ValidationService { return space; } + async fetchSpaceDevices(spaceUuid: string): Promise { + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, disabled: false }, + relations: [ + 'devices', + 'devices.productDevice', + 'devices.tag', + 'devices.subspace', + ], + }); + + if (!space) { + throw new HttpException( + `Space with UUID ${spaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + return space; + } + async validateSpaceModel(spaceModelUuid: string): Promise { const spaceModel = await this.spaceModelRepository.findOne({ where: { uuid: spaceModelUuid }, @@ -125,4 +145,81 @@ export class ValidationService { return spaceModel; } + + async getFullSpaceHierarchy( + space: SpaceEntity, + ): Promise<{ uuid: string; spaceName: string }[]> { + try { + // Fetch only the relevant spaces, starting with the target space + const targetSpace = await this.spaceRepository.findOne({ + where: { uuid: space.uuid }, + relations: ['parent', 'children'], + }); + + // Fetch only the ancestors of the target space + const ancestors = await this.fetchAncestors(targetSpace); + + // Optionally, fetch descendants if required + const descendants = await this.fetchDescendants(targetSpace); + + const fullHierarchy = [...ancestors, targetSpace, ...descendants].map( + (space) => ({ + uuid: space.uuid, + spaceName: space.spaceName, + }), + ); + + return fullHierarchy; + } catch (error) { + console.error('Error fetching space hierarchy:', error.message); + throw new HttpException( + 'Error fetching space hierarchy', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async fetchAncestors(space: SpaceEntity): Promise { + const ancestors: SpaceEntity[] = []; + + let currentSpace = space; + while (currentSpace && currentSpace.parent) { + // Fetch the parent space + const parent = await this.spaceRepository.findOne({ + where: { uuid: currentSpace.parent.uuid }, + relations: ['parent'], // To continue fetching upwards + }); + + if (parent) { + ancestors.push(parent); + currentSpace = parent; + } else { + currentSpace = null; + } + } + + // Return the ancestors in reverse order to have the root at the start + return ancestors.reverse(); + } + + private async fetchDescendants(space: SpaceEntity): Promise { + const descendants: SpaceEntity[] = []; + + // Fetch the immediate children of the current space + const children = await this.spaceRepository.find({ + where: { parent: { uuid: space.uuid } }, + relations: ['children'], // To continue fetching downwards + }); + + for (const child of children) { + // Add the child to the descendants list + descendants.push(child); + + // Recursively fetch the child's descendants + const childDescendants = await this.fetchDescendants(child); + descendants.push(...childDescendants); + } + + return descendants; + } }