From 00cace7b1215fea10c020110879a200404cd83b7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:57:06 +0300 Subject: [PATCH 01/12] Add UUID index to DeviceEntity and set productDevice to lazy loading --- libs/common/src/modules/device/entities/device.entity.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 3a46e07..03a5d10 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; +import { Column, Entity, ManyToOne, OneToMany, Unique, Index } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto } from '../dtos/device.dto'; import { SpaceEntity } from '../../space/entities'; @@ -48,8 +48,14 @@ export class DeviceEntity extends AbstractEntity { @ManyToOne(() => ProductEntity, (product) => product.devicesProductEntity, { nullable: false, + lazy: true, }) productDevice: ProductEntity; + + @Index() + @Column({ nullable: false }) + uuid: string; + constructor(partial: Partial) { super(); Object.assign(this, partial); From 181aaf6c1c221de7241aa0f944de1e9e83e0d3fd Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:28:49 +0300 Subject: [PATCH 02/12] add password validator --- src/validators/password.validator.ts | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/validators/password.validator.ts diff --git a/src/validators/password.validator.ts b/src/validators/password.validator.ts new file mode 100644 index 0000000..4828b60 --- /dev/null +++ b/src/validators/password.validator.ts @@ -0,0 +1,33 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: false }) +export class IsPasswordStrongConstraint + implements ValidatorConstraintInterface +{ + validate(password: string) { + const regex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + return regex.test(password); + } + + defaultMessage() { + return 'password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one numeric digit, and one special character.'; + } +} + +export function IsPasswordStrong(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsPasswordStrongConstraint, + }); + }; +} From 6040976c07bd71ad801f10db4f219f7db66944a7 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:29:10 +0300 Subject: [PATCH 03/12] add sign up password validator --- src/auth/dtos/user-auth.dto.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/auth/dtos/user-auth.dto.ts b/src/auth/dtos/user-auth.dto.ts index 729b735..1d6e2a5 100644 --- a/src/auth/dtos/user-auth.dto.ts +++ b/src/auth/dtos/user-auth.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; +import { IsPasswordStrong } from 'src/validators/password.validator'; export class UserSignUpDto { @ApiProperty({ @@ -16,6 +17,10 @@ export class UserSignUpDto { }) @IsString() @IsNotEmpty() + @IsPasswordStrong({ + message: + 'password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one numeric digit, and one special character.', + }) public password: string; @ApiProperty({ From 7ba4271b6ee244b739b51d9bcb5bde2edff9ceaf Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:29:24 +0300 Subject: [PATCH 04/12] add forget password validator --- src/auth/dtos/user-password.dto.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/auth/dtos/user-password.dto.ts b/src/auth/dtos/user-password.dto.ts index fe2118c..9a93935 100644 --- a/src/auth/dtos/user-password.dto.ts +++ b/src/auth/dtos/user-password.dto.ts @@ -1,14 +1,25 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; +import { IsPasswordStrong } from 'src/validators/password.validator'; export class ForgetPasswordDto { - @ApiProperty() + @ApiProperty({ + description: 'email', + required: true, + }) @IsEmail() @IsNotEmpty() - email: string; + public email: string; - @ApiProperty() + @ApiProperty({ + description: 'password', + required: true, + }) @IsString() @IsNotEmpty() - password: string; + @IsPasswordStrong({ + message: + 'password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one numeric digit, and one special character.', + }) + public password: string; } 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 05/12] 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 06/12] 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 07/12] Add automation module --- src/automation/automation.module.ts | 12 +- .../controllers/automation.controller.ts | 128 ++++---- src/automation/dtos/automation.dto.ts | 158 +++++++--- .../interface/automation.interface.ts | 28 +- src/automation/services/automation.service.ts | 294 +++++++++++------- 5 files changed, 399 insertions(+), 221 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) { + 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/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 08/12] 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 09/12] 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 10/12] 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 11/12] 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', From 5e957e339554cff901464bb8996247cd11257a9c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:51:51 +0300 Subject: [PATCH 12/12] Refactor door lock controller and service --- .../controllers/door.lock.controller.ts | 58 ++++-- src/door-lock/dtos/add.offline-temp.dto.ts | 20 +- src/door-lock/dtos/update.offline-temp.dto.ts | 12 ++ .../interfaces/door.lock.interface.ts | 7 + src/door-lock/services/door.lock.service.ts | 184 ++++++++++++------ 5 files changed, 188 insertions(+), 93 deletions(-) create mode 100644 src/door-lock/dtos/update.offline-temp.dto.ts diff --git a/src/door-lock/controllers/door.lock.controller.ts b/src/door-lock/controllers/door.lock.controller.ts index f3605ef..44b9276 100644 --- a/src/door-lock/controllers/door.lock.controller.ts +++ b/src/door-lock/controllers/door.lock.controller.ts @@ -9,11 +9,13 @@ import { Get, Delete, UseGuards, + Put, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { AddDoorLockOnlineDto } from '../dtos/add.online-temp.dto'; -import { AddDoorLockOfflineTempDto } from '../dtos/add.offline-temp.dto'; +import { AddDoorLockOfflineTempMultipleTimeDto } from '../dtos/add.offline-temp.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { UpdateDoorLockOfflineTempDto } from '../dtos/update.offline-temp.dto'; @ApiTags('Door Lock Module') @Controller({ @@ -55,13 +57,11 @@ export class DoorLockController { @UseGuards(JwtAuthGuard) @Post('temporary-password/offline/one-time/:doorLockUuid') async addOfflineOneTimeTemporaryPassword( - @Body() addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, @Param('doorLockUuid') doorLockUuid: string, ) { try { const temporaryPassword = await this.doorLockService.addOfflineOneTimeTemporaryPassword( - addDoorLockOfflineTempDto, doorLockUuid, ); @@ -82,13 +82,14 @@ export class DoorLockController { @UseGuards(JwtAuthGuard) @Post('temporary-password/offline/multiple-time/:doorLockUuid') async addOfflineMultipleTimeTemporaryPassword( - @Body() addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + @Body() + addDoorLockOfflineTempMultipleTimeDto: AddDoorLockOfflineTempMultipleTimeDto, @Param('doorLockUuid') doorLockUuid: string, ) { try { const temporaryPassword = await this.doorLockService.addOfflineMultipleTimeTemporaryPassword( - addDoorLockOfflineTempDto, + addDoorLockOfflineTempMultipleTimeDto, doorLockUuid, ); @@ -124,6 +125,29 @@ export class DoorLockController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) + @Delete('temporary-password/online/:doorLockUuid/:passwordId') + async deleteDoorLockPassword( + @Param('doorLockUuid') doorLockUuid: string, + @Param('passwordId') passwordId: string, + ) { + try { + await this.doorLockService.deleteDoorLockPassword( + doorLockUuid, + passwordId, + ); + return { + statusCode: HttpStatus.OK, + message: 'Temporary Password deleted Successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Get('temporary-password/offline/one-time/:doorLockUuid') async getOfflineOneTimeTemporaryPasswords( @Param('doorLockUuid') doorLockUuid: string, @@ -156,21 +180,29 @@ export class DoorLockController { ); } } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Delete('temporary-password/:doorLockUuid/:passwordId') - async deleteDoorLockPassword( + @Put('temporary-password/:doorLockUuid/offline/:passwordId') + async updateOfflineTemporaryPassword( + @Body() + updateDoorLockOfflineTempDto: UpdateDoorLockOfflineTempDto, @Param('doorLockUuid') doorLockUuid: string, @Param('passwordId') passwordId: string, ) { try { - await this.doorLockService.deleteDoorLockPassword( - doorLockUuid, - passwordId, - ); + const temporaryPassword = + await this.doorLockService.updateOfflineTemporaryPassword( + updateDoorLockOfflineTempDto, + doorLockUuid, + passwordId, + ); + return { - statusCode: HttpStatus.OK, - message: 'Temporary Password deleted Successfully', + statusCode: HttpStatus.CREATED, + success: true, + message: 'offline temporary password updated successfully', + data: temporaryPassword, }; } catch (error) { throw new HttpException( diff --git a/src/door-lock/dtos/add.offline-temp.dto.ts b/src/door-lock/dtos/add.offline-temp.dto.ts index f30cd10..5935d38 100644 --- a/src/door-lock/dtos/add.offline-temp.dto.ts +++ b/src/door-lock/dtos/add.offline-temp.dto.ts @@ -1,23 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, Length } from 'class-validator'; - -export class AddDoorLockOfflineTempDto { - @ApiProperty({ - description: 'name', - required: true, - }) - @IsString() - @IsNotEmpty() - public name: string; - @ApiProperty({ - description: 'password', - required: true, - }) - @IsString() - @IsNotEmpty() - @Length(7, 7) - public password: string; +import { IsNotEmpty, IsString } from 'class-validator'; +export class AddDoorLockOfflineTempMultipleTimeDto { @ApiProperty({ description: 'effectiveTime', required: true, diff --git a/src/door-lock/dtos/update.offline-temp.dto.ts b/src/door-lock/dtos/update.offline-temp.dto.ts new file mode 100644 index 0000000..b766b5c --- /dev/null +++ b/src/door-lock/dtos/update.offline-temp.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateDoorLockOfflineTempDto { + @ApiProperty({ + description: 'name', + required: true, + }) + @IsString() + @IsNotEmpty() + public name: string; +} diff --git a/src/door-lock/interfaces/door.lock.interface.ts b/src/door-lock/interfaces/door.lock.interface.ts index 5b89061..99960df 100644 --- a/src/door-lock/interfaces/door.lock.interface.ts +++ b/src/door-lock/interfaces/door.lock.interface.ts @@ -55,3 +55,10 @@ export interface deleteTemporaryPasswordInterface { result: boolean; msg?: string; } +export interface getPasswordOfflineInterface { + success: boolean; + result: { + records: []; + }; + msg?: string; +} diff --git a/src/door-lock/services/door.lock.service.ts b/src/door-lock/services/door.lock.service.ts index e88fee4..a5892a6 100644 --- a/src/door-lock/services/door.lock.service.ts +++ b/src/door-lock/services/door.lock.service.ts @@ -7,13 +7,15 @@ import { createTickInterface, deleteTemporaryPasswordInterface, getPasswordInterface, + getPasswordOfflineInterface, } from '../interfaces/door.lock.interface'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { ProductType } from '@app/common/constants/product-type.enum'; import { PasswordEncryptionService } from './encryption.services'; -import { AddDoorLockOfflineTempDto } from '../dtos/add.offline-temp.dto'; +import { AddDoorLockOfflineTempMultipleTimeDto } from '../dtos/add.offline-temp.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { UpdateDoorLockOfflineTempDto } from '../dtos/update.offline-temp.dto'; @Injectable() export class DoorLockService { @@ -93,18 +95,13 @@ export class DoorLockService { HttpStatus.BAD_REQUEST, ); } - const passwords = await this.getTemporaryPasswordsTuya( + const passwords = await this.getTemporaryOfflinePasswordsTuya( deviceDetails.deviceTuyaUuid, + 'multiple', ); - if (passwords.result.length > 0) { - const passwordFiltered = passwords.result.filter( - (item) => - (!item.schedule_list || item.schedule_list.length === 0) && - item.type === 0, - ); - - return convertKeysToCamelCase(passwordFiltered); + if (passwords.result.records.length > 0) { + return convertKeysToCamelCase(passwords.result.records); } return passwords; @@ -128,18 +125,13 @@ export class DoorLockService { HttpStatus.BAD_REQUEST, ); } - const passwords = await this.getTemporaryPasswordsTuya( + const passwords = await this.getTemporaryOfflinePasswordsTuya( deviceDetails.deviceTuyaUuid, + 'once', ); - if (passwords.result.length > 0) { - const passwordFiltered = passwords.result.filter( - (item) => - (!item.schedule_list || item.schedule_list.length === 0) && - item.type === 0, //temp solution - ); - - return convertKeysToCamelCase(passwordFiltered); + if (passwords.result.records.length > 0) { + return convertKeysToCamelCase(passwords.result.records); } return passwords; @@ -162,13 +154,13 @@ export class DoorLockService { HttpStatus.BAD_REQUEST, ); } - const passwords = await this.getTemporaryPasswordsTuya( + const passwords = await this.getOnlineTemporaryPasswordsTuya( deviceDetails.deviceTuyaUuid, ); if (passwords.result.length > 0) { const passwordFiltered = passwords.result - .filter((item) => item.type === 0) //temp solution + .filter((item) => item.type === 0) .map((password: any) => { if (password.schedule_list?.length > 0) { password.schedule_list = password.schedule_list.map( @@ -200,7 +192,7 @@ export class DoorLockService { ); } } - async getTemporaryPasswordsTuya( + async getOnlineTemporaryPasswordsTuya( doorLockUuid: string, ): Promise { try { @@ -219,25 +211,45 @@ export class DoorLockService { ); } } + async getTemporaryOfflinePasswordsTuya( + doorLockUuid: string, + type: string, + ): Promise { + try { + const path = `/v1.0/devices/${doorLockUuid}/door-lock/offline-temp-password?pwd_type_codes=${type}&target_status=EFFECTIVE&page_no=1&page_size=100`; + + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + return response as getPasswordOfflineInterface; + } catch (error) { + throw new HttpException( + 'Error getting offline temporary passwords from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async addOfflineMultipleTimeTemporaryPassword( - addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, + addDoorLockOfflineTempMultipleTimeDto: AddDoorLockOfflineTempMultipleTimeDto, doorLockUuid: string, ) { try { - const createOnlinePass = await this.addOnlineTemporaryPassword( - addDoorLockOfflineTempDto, - doorLockUuid, - 'multiple', - false, - ); - if (!createOnlinePass) { + const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } else if (deviceDetails.productDevice.prodType !== ProductType.DL) { + throw new HttpException( + 'This is not a door lock device', + HttpStatus.BAD_REQUEST, + ); } const createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya( - addDoorLockOfflineTempDto, - createOnlinePass.id, - createOnlinePass.deviceTuyaUuid, + deviceDetails.deviceTuyaUuid, 'multiple', + addDoorLockOfflineTempMultipleTimeDto, ); if (!createOnceOfflinePass.success) { throw new HttpException( @@ -255,25 +267,22 @@ export class DoorLockService { ); } } - async addOfflineOneTimeTemporaryPassword( - addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto, - doorLockUuid: string, - ) { + async addOfflineOneTimeTemporaryPassword(doorLockUuid: string) { try { - const createOnlinePass = await this.addOnlineTemporaryPassword( - addDoorLockOfflineTempDto, - doorLockUuid, - 'once', - false, - ); - if (!createOnlinePass) { + const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } else if (deviceDetails.productDevice.prodType !== ProductType.DL) { + throw new HttpException( + 'This is not a door lock device', + HttpStatus.BAD_REQUEST, + ); } const createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya( - addDoorLockOfflineTempDto, - createOnlinePass.id, - createOnlinePass.deviceTuyaUuid, + deviceDetails.deviceTuyaUuid, 'once', + null, ); if (!createOnceOfflinePass.success) { throw new HttpException( @@ -292,10 +301,9 @@ export class DoorLockService { } } async addOfflineTemporaryPasswordTuya( - addDoorLockDto: AddDoorLockOnlineInterface, - onlinePassId: number, doorLockUuid: string, type: string, + addDoorLockOfflineTempMultipleTimeDto: AddDoorLockOfflineTempMultipleTimeDto, ): Promise { try { const path = `/v1.1/devices/${doorLockUuid}/door-lock/offline-temp-password`; @@ -304,14 +312,12 @@ export class DoorLockService { method: 'POST', path, body: { - name: addDoorLockDto.name, ...(type === 'multiple' && { - effective_time: addDoorLockDto.effectiveTime, - invalid_time: addDoorLockDto.invalidTime, + effective_time: addDoorLockOfflineTempMultipleTimeDto.effectiveTime, + invalid_time: addDoorLockOfflineTempMultipleTimeDto.invalidTime, }), type, - password_id: onlinePassId, }, }); @@ -326,8 +332,6 @@ export class DoorLockService { async addOnlineTemporaryPassword( addDoorLockDto: AddDoorLockOnlineInterface, doorLockUuid: string, - type: string = 'once', - isOnline: boolean = true, ) { try { const passwordData = await this.getTicketAndEncryptedPassword( @@ -348,8 +352,6 @@ export class DoorLockService { const createPass = await this.addOnlineTemporaryPasswordTuya( addDeviceObj, passwordData.deviceTuyaUuid, - type, - addDeviceObj.scheduleList ? isOnline : false, ); if (!createPass.success) { @@ -429,13 +431,11 @@ export class DoorLockService { async addOnlineTemporaryPasswordTuya( addDeviceObj: addDeviceObjectInterface, doorLockUuid: string, - type: string, - isOnline: boolean = true, ): Promise { try { const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-password`; let scheduleList; - if (isOnline) { + if (addDeviceObj.scheduleList.length > 0) { scheduleList = addDeviceObj.scheduleList.map((schedule) => ({ effective_time: this.timeToMinutes(schedule.effectiveTime), invalid_time: this.timeToMinutes(schedule.invalidTime), @@ -453,11 +453,11 @@ export class DoorLockService { invalid_time: addDeviceObj.invalidTime, password_type: 'ticket', ticket_id: addDeviceObj.ticketId, - ...(isOnline && { + ...(addDeviceObj.scheduleList.length > 0 && { schedule_list: scheduleList, }), - type: '0', //temporary solution, + type: '0', }, }); @@ -579,4 +579,64 @@ export class DoorLockService { throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); } } + + async updateOfflineTemporaryPassword( + updateDoorLockOfflineTempDto: UpdateDoorLockOfflineTempDto, + doorLockUuid: string, + passwordId: string, + ) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(doorLockUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } else if (deviceDetails.productDevice.prodType !== ProductType.DL) { + throw new HttpException( + 'This is not a door lock device', + HttpStatus.BAD_REQUEST, + ); + } + const updateOfflinePass = await this.updateOfflineTemporaryPasswordTuya( + deviceDetails.deviceTuyaUuid, + updateDoorLockOfflineTempDto, + passwordId, + ); + if (!updateOfflinePass.success) { + throw new HttpException(updateOfflinePass.msg, HttpStatus.BAD_REQUEST); + } + return { + result: updateOfflinePass.result, + }; + } catch (error) { + throw new HttpException( + error.message || 'Error updating offline temporary password from Tuya', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async updateOfflineTemporaryPasswordTuya( + doorLockUuid: string, + updateDoorLockOfflineTempDto: UpdateDoorLockOfflineTempDto, + passwordId: string, + ): Promise { + try { + const path = `/v1.0/cloud/lock/${doorLockUuid}/door-lock/offline-temp-password/${passwordId}`; + + const response = await this.tuya.request({ + method: 'PUT', + path, + body: { + password_name: updateDoorLockOfflineTempDto.name, + }, + }); + + return response as createTickInterface; + } catch (error) { + throw new HttpException( + 'Error updating offline temporary password from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } }