diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 463d524..6e0b948 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -641,6 +641,11 @@ export class ControllerRoute { 'Delete scenes by device uuid and switch name'; public static readonly DELETE_SCENES_BY_SWITCH_NAME_DESCRIPTION = 'This endpoint deletes all scenes associated with a specific switch device.'; + + public static readonly POPULATE_TUYA_CONST_UUID_SUMMARY = + 'Populate Tuya const UUID'; + public static readonly POPULATE_TUYA_CONST_UUID_DESCRIPTION = + 'This endpoint populates the Tuya const UUID for all devices.'; }; }; static DEVICE_COMMISSION = class { diff --git a/libs/common/src/integrations/tuya/services/tuya.service.ts b/libs/common/src/integrations/tuya/services/tuya.service.ts index 67a4eff..19dc6d8 100644 --- a/libs/common/src/integrations/tuya/services/tuya.service.ts +++ b/libs/common/src/integrations/tuya/services/tuya.service.ts @@ -49,12 +49,12 @@ export class TuyaService { path, }); - if (!response.success) { - throw new HttpException( - `Error fetching device details: ${response.msg}`, - HttpStatus.BAD_REQUEST, - ); - } + // if (!response.success) { + // throw new HttpException( + // `Error fetching device details: ${response.msg}`, + // HttpStatus.BAD_REQUEST, + // ); + // } return response.result; } diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 33f13d4..2b4db9a 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -28,6 +28,11 @@ export class DeviceEntity extends AbstractEntity { }) deviceTuyaUuid: string; + @Column({ + nullable: true, + }) + deviceTuyaConstUuid: string; + @Column({ nullable: true, default: true, diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index ffc073a..703d0fc 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -1,39 +1,41 @@ -import { DeviceService } from '../services/device.service'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { RoleType } from '@app/common/constants/role.type.enum'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { Body, Controller, - Get, - Post, - Query, - Param, - UseGuards, - Put, Delete, + Get, + Param, + Post, + Put, + Query, Req, + UnauthorizedException, + UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Permissions } from 'src/decorators/permissions.decorator'; +import { PermissionsGuard } from 'src/guards/permissions.guard'; +import { CheckRoomGuard } from 'src/guards/room.guard'; +import { CheckFourAndSixSceneDeviceTypeGuard } from 'src/guards/scene.device.type.guard'; import { AddDeviceDto, AddSceneToFourSceneDeviceDto, AssignDeviceToSpaceDto, UpdateDeviceDto, } from '../dtos/add.device.dto'; -import { GetDeviceLogsDto } from '../dtos/get.device.dto'; import { - ControlDeviceDto, BatchControlDevicesDto, BatchStatusDevicesDto, + ControlDeviceDto, GetSceneFourSceneDeviceDto, } from '../dtos/control.device.dto'; -import { CheckRoomGuard } from 'src/guards/room.guard'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { CheckFourAndSixSceneDeviceTypeGuard } from 'src/guards/scene.device.type.guard'; -import { ControllerRoute } from '@app/common/constants/controller-route'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { DeviceSceneParamDto } from '../dtos/device.param.dto'; import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto'; -import { PermissionsGuard } from 'src/guards/permissions.guard'; -import { Permissions } from 'src/decorators/permissions.decorator'; +import { DeviceSceneParamDto } from '../dtos/device.param.dto'; +import { GetDeviceLogsDto } from '../dtos/get.device.dto'; +import { DeviceService } from '../services/device.service'; @ApiTags('Device Module') @Controller({ @@ -340,4 +342,22 @@ export class DeviceController { projectUuid, ); } + + @ApiBearerAuth() + @UseGuards(PermissionsGuard) + @Permissions('DEVICE_UPDATE') + @Post('/populate-tuya-const-uuids') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.POPULATE_TUYA_CONST_UUID_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.POPULATE_TUYA_CONST_UUID_DESCRIPTION, + }) + async populateTuyaConstUuid(@Req() req: any): Promise { + const userUuid = req['user']?.userUuid; + const userRole = req['user']?.role; + if (!userUuid || (userRole && userRole !== RoleType.SUPER_ADMIN)) { + throw new UnauthorizedException('Unauthorized to perform this action'); + } + return this.deviceService.addTuyaConstUuidToDevices(); + } } diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index a73664b..e4c0da5 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -98,875 +98,6 @@ export class DeviceService { }); } - async getDeviceByDeviceUuid( - deviceUuid: string, - withProductDevice: boolean = true, - projectUuid: string, - ) { - const relations = ['subspace']; - - if (withProductDevice) { - relations.push('productDevice'); - } - - return this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - spaceDevice: { - community: { project: { uuid: projectUuid } }, - spaceName: Not(ORPHAN_SPACE_NAME), - }, - }, - relations, - }); - } - async deleteDevice( - devices: DeviceEntity[], - orphanSpace: SpaceEntity, - queryRunner: QueryRunner, - ): Promise { - try { - const deviceIds = devices.map((device) => device.uuid); - - if (deviceIds.length > 0) { - await queryRunner.manager - .createQueryBuilder() - .update(DeviceEntity) - .set({ spaceDevice: orphanSpace }) - .whereInIds(deviceIds) - .execute(); - } - } catch (error) { - throw new HttpException( - `Failed to update devices to orphan space: ${error.message}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) { - return await this.deviceRepository.findOne({ - where: { - deviceTuyaUuid, - }, - relations: ['productDevice'], - }); - } - - async addNewDevice(addDeviceDto: AddDeviceDto, projectUuid: string) { - try { - const device = await this.getDeviceDetailsByDeviceIdTuya( - addDeviceDto.deviceTuyaUuid, - ); - - if (!device.productUuid) { - throw new Error('Product UUID is missing for the device.'); - } - const existingConflictingDevice = await this.deviceRepository.exists({ - where: { - spaceDevice: { uuid: addDeviceDto.spaceUuid }, - productDevice: { uuid: device.productUuid }, - tag: { uuid: addDeviceDto.tagUuid }, - }, - }); - if (existingConflictingDevice) { - throw new HttpException( - 'Device with the same product type and tag already exists in this space', - HttpStatus.BAD_REQUEST, - ); - } - const deviceSaved = await this.deviceRepository.save({ - deviceTuyaUuid: addDeviceDto.deviceTuyaUuid, - productDevice: { uuid: device.productUuid }, - spaceDevice: { uuid: addDeviceDto.spaceUuid }, - tag: { uuid: addDeviceDto.tagUuid }, - name: addDeviceDto.deviceName, - }); - if (deviceSaved.uuid) { - const deviceStatus: BaseResponseDto = - await this.getDevicesInstructionStatus(deviceSaved.uuid, projectUuid); - if (deviceStatus.data.productUuid) { - await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ - deviceUuid: deviceSaved.uuid, - deviceTuyaUuid: addDeviceDto.deviceTuyaUuid, - status: deviceStatus.data.status, - productUuid: deviceStatus.data.productUuid, - productType: deviceStatus.data.productType, - }); - } - } - return new SuccessResponseDto({ - message: `Device added successfully`, - data: deviceSaved, - statusCode: HttpStatus.CREATED, - }); - } 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 transferDeviceInSpaces( - assignDeviceToSpaceDto: AssignDeviceToSpaceDto, - projectUuid: string, - ) { - try { - await this.deviceRepository.update( - { - uuid: assignDeviceToSpaceDto.deviceUuid, - spaceDevice: { - community: { project: { uuid: projectUuid } }, - spaceName: Not(ORPHAN_SPACE_NAME), - }, - }, - { - spaceDevice: { uuid: assignDeviceToSpaceDto.spaceUuid }, - }, - ); - const device = await this.deviceRepository.findOne({ - where: { - uuid: assignDeviceToSpaceDto.deviceUuid, - }, - relations: ['spaceDevice', 'spaceDevice.parent'], - }); - if (device.spaceDevice.parent.spaceTuyaUuid) { - await this.transferDeviceInSpacesTuya( - device.deviceTuyaUuid, - device.spaceDevice.parent.spaceTuyaUuid, - ); - } - - return new SuccessResponseDto({ - message: `Device transferred successfully to spaceUuid: ${assignDeviceToSpaceDto.spaceUuid}`, - data: { - uuid: device.uuid, - spaceUuid: device.spaceDevice.uuid, - }, - statusCode: HttpStatus.CREATED, - }); - } 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 updateDeviceName(deviceUuid: string, deviceName: string) { - try { - await this.deviceRepository.update( - { uuid: deviceUuid }, - { name: deviceName }, - ); - } catch (error) { - throw new HttpException( - 'Error updating device name', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async controlDevice( - controlDeviceDto: ControlDeviceDto, - deviceUuid: string, - projectUuid: string, - ) { - try { - const deviceDetails = await this.getDeviceByDeviceUuid( - deviceUuid, - false, - projectUuid, - ); - - if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { - throw new NotFoundException('Device Not Found'); - } - const response = await this.controlDeviceTuya( - deviceDetails.deviceTuyaUuid, - controlDeviceDto, - ); - - if (response.success) { - return new SuccessResponseDto({ - message: `Device controlled successfully`, - data: response, - statusCode: HttpStatus.CREATED, - }); - } 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, - projectUuid: string, - ) { - const { devicesUuid, operationType } = batchControlDevicesDto; - - if (operationType === BatchDeviceTypeEnum.RESET) { - return await this.batchFactoryResetDevices( - batchControlDevicesDto, - projectUuid, - ); - } else if (operationType === BatchDeviceTypeEnum.COMMAND) { - 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, - true, - projectUuid, - ); - 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 new SuccessResponseDto({ - message: `Devices controlled successfully`, - data: { successResults, failedResults }, - statusCode: HttpStatus.CREATED, - }); - } catch (error) { - throw new HttpException( - error.message || 'Device Not Found', - error.status || HttpStatus.NOT_FOUND, - ); - } - } - } - async batchStatusDevices( - batchStatusDevicesDto: BatchStatusDevicesDto, - projectUuid: string, - ) { - 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, - projectUuid, - ); - return { deviceUuid, result }; - }), - ); - - return new SuccessResponseDto({ - message: `Devices status fetched successfully`, - data: { - status: statuses[0].result.data, - devices: statuses, - }, - statusCode: HttpStatus.OK, - }); - } 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, - projectUuid: string, - ) { - 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, - true, - projectUuid, - ); - 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) { - if (error instanceof HttpException) { - throw error; - } - throw new HttpException( - error.message || 'Device Not Found', - error.status || HttpStatus.NOT_FOUND, - ); - } - } - async getDeviceDetailsByDeviceId(deviceUuid: string, projectUuid: string) { - try { - const deviceDetails = await this.getDeviceByDeviceUuid( - deviceUuid, - true, - projectUuid, - ); - - if (!deviceDetails) { - throw new NotFoundException('Device Not Found'); - } - - const response = await this.getDeviceDetailsByDeviceIdTuya( - deviceDetails.deviceTuyaUuid, - ); - const macAddress = await this.getMacAddressByDeviceIdTuya( - deviceDetails.deviceTuyaUuid, - ); - - return new SuccessResponseDto({ - message: `Device details fetched successfully`, - data: { - ...response, - uuid: deviceDetails.uuid, - productUuid: deviceDetails.productDevice.uuid, - productType: deviceDetails.productDevice.prodType, - macAddress: macAddress.mac, - subspace: deviceDetails.subspace ? deviceDetails.subspace : {}, - }, - statusCode: HttpStatus.OK, - }); - } catch (error) { - throw new HttpException( - error.message || 'Device Not Found', - HttpStatus.NOT_FOUND, - ); - } - } - async updateDevice( - deviceUuid: string, - updateDeviceDto: UpdateDeviceDto, - projectUuid: string, - ) { - try { - const device = await this.getDeviceByDeviceUuid( - deviceUuid, - true, - projectUuid, - ); - if (device.uuid) { - await this.updateDeviceName(deviceUuid, updateDeviceDto.deviceName); - } - - return new SuccessResponseDto({ - message: `Device updated successfully`, - data: { - uuid: device.uuid, - deviceName: updateDeviceDto.deviceName, - }, - statusCode: HttpStatus.CREATED, - }); - } 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, - }, - }); - const deviceDetails = await this.deviceRepository.findOne({ - where: { - deviceTuyaUuid: deviceId, - }, - }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { productId, id, productName, uuid, name, ...rest } = - camelCaseResponse.result; - - return { - ...rest, - productUuid: product.uuid, - name: deviceDetails?.name, - productName: product.name, - } as GetDeviceDetailsInterface; - } catch (error) { - console.log('error', 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, - projectUuid: string, - ): Promise { - const deviceDetails = await this.getDeviceByDeviceUuid( - deviceUuid, - true, - projectUuid, - ); - - if (!deviceDetails) { - throw new NotFoundException('Device Not Found'); - } - try { - const response = await this.getDeviceInstructionByDeviceIdTuya( - deviceDetails.deviceTuyaUuid, - ); - - return new SuccessResponseDto({ - message: `Device instructions fetched successfully`, - data: { - 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, - }; - }), - }, - statusCode: HttpStatus.CREATED, - }); - } 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, projectUuid: string) { - try { - const deviceDetails = await this.getDeviceByDeviceUuid( - deviceUuid, - true, - projectUuid, - ); - - if (!deviceDetails) { - throw new NotFoundException('Device Not Found'); - } - const deviceStatus = await this.getDevicesInstructionStatusTuya( - deviceDetails.deviceTuyaUuid, - ); - - return new SuccessResponseDto({ - message: `Device instructions status fetched successfully`, - data: { - productUuid: deviceDetails.productDevice.uuid, - productType: deviceDetails.productDevice.prodType, - status: deviceStatus.result[0].status, - }, - statusCode: HttpStatus.OK, - }); - } catch (error) { - throw new HttpException( - 'Error fetching device functions status', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getDevicesStatus(deviceUuid: string, projectUuid: string) { - try { - const deviceDetails = await this.getDeviceByDeviceUuid( - deviceUuid, - true, - projectUuid, - ); - - if (!deviceDetails) { - throw new NotFoundException('Device Not Found'); - } - if (deviceDetails.productDevice.prodType === ProductType.PC) { - return await this.getPowerClampInstructionStatus(deviceDetails); - } else { - return await this.getDevicesInstructionStatus(deviceUuid, projectUuid); - } - } 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, - ); - } - } - - async getDevicesInGateway(gatewayUuid: string, projectUuid: string) { - try { - const deviceDetails = await this.getDeviceByDeviceUuid( - gatewayUuid, - true, - projectUuid, - ); - - 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 new SuccessResponseDto({ - message: `Devices fetched successfully`, - data: { - uuid: deviceDetails.uuid, - productUuid: deviceDetails.productDevice.uuid, - productType: deviceDetails.productDevice.prodType, - devices: devices.filter((device) => device !== null), - }, - statusCode: HttpStatus.OK, - }); - } 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, - projectUuid: string, - ) { - try { - const deviceDetails = await this.getDeviceByDeviceUuid( - deviceUuid, - false, - projectUuid, - ); - - if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { - throw new NotFoundException('Device Not Found'); - } - const response = await this.updateDeviceFirmwareTuya( - deviceDetails.deviceTuyaUuid, - firmwareVersion, - ); - - if (response.success) { - return new SuccessResponseDto({ - message: `Device firmware updated successfully`, - data: response, - statusCode: HttpStatus.CREATED, - }); - } 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 getAllDevices( param: ProjectParam, { deviceType, spaces }: GetDevicesFilterDto, @@ -1149,131 +280,620 @@ export class DeviceService { ); } } - async getDeviceLogsTuya( - deviceId: string, - code: string, - startTime: string = (Date.now() - 1 * 60 * 60 * 1000).toString(), - endTime: string = Date.now().toString(), - ): Promise { + + async getDeviceByDeviceUuid( + deviceUuid: string, + withProductDevice: boolean = true, + projectUuid: string, + ) { + const relations = ['subspace']; + + if (withProductDevice) { + relations.push('productDevice'); + } + + return this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + spaceDevice: { + community: { project: { uuid: projectUuid } }, + spaceName: Not(ORPHAN_SPACE_NAME), + }, + }, + relations, + }); + } + async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) { + return await this.deviceRepository.findOne({ + where: { + deviceTuyaUuid, + }, + relations: ['productDevice'], + }); + } + + async addNewDevice(addDeviceDto: AddDeviceDto, projectUuid: string) { 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, + const device = await this.getDeviceDetailsByDeviceIdTuya( + addDeviceDto.deviceTuyaUuid, + ); + + if (!device.productUuid) { + throw new Error('Product UUID is missing for the device.'); + } + const existingConflictingDevice = await this.deviceRepository.exists({ + where: { + spaceDevice: { uuid: addDeviceDto.spaceUuid }, + productDevice: { uuid: device.productUuid }, + tag: { uuid: addDeviceDto.tagUuid }, + }, }); - // Convert keys to camel case - const camelCaseResponse = convertKeysToCamelCase(response); - const logs = camelCaseResponse.result.logs ?? []; - return { - startTime, - endTime, - data: logs, - } as getDeviceLogsInterface; + if (existingConflictingDevice) { + throw new HttpException( + 'Device with the same product type and tag already exists in this space', + HttpStatus.BAD_REQUEST, + ); + } + const deviceSaved = await this.deviceRepository.save({ + deviceTuyaUuid: addDeviceDto.deviceTuyaUuid, + productDevice: { uuid: device.productUuid }, + spaceDevice: { uuid: addDeviceDto.spaceUuid }, + tag: { uuid: addDeviceDto.tagUuid }, + name: addDeviceDto.deviceName, + }); + if (deviceSaved.uuid) { + const deviceStatus: BaseResponseDto = + await this.getDevicesInstructionStatus(deviceSaved.uuid, projectUuid); + if (deviceStatus.data.productUuid) { + await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ + deviceUuid: deviceSaved.uuid, + deviceTuyaUuid: addDeviceDto.deviceTuyaUuid, + status: deviceStatus.data.status, + productUuid: deviceStatus.data.productUuid, + productType: deviceStatus.data.productType, + }); + } + } + return new SuccessResponseDto({ + message: `Device added successfully`, + data: deviceSaved, + statusCode: HttpStatus.CREATED, + }); + } 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 deleteDevice( + devices: DeviceEntity[], + orphanSpace: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + try { + const deviceIds = devices.map((device) => device.uuid); + + if (deviceIds.length > 0) { + await queryRunner.manager + .createQueryBuilder() + .update(DeviceEntity) + .set({ spaceDevice: orphanSpace }) + .whereInIds(deviceIds) + .execute(); + } } catch (error) { throw new HttpException( - 'Error fetching device logs from Tuya', + `Failed to update devices to orphan space: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async getPowerClampInstructionStatus(deviceDetails: any) { + async transferDeviceInSpaces( + assignDeviceToSpaceDto: AssignDeviceToSpaceDto, + projectUuid: string, + ) { try { - 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; + await this.deviceRepository.update( + { + uuid: assignDeviceToSpaceDto.deviceUuid, + spaceDevice: { + community: { project: { uuid: projectUuid } }, + spaceName: Not(ORPHAN_SPACE_NAME), + }, }, { - phaseA: [] as { code: string; value: any }[], - phaseB: [] as { code: string; value: any }[], - phaseC: [] as { code: string; value: any }[], - general: [] as { code: string; value: any }[], + spaceDevice: { uuid: assignDeviceToSpaceDto.spaceUuid }, }, ); + const device = await this.deviceRepository.findOne({ + where: { + uuid: assignDeviceToSpaceDto.deviceUuid, + }, + relations: ['spaceDevice', 'spaceDevice.parent'], + }); + if (device.spaceDevice.parent.spaceTuyaUuid) { + await this.transferDeviceInSpacesTuya( + device.deviceTuyaUuid, + device.spaceDevice.parent.spaceTuyaUuid, + ); + } + + return new SuccessResponseDto({ + message: `Device transferred successfully to spaceUuid: ${assignDeviceToSpaceDto.spaceUuid}`, + data: { + uuid: device.uuid, + spaceUuid: device.spaceDevice.uuid, + }, + statusCode: HttpStatus.CREATED, + }); + } 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 controlDevice( + controlDeviceDto: ControlDeviceDto, + deviceUuid: string, + projectUuid: string, + ) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid( + deviceUuid, + false, + projectUuid, + ); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new NotFoundException('Device Not Found'); + } + const response = await this.controlDeviceTuya( + deviceDetails.deviceTuyaUuid, + controlDeviceDto, + ); + + if (response.success) { + return new SuccessResponseDto({ + message: `Device controlled successfully`, + data: response, + statusCode: HttpStatus.CREATED, + }); + } 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 batchControlDevices( + batchControlDevicesDto: BatchControlDevicesDto, + projectUuid: string, + ) { + const { devicesUuid, operationType } = batchControlDevicesDto; + + if (operationType === BatchDeviceTypeEnum.RESET) { + return await this.batchFactoryResetDevices( + batchControlDevicesDto, + projectUuid, + ); + } else if (operationType === BatchDeviceTypeEnum.COMMAND) { + 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, + true, + projectUuid, + ); + 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 new SuccessResponseDto({ + message: `Devices controlled successfully`, + data: { successResults, failedResults }, + statusCode: HttpStatus.CREATED, + }); + } catch (error) { + throw new HttpException( + error.message || 'Device Not Found', + error.status || HttpStatus.NOT_FOUND, + ); + } + } + } + async batchStatusDevices( + batchStatusDevicesDto: BatchStatusDevicesDto, + projectUuid: string, + ) { + 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, + projectUuid, + ); + return { deviceUuid, result }; + }), + ); return new SuccessResponseDto({ - message: `Power clamp functions status fetched successfully`, + message: `Devices status fetched successfully`, data: { - productUuid: deviceDetails.productDevice.uuid, - productType: deviceDetails.productDevice.prodType, - status: { - phaseA: groupedStatus.phaseA, - phaseB: groupedStatus.phaseB, - phaseC: groupedStatus.phaseC, - general: groupedStatus.general, - }, + status: statuses[0].result.data, + devices: statuses, }, statusCode: HttpStatus.OK, }); } catch (error) { throw new HttpException( - error.message || 'Error fetching power clamp functions status', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, + error.message || 'Device Not Found', + error.status || HttpStatus.NOT_FOUND, ); } } - async getPowerClampInstructionStatusTuya( - deviceUuid: string, - ): Promise { + + async getDeviceDetailsByDeviceId(deviceUuid: string, projectUuid: string) { try { - const path = `/v2.0/cloud/thing/${deviceUuid}/shadow/properties`; - const response = await this.tuya.request({ - method: 'GET', - path, - query: { - device_ids: deviceUuid, + const deviceDetails = await this.getDeviceByDeviceUuid( + deviceUuid, + true, + projectUuid, + ); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } + + const response = await this.getDeviceDetailsByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); + const macAddress = await this.getMacAddressByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); + + return new SuccessResponseDto({ + message: `Device details fetched successfully`, + data: { + ...response, + uuid: deviceDetails.uuid, + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + macAddress: macAddress.mac, + subspace: deviceDetails.subspace ? deviceDetails.subspace : {}, }, + statusCode: HttpStatus.OK, }); - const camelCaseResponse = convertKeysToCamelCase(response); - return camelCaseResponse as GetPowerClampFunctionsStatusInterface; } catch (error) { throw new HttpException( - 'Error fetching power clamp functions status from Tuya', + error.message || 'Device Not Found', + HttpStatus.NOT_FOUND, + ); + } + } + async updateDevice( + deviceUuid: string, + updateDeviceDto: UpdateDeviceDto, + projectUuid: string, + ) { + try { + const device = await this.getDeviceByDeviceUuid( + deviceUuid, + true, + projectUuid, + ); + if (device.uuid) { + await this.updateDeviceName(deviceUuid, updateDeviceDto.deviceName); + } + + return new SuccessResponseDto({ + message: `Device updated successfully`, + data: { + uuid: device.uuid, + deviceName: updateDeviceDto.deviceName, + }, + statusCode: HttpStatus.CREATED, + }); + } catch (error) { + throw new HttpException( + 'Error updating device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDeviceDetailsByDeviceIdTuya( + deviceId: string, + ): Promise { + console.log('fetching device details from Tuya for deviceId:', deviceId); + try { + const deviceDetails = await this.deviceRepository.findOne({ + where: { + deviceTuyaUuid: deviceId, + }, + relations: ['productDevice'], + }); + + let result = await this.tuyaService.getDeviceDetails(deviceId); + + if (!result) { + const updatedDeviceTuyaUuid = ( + await this.updateDeviceTuyaUuidFromTuya(deviceDetails) + ).deviceTuyaUuid; + + // Retry with the updated deviceTuyaUuid + result = await this.tuyaService.getDeviceDetails(updatedDeviceTuyaUuid); + } + // Convert keys to camel case + const camelCaseResponse = convertKeysToCamelCase(result); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { productId, id, productName, uuid, name, ...rest } = + camelCaseResponse; + + return { + ...rest, + productUuid: deviceDetails.productDevice.uuid, + name: deviceDetails?.name, + productName: deviceDetails.productDevice.name, + } as GetDeviceDetailsInterface; + } catch (error) { + console.log('error', error); + + throw new HttpException( + 'Error fetching device details from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDeviceInstructionByDeviceId( + deviceUuid: string, + projectUuid: string, + ): Promise { + const deviceDetails = await this.getDeviceByDeviceUuid( + deviceUuid, + true, + projectUuid, + ); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } + try { + const response = await this.getDeviceInstructionByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); + + return new SuccessResponseDto({ + message: `Device instructions fetched successfully`, + data: { + 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, + }; + }), + }, + statusCode: HttpStatus.CREATED, + }); + } catch (error) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + } + + async getDevicesStatus(deviceUuid: string, projectUuid: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid( + deviceUuid, + true, + projectUuid, + ); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } + if (deviceDetails.productDevice.prodType === ProductType.PC) { + return await this.getPowerClampInstructionStatus(deviceDetails); + } else { + return await this.getDevicesInstructionStatus(deviceUuid, projectUuid); + } + } catch (error) { + throw new HttpException( + 'Error fetching device functions status', HttpStatus.INTERNAL_SERVER_ERROR, ); } } - private async fetchAncestors(space: SpaceEntity): Promise { - const ancestors: SpaceEntity[] = []; + async getDevicesInGateway(gatewayUuid: string, projectUuid: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid( + gatewayUuid, + true, + projectUuid, + ); - 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; + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } else if (deviceDetails.productDevice.prodType !== ProductType.GW) { + throw new BadRequestException('This is not a gateway device'); } - } - // Return the ancestors in reverse order to have the root at the start - return ancestors.reverse(); + 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 new SuccessResponseDto({ + message: `Devices fetched successfully`, + data: { + uuid: deviceDetails.uuid, + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + devices: devices.filter((device) => device !== null), + }, + statusCode: HttpStatus.OK, + }); + } catch (error) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + } + + async updateDeviceFirmware( + deviceUuid: string, + firmwareVersion: number, + projectUuid: string, + ) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid( + deviceUuid, + false, + projectUuid, + ); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new NotFoundException('Device Not Found'); + } + const response = await this.updateDeviceFirmwareTuya( + deviceDetails.deviceTuyaUuid, + firmwareVersion, + ); + + if (response.success) { + return new SuccessResponseDto({ + message: `Device firmware updated successfully`, + data: response, + statusCode: HttpStatus.CREATED, + }); + } else { + throw new HttpException( + response.msg || 'Unknown error', + HttpStatus.BAD_REQUEST, + ); + } + } catch (error) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } } async addSceneToSceneDevice( @@ -1537,19 +1157,6 @@ export class DeviceService { } } - private async validateProject(uuid: string) { - const project = await this.projectRepository.findOne({ - where: { uuid }, - }); - if (!project) { - throw new HttpException( - `A project with the uuid '${uuid}' doesn't exists.`, - HttpStatus.BAD_REQUEST, - ); - } - return project; - } - async getParentHierarchy( space: SpaceEntity, ): Promise<{ uuid: string; spaceName: string }[]> { @@ -1578,7 +1185,69 @@ export class DeviceService { } } - async getDoorLockDevices(projectUuid: string, spaces?: string[]) { + async addDevicesToOrphanSpace( + space: SpaceEntity, + project: ProjectEntity, + queryRunner: QueryRunner, + ) { + const spaceRepository = queryRunner.manager.getRepository(SpaceEntity); + const deviceRepository = queryRunner.manager.getRepository(DeviceEntity); + try { + const orphanSpace = await spaceRepository.findOne({ + where: { + community: { + name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`, + }, + spaceName: ORPHAN_SPACE_NAME, + }, + }); + + if (!orphanSpace) { + throw new HttpException( + `Orphan space not found in community ${project.name}`, + HttpStatus.NOT_FOUND, + ); + } + + await deviceRepository.update( + { uuid: In(space.devices.map((device) => device.uuid)) }, + { spaceDevice: orphanSpace }, + ); + } catch (error) { + throw new Error( + `Failed to add devices to orphan spaces: ${error.message}`, + ); + } + } + + async addTuyaConstUuidToDevices() { + const devices = await this.deviceRepository.find(); + const updatedDevices = []; + for (const device of devices) { + if (!device.deviceTuyaConstUuid) { + const path = `/v1.1/iot-03/devices/${device.deviceTuyaUuid}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + if (!response?.success) { + console.error( + `Failed to fetch Tuya constant UUID for device ${device.deviceTuyaUuid}`, + ); + continue; + } + const camelCaseResponse = convertKeysToCamelCase(response); + const tuyaConstUuid = camelCaseResponse?.result.uuid; + if (tuyaConstUuid) { + device.deviceTuyaConstUuid = tuyaConstUuid; + updatedDevices.push(device); + } + } + } + await this.deviceRepository.save(updatedDevices); + } + + private async getDoorLockDevices(projectUuid: string, spaces?: string[]) { await this.validateProject(projectUuid); const devices = await this.deviceRepository.find({ @@ -1635,6 +1304,452 @@ export class DeviceService { statusCode: HttpStatus.OK, }); } + + private 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, + ); + } + } + + private async updateDeviceName(deviceUuid: string, deviceName: string) { + try { + await this.deviceRepository.update( + { uuid: deviceUuid }, + { name: deviceName }, + ); + } catch (error) { + throw new HttpException( + 'Error updating device name', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + private 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`); + } + } + } + private async batchFactoryResetDevices( + batchFactoryResetDevicesDto: BatchFactoryResetDevicesDto, + projectUuid: string, + ) { + 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, + true, + projectUuid, + ); + 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) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Device Not Found', + error.status || HttpStatus.NOT_FOUND, + ); + } + } + private async updateDeviceTuyaUuidFromTuya(device: DeviceEntity) { + console.log(`looking for device with Tuya UUID: ${device.deviceTuyaUuid}`); + try { + let last_id = null; + let deviceFound = false; + + while (!deviceFound) { + const path = `/v2.0/cloud/thing/device`; + const response = await this.tuya.request({ + method: 'GET', + path, + query: { + product_ids: device.productDevice.prodId, + page_size: 20, // maximum allowed by Tuya + last_id, + }, + }); + if ( + !response.success || + !response.result || + !(response.result as any[]).length + ) { + throw new NotFoundException('Device not found in Tuya'); + } + + const devicesTuya = response.result as any[]; + for (const dev of devicesTuya) { + if (dev.uuid == device.deviceTuyaConstUuid) { + deviceFound = true; + device.deviceTuyaUuid = dev.id; + break; + } + } + if (!deviceFound) { + last_id = devicesTuya[devicesTuya.length - 1].id; + } + } + console.log(`found device with Tuya UUID: ${device.deviceTuyaUuid}`); + return this.deviceRepository.save(device); + } catch (error) { + console.log(error); + throw new HttpException( + 'Error fetching device by product ID from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + private 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, + ); + } + } + private 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, + ); + } + } + private async getDevicesInstructionStatus( + deviceUuid: string, + projectUuid: string, + ) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid( + deviceUuid, + true, + projectUuid, + ); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } + const deviceStatus = await this.getDevicesInstructionStatusTuya( + deviceDetails.deviceTuyaUuid, + ); + + return new SuccessResponseDto({ + message: `Device instructions status fetched successfully`, + data: { + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + status: deviceStatus.result[0].status, + }, + statusCode: HttpStatus.OK, + }); + } catch (error) { + throw new HttpException( + 'Error fetching device functions status', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + private 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 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, + ); + } + } + private 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, + ); + } + } + private 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, + ); + } + } + private async getPowerClampInstructionStatus(deviceDetails: any) { + try { + 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 new SuccessResponseDto({ + message: `Power clamp functions status fetched successfully`, + data: { + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + status: { + phaseA: groupedStatus.phaseA, + phaseB: groupedStatus.phaseB, + phaseC: groupedStatus.phaseC, + general: groupedStatus.general, + }, + }, + statusCode: HttpStatus.OK, + }); + } catch (error) { + throw new HttpException( + error.message || 'Error fetching power clamp functions status', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + private 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 validateProject(uuid: string) { + const project = await this.projectRepository.findOne({ + where: { uuid }, + }); + if (!project) { + throw new HttpException( + `A project with the uuid '${uuid}' doesn't exists.`, + HttpStatus.BAD_REQUEST, + ); + } + return project; + } async getAllDevicesBySpaceOrCommunityWithChild( query: GetDevicesBySpaceOrCommunityDto, ): Promise { @@ -1686,7 +1801,9 @@ export class DeviceService { ); } } - async getAllDevicesBySpace(spaceUuid: string): Promise { + private async getAllDevicesBySpace( + spaceUuid: string, + ): Promise { const space = await this.spaceRepository.findOne({ where: { uuid: spaceUuid }, relations: ['children', 'devices', 'devices.productDevice'], @@ -1720,7 +1837,7 @@ export class DeviceService { return allDevices; } - async getAllDevicesByCommunity( + private async getAllDevicesByCommunity( communityUuid: string, ): Promise { const community = await this.communityRepository.findOne({ @@ -1769,39 +1886,4 @@ export class DeviceService { return allDevices; } - - async addDevicesToOrphanSpace( - space: SpaceEntity, - project: ProjectEntity, - queryRunner: QueryRunner, - ) { - const spaceRepository = queryRunner.manager.getRepository(SpaceEntity); - const deviceRepository = queryRunner.manager.getRepository(DeviceEntity); - try { - const orphanSpace = await spaceRepository.findOne({ - where: { - community: { - name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`, - }, - spaceName: ORPHAN_SPACE_NAME, - }, - }); - - if (!orphanSpace) { - throw new HttpException( - `Orphan space not found in community ${project.name}`, - HttpStatus.NOT_FOUND, - ); - } - - await deviceRepository.update( - { uuid: In(space.devices.map((device) => device.uuid)) }, - { spaceDevice: orphanSpace }, - ); - } catch (error) { - throw new Error( - `Failed to add devices to orphan spaces: ${error.message}`, - ); - } - } }