From f31bc09e6cbc64e63662ebeb96e3100482b978f9 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:58:01 +0300 Subject: [PATCH 1/7] test --- libs/common/src/database/database.module.ts | 2 +- src/automation/automation.module.ts | 23 ++ .../controllers/automation.controller.ts | 144 +++++++++ src/automation/controllers/index.ts | 1 + src/automation/dtos/automation.dto.ts | 134 ++++++++ src/automation/dtos/index.ts | 1 + .../interface/automation.interface.ts | 23 ++ src/automation/services/automation.service.ts | 304 ++++++++++++++++++ src/automation/services/index.ts | 1 + 9 files changed, 632 insertions(+), 1 deletion(-) create mode 100644 src/automation/automation.module.ts create mode 100644 src/automation/controllers/automation.controller.ts create mode 100644 src/automation/controllers/index.ts create mode 100644 src/automation/dtos/automation.dto.ts create mode 100644 src/automation/dtos/index.ts create mode 100644 src/automation/interface/automation.interface.ts create mode 100644 src/automation/services/automation.service.ts create mode 100644 src/automation/services/index.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index ef5a0a1..14b3d56 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -58,7 +58,7 @@ import { DeviceNotificationEntity } from '../modules/device-notification/entitie maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion) }, continuationLocalStorage: true, - ssl: Boolean(JSON.parse(configService.get('DB_SSL'))), + // ssl: Boolean(JSON.parse(configService.get('DB_SSL'))), }), }), ], diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts new file mode 100644 index 0000000..d9f9671 --- /dev/null +++ b/src/automation/automation.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { SceneService } from './services/automation.service'; +import { SceneController } 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: [SceneController], + providers: [ + SceneService, + SpaceRepository, + DeviceService, + DeviceRepository, + ProductRepository, + ], + exports: [SceneService], +}) +export class SceneModule {} diff --git a/src/automation/controllers/automation.controller.ts b/src/automation/controllers/automation.controller.ts new file mode 100644 index 0000000..810eaf4 --- /dev/null +++ b/src/automation/controllers/automation.controller.ts @@ -0,0 +1,144 @@ +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, + UpdateSceneTapToRunDto, +} 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 sceneAutomation = + await this.automationService.addAutomation(addAutomationDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Automation added successfully', + data: sceneAutomation, + }; + } 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.automationService.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.automationService.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.automationService.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.automationService.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.automationService.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/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..b930927 --- /dev/null +++ b/src/automation/dtos/automation.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 AddAutomationDto { + @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/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..00ebcae --- /dev/null +++ b/src/automation/interface/automation.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/automation/services/automation.service.ts b/src/automation/services/automation.service.ts new file mode 100644 index 0000000..fe54ac0 --- /dev/null +++ b/src/automation/services/automation.service.ts @@ -0,0 +1,304 @@ +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { AddAutomationDto, 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/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) => { + 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: addAutomationDto.sceneName, + type: 'scene', + decision_expr: addAutomationDto.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 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 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); + const actions = responseData.actions.map((action) => { + return { + ...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; + } + } + } + + return { + id: responseData.id, + name: responseData.name, + status: responseData.status, + type: 'tap_to_run', + actions: 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: AddAutomationDto = { + ...updateSceneTapToRunDto, + unitUuid: null, + }; + const newTapToRunScene = await this.addAutomation( + 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/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'; From 6442bee6b85b1e46e7f329737e57426a30730cad Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 19 Jul 2024 19:09:05 +0300 Subject: [PATCH 2/7] remove comments --- libs/common/src/database/database.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 14b3d56..ef5a0a1 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -58,7 +58,7 @@ import { DeviceNotificationEntity } from '../modules/device-notification/entitie maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion) }, continuationLocalStorage: true, - // ssl: Boolean(JSON.parse(configService.get('DB_SSL'))), + ssl: Boolean(JSON.parse(configService.get('DB_SSL'))), }), }), ], From 9ad51fcffc54f9f8e261faabd95f3553a0c4fd7f Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:18:32 +0300 Subject: [PATCH 3/7] Add automation module --- src/automation/automation.module.ts | 12 +- .../controllers/automation.controller.ts | 128 ++++---- src/automation/dtos/automation.dto.ts | 156 +++++++--- .../interface/automation.interface.ts | 28 +- src/automation/services/automation.service.ts | 294 +++++++++++------- 5 files changed, 398 insertions(+), 220 deletions(-) diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts index d9f9671..ebb8bf0 100644 --- a/src/automation/automation.module.ts +++ b/src/automation/automation.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { SceneService } from './services/automation.service'; -import { SceneController } from './controllers/automation.controller'; +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'; @@ -10,14 +10,14 @@ import { ProductRepository } from '@app/common/modules/product/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule], - controllers: [SceneController], + controllers: [AutomationController], providers: [ - SceneService, + AutomationService, SpaceRepository, DeviceService, DeviceRepository, ProductRepository, ], - exports: [SceneService], + exports: [AutomationService], }) -export class SceneModule {} +export class AutomationModule {} diff --git a/src/automation/controllers/automation.controller.ts b/src/automation/controllers/automation.controller.ts index 810eaf4..4328602 100644 --- a/src/automation/controllers/automation.controller.ts +++ b/src/automation/controllers/automation.controller.ts @@ -14,7 +14,8 @@ import { import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { AddAutomationDto, - UpdateSceneTapToRunDto, + UpdateAutomationDto, + UpdateAutomationStatusDto, } from '../dtos/automation.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @@ -31,13 +32,13 @@ export class AutomationController { @Post() async addAutomation(@Body() addAutomationDto: AddAutomationDto) { try { - const sceneAutomation = + const automation = await this.automationService.addAutomation(addAutomationDto); return { statusCode: HttpStatus.CREATED, success: true, message: 'Automation added successfully', - data: sceneAutomation, + data: automation, }; } catch (error) { throw new HttpException( @@ -48,12 +49,12 @@ export class AutomationController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('tap-to-run/:unitUuid') - async getTapToRunSceneByUnit(@Param('unitUuid') unitUuid: string) { + @Get(':unitUuid') + async getAutomationByUnit(@Param('unitUuid') unitUuid: string) { try { - const tapToRunScenes = - await this.automationService.getTapToRunSceneByUnit(unitUuid); - return tapToRunScenes; + const automation = + await this.automationService.getAutomationByUnit(unitUuid); + return automation; } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -63,51 +64,12 @@ export class AutomationController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Delete('tap-to-run/:unitUuid/:sceneId') - async deleteTapToRunScene( - @Param('unitUuid') unitUuid: string, - @Param('sceneId') sceneId: string, - ) { + @Get('details/:automationId') + async getAutomationDetails(@Param('automationId') automationId: string) { try { - await this.automationService.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.automationService.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.automationService.getTapToRunSceneDetails(sceneId); - return tapToRunScenes; + const automation = + await this.automationService.getAutomationDetails(automationId); + return automation; } catch (error) { throw new HttpException( error.message || 'Internal server error', @@ -118,21 +80,65 @@ export class AutomationController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Put('tap-to-run/:sceneId') - async updateTapToRunScene( - @Body() updateSceneTapToRunDto: UpdateSceneTapToRunDto, - @Param('sceneId') sceneId: string, + @Delete(':unitUuid/:automationId') + async deleteAutomation( + @Param('unitUuid') unitUuid: string, + @Param('automationId') automationId: string, ) { try { - const tapToRunScene = await this.automationService.updateTapToRunScene( - updateSceneTapToRunDto, - sceneId, + 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: 'Scene updated successfully', - data: tapToRunScene, + 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( diff --git a/src/automation/dtos/automation.dto.ts b/src/automation/dtos/automation.dto.ts index b930927..8d43084 100644 --- a/src/automation/dtos/automation.dto.ts +++ b/src/automation/dtos/automation.dto.ts @@ -6,9 +6,67 @@ import { 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)', @@ -35,100 +93,122 @@ class ExecutorProperty { } class Action { - @ApiProperty({ - description: 'Entity ID', - required: true, - }) + @ApiProperty({ description: 'Entity ID', required: true }) @IsString() @IsNotEmpty() public entityId: string; - @ApiProperty({ - description: 'Action executor', - required: true, - }) + @ApiProperty({ description: 'Action executor', required: true }) @IsString() @IsNotEmpty() public actionExecutor: string; @ApiProperty({ description: 'Executor property', - required: false, // Set required to false + required: false, type: ExecutorProperty, }) @ValidateNested() @Type(() => ExecutorProperty) - @IsOptional() // Make executorProperty optional + @IsOptional() public executorProperty?: ExecutorProperty; } export class AddAutomationDto { - @ApiProperty({ - description: 'Unit UUID', - required: true, - }) + @ApiProperty({ description: 'Unit ID', required: true }) @IsString() @IsNotEmpty() public unitUuid: string; - @ApiProperty({ - description: 'Scene name', - required: true, - }) + @ApiProperty({ description: 'Automation name', required: true }) @IsString() @IsNotEmpty() - public sceneName: string; + public name: string; - @ApiProperty({ - description: 'Decision expression', - required: true, - }) + @ApiProperty({ description: 'Decision expression', required: true }) @IsString() @IsNotEmpty() public decisionExpr: string; @ApiProperty({ - description: 'Actions', + description: 'Effective time', required: true, - type: [Action], + 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 UpdateSceneTapToRunDto { - @ApiProperty({ - description: 'Scene name', - required: true, - }) + +export class UpdateAutomationDto { + @ApiProperty({ description: 'Automation name', required: true }) @IsString() @IsNotEmpty() - public sceneName: string; + public name: string; - @ApiProperty({ - description: 'Decision expression', - required: true, - }) + @ApiProperty({ description: 'Decision expression', required: true }) @IsString() @IsNotEmpty() public decisionExpr: string; @ApiProperty({ - description: 'Actions', + description: 'Effective time', required: true, - type: [Action], + 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; - constructor(dto: Partial) { + @ApiProperty({ description: 'Is enable', required: true }) + @IsBoolean() + @IsNotEmpty() + public isEnable: boolean; + + constructor(dto: Partial) { Object.assign(this, dto); } } diff --git a/src/automation/interface/automation.interface.ts b/src/automation/interface/automation.interface.ts index 00ebcae..6ddec4d 100644 --- a/src/automation/interface/automation.interface.ts +++ b/src/automation/interface/automation.interface.ts @@ -1,11 +1,11 @@ -export interface AddTapToRunSceneInterface { +export interface AddAutomationInterface { success: boolean; msg?: string; result: { id: string; }; } -export interface GetTapToRunSceneByUnitInterface { +export interface GetAutomationByUnitInterface { success: boolean; msg?: string; result: { @@ -16,8 +16,30 @@ export interface GetTapToRunSceneByUnitInterface { }>; }; } -export interface DeleteTapToRunSceneInterface { +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 index fe54ac0..93b6688 100644 --- a/src/automation/services/automation.service.ts +++ b/src/automation/services/automation.service.ts @@ -5,16 +5,17 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddAutomationDto, UpdateSceneTapToRunDto } from '../dtos'; +import { 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 { - AddTapToRunSceneInterface, - DeleteTapToRunSceneInterface, - GetTapToRunSceneByUnitInterface, + AddAutomationInterface, + AutomationResponseData, + DeleteAutomationInterface, + GetAutomationByUnitInterface, } from '../interface/automation.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; @@ -36,11 +37,12 @@ export class AutomationService { }); } - async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { + async addAutomation(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'); @@ -48,14 +50,15 @@ export class AutomationService { } else { unitSpaceTuyaId = spaceTuyaId; } - const actions = addAutomationDto.actions.map((action) => { - return { - ...action, - }; - }); - const convertedData = convertKeysToSnakeCase(actions); - for (const action of convertedData) { + 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, @@ -66,16 +69,34 @@ export class AutomationService { } } } + + 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: AddTapToRunSceneInterface = await this.tuya.request({ + const response: AddAutomationInterface = await this.tuya.request({ method: 'POST', path, body: { space_id: unitSpaceTuyaId, - name: addAutomationDto.sceneName, - type: 'scene', + name: addAutomationDto.name, + effective_time: { + ...addAutomationDto.effectiveTime, + timezone_id: 'Asia/Dubai', + }, + type: 'automation', decision_expr: addAutomationDto.decisionExpr, - actions: convertedData, + conditions: conditions, + actions: actions, }, }); if (!response.success) { @@ -89,7 +110,7 @@ export class AutomationService { throw err; // Re-throw BadRequestException } else { throw new HttpException( - err.message || 'Scene not found', + err.message || 'Automation not found', err.status || HttpStatus.NOT_FOUND, ); } @@ -125,20 +146,18 @@ export class AutomationService { } } } - async getTapToRunSceneByUnit(unitUuid: string) { + 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=scene`; - const response: GetTapToRunSceneByUnitInterface = await this.tuya.request( - { - method: 'GET', - path, - }, - ); + 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); @@ -149,7 +168,7 @@ export class AutomationService { id: item.id, name: item.name, status: item.status, - type: 'tap_to_run', + type: 'automation', }; }); } catch (err) { @@ -157,15 +176,91 @@ export class AutomationService { throw err; // Re-throw BadRequestException } else { throw new HttpException( - err.message || 'Scene not found', + err.message || 'Automation not found', err.status || HttpStatus.NOT_FOUND, ); } } } - async deleteTapToRunScene( + 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, - sceneId: string, + automationId: string, spaceTuyaId = null, ) { try { @@ -180,14 +275,14 @@ export class AutomationService { unitSpaceTuyaId = spaceTuyaId; } - const path = `/v2.0/cloud/scene/rule?ids=${sceneId}&space_id=${unitSpaceTuyaId}`; - const response: DeleteTapToRunSceneInterface = await this.tuya.request({ + 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('Scene not found', HttpStatus.NOT_FOUND); + throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); } return response; @@ -196,106 +291,81 @@ export class AutomationService { throw err; // Re-throw BadRequestException } else { throw new HttpException( - err.message || 'Scene not found', + err.message || 'Automation 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); - const actions = responseData.actions.map((action) => { - return { - ...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; - } - } - } - - return { - id: responseData.id, - name: responseData.name, - status: responseData.status, - type: 'tap_to_run', - actions: 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, + async updateAutomation( + updateAutomationDto: UpdateAutomationDto, + automationId: string, ) { try { - const spaceTuyaId = await this.getTapToRunSceneDetails(sceneId, true); + const spaceTuyaId = await this.getAutomationDetails(automationId, true); if (!spaceTuyaId.spaceId) { - throw new HttpException("Scene doesn't exist", HttpStatus.NOT_FOUND); + throw new HttpException( + "Automation doesn't exist", + HttpStatus.NOT_FOUND, + ); } - const addSceneTapToRunDto: AddAutomationDto = { - ...updateSceneTapToRunDto, + const addAutomation = { + ...updateAutomationDto, unitUuid: null, }; - const newTapToRunScene = await this.addAutomation( - addSceneTapToRunDto, + const newAutomation = await this.addAutomation( + addAutomation, spaceTuyaId.spaceId, ); - if (newTapToRunScene.id) { - await this.deleteTapToRunScene(null, sceneId, spaceTuyaId.spaceId); - return newTapToRunScene; + 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 || 'Scene not found', + 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, ); } From ae02b0b288335e1bc62e530ee7cad9e6111e88b3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:19:05 +0300 Subject: [PATCH 4/7] Transfer device to parent space in Tuya when updating device in room --- src/device/services/device.service.ts | 28 ++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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); From c0860c29276b1973b2ec7753d0a64d6d8c8acbd7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:19:14 +0300 Subject: [PATCH 5/7] Add AutomationModule to app.module.ts imports --- src/app.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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: [ From 5f27695a0b75616902204b779cb538bdfcb1e1be Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:27:13 +0300 Subject: [PATCH 6/7] Refactor snake case converter function and improve code readability --- libs/common/src/helper/snakeCaseConverter.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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; From 9a9201b76926796fdbef2cb92a1d8d641fc26a9b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:27:22 +0300 Subject: [PATCH 7/7] Refactor automation DTOs and service --- src/automation/dtos/automation.dto.ts | 4 ++-- src/automation/services/automation.service.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/automation/dtos/automation.dto.ts b/src/automation/dtos/automation.dto.ts index 8d43084..6495b8b 100644 --- a/src/automation/dtos/automation.dto.ts +++ b/src/automation/dtos/automation.dto.ts @@ -123,7 +123,7 @@ export class AddAutomationDto { @ApiProperty({ description: 'Automation name', required: true }) @IsString() @IsNotEmpty() - public name: string; + public automationName: string; @ApiProperty({ description: 'Decision expression', required: true }) @IsString() @@ -163,7 +163,7 @@ export class UpdateAutomationDto { @ApiProperty({ description: 'Automation name', required: true }) @IsString() @IsNotEmpty() - public name: string; + public automationName: string; @ApiProperty({ description: 'Decision expression', required: true }) @IsString() diff --git a/src/automation/services/automation.service.ts b/src/automation/services/automation.service.ts index 93b6688..ce9bfa5 100644 --- a/src/automation/services/automation.service.ts +++ b/src/automation/services/automation.service.ts @@ -5,7 +5,11 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { UpdateAutomationDto, UpdateAutomationStatusDto } from '../dtos'; +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'; @@ -37,7 +41,7 @@ export class AutomationService { }); } - async addAutomation(addAutomationDto, spaceTuyaId = null) { + async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { try { let unitSpaceTuyaId; if (!spaceTuyaId) { @@ -88,7 +92,7 @@ export class AutomationService { path, body: { space_id: unitSpaceTuyaId, - name: addAutomationDto.name, + name: addAutomationDto.automationName, effective_time: { ...addAutomationDto.effectiveTime, timezone_id: 'Asia/Dubai',