From a3eaa0fa3c13802471c133a2b984ab6630d4223c Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Tue, 5 Nov 2024 00:48:54 +0400 Subject: [PATCH] Fixed automation bugs --- .../tuya/interfaces/automation.interface.ts | 9 + .../src/integrations/tuya/interfaces/index.ts | 1 + .../interfaces/tuya.response.interface.ts | 2 +- .../tuya/services/tuya.service.ts | 76 ++++-- .../controllers/automation.controller.ts | 12 +- src/automation/dtos/automation.dto.ts | 2 +- .../dtos/delete.automation.param.dto.ts | 9 +- .../interface/automation.interface.ts | 11 + src/automation/services/automation.service.ts | 248 ++++++++++-------- .../subspace/subspace-device.service.ts | 8 +- 10 files changed, 227 insertions(+), 151 deletions(-) create mode 100644 libs/common/src/integrations/tuya/interfaces/automation.interface.ts diff --git a/libs/common/src/integrations/tuya/interfaces/automation.interface.ts b/libs/common/src/integrations/tuya/interfaces/automation.interface.ts new file mode 100644 index 0000000..fb061c2 --- /dev/null +++ b/libs/common/src/integrations/tuya/interfaces/automation.interface.ts @@ -0,0 +1,9 @@ +import { TuyaResponseInterface } from './tuya.response.interface'; + +export interface AddTuyaResponseInterface extends TuyaResponseInterface { + result: { + id: string; + }; + t?: number; + tid?: string; +} diff --git a/libs/common/src/integrations/tuya/interfaces/index.ts b/libs/common/src/integrations/tuya/interfaces/index.ts index 506fc06..06eff87 100644 --- a/libs/common/src/integrations/tuya/interfaces/index.ts +++ b/libs/common/src/integrations/tuya/interfaces/index.ts @@ -1,2 +1,3 @@ export * from './tuya.response.interface'; export * from './tap-to-run-action.interface'; +export * from './automation.interface'; diff --git a/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts b/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts index 3e3a2a9..141613d 100644 --- a/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts +++ b/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts @@ -1,5 +1,5 @@ export interface TuyaResponseInterface { success: boolean; msg?: string; - result: boolean; + result: boolean | { id: string }; } diff --git a/libs/common/src/integrations/tuya/services/tuya.service.ts b/libs/common/src/integrations/tuya/services/tuya.service.ts index 9aabb9a..ff69aa9 100644 --- a/libs/common/src/integrations/tuya/services/tuya.service.ts +++ b/libs/common/src/integrations/tuya/services/tuya.service.ts @@ -1,7 +1,11 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; -import { ConvertedAction, TuyaResponseInterface } from '../interfaces'; +import { + AddTuyaResponseInterface, + ConvertedAction, + TuyaResponseInterface, +} from '../interfaces'; @Injectable() export class TuyaService { @@ -144,32 +148,72 @@ export class TuyaService { ) { const path = `/v2.0/cloud/scene/rule`; + const response: AddTuyaResponseInterface = 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; + } + + async deleteAutomation(spaceId: string, automationUuid: string) { + const path = `/v2.0/cloud/scene/rule?ids=${automationUuid}&space_id=${spaceId}`; + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + if (!response.success) { + throw new HttpException( + 'Failed to delete automation', + HttpStatus.NOT_FOUND, + ); + } + + return response; + } + + async updateAutomationState( + spaceId: string, + automationUuid: string, + isEnable: boolean, + ) { + const path = `/v2.0/cloud/scene/rule/state?space_id=${spaceId}`; + try { - const response = await this.tuya.request({ - method: 'POST', + const response: TuyaResponseInterface = await this.tuya.request({ + method: 'PUT', path, body: { - space_id: spaceId, - name: automationName, - effective_time: { - ...effectiveTime, - timezone_id: 'Asia/Dubai', - }, - type: 'automation', - decision_expr: decisionExpr, - conditions: conditions, - actions: actions, + ids: automationUuid, + is_enable: isEnable, }, }); if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); } - return response.result; + return response; } catch (error) { throw new HttpException( - error.message || 'Failed to create automation in Tuya', + error.message || 'Failed to update automation state', error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } diff --git a/src/automation/controllers/automation.controller.ts b/src/automation/controllers/automation.controller.ts index 8e83ef3..49a5012 100644 --- a/src/automation/controllers/automation.controller.ts +++ b/src/automation/controllers/automation.controller.ts @@ -18,11 +18,7 @@ import { } from '../dtos/automation.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { - AutomationParamDto, - DeleteAutomationParamDto, - SpaceParamDto, -} from '../dtos'; +import { AutomationParamDto, SpaceParamDto } from '../dtos'; @ApiTags('Automation Module') @Controller({ @@ -45,6 +41,7 @@ export class AutomationController { data: automation, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':spaceUuid') @@ -67,14 +64,15 @@ export class AutomationController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Delete(':unitUuid/:automationId') - async deleteAutomation(@Param() param: DeleteAutomationParamDto) { + @Delete(':automationUuid') + async deleteAutomation(@Param() param: AutomationParamDto) { await this.automationService.deleteAutomation(param); return { statusCode: HttpStatus.OK, message: 'Automation Deleted Successfully', }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put(':automationUuid') diff --git a/src/automation/dtos/automation.dto.ts b/src/automation/dtos/automation.dto.ts index 60104ac..6241cce 100644 --- a/src/automation/dtos/automation.dto.ts +++ b/src/automation/dtos/automation.dto.ts @@ -10,7 +10,7 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; -class EffectiveTime { +export class EffectiveTime { @ApiProperty({ description: 'Start time', required: true }) @IsString() @IsNotEmpty() diff --git a/src/automation/dtos/delete.automation.param.dto.ts b/src/automation/dtos/delete.automation.param.dto.ts index 96ee49b..6421a80 100644 --- a/src/automation/dtos/delete.automation.param.dto.ts +++ b/src/automation/dtos/delete.automation.param.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsUUID } from 'class-validator'; +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; export class DeleteAutomationParamDto { @ApiProperty({ @@ -10,9 +10,10 @@ export class DeleteAutomationParamDto { spaceUuid: string; @ApiProperty({ - description: 'UUID of the Automation', - example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + description: 'TuyaId of the automation', + example: 'SfFi2Tbn09btes84', }) - @IsUUID() + @IsString() + @IsNotEmpty() automationUuid: string; } diff --git a/src/automation/interface/automation.interface.ts b/src/automation/interface/automation.interface.ts index eed3ef7..9d7f40f 100644 --- a/src/automation/interface/automation.interface.ts +++ b/src/automation/interface/automation.interface.ts @@ -1,3 +1,5 @@ +import { EffectiveTime } from '../dtos'; + export interface AddAutomationInterface { success: boolean; msg?: string; @@ -48,3 +50,12 @@ export interface AutomationDetailsResult { name: string; type: string; } + +export interface AddAutomationParams { + actions: Action[]; + conditions: Condition[]; + automationName: string; + effectiveTime: EffectiveTime; + decisionExpr: string; + spaceTuyaId: string; +} diff --git a/src/automation/services/automation.service.ts b/src/automation/services/automation.service.ts index a34eb49..468a4ae 100644 --- a/src/automation/services/automation.service.ts +++ b/src/automation/services/automation.service.ts @@ -7,7 +7,7 @@ import { import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddAutomationDto, - DeleteAutomationParamDto, + AutomationParamDto, UpdateAutomationDto, UpdateAutomationStatusDto, } from '../dtos'; @@ -17,11 +17,10 @@ import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; import { DeviceService } from 'src/device/services'; import { Action, - AddAutomationInterface, + AddAutomationParams, AutomationDetailsResult, AutomationResponseData, Condition, - DeleteAutomationInterface, GetAutomationBySpaceInterface, } from '../interface/automation.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; @@ -30,6 +29,7 @@ import { EntityTypeEnum, } from '@app/common/constants/automation.enum'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; @Injectable() export class AutomationService { @@ -50,39 +50,26 @@ export class AutomationService { }); } - async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { + async addAutomation(addAutomationDto: AddAutomationDto) { try { - const { automationName, effectiveTime, decisionExpr } = addAutomationDto; - const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid); - - const actions = await this.processEntities( - addAutomationDto.actions, - 'actionExecutor', - { [ActionExecutorEnum.DEVICE_ISSUE]: true }, - this.deviceService, - ); - - const conditions = await this.processEntities( - addAutomationDto.conditions, - 'entityType', - { [EntityTypeEnum.DEVICE_REPORT]: true }, - this.deviceService, - ); - - const response = (await this.tuyaService.createAutomation( - space.spaceTuyaUuid, + const { automationName, effectiveTime, decisionExpr, - conditions, actions, - )) as AddAutomationInterface; - - return { - id: response?.result.id, - }; + conditions, + } = addAutomationDto; + const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid); + const response = await this.add({ + automationName, + effectiveTime, + decisionExpr, + actions, + conditions, + spaceTuyaId: space.spaceTuyaUuid, + }); + return response; } catch (err) { - console.log(err); if (err instanceof BadRequestException) { throw err; } else { @@ -94,6 +81,33 @@ export class AutomationService { } } + async add(params: AddAutomationParams) { + try { + const formattedActions = await this.prepareActions(params.actions); + const formattedCondition = await this.prepareConditions( + params.conditions, + ); + + const response = await this.tuyaService.createAutomation( + params.spaceTuyaId, + params.automationName, + params.effectiveTime, + params.decisionExpr, + formattedCondition, + formattedActions, + ); + + return { + id: response?.result.id, + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to add automation', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getSpaceByUuid(spaceUuid: string) { try { const space = await this.spaceRepository.findOne({ @@ -120,6 +134,7 @@ export class AutomationService { } } } + async getAutomationBySpace(spaceUuid: string) { try { const space = await this.getSpaceByUuid(spaceUuid); @@ -277,34 +292,45 @@ export class AutomationService { } } - async deleteAutomation(param: DeleteAutomationParamDto, spaceTuyaId = null) { + async deleteAutomation(param: AutomationParamDto) { try { - const { automationUuid, spaceUuid } = param; - let tuyaSpaceId; - if (!spaceTuyaId) { - const space = await this.getSpaceByUuid(spaceUuid); - tuyaSpaceId = space.spaceTuyaUuid; - if (!tuyaSpaceId) { - throw new BadRequestException(`Invalid space UUID ${spaceUuid}`); - } - } else { - tuyaSpaceId = spaceTuyaId; + const { automationUuid } = param; + + const automation = await this.getAutomationDetails(automationUuid, true); + + if (!automation && !automation.spaceId) { + throw new HttpException( + `Invalid automationid ${automationUuid}`, + HttpStatus.BAD_REQUEST, + ); } - - const path = `/v2.0/cloud/scene/rule?ids=${automationUuid}&space_id=${tuyaSpaceId}`; - const response: DeleteAutomationInterface = await this.tuya.request({ - method: 'DELETE', - path, - }); - - if (!response.success) { - throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); - } - + const response = this.tuyaService.deleteAutomation( + automation.spaceId, + automationUuid, + ); return response; } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException( + err.message || 'Automation not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + + async delete(tuyaSpaceId: string, automationUuid: string) { + try { + const response = await this.tuyaService.deleteAutomation( + tuyaSpaceId, + automationUuid, + ); + return response; + } catch (err) { + if (err instanceof HttpException) { + throw err; } else { throw new HttpException( err.message || 'Automation not found', @@ -318,28 +344,28 @@ export class AutomationService { updateAutomationDto: UpdateAutomationDto, automationUuid: string, ) { + const { actions, conditions, automationName, effectiveTime, decisionExpr } = + updateAutomationDto; try { - const spaceTuyaId = await this.getAutomationDetails(automationUuid, true); - if (!spaceTuyaId.spaceId) { + const automation = await this.getAutomationDetails(automationUuid, true); + if (!automation.spaceId) { throw new HttpException( "Automation doesn't exist", HttpStatus.NOT_FOUND, ); } - const addAutomation = { - ...updateAutomationDto, - spaceUuid: null, - }; - const newAutomation = await this.addAutomation( - addAutomation, - spaceTuyaId.spaceId, - ); - const params: DeleteAutomationParamDto = { - spaceUuid: spaceTuyaId.spaceId, - automationUuid: automationUuid, - }; + + const newAutomation = await this.add({ + actions, + conditions, + automationName, + effectiveTime, + decisionExpr, + spaceTuyaId: automation.spaceId, + }); + if (newAutomation.id) { - await this.deleteAutomation(null, params); + await this.delete(automation.spaceId, automationUuid); return newAutomation; } } catch (err) { @@ -357,29 +383,21 @@ export class AutomationService { updateAutomationStatusDto: UpdateAutomationStatusDto, automationUuid: string, ) { + const { isEnable, spaceUuid } = updateAutomationStatusDto; try { - const space = await this.getSpaceByUuid( - updateAutomationStatusDto.spaceUuid, - ); + const space = await this.getSpaceByUuid(spaceUuid); if (!space.spaceTuyaUuid) { - throw new BadRequestException( - `Invalid space UUID ${updateAutomationStatusDto.spaceUuid}`, + throw new HttpException( + `Invalid space UUID ${spaceUuid}`, + HttpStatus.NOT_FOUND, ); } - const path = `/v2.0/cloud/scene/rule/state?space_id=${space.spaceTuyaUuid}`; - const response: DeleteAutomationInterface = await this.tuya.request({ - method: 'PUT', - path, - body: { - ids: automationUuid, - is_enable: updateAutomationStatusDto.isEnable, - }, - }); - - if (!response.success) { - throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); - } + const response = await this.tuyaService.updateAutomationState( + space.spaceTuyaUuid, + automationUuid, + isEnable, + ); return response; } catch (err) { @@ -394,41 +412,41 @@ 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; + private async prepareActions(actions: Action[]): Promise { + const convertedData = convertKeysToSnakeCase(actions) as ConvertedAction[]; - // Check if entity needs device UUID lookup - const key = processedEntity[lookupKey]; - if ( - entityTypeOrExecutorMap[key as ActionExecutorEnum | EntityTypeEnum] - ) { - const device = await deviceService.getDeviceByDeviceUuid( - processedEntity.entityId, + await Promise.all( + convertedData.map(async (action) => { + if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) { + const device = await this.deviceService.getDeviceByDeviceUuid( + action.entity_id, false, ); if (device) { - processedEntity.entityId = device.deviceTuyaUuid; + action.entity_id = device.deviceTuyaUuid; } } - - return processedEntity; }), ); + + return convertedData; + } + + private async prepareConditions(conditions: Condition[]) { + const convertedData = convertKeysToSnakeCase(conditions); + await Promise.all( + convertedData.map(async (condition) => { + if (condition.entity_type === EntityTypeEnum.DEVICE_REPORT) { + const device = await this.deviceService.getDeviceByDeviceUuid( + condition.entity_id, + false, + ); + if (device) { + condition.entity_id = device.deviceTuyaUuid; + } + } + }), + ); + return convertedData; } } diff --git a/src/space/services/subspace/subspace-device.service.ts b/src/space/services/subspace/subspace-device.service.ts index 01c6f8a..3b453fd 100644 --- a/src/space/services/subspace/subspace-device.service.ts +++ b/src/space/services/subspace/subspace-device.service.ts @@ -70,16 +70,10 @@ export class SubspaceDeviceService { device.subspace = subspace; - console.log( - 'Starting to save device to subspace:', - new Date(), - device.subspace, - ); const newDevice = await this.deviceRepository.save(device); - console.log('Device saved to subspace:', new Date(), newDevice); return new SuccessResponseDto({ - data: device, + data: newDevice, message: 'Successfully associated device to subspace', }); } catch (error) {