import { Injectable, HttpException, HttpStatus, BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddAutomationDto, UpdateSceneTapToRunDto } from '../dtos'; import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface'; import { ConfigService } from '@nestjs/config'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; import { DeviceService } from 'src/device/services'; import { AddTapToRunSceneInterface, DeleteTapToRunSceneInterface, GetTapToRunSceneByUnitInterface, } from '../interface/automation.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; @Injectable() export class AutomationService { private tuya: TuyaContext; constructor( private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, private readonly deviceService: DeviceService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); this.tuya = new TuyaContext({ baseUrl: tuyaEuUrl, accessKey, secretKey, }); } async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { try { let unitSpaceTuyaId; if (!spaceTuyaId) { const unitDetails = await this.getUnitByUuid(addAutomationDto.unitUuid); unitSpaceTuyaId = unitDetails.spaceTuyaUuid; if (!unitDetails) { throw new BadRequestException('Invalid unit UUID'); } } else { unitSpaceTuyaId = spaceTuyaId; } const actions = addAutomationDto.actions.map((action) => { return { ...action, }; }); const convertedData = convertKeysToSnakeCase(actions); for (const action of convertedData) { if (action.action_executor === 'device_issue') { const device = await this.deviceService.getDeviceByDeviceUuid( action.entity_id, false, ); if (device) { action.entity_id = device.deviceTuyaUuid; } } } const path = `/v2.0/cloud/scene/rule`; const response: AddTapToRunSceneInterface = await this.tuya.request({ method: 'POST', path, body: { space_id: unitSpaceTuyaId, name: addAutomationDto.sceneName, type: 'scene', decision_expr: addAutomationDto.decisionExpr, actions: convertedData, }, }); if (!response.success) { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); } return { id: response.result.id, }; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException( err.message || 'Scene not found', err.status || HttpStatus.NOT_FOUND, ); } } } async getUnitByUuid(unitUuid: string): Promise { try { const unit = await this.spaceRepository.findOne({ where: { uuid: unitUuid, spaceType: { type: 'unit', }, }, relations: ['spaceType'], }); if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { throw new BadRequestException('Invalid unit UUID'); } return { uuid: unit.uuid, createdAt: unit.createdAt, updatedAt: unit.updatedAt, name: unit.spaceName, type: unit.spaceType.type, spaceTuyaUuid: unit.spaceTuyaUuid, }; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); } } } async getTapToRunSceneByUnit(unitUuid: string) { try { const unit = await this.getUnitByUuid(unitUuid); if (!unit.spaceTuyaUuid) { throw new BadRequestException('Invalid unit UUID'); } const path = `/v2.0/cloud/scene/rule?space_id=${unit.spaceTuyaUuid}&type=scene`; const response: GetTapToRunSceneByUnitInterface = await this.tuya.request( { method: 'GET', path, }, ); if (!response.success) { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); } return response.result.list.map((item) => { return { id: item.id, name: item.name, status: item.status, type: 'tap_to_run', }; }); } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException( err.message || 'Scene not found', err.status || HttpStatus.NOT_FOUND, ); } } } async deleteTapToRunScene( unitUuid: string, sceneId: string, spaceTuyaId = null, ) { try { let unitSpaceTuyaId; if (!spaceTuyaId) { const unitDetails = await this.getUnitByUuid(unitUuid); unitSpaceTuyaId = unitDetails.spaceTuyaUuid; if (!unitSpaceTuyaId) { throw new BadRequestException('Invalid unit UUID'); } } else { unitSpaceTuyaId = spaceTuyaId; } const path = `/v2.0/cloud/scene/rule?ids=${sceneId}&space_id=${unitSpaceTuyaId}`; const response: DeleteTapToRunSceneInterface = await this.tuya.request({ method: 'DELETE', path, }); if (!response.success) { throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); } return response; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException( err.message || 'Scene not found', err.status || HttpStatus.NOT_FOUND, ); } } } async triggerTapToRunScene(sceneId: string) { try { const path = `/v2.0/cloud/scene/rule/${sceneId}/actions/trigger`; const response: DeleteTapToRunSceneInterface = await this.tuya.request({ method: 'POST', path, }); if (!response.success) { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); } return response; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException( err.message || 'Scene not found', err.status || HttpStatus.NOT_FOUND, ); } } } async getTapToRunSceneDetails(sceneId: string, withSpaceId = false) { try { const path = `/v2.0/cloud/scene/rule/${sceneId}`; const response = await this.tuya.request({ method: 'GET', path, }); if (!response.success) { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); } const responseData = convertKeysToCamelCase(response.result); const actions = responseData.actions.map((action) => { return { ...action, }; }); for (const action of actions) { if (action.actionExecutor === 'device_issue') { const device = await this.deviceService.getDeviceByDeviceTuyaUuid( action.entityId, ); if (device) { action.entityId = device.uuid; } } } return { id: responseData.id, name: responseData.name, status: responseData.status, type: 'tap_to_run', actions: actions, ...(withSpaceId && { spaceId: responseData.spaceId }), }; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); } } } async updateTapToRunScene( updateSceneTapToRunDto: UpdateSceneTapToRunDto, sceneId: string, ) { try { const spaceTuyaId = await this.getTapToRunSceneDetails(sceneId, true); if (!spaceTuyaId.spaceId) { throw new HttpException("Scene doesn't exist", HttpStatus.NOT_FOUND); } const addSceneTapToRunDto: AddAutomationDto = { ...updateSceneTapToRunDto, unitUuid: null, }; const newTapToRunScene = await this.addAutomation( addSceneTapToRunDto, spaceTuyaId.spaceId, ); if (newTapToRunScene.id) { await this.deleteTapToRunScene(null, sceneId, spaceTuyaId.spaceId); return newTapToRunScene; } } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException( err.message || 'Scene not found', err.status || HttpStatus.NOT_FOUND, ); } } } }