Files
backend/src/scene/services/scene.service.ts
2024-11-04 00:00:33 -06:00

577 lines
16 KiB
TypeScript

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<BaseResponseDto> {
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<SceneEntity> {
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<SceneDetailsResult> {
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<BaseResponseDto> {
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<SceneDetails> {
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<BaseResponseDto> {
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<SceneEntity> {
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<ConvertedAction[]> {
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<SceneIconEntity> {
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,
);
}
}
}
}