import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository'; import { Injectable, HttpException, HttpStatus, NotFoundException, BadRequestException, forwardRef, Inject, } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { AddDeviceDto, AddSceneToFourSceneDeviceDto, UpdateDeviceDto, UpdateDeviceInSpaceDto, } from '../dtos/add.device.dto'; import { DeviceInstructionResponse, GetDeviceDetailsFunctionsInterface, GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, GetMacAddressInterface, GetPowerClampFunctionsStatusInterface, controlDeviceInterface, getDeviceLogsInterface, updateDeviceFirmwareInterface, } from '../interfaces/get.device.interface'; import { GetDeviceBySpaceUuidDto, GetDeviceLogsDto, } from '../dtos/get.device.dto'; import { BatchControlDevicesDto, BatchFactoryResetDevicesDto, BatchStatusDevicesDto, ControlDeviceDto, GetSceneFourSceneDeviceDto, } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { PermissionType } from '@app/common/constants/permission-type.enum'; import { In } from 'typeorm'; import { ProductType } from '@app/common/constants/product-type.enum'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { DeviceStatuses } from '@app/common/constants/device-status.enum'; import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; import { BatteryStatus } from '@app/common/constants/battery-status.enum'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { SceneService } from 'src/scene/services'; import { AddAutomationDto } from 'src/automation/dtos'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum'; @Injectable() export class DeviceService { private tuya: TuyaContext; constructor( private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, private readonly sceneDeviceRepository: SceneDeviceRepository, private readonly productRepository: ProductRepository, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly spaceRepository: SpaceRepository, @Inject(forwardRef(() => SceneService)) private readonly sceneService: SceneService, private readonly tuyaService: TuyaService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); this.tuya = new TuyaContext({ baseUrl: tuyaEuUrl, accessKey, secretKey, }); } async getDeviceByDeviceUuid( deviceUuid: string, withProductDevice: boolean = true, ) { const relations = ['subspace']; if (withProductDevice) { relations.push('productDevice'); } return this.deviceRepository.findOne({ where: { uuid: deviceUuid }, relations, }); } async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) { return await this.deviceRepository.findOne({ where: { deviceTuyaUuid, }, relations: ['productDevice'], }); } async addDeviceUser(addDeviceDto: AddDeviceDto) { try { const device = await this.getDeviceDetailsByDeviceIdTuya( addDeviceDto.deviceTuyaUuid, ); if (!device.productUuid) { throw new Error('Product UUID is missing for the device.'); } const deviceSaved = await this.deviceRepository.save({ deviceTuyaUuid: addDeviceDto.deviceTuyaUuid, productDevice: { uuid: device.productUuid }, user: { uuid: addDeviceDto.userUuid, }, }); if (deviceSaved.uuid) { const deviceStatus = await this.getDevicesInstructionStatus( deviceSaved.uuid, ); if (deviceStatus.productUuid) { await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ deviceUuid: deviceSaved.uuid, deviceTuyaUuid: addDeviceDto.deviceTuyaUuid, status: deviceStatus.status, productUuid: deviceStatus.productUuid, productType: deviceStatus.productType, }); } } return deviceSaved; } catch (error) { if (error.code === CommonErrorCodes.DUPLICATE_ENTITY) { throw new HttpException( 'Device already exists', HttpStatus.BAD_REQUEST, ); } else { throw new HttpException( error.message || 'Failed to add device in space', error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async getDevicesByUser( userUuid: string, ): Promise { try { const devices = await this.deviceRepository.find({ where: { user: { uuid: userUuid }, isActive: true, permission: { userUuid, permissionType: { type: In([PermissionType.READ, PermissionType.CONTROLLABLE]), }, }, }, relations: [ 'spaceDevice', 'productDevice', 'permission', 'permission.permissionType', ], }); const devicesData = await Promise.all( devices.map(async (device) => { return { haveRoom: device.spaceDevice ? true : false, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, permissionType: device.permission[0].permissionType.type, ...(await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, )), uuid: device.uuid, } as GetDeviceDetailsInterface; }), ); return devicesData; } catch (error) { // Handle the error here throw new HttpException( 'User does not have any devices', HttpStatus.NOT_FOUND, ); } } async getDevicesBySpaceId( getDeviceBySpaceUuidDto: GetDeviceBySpaceUuidDto, userUuid: string, ): Promise { try { const devices = await this.deviceRepository.find({ where: { spaceDevice: { uuid: getDeviceBySpaceUuidDto.spaceUuid }, isActive: true, permission: { userUuid, permissionType: { type: In([PermissionType.READ, PermissionType.CONTROLLABLE]), }, }, }, relations: [ 'spaceDevice', 'productDevice', 'permission', 'permission.permissionType', ], }); const devicesData = await Promise.all( devices.map(async (device) => { return { haveRoom: device.spaceDevice ? true : false, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, permissionType: device.permission[0].permissionType.type, ...(await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, )), uuid: device.uuid, } as GetDeviceDetailsInterface; }), ); return devicesData; } catch (error) { // Handle the error here throw new HttpException( 'Error fetching devices by space', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async updateDeviceInSpace(updateDeviceInSpaceDto: UpdateDeviceInSpaceDto) { try { await this.deviceRepository.update( { uuid: updateDeviceInSpaceDto.deviceUuid }, { spaceDevice: { uuid: updateDeviceInSpaceDto.spaceUuid }, }, ); const device = await this.deviceRepository.findOne({ where: { uuid: updateDeviceInSpaceDto.deviceUuid, }, relations: ['spaceDevice', 'spaceDevice.parent'], }); if (device.spaceDevice.parent.spaceTuyaUuid) { await this.transferDeviceInSpacesTuya( device.deviceTuyaUuid, device.spaceDevice.parent.spaceTuyaUuid, ); } return { uuid: device.uuid, spaceUuid: device.spaceDevice.uuid, }; } catch (error) { throw new HttpException( 'Failed to add device in space', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async transferDeviceInSpacesTuya( deviceId: string, spaceId: string, ): Promise { try { const path = `/v2.0/cloud/thing/${deviceId}/transfer`; const response = await this.tuya.request({ method: 'POST', path, body: { space_id: spaceId }, }); return response as controlDeviceInterface; } catch (error) { throw new HttpException( 'Error transferring device in spaces from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async updateDeviceNameTuya( deviceId: string, deviceName: string, ): Promise { try { const path = `/v2.0/cloud/thing/${deviceId}/attribute`; const response = await this.tuya.request({ method: 'POST', path, body: { type: 1, data: deviceName }, }); return response as controlDeviceInterface; } catch (error) { throw new HttpException( 'Error updating device name from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false); if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { throw new NotFoundException('Device Not Found'); } const response = await this.controlDeviceTuya( deviceDetails.deviceTuyaUuid, controlDeviceDto, ); if (response.success) { return response; } else { throw new HttpException( response.msg || 'Unknown error', HttpStatus.BAD_REQUEST, ); } } catch (error) { throw new HttpException( error.message || 'Device Not Found', error.status || HttpStatus.NOT_FOUND, ); } } async factoryResetDeviceTuya( deviceUuid: string, ): Promise { try { const path = `/v2.0/cloud/thing/${deviceUuid}/reset`; const response = await this.tuya.request({ method: 'POST', path, }); return response as controlDeviceInterface; } catch (error) { throw new HttpException( 'Error factory resetting device from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async controlDeviceTuya( deviceUuid: string, controlDeviceDto: ControlDeviceDto, ): Promise { try { const path = `/v1.0/iot-03/devices/${deviceUuid}/commands`; const response = await this.tuya.request({ method: 'POST', path, body: { commands: [ { code: controlDeviceDto.code, value: controlDeviceDto.value }, ], }, }); return response as controlDeviceInterface; } catch (error) { throw new HttpException( 'Error control device from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async batchControlDevices(batchControlDevicesDto: BatchControlDevicesDto) { const { devicesUuid } = batchControlDevicesDto; try { // Check if all devices have the same product UUID await this.checkAllDevicesHaveSameProductUuid(devicesUuid); // Perform all operations concurrently const results = await Promise.allSettled( devicesUuid.map(async (deviceUuid) => { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); const result = await this.controlDeviceTuya( deviceDetails.deviceTuyaUuid, batchControlDevicesDto, ); return { deviceUuid, result }; }), ); // Separate successful and failed operations const successResults = []; const failedResults = []; for (const result of results) { if (result.status === DeviceStatuses.FULLFILLED) { const { deviceUuid, result: operationResult } = result.value; if (operationResult.success) { // Add to success results if operationResult.success is true successResults.push({ deviceUuid, result: operationResult }); } else { // Add to failed results if operationResult.success is false failedResults.push({ deviceUuid, error: operationResult.msg }); } } else { // Add to failed results if promise is rejected failedResults.push({ deviceUuid: devicesUuid[results.indexOf(result)], error: result.reason.message, }); } } return { successResults, failedResults }; } catch (error) { throw new HttpException( error.message || 'Device Not Found', error.status || HttpStatus.NOT_FOUND, ); } } async batchStatusDevices(batchStatusDevicesDto: BatchStatusDevicesDto) { const { devicesUuid } = batchStatusDevicesDto; const devicesUuidArray = devicesUuid.split(','); try { await this.checkAllDevicesHaveSameProductUuid(devicesUuidArray); const statuses = await Promise.all( devicesUuidArray.map(async (deviceUuid) => { const result = await this.getDevicesInstructionStatus(deviceUuid); return { deviceUuid, result }; }), ); return { status: statuses[0].result, devices: statuses, }; } catch (error) { throw new HttpException( error.message || 'Device Not Found', error.status || HttpStatus.NOT_FOUND, ); } } async checkAllDevicesHaveSameProductUuid(deviceUuids: string[]) { const firstDevice = await this.deviceRepository.findOne({ where: { uuid: deviceUuids[0], isActive: true }, relations: ['productDevice'], }); if (!firstDevice) { throw new BadRequestException('First device not found'); } const firstProductType = firstDevice.productDevice.prodType; for (let i = 1; i < deviceUuids.length; i++) { const device = await this.deviceRepository.findOne({ where: { uuid: deviceUuids[i], isActive: true }, relations: ['productDevice'], }); if (!device) { throw new BadRequestException(`Device ${deviceUuids[i]} not found`); } if (device.productDevice.prodType !== firstProductType) { throw new BadRequestException(`Devices have different product types`); } } } async batchFactoryResetDevices( batchFactoryResetDevicesDto: BatchFactoryResetDevicesDto, ) { const { devicesUuid } = batchFactoryResetDevicesDto; try { // Check if all devices have the same product UUID await this.checkAllDevicesHaveSameProductUuid(devicesUuid); // Perform all operations concurrently const results = await Promise.allSettled( devicesUuid.map(async (deviceUuid) => { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); const result = await this.factoryResetDeviceTuya( deviceDetails.deviceTuyaUuid, ); return { deviceUuid, result }; }), ); // Separate successful and failed operations const successResults = []; const failedResults = []; for (const result of results) { if (result.status === DeviceStatuses.FULLFILLED) { const { deviceUuid, result: operationResult } = result.value; if (operationResult.success) { // Add to success results if operationResult.success is true successResults.push({ deviceUuid, result: operationResult }); // Update isActive to false in the repository for the successfully reset device await this.deviceRepository.update( { uuid: deviceUuid }, { isActive: false }, ); } else { // Add to failed results if operationResult.success is false failedResults.push({ deviceUuid, error: operationResult.msg }); } } else { // Add to failed results if promise is rejected failedResults.push({ deviceUuid: devicesUuid[results.indexOf(result)], error: result.reason.message, }); } } return { successResults, failedResults }; } catch (error) { throw new HttpException( error.message || 'Device Not Found', error.status || HttpStatus.NOT_FOUND, ); } } async getDeviceDetailsByDeviceId(deviceUuid: string, userUuid: string) { try { const userDevicePermission = await this.getUserDevicePermission( userUuid, deviceUuid, ); const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); } const response = await this.getDeviceDetailsByDeviceIdTuya( deviceDetails.deviceTuyaUuid, ); const macAddress = await this.getMacAddressByDeviceIdTuya( deviceDetails.deviceTuyaUuid, ); return { ...response, uuid: deviceDetails.uuid, productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, permissionType: userDevicePermission, macAddress: macAddress.mac, subspace: deviceDetails.subspace ? deviceDetails.subspace : {}, }; } catch (error) { throw new HttpException( error.message || 'Device Not Found', HttpStatus.NOT_FOUND, ); } } async updateDevice(deviceUuid: string, updateDeviceDto: UpdateDeviceDto) { try { const device = await this.getDeviceByDeviceUuid(deviceUuid); if (device.deviceTuyaUuid) { await this.updateDeviceNameTuya( device.deviceTuyaUuid, updateDeviceDto.deviceName, ); } return { uuid: device.uuid, deviceName: updateDeviceDto.deviceName, }; } catch (error) { throw new HttpException( 'Error updating device', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getDeviceDetailsByDeviceIdTuya( deviceId: string, ): Promise { try { const path = `/v1.1/iot-03/devices/${deviceId}`; const response = await this.tuya.request({ method: 'GET', path, }); // Convert keys to camel case const camelCaseResponse = convertKeysToCamelCase(response); const product = await this.productRepository.findOne({ where: { prodId: camelCaseResponse.result.productId, }, }); // eslint-disable-next-line @typescript-eslint/no-unused-vars const { productId, id, ...rest } = camelCaseResponse.result; return { ...rest, productUuid: product.uuid, } as GetDeviceDetailsInterface; } catch (error) { throw new HttpException( 'Error fetching device details from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getMacAddressByDeviceIdTuya( deviceId: string, ): Promise { try { const path = `/v1.0/devices/factory-infos?device_ids=${deviceId}`; const response = await this.tuya.request({ method: 'GET', path, }); return response.result[0]; } catch (error) { throw new HttpException( 'Error fetching mac address device from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getDeviceInstructionByDeviceId( deviceUuid: string, ): Promise { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); } try { const response = await this.getDeviceInstructionByDeviceIdTuya( deviceDetails.deviceTuyaUuid, ); return { productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, functions: response.result.functions.map((fun: any) => { return { code: fun.code, values: fun.values, dataType: fun.type, }; }), }; } catch (error) { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } async getDeviceInstructionByDeviceIdTuya( deviceId: string, ): Promise { try { const path = `/v1.0/iot-03/devices/${deviceId}/functions`; const response = await this.tuya.request({ method: 'GET', path, }); return response as GetDeviceDetailsFunctionsInterface; } catch (error) { throw new HttpException( 'Error fetching device functions from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getDevicesInstructionStatus(deviceUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); } const deviceStatus = await this.getDevicesInstructionStatusTuya( deviceDetails.deviceTuyaUuid, ); return { productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, status: deviceStatus.result[0].status, }; } catch (error) { throw new HttpException( 'Error fetching device functions status', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getDevicesInstructionStatusTuya( deviceUuid: string, ): Promise { try { const path = `/v1.0/iot-03/devices/status`; const response = await this.tuya.request({ method: 'GET', path, query: { device_ids: deviceUuid, }, }); return response as GetDeviceDetailsFunctionsStatusInterface; } catch (error) { throw new HttpException( 'Error fetching device functions status from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } private async getUserDevicePermission(userUuid: string, deviceUuid: string) { const device = await this.deviceRepository.findOne({ where: { uuid: deviceUuid, permission: { userUuid: userUuid, }, }, relations: ['permission', 'permission.permissionType'], }); return device.permission[0].permissionType.type; } async getDevicesInGateway(gatewayUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(gatewayUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); } else if (deviceDetails.productDevice.prodType !== ProductType.GW) { throw new BadRequestException('This is not a gateway device'); } const response = await this.getDevicesInGatewayTuya( deviceDetails.deviceTuyaUuid, ); const devices = await Promise.all( response.map(async (device: any) => { try { const deviceDetails = await this.getDeviceByDeviceTuyaUuid( device.id, ); if (deviceDetails.deviceTuyaUuid) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { id, ...rest } = device; return { ...rest, tuyaUuid: deviceDetails.deviceTuyaUuid, uuid: deviceDetails.uuid, productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, }; } return null; } catch (error) { return null; } }), ); return { uuid: deviceDetails.uuid, productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, devices: devices.filter((device) => device !== null), }; } catch (error) { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } async getDevicesInGatewayTuya( deviceId: string, ): Promise { try { const path = `/v1.0/devices/${deviceId}/sub-devices`; const response: any = await this.tuya.request({ method: 'GET', path, }); const camelCaseResponse = response.result.map((device: any) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { product_id, category, ...rest } = device; const camelCaseDevice = convertKeysToCamelCase({ ...rest }); return camelCaseDevice as GetDeviceDetailsInterface[]; }); return camelCaseResponse; } catch (error) { throw new HttpException( 'Error fetching device details from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async updateDeviceFirmware(deviceUuid: string, firmwareVersion: number) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false); if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { throw new NotFoundException('Device Not Found'); } const response = await this.updateDeviceFirmwareTuya( deviceDetails.deviceTuyaUuid, firmwareVersion, ); if (response.success) { return response; } else { throw new HttpException( response.msg || 'Unknown error', HttpStatus.BAD_REQUEST, ); } } catch (error) { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } async updateDeviceFirmwareTuya( deviceUuid: string, firmwareVersion: number, ): Promise { try { const path = `/v2.0/cloud/thing/${deviceUuid}/firmware/${firmwareVersion}`; const response = await this.tuya.request({ method: 'POST', path, }); return response as updateDeviceFirmwareInterface; } catch (error) { throw new HttpException( 'Error updating device firmware from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getDevicesBySpaceUuid(SpaceUuid: string) { try { const spaces = await this.spaceRepository.find({ where: { parent: { uuid: SpaceUuid, }, devices: { isActive: true, }, }, relations: ['devices', 'devices.productDevice'], }); const devices = spaces.flatMap((space) => { return space.devices.map((device) => device); }); const devicesData = await Promise.all( devices.map(async (device) => { return { haveRoom: true, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, permissionType: PermissionType.CONTROLLABLE, ...(await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, )), uuid: device.uuid, } as GetDeviceDetailsInterface; }), ); return devicesData; } catch (error) { throw new HttpException( 'This space does not have any devices', HttpStatus.NOT_FOUND, ); } } async getAllDevices(): Promise { try { const devices = await this.deviceRepository.find({ where: { isActive: true }, relations: [ 'spaceDevice.parent', 'spaceDevice.community', 'productDevice', 'permission', 'permission.permissionType', ], }); const devicesData = await Promise.allSettled( devices.map(async (device) => { let battery = null; // Check if the device is a door lock (DL) if (device.productDevice.prodType === ProductType.DL) { const doorLockInstructionsStatus = await this.getDevicesInstructionStatus(device.uuid); const batteryStatus: any = doorLockInstructionsStatus.status.find( (status: any) => status.code === BatteryStatus.RESIDUAL_ELECTRICITY, ); if (batteryStatus) { battery = batteryStatus.value; } } // Check if the device is a door sensor (DS) if (device.productDevice.prodType === ProductType.DS) { const doorSensorInstructionsStatus = await this.getDevicesInstructionStatus(device.uuid); const batteryStatus: any = doorSensorInstructionsStatus.status.find( (status: any) => status.code === BatteryStatus.BATTERY_PERCENTAGE, ); if (batteryStatus) { battery = batteryStatus.value; } } // Check if the device is a water leak sensor (WL) if (device.productDevice.prodType === ProductType.WL) { const doorSensorInstructionsStatus = await this.getDevicesInstructionStatus(device.uuid); const batteryStatus: any = doorSensorInstructionsStatus.status.find( (status: any) => status.code === BatteryStatus.BATTERY_PERCENTAGE, ); if (batteryStatus) { battery = batteryStatus.value; } } const spaceHierarchy = await this.getFullSpaceHierarchy( device?.spaceDevice, ); const orderedHierarchy = spaceHierarchy.reverse(); return { spaces: orderedHierarchy.map((space) => ({ uuid: space.uuid, spaceName: space.spaceName, })), productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, community: { uuid: device.spaceDevice.community.uuid, name: device.spaceDevice.community.name, }, // permissionType: device.permission[0].permissionType.type, ...(await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, )), uuid: device.uuid, ...(battery && { battery }), } as GetDeviceDetailsInterface; }), ); // Filter out rejected promises and extract the fulfilled values const fulfilledDevices = devicesData .filter((result) => result.status === DeviceStatuses.FULLFILLED) .map( (result) => (result as PromiseFulfilledResult).value, ); return fulfilledDevices; } catch (error) { throw new HttpException( error.message || 'Internal server error', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getDeviceLogs(deviceUuid: string, query: GetDeviceLogsDto) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); } const response = await this.getDeviceLogsTuya( deviceDetails.deviceTuyaUuid, query.code, query.startTime, query.endTime, ); return { deviceUuid, ...response, }; } catch (error) { throw new HttpException( error.message || 'Device Not Found', HttpStatus.NOT_FOUND, ); } } async getDeviceLogsTuya( deviceId: string, code: string, startTime: string = (Date.now() - 1 * 60 * 60 * 1000).toString(), endTime: string = Date.now().toString(), ): Promise { try { const path = `/v2.0/cloud/thing/${deviceId}/report-logs?start_time=${startTime}&end_time=${endTime}&codes=${code}&size=50`; const response = await this.tuya.request({ method: 'GET', path, }); // Convert keys to camel case const camelCaseResponse = convertKeysToCamelCase(response); const logs = camelCaseResponse.result.logs ?? []; return { startTime, endTime, data: logs, } as getDeviceLogsInterface; } catch (error) { throw new HttpException( 'Error fetching device logs from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } 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, ); } } async getPowerClampInstructionStatus(powerClampUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(powerClampUuid); if (!deviceDetails) { throw new NotFoundException('Device Not Found'); } else if (deviceDetails.productDevice.prodType !== ProductType.PC) { throw new BadRequestException('This is not a power clamp device'); } const deviceStatus = await this.getPowerClampInstructionStatusTuya( deviceDetails.deviceTuyaUuid, ); const statusList = deviceStatus.result.properties as { code: string; value: any; }[]; const groupedStatus = statusList.reduce( (acc, currentStatus) => { const { code } = currentStatus; if (code.endsWith('A')) { acc.phaseA.push(currentStatus); } else if (code.endsWith('B')) { acc.phaseB.push(currentStatus); } else if (code.endsWith('C')) { acc.phaseC.push(currentStatus); } else { acc.general.push(currentStatus); } return acc; }, { phaseA: [] as { code: string; value: any }[], phaseB: [] as { code: string; value: any }[], phaseC: [] as { code: string; value: any }[], general: [] as { code: string; value: any }[], }, ); return { productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, status: { phaseA: groupedStatus.phaseA, phaseB: groupedStatus.phaseB, phaseC: groupedStatus.phaseC, general: groupedStatus.general, }, }; } catch (error) { throw new HttpException( error.message || 'Error fetching power clamp functions status', error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getPowerClampInstructionStatusTuya( deviceUuid: string, ): Promise { try { const path = `/v2.0/cloud/thing/${deviceUuid}/shadow/properties`; const response = await this.tuya.request({ method: 'GET', path, query: { device_ids: deviceUuid, }, }); const camelCaseResponse = convertKeysToCamelCase(response); return camelCaseResponse as GetPowerClampFunctionsStatusInterface; } catch (error) { throw new HttpException( 'Error fetching power clamp functions status from Tuya', 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; } async addSceneToSceneDevice( deviceUuid: string, addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, ) { try { const { spaceUuid, sceneUuid, switchName } = addSceneToFourSceneDeviceDto; if (!spaceUuid || !sceneUuid || !switchName) { throw new BadRequestException('Missing required fields in DTO'); } const [sceneData, spaceData, deviceData] = await Promise.all([ this.sceneService.findScene(sceneUuid), this.sceneService.getSpaceByUuid(spaceUuid), this.getDeviceByDeviceUuid(deviceUuid), ]); const shortUuid = deviceUuid.slice(0, 6); // First 6 characters of the UUID const timestamp = Math.floor(Date.now() / 1000); // Current timestamp in seconds const automationName = `Auto_${shortUuid}_${timestamp}`; const addAutomationData: AddAutomationDto = { spaceUuid: spaceData.spaceTuyaUuid, automationName, decisionExpr: AUTOMATION_CONFIG.DECISION_EXPR, effectiveTime: { start: AUTOMATION_CONFIG.DEFAULT_START_TIME, end: AUTOMATION_CONFIG.DEFAULT_END_TIME, loops: AUTOMATION_CONFIG.DEFAULT_LOOPS, }, conditions: [ { code: 1, entityId: deviceData.deviceTuyaUuid, entityType: AUTOMATION_CONFIG.CONDITION_TYPE, expr: { comparator: AUTOMATION_CONFIG.COMPARATOR, statusCode: switchName, statusValue: AUTOMATION_CONFIG.SCENE_STATUS_VALUE, }, }, ], actions: [ { actionExecutor: AUTOMATION_CONFIG.ACTION_EXECUTOR, entityId: sceneData.sceneTuyaUuid, }, ], }; const automation = await this.tuyaService.createAutomation( addAutomationData.spaceUuid, addAutomationData.automationName, addAutomationData.effectiveTime, addAutomationData.decisionExpr, addAutomationData.conditions, addAutomationData.actions, ); if (automation.success) { const existingSceneDevice = await this.sceneDeviceRepository.findOne({ where: { device: { uuid: deviceUuid }, switchName: switchName, }, relations: ['scene', 'device'], }); if (existingSceneDevice) { await this.tuyaService.deleteAutomation( spaceData.spaceTuyaUuid, existingSceneDevice.automationTuyaUuid, ); existingSceneDevice.automationTuyaUuid = automation.result.id; existingSceneDevice.scene = sceneData; existingSceneDevice.device = deviceData; existingSceneDevice.switchName = switchName; return await this.sceneDeviceRepository.save(existingSceneDevice); } else { const sceneDevice = await this.sceneDeviceRepository.save({ scene: sceneData, device: deviceData, automationTuyaUuid: automation.result.id, switchName: switchName, }); return sceneDevice; } } } catch (err) { const errorMessage = err.message || 'Error creating automation'; const errorStatus = err.status || HttpStatus.INTERNAL_SERVER_ERROR; throw new HttpException(errorMessage, errorStatus); } } async getScenesBySceneDevice( deviceUuid: string, getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, ): Promise { try { if (getSceneFourSceneDeviceDto.switchName) { // Query for a single record directly when switchName is provided const sceneDevice = await this.sceneDeviceRepository.findOne({ where: { device: { uuid: deviceUuid }, switchName: getSceneFourSceneDeviceDto.switchName as SceneSwitchesTypeEnum, }, relations: ['device', 'scene'], }); if (!sceneDevice) { throw new HttpException( `No scene found for device with UUID ${deviceUuid} and switch name ${getSceneFourSceneDeviceDto.switchName}`, HttpStatus.NOT_FOUND, ); } const sceneDetails = await this.sceneService.getSceneByUuid( sceneDevice.scene.uuid, ); return { switchName: sceneDevice.switchName, createdAt: sceneDevice.createdAt, updatedAt: sceneDevice.updatedAt, deviceUuid: sceneDevice.device.uuid, scene: sceneDetails.data, }; } // Query for multiple records if switchName is not provided const sceneDevices = await this.sceneDeviceRepository.find({ where: { device: { uuid: deviceUuid } }, relations: ['device', 'scene'], }); if (!sceneDevices.length) { throw new HttpException( `No scenes found for device with UUID ${deviceUuid}`, HttpStatus.NOT_FOUND, ); } const results = await Promise.all( sceneDevices.map(async (sceneDevice) => { const sceneDetails = await this.sceneService.getSceneByUuid( sceneDevice.scene.uuid, ); return { switchName: sceneDevice.switchName, createdAt: sceneDevice.createdAt, updatedAt: sceneDevice.updatedAt, deviceUuid: sceneDevice.device.uuid, scene: sceneDetails.data, }; }), ); return results; } catch (error) { throw new HttpException( error.message || 'Failed to fetch scenes for device', error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } }