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

View File

@ -7,7 +7,7 @@ import { ConfigModule } from '@nestjs/config';
import config from './config';
import { EmailService } from './util/email.service';
import { ErrorMessageService } from 'src/error-message/error-message.service';
import { TuyaService } from './integrations/tuya/tuya.service';
import { TuyaService } from './integrations/tuya/services/tuya.service';
@Module({
providers: [CommonService, EmailService, ErrorMessageService, TuyaService],
exports: [

View File

@ -80,6 +80,36 @@ export class ControllerRoute {
};
};
static SCENE = class {
public static readonly ROUTE = 'scene';
static ACTIONS = class {
public static readonly CREATE_TAP_TO_RUN_SCENE_SUMMARY =
'Create a Tap-to-Run Scene';
public static readonly CREATE_TAP_TO_RUN_SCENE_DESCRIPTION =
'Creates a new Tap-to-Run scene in Tuya and stores the scene in the local database.';
public static readonly DELETE_TAP_TO_RUN_SCENE_SUMMARY =
'Delete a Tap-to-Run Scene';
public static readonly DELETE_TAP_TO_RUN_SCENE_DESCRIPTION =
'Deletes a Tap-to-Run scene from Tuya and removes it from the local database.';
public static readonly TRIGGER_TAP_TO_RUN_SCENE_SUMMARY =
'Trigger a Tap-to-Run Scene';
public static readonly TRIGGER_TAP_TO_RUN_SCENE_DESCRIPTION =
'Triggers an existing Tap-to-Run scene in Tuya by scene UUID, executing its actions immediately.';
public static readonly GET_TAP_TO_RUN_SCENE_SUMMARY =
'Get Tap-to-Run Scene Details';
public static readonly GET_TAP_TO_RUN_SCENE_DESCRIPTION =
'Retrieves detailed information of a specific Tap-to-Run scene identified by the scene UUID.';
public static readonly UPDATE_TAP_TO_RUN_SCENE_SUMMARY =
'Update a Tap-to-Run Scene';
public static readonly UPDATE_TAP_TO_RUN_SCENE_DESCRIPTION =
'Updates an existing Tap-to-Run scene in Tuya and updates the scene in the local database, reflecting any new configurations or actions.';
};
};
static SPACE = class {
public static readonly ROUTE = '/communities/:communityUuid/spaces';
static ACTIONS = class {
@ -120,6 +150,17 @@ export class ControllerRoute {
};
};
static SPACE_SCENE = class {
public static readonly ROUTE =
'/communities/:communityUuid/spaces/:spaceUuid/scenes';
static ACTIONS = class {
public static readonly GET_TAP_TO_RUN_SCENE_BY_SPACE_SUMMARY =
'Retrieve Tap-to-Run Scenes by Space';
public static readonly GET_TAP_TO_RUN_SCENE_BY_SPACE_DESCRIPTION =
'Fetches all Tap-to-Run scenes associated with a specified space UUID. An optional query parameter can filter the results to show only scenes marked for the homepage display.';
};
};
static SPACE_USER = class {
public static readonly ROUTE =
'/communities/:communityUuid/spaces/:spaceUuid/user';

View File

@ -0,0 +1,2 @@
export * from './tuya.response.interface';
export * from './tap-to-run-action.interface';

View File

@ -0,0 +1,11 @@
export interface ConvertedExecutorProperty {
function_code?: string;
function_value?: any;
delay_seconds?: number;
}
export interface ConvertedAction {
entity_id: string;
action_executor: string;
executor_property?: ConvertedExecutorProperty;
}

View File

@ -0,0 +1,5 @@
export interface TuyaResponseInterface {
success: boolean;
msg?: string;
result: boolean;
}

View File

@ -1,6 +1,7 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConvertedAction, TuyaResponseInterface } from '../interfaces';
@Injectable()
export class TuyaService {
@ -86,4 +87,50 @@ export class TuyaService {
);
}
}
async addTapToRunScene(
spaceId: string,
sceneName: string,
actions: ConvertedAction[],
decisionExpr: string,
) {
const path = `/v2.0/cloud/scene/rule`;
const response = await this.tuya.request({
method: 'POST',
path,
body: {
space_id: spaceId,
name: sceneName,
type: 'scene',
decision_expr: decisionExpr,
actions: actions,
},
});
if (response.success) {
return response;
} else {
throw new HttpException(
`Error fetching scene rule: ${response.msg}`,
HttpStatus.BAD_REQUEST,
);
}
}
async triggerScene(sceneId: string): Promise<TuyaResponseInterface> {
const path = `/v2.0/cloud/scene/rule/${sceneId}/actions/trigger`;
const response: TuyaResponseInterface = await this.tuya.request({
method: 'POST',
path,
});
if (!response.success) {
throw new HttpException(
response.msg || 'Error triggering scene',
HttpStatus.BAD_REQUEST,
);
}
return response;
}
}

View File

@ -36,8 +36,6 @@ export function buildTypeORMIncludeQuery(
}
});
console.log(`Including relations for ${modelName}:`, relations);
return relations;
}

