Fixed scene

This commit is contained in:
hannathkadher
2024-11-02 19:36:56 +04:00
parent 2b6168058a
commit 7e9894b1d3
22 changed files with 672 additions and 413 deletions
+360 -344
View File
@@ -6,19 +6,19 @@ import {
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
Action,
AddSceneIconDto,
AddSceneTapToRunDto,
DeleteSceneParamDto,
GetSceneDto,
SceneParamDto,
UpdateSceneTapToRunDto,
} from '../dtos';
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,
SceneDetails,
SceneDetailsResult,
} from '../interface/scene.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
@@ -28,354 +28,241 @@ import {
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
import { SceneEntity } from '@app/common/modules/scene/entities';
import { TuyaService } from '@app/common/integrations/tuya/tuya.service';
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 {
private tuya: TuyaContext;
constructor(
private readonly configService: ConfigService,
private readonly spaceRepository: SpaceRepository,
private readonly sceneIconRepository: SceneIconRepository,
private readonly sceneRepository: SceneRepository,
private readonly deviceService: DeviceService,
private readonly tuyaService: TuyaService,
) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
const tuyaEuUrl = this.configService.get<string>('tuya-config.TUYA_EU_URL');
this.tuya = new TuyaContext({
baseUrl: tuyaEuUrl,
accessKey,
secretKey,
});
}
) {}
async addTapToRunScene(
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,
spaceTuyaId = null,
spaceUuid?: string,
) {
const { sceneName, decisionExpr, actions } = addSceneTapToRunDto;
try {
let tuyaSpaceId;
if (!spaceTuyaId) {
const space = await this.getSpaceByUuid(addSceneTapToRunDto.spaceUuid);
tuyaSpaceId = space.spaceTuyaUuid;
if (!space) {
throw new BadRequestException(
`Invalid space UUID ${addSceneTapToRunDto.spaceUuid}`,
);
}
} else {
tuyaSpaceId = spaceTuyaId;
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,
);
}
const actions = addSceneTapToRunDto.actions.map((action) => {
return {
...action,
};
});
const convertedData = convertKeysToSnakeCase(actions);
for (const action of convertedData) {
if (action.action_executor === ActionExecutorEnum.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: tuyaSpaceId,
name: addSceneTapToRunDto.sceneName,
type: 'scene',
decision_expr: addSceneTapToRunDto.decisionExpr,
actions: convertedData,
},
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
} else {
const defaultSceneIcon = await this.sceneIconRepository.findOne({
where: { iconType: SceneIconType.Default },
});
await this.sceneRepository.save({
sceneTuyaUuid: response.result.id,
sceneIcon: {
uuid: addSceneTapToRunDto.iconUuid
? addSceneTapToRunDto.iconUuid
: defaultSceneIcon.uuid,
},
showInHomePage: addSceneTapToRunDto.showInHomePage,
spaceUuid: spaceUuid ? spaceUuid : addSceneTapToRunDto.spaceUuid,
});
}
return {
id: response.result.id,
};
return response;
} catch (err) {
console.log(err);
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
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(
err.message || `Scene not found`,
err.status || HttpStatus.NOT_FOUND,
'An Internal error has been occured',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async getSpaceByUuid(spaceUuid: string) {
async findScenesBySpace(spaceUuid: string, filter: GetSceneDto) {
try {
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
},
relations: ['community'],
});
if (!space) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
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; // Re-throw BadRequestException
} else {
throw new HttpException(
`Space with id ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
}
}
async getTapToRunSceneBySpace(spaceUuid: string, inHomePage: GetSceneDto) {
try {
const showInHomePage = inHomePage?.showInHomePage;
await this.getSpaceByUuid(spaceUuid);
const showInHomePage = filter?.showInHomePage;
const scenesData = await this.sceneRepository.find({
where: {
spaceUuid,
...(showInHomePage ? { showInHomePage } : {}),
...(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) => {
const sceneData = await this.getTapToRunSceneDetails(
scene.uuid,
false,
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { actions, ...rest } = sceneData;
return {
uuid: scene.uuid,
...rest,
};
const { actions, ...sceneDetails } = await this.getScene(scene);
return sceneDetails;
}),
);
return scenes;
} 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(param: DeleteSceneParamDto, spaceTuyaId = null) {
const { spaceUuid, sceneUuid } = param;
try {
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 path = `/v2.0/cloud/scene/rule?ids=${sceneUuid}&space_id=${tuyaSpaceId}`;
const response: DeleteTapToRunSceneInterface = await this.tuya.request({
method: 'DELETE',
path,
});
console.log(path);
if (!response.success) {
throw new HttpException('Scene not found', HttpStatus.NOT_FOUND);
} else {
await this.sceneRepository.delete({ sceneTuyaUuid: sceneUuid });
}
return response;
} catch (err) {
console.log(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) {
const scene = await this.sceneRepository.findOne({
where: { uuid: sceneId },
relations: ['sceneIcon'],
});
if (!scene) {
throw new HttpException(
`Scene with ${sceneId} is not found`,
HttpStatus.NOT_FOUND,
console.error(
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
err.message,
);
}
try {
const sceneTuyaUuid = scene.sceneTuyaUuid;
const path = `/v2.0/cloud/scene/rule/${sceneTuyaUuid}`;
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 === 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.getTapToRunSceneDetailsTuya(
action.entityId,
);
if (sceneDetails.id) {
action.name = sceneDetails.name;
action.type = sceneDetails.type;
}
}
}
return {
id: responseData.id,
name: responseData.name,
status: responseData.status,
icon: scene.sceneIcon?.icon,
iconUuid: scene.sceneIcon?.uuid,
showInHome: scene.showInHomePage,
type: 'tap_to_run',
actions: actions,
...(withSpaceId && { spaceId: responseData.spaceId }),
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
throw err;
} else {
console.log(err);
throw new HttpException(
`Scene not found for ${sceneId}`,
HttpStatus.NOT_FOUND,
'An error occurred while retrieving scenes',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async getTapToRunSceneDetailsTuya(
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 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 response = await this.tuyaService.getSceneRule(sceneId);
const camelCaseResponse = convertKeysToCamelCase(response);
const { id, name, type } = camelCaseResponse.result;
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) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
console.error(
`Error fetching scene details for scene ID ${sceneId}:`,
err,
);
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
'Scene not found for Tuya',
HttpStatus.NOT_FOUND,
'An error occurred while fetching scene details from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -385,23 +272,9 @@ export class SceneService {
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
sceneUuid: string,
) {
const scene = await this.sceneRepository.findOne({
where: { uuid: sceneUuid },
});
if (!scene) {
throw new HttpException(
`Scene with ${sceneUuid} is not found`,
HttpStatus.NOT_FOUND,
);
}
try {
const spaceTuyaId = await this.getTapToRunSceneDetails(sceneUuid, true);
if (!spaceTuyaId.spaceId) {
throw new HttpException("Scene doesn't exist", HttpStatus.NOT_FOUND);
}
const scene = await this.findScene(sceneUuid);
const space = await this.getSpaceByUuid(scene.spaceUuid);
const addSceneTapToRunDto: AddSceneTapToRunDto = {
...updateSceneTapToRunDto,
@@ -410,38 +283,38 @@ export class SceneService {
showInHomePage: updateSceneTapToRunDto.showInHomePage,
};
const newTapToRunScene = await this.addTapToRunScene(
const createdTuyaSceneResponse = await this.createSceneExternalService(
space.spaceTuyaUuid,
addSceneTapToRunDto,
spaceTuyaId.spaceId,
scene.spaceUuid,
);
const newSceneTuyaUuid = createdTuyaSceneResponse.result?.id;
const param: DeleteSceneParamDto = {
spaceUuid: scene.spaceUuid,
sceneUuid: scene.sceneTuyaUuid,
};
console.log(param);
if (newTapToRunScene.id) {
await this.deleteTapToRunScene(param, spaceTuyaId.spaceId);
await this.sceneRepository.update(
{ sceneTuyaUuid: sceneUuid },
{
sceneTuyaUuid: newTapToRunScene.id,
showInHomePage: addSceneTapToRunDto.showInHomePage,
sceneIcon: {
uuid: addSceneTapToRunDto.iconUuid,
},
spaceUuid: scene.spaceUuid,
},
if (!newSceneTuyaUuid) {
throw new HttpException(
`Failed to create a external new scene`,
HttpStatus.BAD_GATEWAY,
);
return newTapToRunScene;
}
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) {
console.log(err);
if (err instanceof BadRequestException) {
if (err instanceof HttpException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
@@ -469,6 +342,7 @@ export class SceneService {
);
}
}
async getAllIcons() {
try {
const icons = await this.sceneIconRepository.find();
@@ -487,25 +361,99 @@ export class SceneService {
}
}
async deleteScene(params: DeleteSceneParamDto) {
async getSceneByUuid(sceneUuid: string): Promise<BaseResponseDto> {
try {
const { sceneUuid, spaceUuid } = params;
const scene = await this.findScene(sceneUuid);
const space = await this.getSpaceByUuid(spaceUuid);
if (!space.spaceTuyaUuid) {
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(
`Invalid space UUID: ${spaceUuid}`,
HttpStatus.BAD_REQUEST,
'An error occurred while retrieving scene details',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const response = await this.delete(
}
}
async getScene(scene: SceneEntity): Promise<SceneDetails> {
try {
const { actions, name, status } = await this.fetchSceneDetailsFromTuya(
scene.sceneTuyaUuid,
space.spaceTuyaUuid,
);
return response;
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; // Re-throw existing HttpException
throw err;
} else {
throw new HttpException(
err.message || `Scene not found for id ${params.sceneUuid}`,
@@ -517,9 +465,8 @@ export class SceneService {
async findScene(sceneUuid: string): Promise<SceneEntity> {
const scene = await this.sceneRepository.findOne({
where: {
uuid: sceneUuid,
},
where: { uuid: sceneUuid },
relations: ['sceneIcon'],
});
if (!scene) {
@@ -549,4 +496,73 @@ export class SceneService {
}
}
}
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,
);
}
}
}
}