diff --git a/libs/common/src/integrations/tuya/services/tuya.service.ts b/libs/common/src/integrations/tuya/services/tuya.service.ts index 80205af..9aabb9a 100644 --- a/libs/common/src/integrations/tuya/services/tuya.service.ts +++ b/libs/common/src/integrations/tuya/services/tuya.service.ts @@ -133,4 +133,45 @@ export class TuyaService { return response; } + + async createAutomation( + spaceId: string, + automationName: string, + effectiveTime: any, + decisionExpr: string, + conditions: any[], + actions: any[], + ) { + const path = `/v2.0/cloud/scene/rule`; + + try { + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + space_id: spaceId, + name: automationName, + effective_time: { + ...effectiveTime, + timezone_id: 'Asia/Dubai', + }, + type: 'automation', + decision_expr: decisionExpr, + conditions: conditions, + actions: actions, + }, + }); + + if (!response.success) { + throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } + + return response.result; + } catch (error) { + throw new HttpException( + error.message || 'Failed to create automation in Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts index 49c3b02..3a3d7db 100644 --- a/src/automation/automation.module.ts +++ b/src/automation/automation.module.ts @@ -8,12 +8,14 @@ import { DeviceService } from 'src/device/services'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], controllers: [AutomationController], providers: [ AutomationService, + TuyaService, SpaceRepository, DeviceService, DeviceRepository, diff --git a/src/automation/services/automation.service.ts b/src/automation/services/automation.service.ts index 793fa9f..a34eb49 100644 --- a/src/automation/services/automation.service.ts +++ b/src/automation/services/automation.service.ts @@ -16,9 +16,11 @@ import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; import { DeviceService } from 'src/device/services'; import { + Action, AddAutomationInterface, AutomationDetailsResult, AutomationResponseData, + Condition, DeleteAutomationInterface, GetAutomationBySpaceInterface, } from '../interface/automation.interface'; @@ -27,6 +29,7 @@ import { ActionExecutorEnum, EntityTypeEnum, } from '@app/common/constants/automation.enum'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; @Injectable() export class AutomationService { @@ -35,6 +38,7 @@ export class AutomationService { private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, private readonly deviceService: DeviceService, + private readonly tuyaService: TuyaService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -48,77 +52,39 @@ export class AutomationService { async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { try { - let tuyaSpaceId; - if (!spaceTuyaId) { - const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid); + const { automationName, effectiveTime, decisionExpr } = addAutomationDto; + const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid); - tuyaSpaceId = space.spaceTuyaUuid; - if (!space) { - throw new BadRequestException( - `Invalid space UUID ${addAutomationDto.spaceUuid}`, - ); - } - } else { - tuyaSpaceId = spaceTuyaId; - } - - const actions = addAutomationDto.actions.map((action) => - convertKeysToSnakeCase(action), - ); - const conditions = addAutomationDto.conditions.map((condition) => - convertKeysToSnakeCase(condition), + const actions = await this.processEntities( + addAutomationDto.actions, + 'actionExecutor', + { [ActionExecutorEnum.DEVICE_ISSUE]: true }, + this.deviceService, ); - for (const action of actions) { - if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) { - const device = await this.deviceService.getDeviceByDeviceUuid( - action.entity_id, - false, - ); - if (device) { - action.entity_id = device.deviceTuyaUuid; - } - } - } + const conditions = await this.processEntities( + addAutomationDto.conditions, + 'entityType', + { [EntityTypeEnum.DEVICE_REPORT]: true }, + this.deviceService, + ); - for (const condition of conditions) { - if (condition.entity_type === EntityTypeEnum.DEVICE_REPORT) { - const device = await this.deviceService.getDeviceByDeviceUuid( - condition.entity_id, - false, - ); - if (device) { - condition.entity_id = device.deviceTuyaUuid; - } - } - } + const response = (await this.tuyaService.createAutomation( + space.spaceTuyaUuid, + automationName, + effectiveTime, + decisionExpr, + conditions, + actions, + )) as AddAutomationInterface; - const path = `/v2.0/cloud/scene/rule`; - const response: AddAutomationInterface = await this.tuya.request({ - method: 'POST', - path, - body: { - space_id: tuyaSpaceId, - name: addAutomationDto.automationName, - effective_time: { - ...addAutomationDto.effectiveTime, - timezone_id: 'Asia/Dubai', - }, - type: 'automation', - decision_expr: addAutomationDto.decisionExpr, - conditions: conditions, - actions: actions, - }, - }); - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } return { - id: response.result.id, + id: response?.result.id, }; } catch (err) { + console.log(err); if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + throw err; } else { throw new HttpException( err.message || 'Automation not found', @@ -127,6 +93,7 @@ export class AutomationService { } } } + async getSpaceByUuid(spaceUuid: string) { try { const space = await this.spaceRepository.findOne({ @@ -189,6 +156,7 @@ export class AutomationService { } } } + async getTapToRunSceneDetailsTuya( sceneUuid: string, ): Promise { @@ -425,4 +393,42 @@ export class AutomationService { } } } + + async processEntities( + entities: T[], // Accepts either Action[] or Condition[] + lookupKey: keyof T, // The key to look up, specific to T + entityTypeOrExecutorMap: { + [key in ActionExecutorEnum | EntityTypeEnum]?: boolean; + }, + deviceService: { + getDeviceByDeviceUuid: ( + id: string, + flag: boolean, + ) => Promise<{ deviceTuyaUuid: string } | null>; + }, + ): Promise { + // Returns the same type as provided in the input + return Promise.all( + entities.map(async (entity) => { + // Convert keys to snake case (assuming a utility function exists) + const processedEntity = convertKeysToSnakeCase(entity) as T; + + // Check if entity needs device UUID lookup + const key = processedEntity[lookupKey]; + if ( + entityTypeOrExecutorMap[key as ActionExecutorEnum | EntityTypeEnum] + ) { + const device = await deviceService.getDeviceByDeviceUuid( + processedEntity.entityId, + false, + ); + if (device) { + processedEntity.entityId = device.deviceTuyaUuid; + } + } + + return processedEntity; + }), + ); + } } diff --git a/src/space/services/space-device.service.ts b/src/space/services/space-device.service.ts index 1be3ea7..9d9eac8 100644 --- a/src/space/services/space-device.service.ts +++ b/src/space/services/space-device.service.ts @@ -20,35 +20,42 @@ export class SpaceDeviceService { async listDevicesInSpace(params: GetSpaceParam): Promise { const { spaceUuid, communityUuid } = params; + try { + const space = await this.validateCommunityAndSpace( + communityUuid, + spaceUuid, + ); - const space = await this.validateCommunityAndSpace( - communityUuid, - spaceUuid, - ); + const detailedDevices = await Promise.all( + space.devices.map(async (device) => { + const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + ); - const detailedDevices = await Promise.all( - space.devices.map(async (device) => { - const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya( - device.deviceTuyaUuid, - ); + return { + uuid: device.uuid, + deviceTuyaUuid: device.deviceTuyaUuid, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + isActive: device.isActive, + createdAt: device.createdAt, + updatedAt: device.updatedAt, + ...tuyaDetails, + }; + }), + ); - return { - uuid: device.uuid, - deviceTuyaUuid: device.deviceTuyaUuid, - productUuid: device.productDevice.uuid, - productType: device.productDevice.prodType, - isActive: device.isActive, - createdAt: device.createdAt, - updatedAt: device.updatedAt, - ...tuyaDetails, - }; - }), - ); - - return new SuccessResponseDto({ - data: detailedDevices, - message: 'Successfully retrieved list of devices', - }); + return new SuccessResponseDto({ + data: detailedDevices, + message: 'Successfully retrieved list of devices', + }); + } catch (error) { + console.error('Error listing devices in space:', error); + throw new HttpException( + error.message || 'Failed to retrieve devices in space', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) { @@ -80,7 +87,6 @@ export class SpaceDeviceService { deviceId: string, ): Promise { try { - // Fetch details from TuyaService const tuyaDeviceDetails = await this.tuyaService.getDeviceDetails(deviceId); @@ -101,7 +107,7 @@ export class SpaceDeviceService { } as GetDeviceDetailsInterface; } catch (error) { throw new HttpException( - 'Error fetching device details from Tuya', + `Error fetching device details from Tuya for device id ${deviceId}`, HttpStatus.INTERNAL_SERVER_ERROR, ); }