diff --git a/libs/common/src/constants/automation.enum.ts b/libs/common/src/constants/automation.enum.ts index d1503cb..1713c9a 100644 --- a/libs/common/src/constants/automation.enum.ts +++ b/libs/common/src/constants/automation.enum.ts @@ -2,6 +2,8 @@ export enum ActionExecutorEnum { DEVICE_ISSUE = 'device_issue', DELAY = 'delay', RULE_TRIGGER = 'rule_trigger', + RULE_DISABLE = 'rule_disable', + RULE_ENABLE = 'rule_enable', } export enum EntityTypeEnum { diff --git a/libs/common/src/modules/automation/repositories/index.ts b/libs/common/src/modules/automation/repositories/index.ts new file mode 100644 index 0000000..de055c3 --- /dev/null +++ b/libs/common/src/modules/automation/repositories/index.ts @@ -0,0 +1 @@ +export * from './automation.repository'; diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts index 0a05f0e..810c8ec 100644 --- a/src/automation/automation.module.ts +++ b/src/automation/automation.module.ts @@ -15,6 +15,7 @@ import { SceneRepository, } from '@app/common/modules/scene/repositories'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -30,6 +31,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito SceneIconRepository, SceneRepository, SceneDeviceRepository, + AutomationRepository, ], exports: [AutomationService], }) diff --git a/src/automation/interface/automation.interface.ts b/src/automation/interface/automation.interface.ts index 01523a0..146ec56 100644 --- a/src/automation/interface/automation.interface.ts +++ b/src/automation/interface/automation.interface.ts @@ -60,4 +60,5 @@ export interface AddAutomationParams { effectiveTime: EffectiveTime; decisionExpr: string; spaceTuyaId: string; + spaceUuid: string; } diff --git a/src/automation/services/automation.service.ts b/src/automation/services/automation.service.ts index d28b944..166e2d3 100644 --- a/src/automation/services/automation.service.ts +++ b/src/automation/services/automation.service.ts @@ -21,7 +21,6 @@ import { AutomationDetailsResult, AutomationResponseData, Condition, - GetAutomationBySpaceInterface, } from '../interface/automation.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { @@ -35,6 +34,11 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; import { SceneRepository } from '@app/common/modules/scene/repositories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { AutomationEntity } from '@app/common/modules/automation/entities'; +import { DeleteTapToRunSceneInterface } from 'src/scene/interface/scene.interface'; @Injectable() export class AutomationService { @@ -46,6 +50,7 @@ export class AutomationService { private readonly tuyaService: TuyaService, private readonly sceneDeviceRepository: SceneDeviceRepository, private readonly sceneRepository: SceneRepository, + private readonly automationRepository: AutomationRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -57,7 +62,9 @@ export class AutomationService { }); } - async addAutomation(addAutomationDto: AddAutomationDto) { + async addAutomation( + addAutomationDto: AddAutomationDto, + ): Promise { try { const { automationName, @@ -65,30 +72,37 @@ export class AutomationService { decisionExpr, actions, conditions, + spaceUuid, } = addAutomationDto; - const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid); - const response = await this.add({ + const space = await this.getSpaceByUuid(spaceUuid); + const automation = await this.add({ automationName, effectiveTime, decisionExpr, actions, conditions, spaceTuyaId: space.spaceTuyaUuid, + spaceUuid, + }); + return new SuccessResponseDto({ + message: `Successfully created new automation with uuid ${automation.uuid}`, + data: automation, + statusCode: HttpStatus.CREATED, }); - return response; } catch (err) { - if (err instanceof BadRequestException) { - throw err; - } else { - throw new HttpException( - err.message || 'Automation not found', - err.status || HttpStatus.NOT_FOUND, - ); - } + console.error( + `Error in creating automation for space UUID ${addAutomationDto.spaceUuid}:`, + err.message, + ); + throw err instanceof HttpException + ? err + : new HttpException( + 'Failed to create automation', + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } - - async add(params: AddAutomationParams) { + async createAutomationExternalService(params: AddAutomationParams) { try { const formattedActions = await this.prepareActions(params.actions); const formattedCondition = await this.prepareConditions( @@ -104,14 +118,54 @@ export class AutomationService { formattedActions, ); - return { - id: response?.result.id, - }; - } catch (error) { - throw new HttpException( - error.message || 'Failed to add automation', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (!response.result?.id) { + throw new HttpException( + 'Failed to create automation in Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return response; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else if (err.message?.includes('tuya')) { + throw new HttpException( + 'API error: Failed to create automation', + HttpStatus.BAD_GATEWAY, + ); + } else { + throw new HttpException( + `An Internal error has been occured ${err}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + async add(params: AddAutomationParams) { + try { + const response = await this.createAutomationExternalService(params); + + const automation = await this.automationRepository.save({ + automationTuyaUuid: response.result.id, + space: { uuid: params.spaceUuid }, + }); + + return automation; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else if (err.message?.includes('tuya')) { + throw new HttpException( + 'API error: Failed to create automation', + HttpStatus.BAD_GATEWAY, + ); + } else { + throw new HttpException( + 'Database error: Failed to save automation', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } } @@ -145,27 +199,42 @@ export class AutomationService { async getAutomationBySpace(spaceUuid: string) { try { const space = await this.getSpaceByUuid(spaceUuid); + + const automationData = await this.automationRepository.find({ + where: { + space: { uuid: spaceUuid }, + disabled: false, + }, + relations: ['space'], + }); + if (!space.spaceTuyaUuid) { throw new BadRequestException(`Invalid space UUID ${spaceUuid}`); } + const automations = await Promise.all( + automationData.map(async (automation) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const automationDetails = await this.tuyaService.getSceneRule( + automation.automationTuyaUuid, + ); - const path = `/v2.0/cloud/scene/rule?space_id=${space.spaceTuyaUuid}&type=automation`; - const response: GetAutomationBySpaceInterface = await this.tuya.request({ - method: 'GET', - path, - }); - - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } - - return response.result.list - .filter((item) => item.name && !item.name.startsWith(AUTO_PREFIX)) - .map((item) => { return { - id: item.id, - name: item.name, - status: item.status, + uuid: automation.uuid, + ...automationDetails, + }; + }), + ); + + return automations + .filter( + (item: any) => + item.result.name && !item.result.name.startsWith(AUTO_PREFIX), + ) + .map((item: any) => { + return { + uuid: item.uuid, + name: item.result.name, + status: item.result.status, type: AUTOMATION_TYPE, }; }); @@ -180,7 +249,45 @@ export class AutomationService { } } } + async findAutomationBySpace(spaceUuid: string) { + try { + await this.getSpaceByUuid(spaceUuid); + const automationData = await this.automationRepository.find({ + where: { + space: { uuid: spaceUuid }, + disabled: false, + }, + relations: ['space'], + }); + + const automations = await Promise.all( + automationData.map(async (automation) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { actions, ...automationDetails } = + await this.getAutomation(automation); + + return automationDetails; + }), + ); + + return automations; + } catch (err) { + console.error( + `Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`, + err.message, + ); + + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException( + 'An error occurred while retrieving scenes', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } async getTapToRunSceneDetailsTuya( sceneUuid: string, ): Promise { @@ -213,13 +320,34 @@ export class AutomationService { } } } - async getAutomationDetails(automationUuid: string, withSpaceId = false) { + async getAutomationDetails(automationUuid: string) { try { - const path = `/v2.0/cloud/scene/rule/${automationUuid}`; - const response = await this.tuya.request({ - method: 'GET', - path, - }); + const automation = await this.findAutomation(automationUuid); + + const automationDetails = await this.getAutomation(automation); + + return automationDetails; + } catch (error) { + console.error( + `Error fetching automation details for automationUuid ${automationUuid}:`, + error, + ); + + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException( + 'An error occurred while retrieving automation details', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + async getAutomation(automation: AutomationEntity) { + try { + const response = await this.tuyaService.getSceneRule( + automation.automationTuyaUuid, + ); if (!response.success) { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); @@ -261,6 +389,11 @@ export class AutomationService { action.entityId = scene.uuid; action.iconUuid = scene.sceneIcon.uuid; action.icon = scene.sceneIcon.icon; + } else if (sceneDetails.type === ActionTypeEnum.AUTOMATION) { + const automation = await this.automationRepository.findOne({ + where: { automationTuyaUuid: action.entityId }, + }); + action.entityId = automation.uuid; } action.name = sceneDetails.name; action.type = sceneDetails.type; @@ -291,91 +424,95 @@ export class AutomationService { responseData.effectiveTime || {}; return { - id: responseData.id, + uuid: automation.uuid, name: responseData.name, status: responseData.status, type: 'automation', ...(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { spaceId, runningMode, ...rest } = responseData; + const { spaceId, id, runningMode, ...rest } = responseData; return rest; })(), actions, conditions, effectiveTime: effectiveTimeWithoutTimeZoneId, // Use modified effectiveTime - ...(withSpaceId && { spaceId: responseData.spaceId }), }; } catch (err) { if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + throw err; } else { - throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); + throw new HttpException( + `An error occurred while retrieving automation details for ${automation.uuid}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } + async findAutomation(sceneUuid: string): Promise { + const automation = await this.automationRepository.findOne({ + where: { uuid: sceneUuid }, + relations: ['space'], + }); + + if (!automation) { + throw new HttpException( + `Invalid automation with id ${sceneUuid}`, + HttpStatus.NOT_FOUND, + ); + } + return automation; + } async deleteAutomation(param: AutomationParamDto) { + const { automationUuid } = param; + try { - const { automationUuid } = param; - - const automation = await this.getAutomationDetails(automationUuid, true); - - if (!automation && !automation.spaceId) { - throw new HttpException( - `Invalid automationid ${automationUuid}`, - HttpStatus.BAD_REQUEST, - ); - } + const automationData = await this.findAutomation(automationUuid); + const space = await this.getSpaceByUuid(automationData.space.uuid); + await this.delete(automationData.automationTuyaUuid, space.spaceTuyaUuid); const existingSceneDevice = await this.sceneDeviceRepository.findOne({ - where: { automationTuyaUuid: automationUuid }, + where: { automationTuyaUuid: automationData.automationTuyaUuid }, }); if (existingSceneDevice) { await this.sceneDeviceRepository.delete({ - automationTuyaUuid: automationUuid, + automationTuyaUuid: automationData.automationTuyaUuid, }); } - - const response = this.tuyaService.deleteAutomation( - automation.spaceId, - automationUuid, + await this.automationRepository.update( + { + uuid: automationUuid, + }, + { disabled: true }, ); - return response; + return new SuccessResponseDto({ + message: `Automation with ID ${automationUuid} deleted successfully`, + }); } catch (err) { if (err instanceof HttpException) { throw err; } else { throw new HttpException( - err.message || 'Automation not found', + err.message || `Automation not found for id ${param.automationUuid}`, err.status || HttpStatus.NOT_FOUND, ); } } } - - async delete(tuyaSpaceId: string, automationUuid: string) { + async delete(tuyaAutomationId: string, tuyaSpaceId: string) { try { - const existingSceneDevice = await this.sceneDeviceRepository.findOne({ - where: { automationTuyaUuid: automationUuid }, - }); - - if (existingSceneDevice) { - await this.sceneDeviceRepository.delete({ - automationTuyaUuid: automationUuid, - }); - } - const response = await this.tuyaService.deleteAutomation( + const response = (await this.tuyaService.deleteSceneRule( + tuyaAutomationId, tuyaSpaceId, - automationUuid, - ); + )) as DeleteTapToRunSceneInterface; return response; - } catch (err) { - if (err instanceof HttpException) { - throw err; + } catch (error) { + if (error instanceof HttpException) { + throw error; } else { throw new HttpException( - err.message || 'Automation not found', - err.status || HttpStatus.NOT_FOUND, + 'Failed to delete automation rule in Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -429,17 +566,13 @@ export class AutomationService { automationUuid: string, ) { try { - const automation = await this.getAutomationDetails(automationUuid, true); - if (!automation.spaceId) { - throw new HttpException( - "Automation doesn't exist", - HttpStatus.NOT_FOUND, - ); - } + const automation = await this.findAutomation(automationUuid); + const space = await this.getSpaceByUuid(automation.space.uuid); + const updateTuyaAutomationResponse = await this.updateAutomationExternalService( - automation.spaceId, - automation.id, + space.spaceTuyaUuid, + automation.automationTuyaUuid, updateAutomationDto, ); @@ -449,6 +582,16 @@ export class AutomationService { HttpStatus.BAD_GATEWAY, ); } + const updatedScene = await this.automationRepository.update( + { uuid: automationUuid }, + { + space: { uuid: automation.space.uuid }, + }, + ); + return new SuccessResponseDto({ + data: updatedScene, + message: `Automation with ID ${automationUuid} updated successfully`, + }); } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException @@ -466,6 +609,7 @@ export class AutomationService { ) { const { isEnable, spaceUuid } = updateAutomationStatusDto; try { + const automation = await this.findAutomation(automationUuid); const space = await this.getSpaceByUuid(spaceUuid); if (!space.spaceTuyaUuid) { throw new HttpException( @@ -476,7 +620,7 @@ export class AutomationService { const response = await this.tuyaService.updateAutomationState( space.spaceTuyaUuid, - automationUuid, + automation.automationTuyaUuid, isEnable, ); @@ -521,6 +665,14 @@ export class AutomationService { action.entity_id = scene.sceneTuyaUuid; } } + } else if ( + action.action_executor === ActionExecutorEnum.RULE_DISABLE || + action.action_executor === ActionExecutorEnum.RULE_ENABLE + ) { + if (action.action_type === ActionTypeEnum.AUTOMATION) { + const automation = await this.findAutomation(action.entity_id); + action.entity_id = automation.automationTuyaUuid; + } } }), ); diff --git a/src/device/device.module.ts b/src/device/device.module.ts index 39b1be6..2a372cc 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -19,6 +19,7 @@ import { SceneRepository, } from '@app/common/modules/scene/repositories'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; @Module({ imports: [ ConfigModule, @@ -41,6 +42,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito SceneIconRepository, SceneRepository, SceneDeviceRepository, + AutomationRepository, ], exports: [DeviceService], }) diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index 9bbc0d5..3cf563b 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -18,6 +18,7 @@ import { SceneRepository, } from '@app/common/modules/scene/repositories'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -36,6 +37,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito SceneIconRepository, SceneRepository, SceneDeviceRepository, + AutomationRepository, ], exports: [DoorLockService], }) diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts index 9da0156..a6a1435 100644 --- a/src/scene/scene.module.ts +++ b/src/scene/scene.module.ts @@ -14,6 +14,7 @@ import { } from '@app/common/modules/scene/repositories'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -28,6 +29,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito SceneIconRepository, SceneRepository, SceneDeviceRepository, + AutomationRepository, ], exports: [SceneService], }) diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 42891c1..6ff986d 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -23,7 +23,10 @@ import { SceneDetailsResult, } from '../interface/scene.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; -import { ActionExecutorEnum } from '@app/common/constants/automation.enum'; +import { + ActionExecutorEnum, + ActionTypeEnum, +} from '@app/common/constants/automation.enum'; import { SceneIconRepository, SceneRepository, @@ -40,6 +43,7 @@ import { HttpStatusCode } from 'axios'; import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; import { DeviceService } from 'src/device/services'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; @Injectable() export class SceneService { @@ -48,6 +52,7 @@ export class SceneService { private readonly sceneIconRepository: SceneIconRepository, private readonly sceneRepository: SceneRepository, private readonly sceneDeviceRepository: SceneDeviceRepository, + private readonly automationRepository: AutomationRepository, private readonly tuyaService: TuyaService, @Inject(forwardRef(() => DeviceService)) private readonly deviceService: DeviceService, @@ -460,6 +465,12 @@ export class SceneService { ); if (sceneDetails.id) { + if (sceneDetails.type === ActionTypeEnum.AUTOMATION) { + const automation = await this.automationRepository.findOne({ + where: { automationTuyaUuid: action.entityId }, + }); + action.entityId = automation.uuid; + } action.name = sceneDetails.name; action.type = sceneDetails.type; } @@ -568,6 +579,17 @@ export class SceneService { if (device) { action.entity_id = device.deviceTuyaUuid; } + } else if ( + action.action_executor === ActionExecutorEnum.RULE_DISABLE || + action.action_executor === ActionExecutorEnum.RULE_ENABLE + ) { + const automation = await this.automationRepository.findOne({ + where: { uuid: action.entity_id }, + }); + + if (automation) { + action.entity_id = automation.automationTuyaUuid; + } } }), ); diff --git a/src/space/space.module.ts b/src/space/space.module.ts index 9cc0519..8289fd2 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -70,6 +70,7 @@ import { InviteUserRepository, InviteUserSpaceRepository, } from '@app/common/modules/Invite-user/repositiories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; export const CommandHandlers = [DisableSpaceHandler]; @@ -128,6 +129,7 @@ export const CommandHandlers = [DisableSpaceHandler]; TimeZoneRepository, InviteUserRepository, InviteUserSpaceRepository, + AutomationRepository, ], exports: [SpaceService], }) diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index e768ad5..3aaf3a5 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -20,6 +20,7 @@ import { SceneRepository, } from '@app/common/modules/scene/repositories'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -39,6 +40,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito SceneIconRepository, SceneRepository, SceneDeviceRepository, + AutomationRepository, ], exports: [VisitorPasswordService], })