View File

@ -8,7 +8,7 @@ import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
import { SpacePermissionService } from '@app/common/helper/services';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { TuyaService } from '@app/common/integrations/tuya/tuya.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],

View File

@ -10,7 +10,7 @@ import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { CommunityDto } from '@app/common/modules/community/dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { TuyaService } from '@app/common/integrations/tuya/tuya.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@Injectable()
export class CommunityService {

View File

@ -8,24 +8,24 @@ import {
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import {
AddSceneIconDto,
AddSceneTapToRunDto,
GetSceneDto,
UpdateSceneTapToRunDto,
} from '../dtos/scene.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { DeleteSceneParamDto, SceneParamDto, SpaceParamDto } from '../dtos';
import { SceneParamDto } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { ControllerRoute } from '@app/common/constants/controller-route';
@ApiTags('Scene Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: 'scene',
path: ControllerRoute.SCENE.ROUTE,
})
export class SceneController {
constructor(private readonly sceneService: SceneService) {}
@ -33,81 +33,72 @@ export class SceneController {
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post('tap-to-run')
async addTapToRunScene(@Body() addSceneTapToRunDto: AddSceneTapToRunDto) {
const tapToRunScene =
await this.sceneService.addTapToRunScene(addSceneTapToRunDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Scene added successfully',
data: tapToRunScene,
};
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.CREATE_TAP_TO_RUN_SCENE_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.CREATE_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async addTapToRunScene(
@Body() addSceneTapToRunDto: AddSceneTapToRunDto,
): Promise<BaseResponseDto> {
return await this.sceneService.createScene(addSceneTapToRunDto);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('tap-to-run/:spaceUuid')
async getTapToRunSceneByUnit(
@Param() param: SpaceParamDto,
@Query() inHomePage: GetSceneDto,
) {
const tapToRunScenes = await this.sceneService.getTapToRunSceneBySpace(
param.spaceUuid,
inHomePage,
);
return tapToRunScenes;
@Delete('tap-to-run/:sceneUuid')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.DELETE_TAP_TO_RUN_SCENE_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.DELETE_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async deleteTapToRunScene(
@Param() param: SceneParamDto,
): Promise<BaseResponseDto> {
return await this.sceneService.deleteScene(param);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete('tap-to-run/:spaceUuid/:sceneUuid')
async deleteTapToRunScene(@Param() param: DeleteSceneParamDto) {
await this.sceneService.deleteScene(param);
return {
statusCode: HttpStatus.OK,
message: 'Scene Deleted Successfully',
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post('tap-to-run/trigger/:sceneUuid')
@Post('tap-to-run/:sceneUuid/trigger')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.TRIGGER_TAP_TO_RUN_SCENE_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.TRIGGER_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async triggerTapToRunScene(@Param() param: SceneParamDto) {
await this.sceneService.triggerTapToRunScene(param.sceneUuid);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Scene trigger successfully',
};
return await this.sceneService.triggerTapToRunScene(param.sceneUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('tap-to-run/details/:sceneUuid')
async getTapToRunSceneDetails(@Param() param: SceneParamDto) {
const tapToRunScenes = await this.sceneService.getTapToRunSceneDetails(
param.sceneUuid,
);
return tapToRunScenes;
@Get('tap-to-run/:sceneUuid')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_SUMMARY,
description: ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async getTapToRunSceneDetails(
@Param() param: SceneParamDto,
): Promise<BaseResponseDto> {
return await this.sceneService.getSceneByUuid(param.sceneUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('tap-to-run/:sceneUuid')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.UPDATE_TAP_TO_RUN_SCENE_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.UPDATE_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async updateTapToRunScene(
@Body() updateSceneTapToRunDto: UpdateSceneTapToRunDto,
@Param() param: SceneParamDto,
) {
const tapToRunScene = await this.sceneService.updateTapToRunScene(
return await this.sceneService.updateTapToRunScene(
updateSceneTapToRunDto,
param.sceneUuid,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Scene updated successfully',
data: tapToRunScene,
};
}
@ApiBearerAuth()

View File

@ -36,7 +36,7 @@ class ExecutorProperty {
public delaySeconds?: number;
}
class Action {
export class Action {
@ApiProperty({
description: 'Entity ID',
required: true,

View File

@ -1,3 +1,5 @@
import { Action } from '../dtos';
export interface AddTapToRunSceneInterface {
success: boolean;
msg?: string;
@ -25,4 +27,19 @@ export interface SceneDetailsResult {
id: string;
name: string;
type: string;
actions?: any;
status?: string;
}
export interface SceneDetails {
uuid: string;
sceneTuyaId: string;
name: string;
status: string;
icon?: string;
iconUuid?: string;
showInHome: boolean;
type: string;
actions: Action[];
spaceId: string;
}

View File

@ -12,7 +12,7 @@ import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { TuyaService } from '@app/common/integrations/tuya/tuya.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],

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,
);
}
}
}
}

View File

@ -1,4 +1,5 @@
export * from './space.controller';
export * from './space-user.controller';
export * from './space-device.controller';
export * from './space-scene.controller';
export * from './subspace';

View File

@ -0,0 +1,34 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SpaceSceneService } from '../services';
import { GetSceneDto } from '../../scene/dtos';
import { GetSpaceParam } from '../dtos';
@ApiTags('Space Module')
@Controller({
version: '1',
path: ControllerRoute.SPACE_SCENE.ROUTE,
})
export class SpaceSceneController {
constructor(private readonly sceneService: SpaceSceneService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary:
ControllerRoute.SPACE_SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_BY_SPACE_SUMMARY,
description:
ControllerRoute.SPACE_SCENE.ACTIONS
.GET_TAP_TO_RUN_SCENE_BY_SPACE_DESCRIPTION,
})
@Get()
async getTapToRunSceneByUnit(
@Param() params: GetSpaceParam,
@Query() inHomePage: GetSceneDto,
): Promise<BaseResponseDto> {
return await this.sceneService.getScenes(params, inHomePage);
}
}

View File

@ -2,3 +2,4 @@ export * from './space.service';
export * from './space-user.service';
export * from './space-device.service';
export * from './subspace';
export * from './space-scene.service';

View File

@ -1,4 +1,4 @@
import { TuyaService } from '@app/common/integrations/tuya/tuya.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@ -51,10 +51,7 @@ export class SpaceDeviceService {
});
}
private async validateCommunityAndSpace(
communityUuid: string,
spaceUuid: string,
) {
async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) {
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});

View File

@ -0,0 +1,55 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GetSpaceParam } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SpaceService } from './space.service';
import { SceneService } from '../../scene/services';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { GetSceneDto } from '../../scene/dtos';
@Injectable()
export class SpaceSceneService {
constructor(
private readonly spaceSevice: SpaceService,
private readonly sceneSevice: SceneService,
) {}
async getScenes(
params: GetSpaceParam,
getSceneDto: GetSceneDto,
): Promise<BaseResponseDto> {
try {
const { spaceUuid, communityUuid } = params;
await this.spaceSevice.validateCommunityAndSpace(
communityUuid,
spaceUuid,
);
const scenes = await this.sceneSevice.findScenesBySpace(
spaceUuid,
getSceneDto,
);
return new SuccessResponseDto({
message: `Scenes retrieved successfully for space ${spaceUuid}`,
data: scenes,
});
} catch (error) {
console.error('Error retrieving scenes:', error);
if (error instanceof BadRequestException) {
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
} else {
throw new HttpException(
'An error occurred while retrieving scenes',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
}

View File

@ -357,4 +357,29 @@ export class SpaceService {
return rootSpaces; // Return the root spaces with children nested within them
}
async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) {
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});
if (!community) {
this.throwNotFound('Community', communityUuid);
}
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, community: { uuid: communityUuid } },
relations: ['devices', 'devices.productDevice'],
});
if (!space) {
this.throwNotFound('Space', spaceUuid);
}
return space;
}
private throwNotFound(entity: string, uuid: string) {
throw new HttpException(
`${entity} with ID ${uuid} not found`,
HttpStatus.NOT_FOUND,
);
}
}

View File

@ -9,7 +9,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { TuyaService } from '@app/common/integrations/tuya/tuya.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface';

View File

@ -7,9 +7,11 @@ import {
SpaceUserController,
SubSpaceController,
SubSpaceDeviceController,
SpaceSceneController,
} from './controllers';
import {
SpaceDeviceService,
SpaceSceneService,
SpaceService,
SpaceUserService,
SubspaceDeviceService,
@ -25,8 +27,16 @@ import {
UserSpaceRepository,
} from '@app/common/modules/user/repositories';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { TuyaService } from '@app/common/integrations/tuya/tuya.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { SceneService } from '../scene/services';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { DeviceService } from 'src/device/services';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
@Module({
imports: [ConfigModule, SpaceRepositoryModule],
@ -36,6 +46,7 @@ import { ProductRepository } from '@app/common/modules/product/repositories';
SpaceDeviceController,
SubSpaceController,
SubSpaceDeviceController,
SpaceSceneController,
],
providers: [
SpaceService,
@ -51,6 +62,13 @@ import { ProductRepository } from '@app/common/modules/product/repositories';
UserSpaceRepository,
UserRepository,
SpaceUserService,
SpaceSceneService,
SceneService,
SceneIconRepository,
SceneRepository,
DeviceService,
DeviceStatusFirebaseService,
DeviceStatusLogRepository,
],
exports: [SpaceService],
})