diff --git a/libs/common/src/config/tuya.config.ts b/libs/common/src/config/tuya.config.ts index ca4dd73..ba3344e 100644 --- a/libs/common/src/config/tuya.config.ts +++ b/libs/common/src/config/tuya.config.ts @@ -5,6 +5,7 @@ export default registerAs( (): Record => ({ TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID, TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY, + TUYA_EU_URL: process.env.TUYA_EU_URL, TRUN_ON_TUYA_SOCKET: process.env.TRUN_ON_TUYA_SOCKET === 'true' ? true : false, }), diff --git a/libs/common/src/helper/snakeCaseConverter.ts b/libs/common/src/helper/snakeCaseConverter.ts new file mode 100644 index 0000000..3b9ea55 --- /dev/null +++ b/libs/common/src/helper/snakeCaseConverter.ts @@ -0,0 +1,16 @@ +function toSnakeCase(str) { + return str.replace(/([A-Z])/g, '_$1').toLowerCase(); +} + +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) => { + const snakeKey = toSnakeCase(key); + result[snakeKey] = convertKeysToSnakeCase(obj[key]); + return result; + }, {}); + } + return obj; +} diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index b337e91..030db57 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -14,6 +14,10 @@ export class SpaceEntity extends AbstractEntity { nullable: false, }) public uuid: string; + @Column({ + nullable: true, + }) + public spaceTuyaUuid: string; @Column({ nullable: false, diff --git a/src/app.module.ts b/src/app.module.ts index 9a4ef30..59b4275 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,6 +16,7 @@ import { RoleModule } from './role/role.module'; import { SeederModule } from '@app/common/seed/seeder.module'; import { UserNotificationModule } from './user-notification/user-notification.module'; import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; +import { SceneModule } from './scene/scene.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -36,6 +37,7 @@ import { DeviceMessagesSubscriptionModule } from './device-messages/device-messa UserDevicePermissionModule, UserNotificationModule, SeederModule, + SceneModule, ], controllers: [AuthenticationController], }) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index a7cbaae..c3da047 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -35,13 +35,14 @@ export class 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: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); } - private async getDeviceByDeviceUuid( + async getDeviceByDeviceUuid( deviceUuid: string, withProductDevice: boolean = true, ) { diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 94cf8e0..4f1a1f7 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -18,8 +18,9 @@ export class GroupService { ) { 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: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); diff --git a/src/guards/device.guard.ts b/src/guards/device.guard.ts index 5d08598..179ef11 100644 --- a/src/guards/device.guard.ts +++ b/src/guards/device.guard.ts @@ -21,8 +21,9 @@ export class CheckDeviceGuard implements CanActivate { ) { 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: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, }); diff --git a/src/scene/controllers/index.ts b/src/scene/controllers/index.ts new file mode 100644 index 0000000..10059c1 --- /dev/null +++ b/src/scene/controllers/index.ts @@ -0,0 +1 @@ +export * from './scene.controller'; diff --git a/src/scene/controllers/scene.controller.ts b/src/scene/controllers/scene.controller.ts new file mode 100644 index 0000000..40af2a8 --- /dev/null +++ b/src/scene/controllers/scene.controller.ts @@ -0,0 +1,141 @@ +import { SceneService } from '../services/scene.service'; +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } from '../dtos/scene.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Scene Module') +@Controller({ + version: '1', + path: 'scene', +}) +export class SceneController { + constructor(private readonly sceneService: SceneService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('tap-to-run') + async addTapToRunScene(@Body() addSceneTapToRunDto: AddSceneTapToRunDto) { + try { + const tapToRunScene = + await this.sceneService.addTapToRunScene(addSceneTapToRunDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Scene added successfully', + data: tapToRunScene, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('tap-to-run/:unitUuid') + async getTapToRunSceneByUnit(@Param('unitUuid') unitUuid: string) { + try { + const tapToRunScenes = + await this.sceneService.getTapToRunSceneByUnit(unitUuid); + return tapToRunScenes; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete('tap-to-run/:unitUuid/:sceneId') + async deleteTapToRunScene( + @Param('unitUuid') unitUuid: string, + @Param('sceneId') sceneId: string, + ) { + try { + await this.sceneService.deleteTapToRunScene(unitUuid, sceneId); + return { + statusCode: HttpStatus.OK, + message: 'Scene Deleted Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('tap-to-run/trigger/:sceneId') + async triggerTapToRunScene(@Param('sceneId') sceneId: string) { + try { + await this.sceneService.triggerTapToRunScene(sceneId); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Scene trigger successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('tap-to-run/details/:sceneId') + async getTapToRunSceneDetails(@Param('sceneId') sceneId: string) { + try { + const tapToRunScenes = + await this.sceneService.getTapToRunSceneDetails(sceneId); + return tapToRunScenes; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + ``; + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('tap-to-run/:sceneId') + async updateTapToRunScene( + @Body() updateSceneTapToRunDto: UpdateSceneTapToRunDto, + @Param('sceneId') sceneId: string, + ) { + try { + const tapToRunScene = await this.sceneService.updateTapToRunScene( + updateSceneTapToRunDto, + sceneId, + ); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Scene updated successfully', + data: tapToRunScene, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/scene/dtos/index.ts b/src/scene/dtos/index.ts new file mode 100644 index 0000000..b1ca9c8 --- /dev/null +++ b/src/scene/dtos/index.ts @@ -0,0 +1 @@ +export * from './scene.dto'; diff --git a/src/scene/dtos/scene.dto.ts b/src/scene/dtos/scene.dto.ts new file mode 100644 index 0000000..ca39212 --- /dev/null +++ b/src/scene/dtos/scene.dto.ts @@ -0,0 +1,134 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsString, + IsArray, + ValidateNested, + IsOptional, + IsNumber, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +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, // Set required to false + type: ExecutorProperty, + }) + @ValidateNested() + @Type(() => ExecutorProperty) + @IsOptional() // Make executorProperty optional + public executorProperty?: ExecutorProperty; +} + +export class AddSceneTapToRunDto { + @ApiProperty({ + description: 'Unit UUID', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + + @ApiProperty({ + description: 'Scene name', + required: true, + }) + @IsString() + @IsNotEmpty() + public sceneName: string; + + @ApiProperty({ + description: 'Decision expression', + required: true, + }) + @IsString() + @IsNotEmpty() + public decisionExpr: string; + + @ApiProperty({ + description: 'Actions', + required: true, + type: [Action], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Action) + public actions: Action[]; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} +export class UpdateSceneTapToRunDto { + @ApiProperty({ + description: 'Scene name', + required: true, + }) + @IsString() + @IsNotEmpty() + public sceneName: string; + + @ApiProperty({ + description: 'Decision expression', + required: true, + }) + @IsString() + @IsNotEmpty() + public decisionExpr: string; + + @ApiProperty({ + description: 'Actions', + required: true, + type: [Action], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Action) + public actions: Action[]; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/scene/interface/scene.interface.ts b/src/scene/interface/scene.interface.ts new file mode 100644 index 0000000..00ebcae --- /dev/null +++ b/src/scene/interface/scene.interface.ts @@ -0,0 +1,23 @@ +export interface AddTapToRunSceneInterface { + success: boolean; + msg?: string; + result: { + id: string; + }; +} +export interface GetTapToRunSceneByUnitInterface { + success: boolean; + msg?: string; + result: { + list: Array<{ + id: string; + name: string; + status: string; + }>; + }; +} +export interface DeleteTapToRunSceneInterface { + success: boolean; + msg?: string; + result: boolean; +} diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts new file mode 100644 index 0000000..248f2e6 --- /dev/null +++ b/src/scene/scene.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { SceneService } from './services/scene.service'; +import { SceneController } from './controllers/scene.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: [SceneController], + providers: [ + SceneService, + SpaceRepository, + DeviceService, + DeviceRepository, + ProductRepository, + ], + exports: [SceneService], +}) +export class SceneModule {} diff --git a/src/scene/services/index.ts b/src/scene/services/index.ts new file mode 100644 index 0000000..3a7442e --- /dev/null +++ b/src/scene/services/index.ts @@ -0,0 +1 @@ +export * from './scene.service'; diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts new file mode 100644 index 0000000..4ef83b3 --- /dev/null +++ b/src/scene/services/scene.service.ts @@ -0,0 +1,292 @@ +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } 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 { + AddTapToRunSceneInterface, + DeleteTapToRunSceneInterface, + GetTapToRunSceneByUnitInterface, +} from '../interface/scene.interface'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; + +@Injectable() +export class SceneService { + 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 addTapToRunScene( + addSceneTapToRunDto: AddSceneTapToRunDto, + spaceTuyaId = null, + ) { + try { + let unitSpaceTuyaId; + if (!spaceTuyaId) { + const unitDetails = await this.getUnitByUuid( + addSceneTapToRunDto.unitUuid, + ); + unitSpaceTuyaId = unitDetails.spaceTuyaUuid; + if (!unitDetails) { + throw new BadRequestException('Invalid unit UUID'); + } + } else { + unitSpaceTuyaId = spaceTuyaId; + } + + const actions = addSceneTapToRunDto.actions.map((action) => { + return { + ...action, + }; + }); + + const convertedData = convertKeysToSnakeCase(actions); + for (const action of convertedData) { + if (action.action_executor === '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: unitSpaceTuyaId, + name: addSceneTapToRunDto.sceneName, + type: 'scene', + decision_expr: addSceneTapToRunDto.decisionExpr, + actions: convertedData, + }, + }); + 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 || 'Scene 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 building 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 getTapToRunSceneByUnit(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=scene`; + const response: GetTapToRunSceneByUnitInterface = 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: 'tap_to_run', + }; + }); + } 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( + unitUuid: string, + sceneId: 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=${sceneId}&space_id=${unitSpaceTuyaId}`; + const response: DeleteTapToRunSceneInterface = await this.tuya.request({ + method: 'DELETE', + path, + }); + + if (!response.success) { + throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); + } + + 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 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) { + 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 responseData = convertKeysToCamelCase(response.result); + return { + id: responseData.id, + name: responseData.name, + status: responseData.status, + type: 'tap_to_run', + actions: responseData.actions, + ...(withSpaceId && { spaceId: responseData.spaceId }), + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); + } + } + } + async updateTapToRunScene( + updateSceneTapToRunDto: UpdateSceneTapToRunDto, + sceneId: string, + ) { + try { + const spaceTuyaId = await this.getTapToRunSceneDetails(sceneId, true); + if (!spaceTuyaId.spaceId) { + throw new HttpException("Scene doesn't exist", HttpStatus.NOT_FOUND); + } + const addSceneTapToRunDto: AddSceneTapToRunDto = { + ...updateSceneTapToRunDto, + unitUuid: null, + }; + const newTapToRunScene = await this.addTapToRunScene( + addSceneTapToRunDto, + spaceTuyaId.spaceId, + ); + if (newTapToRunScene.id) { + await this.deleteTapToRunScene(null, sceneId, spaceTuyaId.spaceId); + return newTapToRunScene; + } + } 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, + ); + } + } + } +} diff --git a/src/unit/interface/unit.interface.ts b/src/unit/interface/unit.interface.ts index 39fac9a..635f38e 100644 --- a/src/unit/interface/unit.interface.ts +++ b/src/unit/interface/unit.interface.ts @@ -4,6 +4,7 @@ export interface GetUnitByUuidInterface { updatedAt: Date; name: string; type: string; + spaceTuyaUuid: string; } export interface UnitChildInterface { @@ -29,3 +30,8 @@ export interface GetUnitByUserUuidInterface { name: string; type: string; } +export interface addTuyaSpaceInterface { + success: boolean; + result: string; + msg: string; +} diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index e7511c6..1106043 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -14,6 +14,7 @@ import { GetUnitByUuidInterface, RenameUnitByUuidInterface, GetUnitByUserUuidInterface, + addTuyaSpaceInterface, } from '../interface/unit.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; @@ -21,15 +22,29 @@ import { UserSpaceRepository } from '@app/common/modules/user-space/repositories import { generateRandomString } from '@app/common/helper/randomString'; import { UserDevicePermissionService } from 'src/user-device-permission/services'; import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class UnitService { + private tuya: TuyaContext; constructor( + private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, private readonly userSpaceRepository: UserSpaceRepository, private readonly userDevicePermissionService: UserDevicePermissionService, - ) {} + ) { + 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 addUnit(addUnitDto: AddUnitDto) { try { @@ -38,17 +53,41 @@ export class UnitService { type: 'unit', }, }); + const tuyaUnit = await this.addUnitTuya(addUnitDto.unitName); + if (!tuyaUnit.result) { + throw new HttpException('Error creating unit', HttpStatus.BAD_REQUEST); + } const unit = await this.spaceRepository.save({ spaceName: addUnitDto.unitName, parent: { uuid: addUnitDto.floorUuid }, spaceType: { uuid: spaceType.uuid }, + spaceTuyaUuid: tuyaUnit.result, }); return unit; } catch (err) { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); } } + async addUnitTuya(unitName: string): Promise { + try { + const path = `/v2.0/cloud/space/creation`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + name: unitName, + }, + }); + + return response as addTuyaSpaceInterface; + } catch (error) { + throw new HttpException( + 'Error creating unit from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getUnitByUuid(unitUuid: string): Promise { try { @@ -70,6 +109,7 @@ export class UnitService { updatedAt: unit.updatedAt, name: unit.spaceName, type: unit.spaceType.type, + spaceTuyaUuid: unit.spaceTuyaUuid, }; } catch (err) { if (err instanceof BadRequestException) { diff --git a/src/users/services/user.service.ts b/src/users/services/user.service.ts index d6b3610..774293b 100644 --- a/src/users/services/user.service.ts +++ b/src/users/services/user.service.ts @@ -9,9 +9,9 @@ export class UserService { constructor(private readonly configService: ConfigService) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); - // const clientId = this.configService.get('auth-config.CLIENT_ID'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); this.tuya = new TuyaContext({ - baseUrl: 'https://openapi.tuyaeu.com', + baseUrl: tuyaEuUrl, accessKey, secretKey, });