diff --git a/libs/common/src/helper/snakeCaseConverter.ts b/libs/common/src/helper/snakeCaseConverter.ts index 3b9ea55..bd8dce6 100644 --- a/libs/common/src/helper/snakeCaseConverter.ts +++ b/libs/common/src/helper/snakeCaseConverter.ts @@ -4,12 +4,12 @@ function toSnakeCase(str) { export function convertKeysToSnakeCase(obj) { if (Array.isArray(obj)) { - return obj.map((v) => convertKeysToSnakeCase(v)); - } else if (obj !== null && obj.constructor === Object) { - return Object.keys(obj).reduce((result, key) => { + return obj.map(convertKeysToSnakeCase); + } else if (obj !== null && typeof obj === 'object') { + return Object.keys(obj).reduce((acc, key) => { const snakeKey = toSnakeCase(key); - result[snakeKey] = convertKeysToSnakeCase(obj[key]); - return result; + acc[snakeKey] = convertKeysToSnakeCase(obj[key]); + return acc; }, {}); } return obj; diff --git a/src/app.module.ts b/src/app.module.ts index 19cabd3..2cc8b49 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -20,6 +20,7 @@ import { SceneModule } from './scene/scene.module'; import { DoorLockModule } from './door-lock/door.lock.module'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { LoggingInterceptor } from './interceptors/logging.interceptor'; +import { AutomationModule } from './automation/automation.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -41,8 +42,8 @@ import { LoggingInterceptor } from './interceptors/logging.interceptor'; UserNotificationModule, SeederModule, SceneModule, + AutomationModule, DoorLockModule, - // ], controllers: [AuthenticationController], providers: [ diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts new file mode 100644 index 0000000..ebb8bf0 --- /dev/null +++ b/src/automation/automation.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { AutomationService } from './services/automation.service'; +import { AutomationController } from './controllers/automation.controller'; +import { ConfigModule } from '@nestjs/config'; +import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { DeviceService } from 'src/device/services'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; + +@Module({ + imports: [ConfigModule, SpaceRepositoryModule], + controllers: [AutomationController], + providers: [ + AutomationService, + SpaceRepository, + DeviceService, + DeviceRepository, + ProductRepository, + ], + exports: [AutomationService], +}) +export class AutomationModule {} diff --git a/src/automation/controllers/automation.controller.ts b/src/automation/controllers/automation.controller.ts new file mode 100644 index 0000000..4328602 --- /dev/null +++ b/src/automation/controllers/automation.controller.ts @@ -0,0 +1,150 @@ +import { AutomationService } from '../services/automation.service'; +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { + AddAutomationDto, + UpdateAutomationDto, + UpdateAutomationStatusDto, +} from '../dtos/automation.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Automation Module') +@Controller({ + version: '1', + path: 'automation', +}) +export class AutomationController { + constructor(private readonly automationService: AutomationService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async addAutomation(@Body() addAutomationDto: AddAutomationDto) { + try { + const automation = + await this.automationService.addAutomation(addAutomationDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Automation added successfully', + data: automation, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':unitUuid') + async getAutomationByUnit(@Param('unitUuid') unitUuid: string) { + try { + const automation = + await this.automationService.getAutomationByUnit(unitUuid); + return automation; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('details/:automationId') + async getAutomationDetails(@Param('automationId') automationId: string) { + try { + const automation = + await this.automationService.getAutomationDetails(automationId); + return automation; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + ``; + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':unitUuid/:automationId') + async deleteAutomation( + @Param('unitUuid') unitUuid: string, + @Param('automationId') automationId: string, + ) { + try { + await this.automationService.deleteAutomation(unitUuid, automationId); + return { + statusCode: HttpStatus.OK, + message: 'Automation Deleted Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put(':automationId') + async updateAutomation( + @Body() updateAutomationDto: UpdateAutomationDto, + @Param('automationId') automationId: string, + ) { + try { + const automation = await this.automationService.updateAutomation( + updateAutomationDto, + automationId, + ); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Automation updated successfully', + data: automation, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('status/:automationId') + async updateAutomationStatus( + @Body() updateAutomationStatusDto: UpdateAutomationStatusDto, + @Param('automationId') automationId: string, + ) { + try { + await this.automationService.updateAutomationStatus( + updateAutomationStatusDto, + automationId, + ); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Automation status updated successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/automation/controllers/index.ts b/src/automation/controllers/index.ts new file mode 100644 index 0000000..4d9bacc --- /dev/null +++ b/src/automation/controllers/index.ts @@ -0,0 +1 @@ +export * from './automation.controller'; diff --git a/src/automation/dtos/automation.dto.ts b/src/automation/dtos/automation.dto.ts new file mode 100644 index 0000000..6495b8b --- /dev/null +++ b/src/automation/dtos/automation.dto.ts @@ -0,0 +1,214 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsString, + IsArray, + ValidateNested, + IsOptional, + IsNumber, + IsBoolean, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +class EffectiveTime { + @ApiProperty({ description: 'Start time', required: true }) + @IsString() + @IsNotEmpty() + public start: string; + + @ApiProperty({ description: 'End time', required: true }) + @IsString() + @IsNotEmpty() + public end: string; + + @ApiProperty({ description: 'Loops', required: true }) + @IsString() + @IsNotEmpty() + public loops: string; +} + +class Expr { + @ApiProperty({ description: 'Status code', required: true }) + @IsString() + @IsNotEmpty() + public statusCode: string; + + @ApiProperty({ description: 'Comparator', required: true }) + @IsString() + @IsNotEmpty() + public comparator: string; + + @ApiProperty({ description: 'Status value', required: true }) + @IsBoolean() + @IsNotEmpty() + public statusValue: any; +} + +class Condition { + @ApiProperty({ description: 'Condition code', required: true }) + @IsNumber() + @IsNotEmpty() + public code: number; + + @ApiProperty({ description: 'Entity ID', required: true }) + @IsString() + @IsNotEmpty() + public entityId: string; + + @ApiProperty({ description: 'Entity type', required: true }) + @IsString() + @IsNotEmpty() + public entityType: string; + + @ApiProperty({ description: 'Expression', required: true, type: Expr }) + @ValidateNested() + @Type(() => Expr) + @IsNotEmpty() + public expr: Expr; +} + +class ExecutorProperty { + @ApiProperty({ + description: 'Function code (for device issue action)', + required: false, + }) + @IsString() + @IsOptional() + public functionCode?: string; + + @ApiProperty({ + description: 'Function value (for device issue action)', + required: false, + }) + @IsOptional() + public functionValue?: any; + + @ApiProperty({ + description: 'Delay in seconds (for delay action)', + required: false, + }) + @IsNumber() + @IsOptional() + public delaySeconds?: number; +} + +class Action { + @ApiProperty({ description: 'Entity ID', required: true }) + @IsString() + @IsNotEmpty() + public entityId: string; + + @ApiProperty({ description: 'Action executor', required: true }) + @IsString() + @IsNotEmpty() + public actionExecutor: string; + + @ApiProperty({ + description: 'Executor property', + required: false, + type: ExecutorProperty, + }) + @ValidateNested() + @Type(() => ExecutorProperty) + @IsOptional() + public executorProperty?: ExecutorProperty; +} + +export class AddAutomationDto { + @ApiProperty({ description: 'Unit ID', required: true }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + + @ApiProperty({ description: 'Automation name', required: true }) + @IsString() + @IsNotEmpty() + public automationName: string; + + @ApiProperty({ description: 'Decision expression', required: true }) + @IsString() + @IsNotEmpty() + public decisionExpr: string; + + @ApiProperty({ + description: 'Effective time', + required: true, + type: EffectiveTime, + }) + @ValidateNested() + @Type(() => EffectiveTime) + @IsNotEmpty() + public effectiveTime: EffectiveTime; + + @ApiProperty({ description: 'Conditions', required: true, type: [Condition] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Condition) + @IsNotEmpty() + public conditions: Condition[]; + + @ApiProperty({ description: 'Actions', required: true, type: [Action] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Action) + @IsNotEmpty() + public actions: Action[]; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} + +export class UpdateAutomationDto { + @ApiProperty({ description: 'Automation name', required: true }) + @IsString() + @IsNotEmpty() + public automationName: string; + + @ApiProperty({ description: 'Decision expression', required: true }) + @IsString() + @IsNotEmpty() + public decisionExpr: string; + + @ApiProperty({ + description: 'Effective time', + required: true, + type: EffectiveTime, + }) + @ValidateNested() + @Type(() => EffectiveTime) + @IsNotEmpty() + public effectiveTime: EffectiveTime; + + @ApiProperty({ description: 'Conditions', required: true, type: [Condition] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Condition) + @IsNotEmpty() + public conditions: Condition[]; + + @ApiProperty({ description: 'Actions', required: true, type: [Action] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Action) + @IsNotEmpty() + public actions: Action[]; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} +export class UpdateAutomationStatusDto { + @ApiProperty({ description: 'Unit uuid', required: true }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + + @ApiProperty({ description: 'Is enable', required: true }) + @IsBoolean() + @IsNotEmpty() + public isEnable: boolean; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/automation/dtos/index.ts b/src/automation/dtos/index.ts new file mode 100644 index 0000000..4cdad58 --- /dev/null +++ b/src/automation/dtos/index.ts @@ -0,0 +1 @@ +export * from './automation.dto'; diff --git a/src/automation/interface/automation.interface.ts b/src/automation/interface/automation.interface.ts new file mode 100644 index 0000000..6ddec4d --- /dev/null +++ b/src/automation/interface/automation.interface.ts @@ -0,0 +1,45 @@ +export interface AddAutomationInterface { + success: boolean; + msg?: string; + result: { + id: string; + }; +} +export interface GetAutomationByUnitInterface { + success: boolean; + msg?: string; + result: { + list: Array<{ + id: string; + name: string; + status: string; + }>; + }; +} +export interface DeleteAutomationInterface { + success: boolean; + msg?: string; + result: boolean; +} +export interface Action { + actionExecutor: string; + entityId: string; + [key: string]: any; // Allow additional properties +} + +export interface Condition { + entityType: string; + entityId: string; + [key: string]: any; // Allow additional properties +} + +export interface AutomationResponseData { + id: string; + name: string; + status: string; + spaceId?: string; + runningMode?: string; + actions: Action[]; + conditions: Condition[]; + [key: string]: any; // Allow additional properties +} diff --git a/src/automation/services/automation.service.ts b/src/automation/services/automation.service.ts new file mode 100644 index 0000000..ce9bfa5 --- /dev/null +++ b/src/automation/services/automation.service.ts @@ -0,0 +1,378 @@ +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + AddAutomationDto, + UpdateAutomationDto, + UpdateAutomationStatusDto, +} from '../dtos'; +import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface'; +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 { + AddAutomationInterface, + AutomationResponseData, + DeleteAutomationInterface, + GetAutomationByUnitInterface, +} from '../interface/automation.interface'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; + +@Injectable() +export class AutomationService { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly spaceRepository: SpaceRepository, + private readonly deviceService: DeviceService, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + + async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { + try { + let unitSpaceTuyaId; + if (!spaceTuyaId) { + const unitDetails = await this.getUnitByUuid(addAutomationDto.unitUuid); + + unitSpaceTuyaId = unitDetails.spaceTuyaUuid; + if (!unitDetails) { + throw new BadRequestException('Invalid unit UUID'); + } + } else { + unitSpaceTuyaId = spaceTuyaId; + } + + const actions = addAutomationDto.actions.map((action) => + convertKeysToSnakeCase(action), + ); + const conditions = addAutomationDto.conditions.map((condition) => + convertKeysToSnakeCase(condition), + ); + + for (const action of actions) { + if (action.action_executor === 'device_issue') { + const device = await this.deviceService.getDeviceByDeviceUuid( + action.entity_id, + false, + ); + if (device) { + action.entity_id = device.deviceTuyaUuid; + } + } + } + + for (const condition of conditions) { + if (condition.entity_type === 'device_report') { + const device = await this.deviceService.getDeviceByDeviceUuid( + condition.entity_id, + false, + ); + if (device) { + condition.entity_id = device.deviceTuyaUuid; + } + } + } + + const path = `/v2.0/cloud/scene/rule`; + const response: AddAutomationInterface = await this.tuya.request({ + method: 'POST', + path, + body: { + space_id: unitSpaceTuyaId, + name: addAutomationDto.automationName, + effective_time: { + ...addAutomationDto.effectiveTime, + timezone_id: 'Asia/Dubai', + }, + type: 'automation', + decision_expr: addAutomationDto.decisionExpr, + conditions: conditions, + actions: actions, + }, + }); + if (!response.success) { + throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } + return { + id: response.result.id, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Automation not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + async getUnitByUuid(unitUuid: string): Promise { + try { + const unit = await this.spaceRepository.findOne({ + where: { + uuid: unitUuid, + spaceType: { + type: 'unit', + }, + }, + relations: ['spaceType'], + }); + if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { + throw new BadRequestException('Invalid unit UUID'); + } + return { + uuid: unit.uuid, + createdAt: unit.createdAt, + updatedAt: unit.updatedAt, + name: unit.spaceName, + type: unit.spaceType.type, + spaceTuyaUuid: unit.spaceTuyaUuid, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + } + } + async getAutomationByUnit(unitUuid: string) { + try { + const unit = await this.getUnitByUuid(unitUuid); + if (!unit.spaceTuyaUuid) { + throw new BadRequestException('Invalid unit UUID'); + } + + const path = `/v2.0/cloud/scene/rule?space_id=${unit.spaceTuyaUuid}&type=automation`; + const response: GetAutomationByUnitInterface = await this.tuya.request({ + method: 'GET', + path, + }); + + if (!response.success) { + throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } + + return response.result.list.map((item) => { + return { + id: item.id, + name: item.name, + status: item.status, + type: 'automation', + }; + }); + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Automation not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + async getAutomationDetails(automationId: string, withSpaceId = false) { + try { + const path = `/v2.0/cloud/scene/rule/${automationId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + if (!response.success) { + throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } + + const responseData: AutomationResponseData = convertKeysToCamelCase( + response.result, + ); + + const actions = responseData.actions.map((action) => ({ + ...action, + })); + + for (const action of actions) { + if (action.actionExecutor === 'device_issue') { + const device = await this.deviceService.getDeviceByDeviceTuyaUuid( + action.entityId, + ); + + if (device) { + action.entityId = device.uuid; + } + } + } + + const conditions = responseData.conditions.map((condition) => ({ + ...condition, + })); + + for (const condition of conditions) { + if (condition.entityType === 'device_report') { + const device = await this.deviceService.getDeviceByDeviceTuyaUuid( + condition.entityId, + ); + + if (device) { + condition.entityId = device.uuid; + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { timeZoneId, ...effectiveTimeWithoutTimeZoneId } = + responseData.effectiveTime || {}; + + return { + id: responseData.id, + name: responseData.name, + status: responseData.status, + type: 'automation', + ...(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { spaceId, runningMode, ...rest } = responseData; + return rest; + })(), + actions, + conditions, + effectiveTime: effectiveTimeWithoutTimeZoneId, // Use modified effectiveTime + ...(withSpaceId && { spaceId: responseData.spaceId }), + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); + } + } + } + + async deleteAutomation( + unitUuid: string, + automationId: string, + spaceTuyaId = null, + ) { + try { + let unitSpaceTuyaId; + if (!spaceTuyaId) { + const unitDetails = await this.getUnitByUuid(unitUuid); + unitSpaceTuyaId = unitDetails.spaceTuyaUuid; + if (!unitSpaceTuyaId) { + throw new BadRequestException('Invalid unit UUID'); + } + } else { + unitSpaceTuyaId = spaceTuyaId; + } + + const path = `/v2.0/cloud/scene/rule?ids=${automationId}&space_id=${unitSpaceTuyaId}`; + const response: DeleteAutomationInterface = await this.tuya.request({ + method: 'DELETE', + path, + }); + + if (!response.success) { + throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); + } + + return response; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Automation not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + + async updateAutomation( + updateAutomationDto: UpdateAutomationDto, + automationId: string, + ) { + try { + const spaceTuyaId = await this.getAutomationDetails(automationId, true); + if (!spaceTuyaId.spaceId) { + throw new HttpException( + "Automation doesn't exist", + HttpStatus.NOT_FOUND, + ); + } + const addAutomation = { + ...updateAutomationDto, + unitUuid: null, + }; + const newAutomation = await this.addAutomation( + addAutomation, + spaceTuyaId.spaceId, + ); + if (newAutomation.id) { + await this.deleteAutomation(null, automationId, spaceTuyaId.spaceId); + return newAutomation; + } + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Automation not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + async updateAutomationStatus( + updateAutomationStatusDto: UpdateAutomationStatusDto, + automationId: string, + ) { + try { + const unitDetails = await this.getUnitByUuid( + updateAutomationStatusDto.unitUuid, + ); + if (!unitDetails.spaceTuyaUuid) { + throw new BadRequestException('Invalid unit UUID'); + } + + const path = `/v2.0/cloud/scene/rule/state?space_id=${unitDetails.spaceTuyaUuid}`; + const response: DeleteAutomationInterface = await this.tuya.request({ + method: 'PUT', + path, + body: { + ids: automationId, + is_enable: updateAutomationStatusDto.isEnable, + }, + }); + + if (!response.success) { + throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); + } + + return response; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException( + err.message || 'Automation not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } +} diff --git a/src/automation/services/index.ts b/src/automation/services/index.ts new file mode 100644 index 0000000..16f2aeb --- /dev/null +++ b/src/automation/services/index.ts @@ -0,0 +1 @@ +export * from './automation.service'; diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 8cbe772..0848bb3 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -197,8 +197,15 @@ export class DeviceService { where: { uuid: updateDeviceInRoomDto.deviceUuid, }, - relations: ['spaceDevice'], + relations: ['spaceDevice', 'spaceDevice.parent'], }); + if (device.spaceDevice.parent.spaceTuyaUuid) { + await this.transferDeviceInSpacesTuya( + device.deviceTuyaUuid, + device.spaceDevice.parent.spaceTuyaUuid, + ); + } + return { uuid: device.uuid, roomUuid: device.spaceDevice.uuid, @@ -210,7 +217,26 @@ export class DeviceService { ); } } + async transferDeviceInSpacesTuya( + deviceId: string, + spaceId: string, + ): Promise { + try { + const path = `/v2.0/cloud/thing/${deviceId}/transfer`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { space_id: spaceId }, + }); + return response as controlDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error transferring device in spaces from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false);