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 config from './config';
import { EmailService } from './util/email.service'; import { EmailService } from './util/email.service';
import { ErrorMessageService } from 'src/error-message/error-message.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({ @Module({
providers: [CommonService, EmailService, ErrorMessageService, TuyaService], providers: [CommonService, EmailService, ErrorMessageService, TuyaService],
exports: [ 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 { static SPACE = class {
public static readonly ROUTE = '/communities/:communityUuid/spaces'; public static readonly ROUTE = '/communities/:communityUuid/spaces';
static ACTIONS = class { 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 { static SPACE_USER = class {
public static readonly ROUTE = public static readonly ROUTE =
'/communities/:communityUuid/spaces/:spaceUuid/user'; '/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 { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConvertedAction, TuyaResponseInterface } from '../interfaces';
@Injectable() @Injectable()
export class TuyaService { 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; 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 { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
import { SpacePermissionService } from '@app/common/helper/services'; import { SpacePermissionService } from '@app/common/helper/services';
import { CommunityRepository } from '@app/common/modules/community/repositories'; 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({ @Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], 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 { CommunityRepository } from '@app/common/modules/community/repositories';
import { CommunityDto } from '@app/common/modules/community/dtos'; import { CommunityDto } from '@app/common/modules/community/dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; 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() @Injectable()
export class CommunityService { export class CommunityService {

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { Action } from '../dtos';
export interface AddTapToRunSceneInterface { export interface AddTapToRunSceneInterface {
success: boolean; success: boolean;
msg?: string; msg?: string;
@ -25,4 +27,19 @@ export interface SceneDetailsResult {
id: string; id: string;
name: string; name: string;
type: 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, SceneIconRepository,
SceneRepository, SceneRepository,
} from '@app/common/modules/scene/repositories'; } 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({ @Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],

View File

@ -6,19 +6,19 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories';
import { import {
Action,
AddSceneIconDto, AddSceneIconDto,
AddSceneTapToRunDto, AddSceneTapToRunDto,
DeleteSceneParamDto,
GetSceneDto, GetSceneDto,
SceneParamDto,
UpdateSceneTapToRunDto, UpdateSceneTapToRunDto,
} from '../dtos'; } from '../dtos';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { DeviceService } from 'src/device/services'; import { DeviceService } from 'src/device/services';
import { import {
AddTapToRunSceneInterface, AddTapToRunSceneInterface,
DeleteTapToRunSceneInterface, DeleteTapToRunSceneInterface,
SceneDetails,
SceneDetailsResult, SceneDetailsResult,
} from '../interface/scene.interface'; } from '../interface/scene.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
@ -28,354 +28,241 @@ import {
SceneRepository, SceneRepository,
} from '@app/common/modules/scene/repositories'; } from '@app/common/modules/scene/repositories';
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
import { SceneEntity } from '@app/common/modules/scene/entities'; import {
import { TuyaService } from '@app/common/integrations/tuya/tuya.service'; 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() @Injectable()
export class SceneService { export class SceneService {
private tuya: TuyaContext;
constructor( constructor(
private readonly configService: ConfigService,
private readonly spaceRepository: SpaceRepository, private readonly spaceRepository: SpaceRepository,
private readonly sceneIconRepository: SceneIconRepository, private readonly sceneIconRepository: SceneIconRepository,
private readonly sceneRepository: SceneRepository, private readonly sceneRepository: SceneRepository,
private readonly deviceService: DeviceService, private readonly deviceService: DeviceService,
private readonly tuyaService: TuyaService, 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, addSceneTapToRunDto: AddSceneTapToRunDto,
spaceTuyaId = null,
spaceUuid?: string,
) { ) {
const { sceneName, decisionExpr, actions } = addSceneTapToRunDto;
try { try {
let tuyaSpaceId; const formattedActions = await this.prepareActions(actions);
if (!spaceTuyaId) {
const space = await this.getSpaceByUuid(addSceneTapToRunDto.spaceUuid); const response = (await this.tuyaService.addTapToRunScene(
tuyaSpaceId = space.spaceTuyaUuid; spaceTuyaUuid,
if (!space) { sceneName,
throw new BadRequestException( formattedActions,
`Invalid space UUID ${addSceneTapToRunDto.spaceUuid}`, decisionExpr,
); )) as AddTapToRunSceneInterface;
}
} else { if (!response.result?.id) {
tuyaSpaceId = spaceTuyaId; throw new HttpException(
'Failed to create scene in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
const actions = addSceneTapToRunDto.actions.map((action) => { return response;
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,
};
} catch (err) { } catch (err) {
console.log(err); if (err instanceof HttpException) {
if (err instanceof BadRequestException) { throw err;
throw err; // Re-throw BadRequestException } else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create scene',
HttpStatus.BAD_GATEWAY,
);
} else { } else {
throw new HttpException( throw new HttpException(
err.message || `Scene not found`, 'An Internal error has been occured',
err.status || HttpStatus.NOT_FOUND, HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
} }
async getSpaceByUuid(spaceUuid: string) { async findScenesBySpace(spaceUuid: string, filter: GetSceneDto) {
try { try {
const space = await this.spaceRepository.findOne({ await this.getSpaceByUuid(spaceUuid);
where: { const showInHomePage = filter?.showInHomePage;
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;
const scenesData = await this.sceneRepository.find({ const scenesData = await this.sceneRepository.find({
where: { where: {
spaceUuid, 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( const scenes = await Promise.all(
scenesData.map(async (scene) => { scenesData.map(async (scene) => {
const sceneData = await this.getTapToRunSceneDetails(
scene.uuid,
false,
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { actions, ...rest } = sceneData; const { actions, ...sceneDetails } = await this.getScene(scene);
return {
uuid: scene.uuid, return sceneDetails;
...rest,
};
}), }),
); );
return scenes; return scenes;
} catch (err) { } catch (err) {
if (err instanceof BadRequestException) { console.error(
throw err; // Re-throw BadRequestException `Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
} else { err.message,
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,
); );
}
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) { if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException throw err;
} else { } else {
console.log(err);
throw new HttpException( throw new HttpException(
`Scene not found for ${sceneId}`, 'An error occurred while retrieving scenes',
HttpStatus.NOT_FOUND, 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, sceneId: string,
): Promise<SceneDetailsResult> { ): Promise<SceneDetailsResult> {
try { try {
const path = `/v2.0/cloud/scene/rule/${sceneId}`; const response = await this.tuyaService.getSceneRule(sceneId);
const response = await this.tuya.request({
method: 'GET',
path,
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
const camelCaseResponse = convertKeysToCamelCase(response); 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 { return {
id, id,
name, name,
type, type,
status,
actions,
} as SceneDetailsResult; } as SceneDetailsResult;
} catch (err) { } catch (err) {
if (err instanceof BadRequestException) { console.error(
throw err; // Re-throw BadRequestException `Error fetching scene details for scene ID ${sceneId}:`,
err,
);
if (err instanceof HttpException) {
throw err;
} else { } else {
throw new HttpException( throw new HttpException(
'Scene not found for Tuya', 'An error occurred while fetching scene details from Tuya',
HttpStatus.NOT_FOUND, HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@ -385,23 +272,9 @@ export class SceneService {
updateSceneTapToRunDto: UpdateSceneTapToRunDto, updateSceneTapToRunDto: UpdateSceneTapToRunDto,
sceneUuid: string, 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 { try {
const spaceTuyaId = await this.getTapToRunSceneDetails(sceneUuid, true); const scene = await this.findScene(sceneUuid);
const space = await this.getSpaceByUuid(scene.spaceUuid);
if (!spaceTuyaId.spaceId) {
throw new HttpException("Scene doesn't exist", HttpStatus.NOT_FOUND);
}
const addSceneTapToRunDto: AddSceneTapToRunDto = { const addSceneTapToRunDto: AddSceneTapToRunDto = {
...updateSceneTapToRunDto, ...updateSceneTapToRunDto,
@ -410,38 +283,38 @@ export class SceneService {
showInHomePage: updateSceneTapToRunDto.showInHomePage, showInHomePage: updateSceneTapToRunDto.showInHomePage,
}; };
const newTapToRunScene = await this.addTapToRunScene( const createdTuyaSceneResponse = await this.createSceneExternalService(
space.spaceTuyaUuid,
addSceneTapToRunDto, addSceneTapToRunDto,
spaceTuyaId.spaceId,
scene.spaceUuid,
); );
const newSceneTuyaUuid = createdTuyaSceneResponse.result?.id;
const param: DeleteSceneParamDto = { if (!newSceneTuyaUuid) {
spaceUuid: scene.spaceUuid, throw new HttpException(
sceneUuid: scene.sceneTuyaUuid, `Failed to create a external new scene`,
}; HttpStatus.BAD_GATEWAY,
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,
},
); );
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) { } catch (err) {
console.log(err); if (err instanceof HttpException) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException throw err; // Re-throw BadRequestException
} else { } else {
throw new HttpException( throw new HttpException(
@ -469,6 +342,7 @@ export class SceneService {
); );
} }
} }
async getAllIcons() { async getAllIcons() {
try { try {
const icons = await this.sceneIconRepository.find(); const icons = await this.sceneIconRepository.find();
@ -487,25 +361,99 @@ export class SceneService {
} }
} }
async deleteScene(params: DeleteSceneParamDto) { async getSceneByUuid(sceneUuid: string): Promise<BaseResponseDto> {
try { try {
const { sceneUuid, spaceUuid } = params;
const scene = await this.findScene(sceneUuid); const scene = await this.findScene(sceneUuid);
const space = await this.getSpaceByUuid(spaceUuid); const sceneDetails = await this.getScene(scene);
if (!space.spaceTuyaUuid) {
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( throw new HttpException(
`Invalid space UUID: ${spaceUuid}`, 'An error occurred while retrieving scene details',
HttpStatus.BAD_REQUEST, 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, 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) { } catch (err) {
if (err instanceof HttpException) { if (err instanceof HttpException) {
throw err; // Re-throw existing HttpException throw err;
} else { } else {
throw new HttpException( throw new HttpException(
err.message || `Scene not found for id ${params.sceneUuid}`, err.message || `Scene not found for id ${params.sceneUuid}`,
@ -517,9 +465,8 @@ export class SceneService {
async findScene(sceneUuid: string): Promise<SceneEntity> { async findScene(sceneUuid: string): Promise<SceneEntity> {
const scene = await this.sceneRepository.findOne({ const scene = await this.sceneRepository.findOne({
where: { where: { uuid: sceneUuid },
uuid: sceneUuid, relations: ['sceneIcon'],
},
}); });
if (!scene) { 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.controller';
export * from './space-user.controller'; export * from './space-user.controller';
export * from './space-device.controller'; export * from './space-device.controller';
export * from './space-scene.controller';
export * from './subspace'; 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-user.service';
export * from './space-device.service'; export * from './space-device.service';
export * from './subspace'; 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 { CommunityRepository } from '@app/common/modules/community/repositories';
import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@ -51,10 +51,7 @@ export class SpaceDeviceService {
}); });
} }
private async validateCommunityAndSpace( async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) {
communityUuid: string,
spaceUuid: string,
) {
const community = await this.communityRepository.findOne({ const community = await this.communityRepository.findOne({
where: { uuid: communityUuid }, 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 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 { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos';
import { CommunityRepository } from '@app/common/modules/community/repositories'; import { CommunityRepository } from '@app/common/modules/community/repositories';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; 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 { ProductRepository } from '@app/common/modules/product/repositories';
import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface'; import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface';

View File

@ -7,9 +7,11 @@ import {
SpaceUserController, SpaceUserController,
SubSpaceController, SubSpaceController,
SubSpaceDeviceController, SubSpaceDeviceController,
SpaceSceneController,
} from './controllers'; } from './controllers';
import { import {
SpaceDeviceService, SpaceDeviceService,
SpaceSceneService,
SpaceService, SpaceService,
SpaceUserService, SpaceUserService,
SubspaceDeviceService, SubspaceDeviceService,
@ -25,8 +27,16 @@ import {
UserSpaceRepository, UserSpaceRepository,
} from '@app/common/modules/user/repositories'; } from '@app/common/modules/user/repositories';
import { DeviceRepository } from '@app/common/modules/device/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 { 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({ @Module({
imports: [ConfigModule, SpaceRepositoryModule], imports: [ConfigModule, SpaceRepositoryModule],
@ -36,6 +46,7 @@ import { ProductRepository } from '@app/common/modules/product/repositories';
SpaceDeviceController, SpaceDeviceController,
SubSpaceController, SubSpaceController,
SubSpaceDeviceController, SubSpaceDeviceController,
SpaceSceneController,
], ],
providers: [ providers: [
SpaceService, SpaceService,
@ -51,6 +62,13 @@ import { ProductRepository } from '@app/common/modules/product/repositories';
UserSpaceRepository, UserSpaceRepository,
UserRepository, UserRepository,
SpaceUserService, SpaceUserService,
SpaceSceneService,
SceneService,
SceneIconRepository,
SceneRepository,
DeviceService,
DeviceStatusFirebaseService,
DeviceStatusLogRepository,
], ],
exports: [SpaceService], exports: [SpaceService],
}) })