import { Injectable, HttpException, HttpStatus, BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { Action, AddSceneIconDto, AddSceneTapToRunDto, GetSceneDto, SceneParamDto, UpdateSceneTapToRunDto, } from '../dtos'; import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; import { DeviceService } from 'src/device/services'; import { AddTapToRunSceneInterface, DeleteTapToRunSceneInterface, SceneDetails, SceneDetailsResult, } from '../interface/scene.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { ActionExecutorEnum } from '@app/common/constants/automation.enum'; import { SceneIconRepository, SceneRepository, } from '@app/common/modules/scene/repositories'; import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; import { SceneEntity, SceneIconEntity, } from '@app/common/modules/scene/entities'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { HttpStatusCode } from 'axios'; import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; @Injectable() export class SceneService { constructor( private readonly spaceRepository: SpaceRepository, private readonly sceneIconRepository: SceneIconRepository, private readonly sceneRepository: SceneRepository, private readonly deviceService: DeviceService, private readonly tuyaService: TuyaService, ) {} async createScene( addSceneTapToRunDto: AddSceneTapToRunDto, ): Promise { try { const { spaceUuid } = addSceneTapToRunDto; const space = await this.getSpaceByUuid(spaceUuid); const scene = await this.create(space.spaceTuyaUuid, addSceneTapToRunDto); return new SuccessResponseDto({ message: `Successfully created new scene with uuid ${scene.uuid}`, data: scene, statusCode: HttpStatus.CREATED, }); } catch (err) { console.error( `Error in createScene for space UUID ${addSceneTapToRunDto.spaceUuid}:`, err.message, ); throw err instanceof HttpException ? err : new HttpException( 'Failed to create scene', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async create( spaceTuyaUuid: string, addSceneTapToRunDto: AddSceneTapToRunDto, ): Promise { const { iconUuid, showInHomePage, spaceUuid } = addSceneTapToRunDto; try { const [defaultSceneIcon] = await Promise.all([ this.getDefaultSceneIcon(), ]); if (!defaultSceneIcon) { throw new HttpException( 'Default scene icon not found', HttpStatus.INTERNAL_SERVER_ERROR, ); } const response = await this.createSceneExternalService( spaceTuyaUuid, addSceneTapToRunDto, ); const scene = await this.sceneRepository.save({ sceneTuyaUuid: response.result.id, sceneIcon: { uuid: iconUuid || defaultSceneIcon.uuid }, showInHomePage, spaceUuid, }); return scene; } catch (err) { if (err instanceof HttpException) { throw err; } else if (err.message?.includes('tuya')) { throw new HttpException( 'API error: Failed to create scene', HttpStatus.BAD_GATEWAY, ); } else { throw new HttpException( 'Database error: Failed to save scene', HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async createSceneExternalService( spaceTuyaUuid: string, addSceneTapToRunDto: AddSceneTapToRunDto, ) { const { sceneName, decisionExpr, actions } = addSceneTapToRunDto; try { const formattedActions = await this.prepareActions(actions); const response = (await this.tuyaService.addTapToRunScene( spaceTuyaUuid, sceneName, formattedActions, decisionExpr, )) as AddTapToRunSceneInterface; if (!response.result?.id) { throw new HttpException( 'Failed to create scene 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 scene', HttpStatus.BAD_GATEWAY, ); } else { throw new HttpException( 'An Internal error has been occured', HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async findScenesBySpace(spaceUuid: string, filter: GetSceneDto) { try { await this.getSpaceByUuid(spaceUuid); const showInHomePage = filter?.showInHomePage; const scenesData = await this.sceneRepository.find({ where: { spaceUuid, ...(showInHomePage !== undefined ? { showInHomePage } : {}), }, }); if (!scenesData.length) { throw new HttpException( `No scenes found for space UUID ${spaceUuid}`, HttpStatus.NOT_FOUND, ); } const scenes = await Promise.all( scenesData.map(async (scene) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { actions, ...sceneDetails } = await this.getScene(scene); return sceneDetails; }), ); return scenes; } catch (err) { console.error( `Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`, err.message, ); if (err instanceof BadRequestException) { throw err; } else { throw new HttpException( 'An error occurred while retrieving scenes', HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async triggerTapToRunScene(sceneUuid: string) { try { const scene = await this.findScene(sceneUuid); await this.tuyaService.triggerScene(scene.sceneTuyaUuid); return new SuccessResponseDto({ message: `Scene with ID ${sceneUuid} triggered successfully`, }); } catch (err) { if (err instanceof HttpException) { throw err; } else { throw new HttpException( err.message || 'Scene not found', err.status || HttpStatus.NOT_FOUND, ); } } } async fetchSceneDetailsFromTuya( sceneId: string, ): Promise { try { const response = await this.tuyaService.getSceneRule(sceneId); const camelCaseResponse = convertKeysToCamelCase(response); const { id, name, type, status, actions: tuyaActions = [], } = camelCaseResponse.result; const actions = tuyaActions.map((action) => ({ ...action })); return { id, name, type, status, actions, } as SceneDetailsResult; } catch (err) { console.error( `Error fetching scene details for scene ID ${sceneId}:`, err, ); if (err instanceof HttpException) { throw err; } else { throw new HttpException( 'An error occurred while fetching scene details from Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async updateTapToRunScene( updateSceneTapToRunDto: UpdateSceneTapToRunDto, sceneUuid: string, ) { try { const scene = await this.findScene(sceneUuid); const space = await this.getSpaceByUuid(scene.spaceUuid); const addSceneTapToRunDto: AddSceneTapToRunDto = { ...updateSceneTapToRunDto, spaceUuid: scene.spaceUuid, iconUuid: updateSceneTapToRunDto.iconUuid, showInHomePage: updateSceneTapToRunDto.showInHomePage, }; const createdTuyaSceneResponse = await this.createSceneExternalService( space.spaceTuyaUuid, addSceneTapToRunDto, ); const newSceneTuyaUuid = createdTuyaSceneResponse.result?.id; if (!newSceneTuyaUuid) { throw new HttpException( `Failed to create a external new scene`, HttpStatus.BAD_GATEWAY, ); } await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid); const updatedScene = await this.sceneRepository.update( { uuid: sceneUuid }, { sceneTuyaUuid: newSceneTuyaUuid, showInHomePage: addSceneTapToRunDto.showInHomePage, sceneIcon: { uuid: addSceneTapToRunDto.iconUuid, }, spaceUuid: scene.spaceUuid, }, ); return new SuccessResponseDto({ data: updatedScene, message: `Scene with ID ${sceneUuid} updated successfully`, }); } catch (err) { if (err instanceof HttpException) { throw err; // Re-throw BadRequestException } else { throw new HttpException( err.message || `Scene not found for id ${sceneUuid}`, err.status || HttpStatus.NOT_FOUND, ); } } } async addSceneIcon(addSceneIconDto: AddSceneIconDto) { try { const icon = await this.sceneIconRepository.save({ icon: addSceneIconDto.icon, }); return icon; } catch (err) { throw new HttpException( { status: HttpStatus.INTERNAL_SERVER_ERROR, error: 'Failed to add scene icon', message: err.message || 'Unexpected error occurred', }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getAllIcons() { try { const icons = await this.sceneIconRepository.find({ where: { iconType: SceneIconType.Other }, }); // Remove duplicates based on 'icon' property const uniqueIcons = icons.filter( (icon, index, self) => index === self.findIndex((t) => t.icon === icon.icon), ); return uniqueIcons; } catch (err) { throw new HttpException( { statusCode: HttpStatus.INTERNAL_SERVER_ERROR, message: 'Failed to fetch icons', error: err.message || 'An unexpected error occurred while fetching icons', }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getSceneByUuid(sceneUuid: string): Promise { try { const scene = await this.findScene(sceneUuid); const sceneDetails = await this.getScene(scene); return new SuccessResponseDto({ data: sceneDetails, message: `Scene details for ${sceneUuid} retrieved successfully`, }); } catch (error) { console.error( `Error fetching scene details for sceneUuid ${sceneUuid}:`, error, ); if (error instanceof HttpException) { throw error; } else { throw new HttpException( 'An error occurred while retrieving scene details', HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async getScene(scene: SceneEntity): Promise { try { const { actions, name, status } = await this.fetchSceneDetailsFromTuya( scene.sceneTuyaUuid, ); for (const action of actions) { if (action.actionExecutor === ActionExecutorEnum.DEVICE_ISSUE) { const device = await this.deviceService.getDeviceByDeviceTuyaUuid( action.entityId, ); if (device) { action.entityId = device.uuid; } } else if ( action.actionExecutor !== ActionExecutorEnum.DEVICE_ISSUE && action.actionExecutor !== ActionExecutorEnum.DELAY ) { const sceneDetails = await this.fetchSceneDetailsFromTuya( action.entityId, ); if (sceneDetails.id) { action.name = sceneDetails.name; action.type = sceneDetails.type; } } } return { uuid: scene.uuid, sceneTuyaId: scene.sceneTuyaUuid, name, status, icon: scene.sceneIcon?.icon, iconUuid: scene.sceneIcon?.uuid, showInHome: scene.showInHomePage, type: 'tap_to_run', actions, spaceId: scene.spaceUuid, }; } catch (err) { if (err instanceof BadRequestException) { throw err; } else { console.log(err); throw new HttpException( `An error occurred while retrieving scene details for ${scene.uuid}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async deleteScene(params: SceneParamDto): Promise { try { const { sceneUuid } = params; const scene = await this.findScene(sceneUuid); const space = await this.getSpaceByUuid(scene.spaceUuid); await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid); return new SuccessResponseDto({ message: `Scene with ID ${sceneUuid} deleted successfully`, }); } catch (err) { if (err instanceof HttpException) { throw err; } else { throw new HttpException( err.message || `Scene not found for id ${params.sceneUuid}`, err.status || HttpStatus.NOT_FOUND, ); } } } async findScene(sceneUuid: string): Promise { const scene = await this.sceneRepository.findOne({ where: { uuid: sceneUuid }, relations: ['sceneIcon'], }); if (!scene) { throw new HttpException( `Invalid scene with id ${sceneUuid}`, HttpStatus.NOT_FOUND, ); } return scene; } async delete(tuyaSceneId: string, tuyaSpaceId: string) { try { const response = (await this.tuyaService.deleteSceneRule( tuyaSceneId, tuyaSpaceId, )) as DeleteTapToRunSceneInterface; return response; } catch (error) { if (error instanceof HttpException) { throw error; } else { throw new HttpException( 'Failed to delete scene rule in Tuya', HttpStatus.INTERNAL_SERVER_ERROR, ); } } } private async prepareActions(actions: Action[]): Promise { const convertedData = convertKeysToSnakeCase(actions) as ConvertedAction[]; 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) { action.entity_id = device.deviceTuyaUuid; } } }), ); return convertedData; } private async getDefaultSceneIcon(): Promise { const defaultIcon = await this.sceneIconRepository.findOne({ where: { iconType: SceneIconType.Default }, }); return defaultIcon; } async getSpaceByUuid(spaceUuid: string) { try { const space = await this.spaceRepository.findOne({ where: { uuid: spaceUuid, }, relations: ['community'], }); if (!space) { throw new HttpException( `Invalid space UUID ${spaceUuid}`, HttpStatusCode.BadRequest, ); } if (!space.community.externalId) { throw new HttpException( `Space doesn't have any association with tuya${spaceUuid}`, HttpStatusCode.BadRequest, ); } return { uuid: space.uuid, createdAt: space.createdAt, updatedAt: space.updatedAt, name: space.spaceName, spaceTuyaUuid: space.community.externalId, }; } catch (err) { if (err instanceof BadRequestException) { console.log(err); throw err; } else { throw new HttpException( `Space with id ${spaceUuid} not found`, HttpStatus.NOT_FOUND, ); } } } }