import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface'; import { ValidationService } from '../space-validation.service'; import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; import { In, QueryRunner } from 'typeorm'; import { DeviceEntity } from '@app/common/modules/device/entities'; import { TagRepository } from '@app/common/modules/space'; @Injectable() export class SubspaceDeviceService { constructor( private readonly subspaceRepository: SubspaceRepository, private readonly deviceRepository: DeviceRepository, private readonly tuyaService: TuyaService, private readonly productRepository: ProductRepository, private readonly validationService: ValidationService, private readonly tagRepository: TagRepository, ) {} async listDevicesInSubspace( params: GetSubSpaceParam, ): Promise { const { subSpaceUuid, spaceUuid, communityUuid, projectUuid } = params; await this.validationService.checkCommunityAndProjectSpaceExistence( communityUuid, projectUuid, spaceUuid, ); const subspace = await this.findSubspaceWithDevices(subSpaceUuid); const safeFetch = async (device: any) => { try { const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, ); return { uuid: device.uuid, deviceTuyaUuid: device.deviceTuyaUuid, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, isActive: device.isActive, createdAt: device.createdAt, updatedAt: device.updatedAt, ...tuyaDetails, }; } catch (error) { return null; } }; const detailedDevices = await Promise.all(subspace.devices.map(safeFetch)); return new SuccessResponseDto({ data: detailedDevices.filter(Boolean), // Remove nulls message: 'Successfully retrieved list of devices', }); } async associateDeviceToSubspace( params: DeviceSubSpaceParam, ): Promise { const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid, projectUuid } = params; try { await this.validationService.checkCommunityAndProjectSpaceExistence( communityUuid, projectUuid, spaceUuid, ); const subspace = await this.findSubspace(subSpaceUuid); const device = await this.findDevice(deviceUuid); device.subspace = subspace; const newDevice = await this.deviceRepository.save(device); return new SuccessResponseDto({ data: newDevice, message: `Successfully associated device to subspace`, }); } catch (error) { if (error instanceof HttpException) { throw error; } else { throw new HttpException( `Failed to associate device to subspace with error = ${error}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async disassociateDeviceFromSubspace( params: DeviceSubSpaceParam, ): Promise { const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid, projectUuid } = params; try { await this.validationService.checkCommunityAndProjectSpaceExistence( communityUuid, projectUuid, spaceUuid, ); const subspace = await this.findSubspace(subSpaceUuid); const device = await this.findDeviceWithSubspaceAndTag(deviceUuid); if (!device.subspace || device.subspace.uuid !== subspace.uuid) { throw new HttpException( `Device ${deviceUuid} is not associated with the specified subspace ${subSpaceUuid} `, HttpStatus.BAD_REQUEST, ); } device.subspace = null; const updatedDevice = await this.deviceRepository.save(device); return new SuccessResponseDto({ data: updatedDevice, message: 'Successfully dissociated device from subspace', }); } catch (error) { if (error instanceof HttpException) { throw error; } else { throw new HttpException( `Failed to dissociate device from subspace error = ${error}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } } // Helper method to find subspace with devices relation private async findSubspaceWithDevices(subSpaceUuid: string) { const subspace = await this.subspaceRepository.findOne({ where: { uuid: subSpaceUuid }, relations: ['devices', 'devices.productDevice'], }); if (!subspace) { this.throwNotFound('Subspace', subSpaceUuid); } return subspace; } private async findSubspace(subSpaceUuid: string) { const subspace = await this.subspaceRepository.findOne({ where: { uuid: subSpaceUuid }, }); if (!subspace) { this.throwNotFound('Subspace', subSpaceUuid); } return subspace; } private async findDevice(deviceUuid: string) { const device = await this.deviceRepository.findOne({ where: { uuid: deviceUuid }, relations: [ 'subspace', 'tag', 'tag.space', 'tag.subspace', 'spaceDevice', ], }); if (!device) { this.throwNotFound('Device', deviceUuid); } return device; } async deleteSubspaceDevices( devices: DeviceEntity[], queryRunner: QueryRunner, ): Promise { const deviceUuids = devices.map((device) => device.uuid); try { if (deviceUuids.length === 0) { return; } await queryRunner.manager.update( this.deviceRepository.target, { uuid: In(deviceUuids) }, { subspace: null }, ); } catch (error) { throw new HttpException( `Failed to delete devices with IDs ${deviceUuids.join(', ')}: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } private throwNotFound(entity: string, uuid: string) { throw new HttpException( `${entity} with ID ${uuid} not found`, HttpStatus.NOT_FOUND, ); } private async getDeviceDetailsByDeviceIdTuya( deviceId: string, ): Promise { try { const tuyaDeviceDetails = await this.tuyaService.getDeviceDetails(deviceId); const camelCaseResponse = convertKeysToCamelCase(tuyaDeviceDetails); const product = await this.productRepository.findOne({ where: { prodId: camelCaseResponse.productId, }, }); // eslint-disable-next-line @typescript-eslint/no-unused-vars const { uuid, ...rest } = camelCaseResponse; return { ...rest, productUuid: product?.uuid, } as GetDeviceDetailsInterface; } catch (error) { throw new HttpException( `Error fetching device details from Tuya for device uuid ${deviceId}.`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } async findNextTag(): Promise { const tags = await this.tagRepository.find({ select: ['tag'] }); const tagNumbers = tags .map((t) => t.tag.match(/^Tag (\d+)$/)) .filter((match) => match) .map((match) => parseInt(match[1])) .sort((a, b) => a - b); const nextTagNumber = tagNumbers.length ? tagNumbers[tagNumbers.length - 1] + 1 : 1; return nextTagNumber; } private async findDeviceWithSubspaceAndTag(deviceUuid: string) { return await this.deviceRepository.findOne({ where: { uuid: deviceUuid }, relations: ['subspace', 'tag', 'spaceDevice'], select: ['uuid', 'subspace', 'spaceDevice', 'productDevice', 'tag'], }); } }