From a133f78bb33189c22149a5da7129fe78e155fa43 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Mon, 4 Nov 2024 12:20:08 +0400 Subject: [PATCH 1/9] fixed endpoint for disassociate --- .../subspace/subspace-device.controller.ts | 15 +++- .../subspace/subspace-device.service.ts | 86 ++++++++++++++----- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/src/space/controllers/subspace/subspace-device.controller.ts b/src/space/controllers/subspace/subspace-device.controller.ts index 46c4863..189d490 100644 --- a/src/space/controllers/subspace/subspace-device.controller.ts +++ b/src/space/controllers/subspace/subspace-device.controller.ts @@ -1,6 +1,13 @@ import { ControllerRoute } from '@app/common/constants/controller-route'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; +import { + Controller, + Delete, + Get, + Param, + Post, + UseGuards, +} from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos'; import { SubspaceDeviceService } from 'src/space/services'; @@ -55,10 +62,12 @@ export class SubSpaceDeviceController { ControllerRoute.SUBSPACE_DEVICE.ACTIONS .DISASSOCIATE_SUBSPACE_DEVICE_DESCRIPTION, }) - @Post('/:deviceUuid') + @Delete('/:deviceUuid') async disassociateDeviceFromSubspace( @Param() params: DeviceSubSpaceParam, ): Promise { - return await this.subspaceDeviceService.associateDeviceToSubspace(params); + return await this.subspaceDeviceService.disassociateDeviceFromSubspace( + params, + ); } } diff --git a/src/space/services/subspace/subspace-device.service.ts b/src/space/services/subspace/subspace-device.service.ts index 06464b4..3f14a50 100644 --- a/src/space/services/subspace/subspace-device.service.ts +++ b/src/space/services/subspace/subspace-device.service.ts @@ -57,40 +57,84 @@ export class SubspaceDeviceService { message: 'Successfully retrieved list of devices', }); } - async associateDeviceToSubspace(params: DeviceSubSpaceParam) { + + async associateDeviceToSubspace( + params: DeviceSubSpaceParam, + ): Promise { const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid } = params; + try { + await this.validateCommunityAndSpace(communityUuid, spaceUuid); - await this.validateCommunityAndSpace(communityUuid, spaceUuid); + const subspace = await this.findSubspace(subSpaceUuid); + const device = await this.findDevice(deviceUuid); - const subspace = await this.findSubspace(subSpaceUuid); - const device = await this.findDevice(deviceUuid); + device.subspace = subspace; - device.subspace = subspace; - await this.deviceRepository.save(device); + console.log( + 'Starting to save device to subspace:', + new Date(), + device.subspace, + ); + const newDevice = await this.deviceRepository.save(device); + console.log('Device saved to subspace:', new Date(), newDevice); - return new SuccessResponseDto({ - data: device, - message: 'Successfully associated device to subspace', - }); + return new SuccessResponseDto({ + data: device, + message: 'Successfully associated device to subspace', + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException( + 'Failed to associate device to subspace', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } - async disassociateDeviceFromSubspace(params: DeviceSubSpaceParam) { + async disassociateDeviceFromSubspace( + params: DeviceSubSpaceParam, + ): Promise { const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid } = params; + try { + await this.validateCommunityAndSpace(communityUuid, spaceUuid); - await this.validateCommunityAndSpace(communityUuid, spaceUuid); + const subspace = await this.findSubspace(subSpaceUuid); + const device = await this.findDevice(deviceUuid); - await this.findSubspace(subSpaceUuid); - const device = await this.findDevice(deviceUuid); + if (!device.subspace || device.subspace.uuid !== subspace.uuid) { + throw new HttpException( + 'Device is not associated with the specified subspace', + HttpStatus.BAD_REQUEST, + ); + } - device.subspace = null; - await this.deviceRepository.save(device); + device.subspace = null; - return new SuccessResponseDto({ - data: device, - message: 'Successfully dissociated device from subspace', - }); + console.log( + 'Starting to save device with null subspace:', + new Date(), + device.subspace, + ); + const updatedDevice = await this.deviceRepository.save(device); + + return new SuccessResponseDto({ + data: updatedDevice, + message: 'Successfully dissociated device from subspace', + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException( + 'Failed to dissociate device from subspace', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } - // Helper method to validate community and space private async validateCommunityAndSpace( communityUuid: string, From 8cbe910f4caa3fdff53de99c8342a9d71ea67093 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Mon, 4 Nov 2024 19:43:49 +0400 Subject: [PATCH 2/9] fixed automation dto --- src/automation/dtos/automation.param.dto.ts | 9 +++++---- src/scene/services/scene.service.ts | 6 +++--- src/space/services/space-scene.service.ts | 11 +++-------- .../services/subspace/subspace-device.service.ts | 8 +------- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/automation/dtos/automation.param.dto.ts b/src/automation/dtos/automation.param.dto.ts index 4c21461..e0b3990 100644 --- a/src/automation/dtos/automation.param.dto.ts +++ b/src/automation/dtos/automation.param.dto.ts @@ -1,11 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsUUID } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class AutomationParamDto { @ApiProperty({ - description: 'UUID of the automation', - example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + description: 'TuyaId of the automation', + example: 'SfFi2Tbn09btes84', }) - @IsUUID() + @IsString() + @IsNotEmpty() automationUuid: string; } diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 930ee61..ded9408 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -157,7 +157,7 @@ export class SceneService { ); } else { throw new HttpException( - 'An Internal error has been occured', + `An Internal error has been occured ${err}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -178,7 +178,7 @@ export class SceneService { if (!scenesData.length) { throw new HttpException( - `No scenes found for space UUID ${spaceUuid}`, + `No scenes found for space UUID ${spaceUuid} with showInHomePage ${showInHomePage} `, HttpStatus.NOT_FOUND, ); } @@ -199,7 +199,7 @@ export class SceneService { err.message, ); - if (err instanceof BadRequestException) { + if (err instanceof HttpException) { throw err; } else { throw new HttpException( diff --git a/src/space/services/space-scene.service.ts b/src/space/services/space-scene.service.ts index ffc10e0..ac26889 100644 --- a/src/space/services/space-scene.service.ts +++ b/src/space/services/space-scene.service.ts @@ -1,9 +1,4 @@ -import { - BadRequestException, - HttpException, - HttpStatus, - Injectable, -} from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { GetSpaceParam } from '../dtos'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SpaceService } from './space.service'; @@ -42,8 +37,8 @@ export class SpaceSceneService { } catch (error) { console.error('Error retrieving scenes:', error); - if (error instanceof BadRequestException) { - throw new HttpException(error.message, HttpStatus.BAD_REQUEST); + if (error instanceof HttpException) { + throw error; } else { throw new HttpException( 'An error occurred while retrieving scenes', diff --git a/src/space/services/subspace/subspace-device.service.ts b/src/space/services/subspace/subspace-device.service.ts index 3f14a50..01c6f8a 100644 --- a/src/space/services/subspace/subspace-device.service.ts +++ b/src/space/services/subspace/subspace-device.service.ts @@ -112,12 +112,6 @@ export class SubspaceDeviceService { } device.subspace = null; - - console.log( - 'Starting to save device with null subspace:', - new Date(), - device.subspace, - ); const updatedDevice = await this.deviceRepository.save(device); return new SuccessResponseDto({ @@ -217,7 +211,7 @@ export class SubspaceDeviceService { } as GetDeviceDetailsInterface; } catch (error) { throw new HttpException( - 'Error fetching device details from Tuya', + `Error fetching device details from Tuya for device uuid ${deviceId}.`, HttpStatus.INTERNAL_SERVER_ERROR, ); } From 6e2f3bb8b6f05f77859b1b7607c992148c4ac534 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Mon, 4 Nov 2024 20:40:27 +0400 Subject: [PATCH 3/9] excluded tuya device id from response --- src/space/services/space-device.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/space/services/space-device.service.ts b/src/space/services/space-device.service.ts index 9d9eac8..6f0f183 100644 --- a/src/space/services/space-device.service.ts +++ b/src/space/services/space-device.service.ts @@ -100,7 +100,7 @@ export class SpaceDeviceService { }); // Exclude specific keys and add `productUuid` - const { ...rest } = camelCaseResponse; + const { uuid, ...rest } = camelCaseResponse; return { ...rest, productUuid: product?.uuid, From 0d000e4b60d010a0882b3829861331946111084e Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Mon, 4 Nov 2024 20:57:32 +0400 Subject: [PATCH 4/9] updated group module --- src/group/controllers/group.controller.ts | 13 ++++++------- src/group/services/group.service.ts | 17 ++++++++--------- src/space/services/space-device.service.ts | 1 + 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index ed79baf..3098da0 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -2,7 +2,6 @@ import { GroupService } from '../services/group.service'; import { Controller, Get, UseGuards, Param, Req } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { UnitPermissionGuard } from 'src/guards/unit.permission.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; @ApiTags('Group Module') @@ -17,21 +16,21 @@ export class GroupController { @UseGuards(JwtAuthGuard) @Get(':spaceUuid') async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) { - return await this.groupService.getGroupsByUnitUuid(spaceUuid); + return await this.groupService.getGroupsBySpaceUuid(spaceUuid); } - + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':unitUuid/devices/:groupName') + @Get(':spaceUuid/devices/:groupName') async getUnitDevicesByGroupName( - @Param('unitUuid') unitUuid: string, + @Param('spaceUuid') spaceUuid: string, @Param('groupName') groupName: string, @Req() req: any, ) { const userUuid = req.user.uuid; - return await this.groupService.getUnitDevicesByGroupName( - unitUuid, + return await this.groupService.getSpaceDevicesByGroupName( + spaceUuid, groupName, userUuid, ); diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index e8e39f9..9759232 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -27,11 +27,11 @@ export class GroupService { }); } - async getGroupsByUnitUuid(unitUuid: string) { + async getGroupsBySpaceUuid(spaceUuid: string) { try { const spaces = await this.spaceRepository.find({ where: { - uuid: unitUuid, + uuid: spaceUuid, }, relations: ['devices', 'devices.productDevice'], }); @@ -61,23 +61,21 @@ export class GroupService { ); } catch (error) { throw new HttpException( - 'This unit does not have any groups', + 'This space does not have any groups', HttpStatus.NOT_FOUND, ); } } - async getUnitDevicesByGroupName( - unitUuid: string, + async getSpaceDevicesByGroupName( + spaceUuid: string, groupName: string, userUuid: string, ) { try { const spaces = await this.spaceRepository.find({ where: { - parent: { - uuid: unitUuid, - }, + uuid: spaceUuid, devices: { productDevice: { prodType: groupName, @@ -122,8 +120,9 @@ export class GroupService { throw new HttpException('No devices found', HttpStatus.NOT_FOUND); return devices.flat(); // Flatten the array since flatMap was used } catch (error) { + console.log(error); throw new HttpException( - 'This unit does not have any devices for the specified group name', + 'This space does not have any devices for the specified group name', HttpStatus.NOT_FOUND, ); } diff --git a/src/space/services/space-device.service.ts b/src/space/services/space-device.service.ts index 6f0f183..b806c94 100644 --- a/src/space/services/space-device.service.ts +++ b/src/space/services/space-device.service.ts @@ -100,6 +100,7 @@ export class SpaceDeviceService { }); // Exclude specific keys and add `productUuid` + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { uuid, ...rest } = camelCaseResponse; return { ...rest, From aad794b2cded2b2ebc950787eedc528de9b9c61a Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Mon, 4 Nov 2024 22:10:05 +0400 Subject: [PATCH 5/9] fixed automation dtos --- src/automation/controllers/automation.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/automation/controllers/automation.controller.ts b/src/automation/controllers/automation.controller.ts index 675cd0b..8e83ef3 100644 --- a/src/automation/controllers/automation.controller.ts +++ b/src/automation/controllers/automation.controller.ts @@ -77,7 +77,7 @@ export class AutomationController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Put(':automationId') + @Put(':automationUuid') async updateAutomation( @Body() updateAutomationDto: UpdateAutomationDto, @Param() param: AutomationParamDto, @@ -96,7 +96,7 @@ export class AutomationController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Put('status/:automationId') + @Put('status/:automationUuid') async updateAutomationStatus( @Body() updateAutomationStatusDto: UpdateAutomationStatusDto, @Param() param: AutomationParamDto, From a3eaa0fa3c13802471c133a2b984ab6630d4223c Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Tue, 5 Nov 2024 00:48:54 +0400 Subject: [PATCH 6/9] Fixed automation bugs --- .../tuya/interfaces/automation.interface.ts | 9 + .../src/integrations/tuya/interfaces/index.ts | 1 + .../interfaces/tuya.response.interface.ts | 2 +- .../tuya/services/tuya.service.ts | 76 ++++-- .../controllers/automation.controller.ts | 12 +- src/automation/dtos/automation.dto.ts | 2 +- .../dtos/delete.automation.param.dto.ts | 9 +- .../interface/automation.interface.ts | 11 + src/automation/services/automation.service.ts | 248 ++++++++++-------- .../subspace/subspace-device.service.ts | 8 +- 10 files changed, 227 insertions(+), 151 deletions(-) create mode 100644 libs/common/src/integrations/tuya/interfaces/automation.interface.ts diff --git a/libs/common/src/integrations/tuya/interfaces/automation.interface.ts b/libs/common/src/integrations/tuya/interfaces/automation.interface.ts new file mode 100644 index 0000000..fb061c2 --- /dev/null +++ b/libs/common/src/integrations/tuya/interfaces/automation.interface.ts @@ -0,0 +1,9 @@ +import { TuyaResponseInterface } from './tuya.response.interface'; + +export interface AddTuyaResponseInterface extends TuyaResponseInterface { + result: { + id: string; + }; + t?: number; + tid?: string; +} diff --git a/libs/common/src/integrations/tuya/interfaces/index.ts b/libs/common/src/integrations/tuya/interfaces/index.ts index 506fc06..06eff87 100644 --- a/libs/common/src/integrations/tuya/interfaces/index.ts +++ b/libs/common/src/integrations/tuya/interfaces/index.ts @@ -1,2 +1,3 @@ export * from './tuya.response.interface'; export * from './tap-to-run-action.interface'; +export * from './automation.interface'; diff --git a/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts b/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts index 3e3a2a9..141613d 100644 --- a/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts +++ b/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts @@ -1,5 +1,5 @@ export interface TuyaResponseInterface { success: boolean; msg?: string; - result: boolean; + result: boolean | { id: string }; } diff --git a/libs/common/src/integrations/tuya/services/tuya.service.ts b/libs/common/src/integrations/tuya/services/tuya.service.ts index 9aabb9a..ff69aa9 100644 --- a/libs/common/src/integrations/tuya/services/tuya.service.ts +++ b/libs/common/src/integrations/tuya/services/tuya.service.ts @@ -1,7 +1,11 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; -import { ConvertedAction, TuyaResponseInterface } from '../interfaces'; +import { + AddTuyaResponseInterface, + ConvertedAction, + TuyaResponseInterface, +} from '../interfaces'; @Injectable() export class TuyaService { @@ -144,32 +148,72 @@ export class TuyaService { ) { const path = `/v2.0/cloud/scene/rule`; + const response: AddTuyaResponseInterface = await this.tuya.request({ + method: 'POST', + path, + body: { + space_id: spaceId, + name: automationName, + effective_time: { + ...effectiveTime, + timezone_id: 'Asia/Dubai', + }, + type: 'automation', + decision_expr: decisionExpr, + conditions: conditions, + actions: actions, + }, + }); + + if (!response.success) { + throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } + + return response; + } + + async deleteAutomation(spaceId: string, automationUuid: string) { + const path = `/v2.0/cloud/scene/rule?ids=${automationUuid}&space_id=${spaceId}`; + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + if (!response.success) { + throw new HttpException( + 'Failed to delete automation', + HttpStatus.NOT_FOUND, + ); + } + + return response; + } + + async updateAutomationState( + spaceId: string, + automationUuid: string, + isEnable: boolean, + ) { + const path = `/v2.0/cloud/scene/rule/state?space_id=${spaceId}`; + try { - const response = await this.tuya.request({ - method: 'POST', + const response: TuyaResponseInterface = await this.tuya.request({ + method: 'PUT', path, body: { - space_id: spaceId, - name: automationName, - effective_time: { - ...effectiveTime, - timezone_id: 'Asia/Dubai', - }, - type: 'automation', - decision_expr: decisionExpr, - conditions: conditions, - actions: actions, + ids: automationUuid, + is_enable: isEnable, }, }); if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); } - return response.result; + return response; } catch (error) { throw new HttpException( - error.message || 'Failed to create automation in Tuya', + error.message || 'Failed to update automation state', error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } diff --git a/src/automation/controllers/automation.controller.ts b/src/automation/controllers/automation.controller.ts index 8e83ef3..49a5012 100644 --- a/src/automation/controllers/automation.controller.ts +++ b/src/automation/controllers/automation.controller.ts @@ -18,11 +18,7 @@ import { } from '../dtos/automation.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { - AutomationParamDto, - DeleteAutomationParamDto, - SpaceParamDto, -} from '../dtos'; +import { AutomationParamDto, SpaceParamDto } from '../dtos'; @ApiTags('Automation Module') @Controller({ @@ -45,6 +41,7 @@ export class AutomationController { data: automation, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':spaceUuid') @@ -67,14 +64,15 @@ export class AutomationController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Delete(':unitUuid/:automationId') - async deleteAutomation(@Param() param: DeleteAutomationParamDto) { + @Delete(':automationUuid') + async deleteAutomation(@Param() param: AutomationParamDto) { await this.automationService.deleteAutomation(param); return { statusCode: HttpStatus.OK, message: 'Automation Deleted Successfully', }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put(':automationUuid') diff --git a/src/automation/dtos/automation.dto.ts b/src/automation/dtos/automation.dto.ts index 60104ac..6241cce 100644 --- a/src/automation/dtos/automation.dto.ts +++ b/src/automation/dtos/automation.dto.ts @@ -10,7 +10,7 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; -class EffectiveTime { +export class EffectiveTime { @ApiProperty({ description: 'Start time', required: true }) @IsString() @IsNotEmpty() diff --git a/src/automation/dtos/delete.automation.param.dto.ts b/src/automation/dtos/delete.automation.param.dto.ts index 96ee49b..6421a80 100644 --- a/src/automation/dtos/delete.automation.param.dto.ts +++ b/src/automation/dtos/delete.automation.param.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsUUID } from 'class-validator'; +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; export class DeleteAutomationParamDto { @ApiProperty({ @@ -10,9 +10,10 @@ export class DeleteAutomationParamDto { spaceUuid: string; @ApiProperty({ - description: 'UUID of the Automation', - example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + description: 'TuyaId of the automation', + example: 'SfFi2Tbn09btes84', }) - @IsUUID() + @IsString() + @IsNotEmpty() automationUuid: string; } diff --git a/src/automation/interface/automation.interface.ts b/src/automation/interface/automation.interface.ts index eed3ef7..9d7f40f 100644 --- a/src/automation/interface/automation.interface.ts +++ b/src/automation/interface/automation.interface.ts @@ -1,3 +1,5 @@ +import { EffectiveTime } from '../dtos'; + export interface AddAutomationInterface { success: boolean; msg?: string; @@ -48,3 +50,12 @@ export interface AutomationDetailsResult { name: string; type: string; } + +export interface AddAutomationParams { + actions: Action[]; + conditions: Condition[]; + automationName: string; + effectiveTime: EffectiveTime; + decisionExpr: string; + spaceTuyaId: string; +} diff --git a/src/automation/services/automation.service.ts b/src/automation/services/automation.service.ts index a34eb49..468a4ae 100644 --- a/src/automation/services/automation.service.ts +++ b/src/automation/services/automation.service.ts @@ -7,7 +7,7 @@ import { import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddAutomationDto, - DeleteAutomationParamDto, + AutomationParamDto, UpdateAutomationDto, UpdateAutomationStatusDto, } from '../dtos'; @@ -17,11 +17,10 @@ import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; import { DeviceService } from 'src/device/services'; import { Action, - AddAutomationInterface, + AddAutomationParams, AutomationDetailsResult, AutomationResponseData, Condition, - DeleteAutomationInterface, GetAutomationBySpaceInterface, } from '../interface/automation.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; @@ -30,6 +29,7 @@ import { EntityTypeEnum, } from '@app/common/constants/automation.enum'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; @Injectable() export class AutomationService { @@ -50,39 +50,26 @@ export class AutomationService { }); } - async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { + async addAutomation(addAutomationDto: AddAutomationDto) { try { - const { automationName, effectiveTime, decisionExpr } = addAutomationDto; - const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid); - - const actions = await this.processEntities( - addAutomationDto.actions, - 'actionExecutor', - { [ActionExecutorEnum.DEVICE_ISSUE]: true }, - this.deviceService, - ); - - const conditions = await this.processEntities( - addAutomationDto.conditions, - 'entityType', - { [EntityTypeEnum.DEVICE_REPORT]: true }, - this.deviceService, - ); - - const response = (await this.tuyaService.createAutomation( - space.spaceTuyaUuid, + const { automationName, effectiveTime, decisionExpr, - conditions, actions, - )) as AddAutomationInterface; - - return { - id: response?.result.id, - }; + conditions, + } = addAutomationDto; + const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid); + const response = await this.add({ + automationName, + effectiveTime, + decisionExpr, + actions, + conditions, + spaceTuyaId: space.spaceTuyaUuid, + }); + return response; } catch (err) { - console.log(err); if (err instanceof BadRequestException) { throw err; } else { @@ -94,6 +81,33 @@ export class AutomationService { } } + async add(params: AddAutomationParams) { + try { + const formattedActions = await this.prepareActions(params.actions); + const formattedCondition = await this.prepareConditions( + params.conditions, + ); + + const response = await this.tuyaService.createAutomation( + params.spaceTuyaId, + params.automationName, + params.effectiveTime, + params.decisionExpr, + formattedCondition, + formattedActions, + ); + + return { + id: response?.result.id, + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to add automation', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getSpaceByUuid(spaceUuid: string) { try { const space = await this.spaceRepository.findOne({ @@ -120,6 +134,7 @@ export class AutomationService { } } } + async getAutomationBySpace(spaceUuid: string) { try { const space = await this.getSpaceByUuid(spaceUuid); @@ -277,34 +292,45 @@ export class AutomationService { } } - async deleteAutomation(param: DeleteAutomationParamDto, spaceTuyaId = null) { + async deleteAutomation(param: AutomationParamDto) { try { - const { automationUuid, spaceUuid } = param; - let tuyaSpaceId; - if (!spaceTuyaId) { - const space = await this.getSpaceByUuid(spaceUuid); - tuyaSpaceId = space.spaceTuyaUuid; - if (!tuyaSpaceId) { - throw new BadRequestException(`Invalid space UUID ${spaceUuid}`); - } - } else { - tuyaSpaceId = spaceTuyaId; + const { automationUuid } = param; + + const automation = await this.getAutomationDetails(automationUuid, true); + + if (!automation && !automation.spaceId) { + throw new HttpException( + `Invalid automationid ${automationUuid}`, + HttpStatus.BAD_REQUEST, + ); } - - const path = `/v2.0/cloud/scene/rule?ids=${automationUuid}&space_id=${tuyaSpaceId}`; - const response: DeleteAutomationInterface = await this.tuya.request({ - method: 'DELETE', - path, - }); - - if (!response.success) { - throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); - } - + const response = this.tuyaService.deleteAutomation( + automation.spaceId, + automationUuid, + ); return response; } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException( + err.message || 'Automation not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + + async delete(tuyaSpaceId: string, automationUuid: string) { + try { + const response = await this.tuyaService.deleteAutomation( + tuyaSpaceId, + automationUuid, + ); + return response; + } catch (err) { + if (err instanceof HttpException) { + throw err; } else { throw new HttpException( err.message || 'Automation not found', @@ -318,28 +344,28 @@ export class AutomationService { updateAutomationDto: UpdateAutomationDto, automationUuid: string, ) { + const { actions, conditions, automationName, effectiveTime, decisionExpr } = + updateAutomationDto; try { - const spaceTuyaId = await this.getAutomationDetails(automationUuid, true); - if (!spaceTuyaId.spaceId) { + const automation = await this.getAutomationDetails(automationUuid, true); + if (!automation.spaceId) { throw new HttpException( "Automation doesn't exist", HttpStatus.NOT_FOUND, ); } - const addAutomation = { - ...updateAutomationDto, - spaceUuid: null, - }; - const newAutomation = await this.addAutomation( - addAutomation, - spaceTuyaId.spaceId, - ); - const params: DeleteAutomationParamDto = { - spaceUuid: spaceTuyaId.spaceId, - automationUuid: automationUuid, - }; + + const newAutomation = await this.add({ + actions, + conditions, + automationName, + effectiveTime, + decisionExpr, + spaceTuyaId: automation.spaceId, + }); + if (newAutomation.id) { - await this.deleteAutomation(null, params); + await this.delete(automation.spaceId, automationUuid); return newAutomation; } } catch (err) { @@ -357,29 +383,21 @@ export class AutomationService { updateAutomationStatusDto: UpdateAutomationStatusDto, automationUuid: string, ) { + const { isEnable, spaceUuid } = updateAutomationStatusDto; try { - const space = await this.getSpaceByUuid( - updateAutomationStatusDto.spaceUuid, - ); + const space = await this.getSpaceByUuid(spaceUuid); if (!space.spaceTuyaUuid) { - throw new BadRequestException( - `Invalid space UUID ${updateAutomationStatusDto.spaceUuid}`, + throw new HttpException( + `Invalid space UUID ${spaceUuid}`, + HttpStatus.NOT_FOUND, ); } - const path = `/v2.0/cloud/scene/rule/state?space_id=${space.spaceTuyaUuid}`; - const response: DeleteAutomationInterface = await this.tuya.request({ - method: 'PUT', - path, - body: { - ids: automationUuid, - is_enable: updateAutomationStatusDto.isEnable, - }, - }); - - if (!response.success) { - throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); - } + const response = await this.tuyaService.updateAutomationState( + space.spaceTuyaUuid, + automationUuid, + isEnable, + ); return response; } catch (err) { @@ -394,41 +412,41 @@ export class AutomationService { } } - async processEntities( - entities: T[], // Accepts either Action[] or Condition[] - lookupKey: keyof T, // The key to look up, specific to T - entityTypeOrExecutorMap: { - [key in ActionExecutorEnum | EntityTypeEnum]?: boolean; - }, - deviceService: { - getDeviceByDeviceUuid: ( - id: string, - flag: boolean, - ) => Promise<{ deviceTuyaUuid: string } | null>; - }, - ): Promise { - // Returns the same type as provided in the input - return Promise.all( - entities.map(async (entity) => { - // Convert keys to snake case (assuming a utility function exists) - const processedEntity = convertKeysToSnakeCase(entity) as T; + private async prepareActions(actions: Action[]): Promise { + const convertedData = convertKeysToSnakeCase(actions) as ConvertedAction[]; - // Check if entity needs device UUID lookup - const key = processedEntity[lookupKey]; - if ( - entityTypeOrExecutorMap[key as ActionExecutorEnum | EntityTypeEnum] - ) { - const device = await deviceService.getDeviceByDeviceUuid( - processedEntity.entityId, + await Promise.all( + convertedData.map(async (action) => { + if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) { + const device = await this.deviceService.getDeviceByDeviceUuid( + action.entity_id, false, ); if (device) { - processedEntity.entityId = device.deviceTuyaUuid; + action.entity_id = device.deviceTuyaUuid; } } - - return processedEntity; }), ); + + return convertedData; + } + + private async prepareConditions(conditions: Condition[]) { + const convertedData = convertKeysToSnakeCase(conditions); + await Promise.all( + convertedData.map(async (condition) => { + if (condition.entity_type === EntityTypeEnum.DEVICE_REPORT) { + const device = await this.deviceService.getDeviceByDeviceUuid( + condition.entity_id, + false, + ); + if (device) { + condition.entity_id = device.deviceTuyaUuid; + } + } + }), + ); + return convertedData; } } diff --git a/src/space/services/subspace/subspace-device.service.ts b/src/space/services/subspace/subspace-device.service.ts index 01c6f8a..3b453fd 100644 --- a/src/space/services/subspace/subspace-device.service.ts +++ b/src/space/services/subspace/subspace-device.service.ts @@ -70,16 +70,10 @@ export class SubspaceDeviceService { device.subspace = subspace; - console.log( - 'Starting to save device to subspace:', - new Date(), - device.subspace, - ); const newDevice = await this.deviceRepository.save(device); - console.log('Device saved to subspace:', new Date(), newDevice); return new SuccessResponseDto({ - data: device, + data: newDevice, message: 'Successfully associated device to subspace', }); } catch (error) { From cde9e8b602b4cdd1cd7dcdd88d79df5b56879057 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Tue, 5 Nov 2024 11:48:57 +0400 Subject: [PATCH 7/9] Fixed user space permission --- src/space/controllers/space.controller.ts | 3 +- src/space/dtos/add.space.dto.ts | 40 ++++++ .../controllers/user-space.controller.ts | 38 +++++- src/users/dtos/add.space.dto.ts | 68 ++++++++++ src/users/dtos/index.ts | 1 + src/users/services/user-space.service.ts | 118 +++++++++++++++++- src/users/user.module.ts | 8 ++ 7 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 src/users/dtos/add.space.dto.ts diff --git a/src/space/controllers/space.controller.ts b/src/space/controllers/space.controller.ts index cb8cbb8..e0615eb 100644 --- a/src/space/controllers/space.controller.ts +++ b/src/space/controllers/space.controller.ts @@ -111,6 +111,7 @@ export class SpaceController { return this.spaceService.getSpacesHierarchyForSpace(params.spaceUuid); } + //should it be post? @ApiBearerAuth() @UseGuards(JwtAuthGuard) @ApiOperation({ @@ -118,7 +119,7 @@ export class SpaceController { description: ControllerRoute.SPACE.ACTIONS.CREATE_INVITATION_CODE_SPACE_DESCRIPTION, }) - @Post(':spaceUuid/invitation-code') + @Get(':spaceUuid/invitation-code') async generateSpaceInvitationCode( @Param() params: GetSpaceParam, ): Promise { diff --git a/src/space/dtos/add.space.dto.ts b/src/space/dtos/add.space.dto.ts index 9d383bd..a33bb34 100644 --- a/src/space/dtos/add.space.dto.ts +++ b/src/space/dtos/add.space.dto.ts @@ -33,3 +33,43 @@ export class AddSpaceDto { @IsBoolean() isPrivate: boolean; } + +export class AddUserSpaceDto { + @ApiProperty({ + description: 'spaceUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public spaceUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} + +export class AddUserSpaceUsingCodeDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + @ApiProperty({ + description: 'inviteCode', + required: true, + }) + @IsString() + @IsNotEmpty() + public inviteCode: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/users/controllers/user-space.controller.ts b/src/users/controllers/user-space.controller.ts index 519a033..df36a5a 100644 --- a/src/users/controllers/user-space.controller.ts +++ b/src/users/controllers/user-space.controller.ts @@ -1,11 +1,20 @@ import { ControllerRoute } from '@app/common/constants/controller-route'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + UseGuards, +} from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { UserSpaceService } from '../services'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { UserParamDto } from '../dtos'; +import { AddUserSpaceUsingCodeDto, UserParamDto } from '../dtos'; @ApiTags('User Module') @Controller({ @@ -27,4 +36,29 @@ export class UserSpaceController { ): Promise { return this.userSpaceService.getSpacesForUser(params.userUuid); } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('/verify-code') + async verifyCodeAndAddUserSpace( + @Body() dto: AddUserSpaceUsingCodeDto, + @Param() params: UserParamDto, + ) { + try { + await this.userSpaceService.verifyCodeAndAddUserSpace( + dto, + params.userUuid, + ); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user space added successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/users/dtos/add.space.dto.ts b/src/users/dtos/add.space.dto.ts new file mode 100644 index 0000000..635cec9 --- /dev/null +++ b/src/users/dtos/add.space.dto.ts @@ -0,0 +1,68 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsBoolean, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +export class AddSpaceDto { + @ApiProperty({ + description: 'Name of the space (e.g., Floor 1, Unit 101)', + example: 'Unit 101', + }) + @IsString() + @IsNotEmpty() + spaceName: string; + + @ApiProperty({ + description: 'UUID of the parent space (if any, for hierarchical spaces)', + example: 'f5d7e9c3-44bc-4b12-88f1-1b3cda84752e', + required: false, + }) + @IsUUID() + @IsOptional() + parentUuid?: string; + + @ApiProperty({ + description: 'Indicates whether the space is private or public', + example: false, + default: false, + }) + @IsBoolean() + isPrivate: boolean; +} + +export class AddUserSpaceDto { + @ApiProperty({ + description: 'spaceUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public spaceUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} + +export class AddUserSpaceUsingCodeDto { + @ApiProperty({ + description: 'inviteCode', + required: true, + }) + @IsString() + @IsNotEmpty() + public inviteCode: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/users/dtos/index.ts b/src/users/dtos/index.ts index 5ca1ea7..83bb739 100644 --- a/src/users/dtos/index.ts +++ b/src/users/dtos/index.ts @@ -1,3 +1,4 @@ export * from './update.user.dto'; export * from './user-community-param.dto'; export * from './user-param.dto'; +export * from './add.space.dto'; diff --git a/src/users/services/user-space.service.ts b/src/users/services/user-space.service.ts index 68e6f00..ca3b8d9 100644 --- a/src/users/services/user-space.service.ts +++ b/src/users/services/user-space.service.ts @@ -2,10 +2,20 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { UserSpaceRepository } from '@app/common/modules/user/repositories'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { AddUserSpaceDto, AddUserSpaceUsingCodeDto } from '../dtos'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { SpaceEntity } from '@app/common/modules/space/entities'; @Injectable() export class UserSpaceService { - constructor(private readonly userSpaceRepository: UserSpaceRepository) {} + constructor( + private readonly userSpaceRepository: UserSpaceRepository, + private readonly spaceRepository: SpaceRepository, + private readonly userDevicePermissionService: UserDevicePermissionService, + ) {} async getSpacesForUser(userUuid: string): Promise { const userSpaces = await this.userSpaceRepository.find({ @@ -25,4 +35,110 @@ export class UserSpaceService { message: `Spaces for user ${userUuid} retrieved successfully`, }); } + + async verifyCodeAndAddUserSpace( + params: AddUserSpaceUsingCodeDto, + userUuid: string, + ) { + try { + const space = await this.findUnitByInviteCode(params.inviteCode); + + await this.addUserToSpace(userUuid, space.uuid); + + await this.clearUnitInvitationCode(space.uuid); + + const deviceUUIDs = await this.getDeviceUUIDsForSpace(space.uuid); + + await this.addUserPermissionsToDevices(userUuid, deviceUUIDs); + } catch (err) { + throw new HttpException( + 'Invalid invitation code', + HttpStatus.BAD_REQUEST, + ); + } + } + + private async findUnitByInviteCode(inviteCode: string): Promise { + const space = await this.spaceRepository.findOneOrFail({ + where: { + invitationCode: inviteCode, + }, + }); + + return space; + } + + private async addUserToSpace(userUuid: string, spaceUuid: string) { + const user = await this.addUserSpace({ userUuid, spaceUuid }); + + if (user.uuid) { + return user; + } else { + throw new HttpException( + 'Failed to add user to unit', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async addUserSpace(addUserSpaceDto: AddUserSpaceDto) { + try { + return await this.userSpaceRepository.save({ + user: { uuid: addUserSpaceDto.userUuid }, + space: { uuid: addUserSpaceDto.spaceUuid }, + }); + } catch (err) { + if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) { + throw new HttpException( + 'User already belongs to this unit', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async clearUnitInvitationCode(spaceUuid: string) { + await this.spaceRepository.update( + { uuid: spaceUuid }, + { invitationCode: null }, + ); + } + + private async getDeviceUUIDsForSpace( + unitUuid: string, + ): Promise<{ uuid: string }[]> { + const devices = await this.spaceRepository.find({ + where: { uuid: unitUuid }, + relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], + }); + + const allDevices = devices.flatMap((space) => space.devices); + + return allDevices.map((device) => ({ uuid: device.uuid })); + } + + private async addUserPermissionsToDevices( + userUuid: string, + deviceUUIDs: { uuid: string }[], + ): Promise { + const permissionPromises = deviceUUIDs.map(async (device) => { + try { + await this.userDevicePermissionService.addUserPermission({ + userUuid, + deviceUuid: device.uuid, + permissionType: PermissionType.CONTROLLABLE, + }); + } catch (error) { + console.error( + `Failed to add permission for device ${device.uuid}: ${error.message}`, + ); + } + }); + + await Promise.all(permissionPromises); + } } diff --git a/src/users/user.module.ts b/src/users/user.module.ts index d65ee68..20bd06f 100644 --- a/src/users/user.module.ts +++ b/src/users/user.module.ts @@ -12,6 +12,10 @@ import { UserSpaceController } from './controllers'; import { CommunityModule } from 'src/community/community.module'; import { UserSpaceService } from './services'; import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; @Module({ imports: [ConfigModule, CommunityModule], @@ -20,9 +24,13 @@ import { CommunityRepository } from '@app/common/modules/community/repositories' UserService, UserRepository, RegionRepository, + SpaceRepository, TimeZoneRepository, UserSpaceRepository, CommunityRepository, + UserDevicePermissionService, + DeviceUserPermissionRepository, + PermissionTypeRepository, UserSpaceService, ], exports: [UserService], From b572143f04c0a75e6466d3810e837d4427544499 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Tue, 5 Nov 2024 12:25:42 +0400 Subject: [PATCH 8/9] add subspace relation --- src/space/services/subspace/subspace-device.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/space/services/subspace/subspace-device.service.ts b/src/space/services/subspace/subspace-device.service.ts index 3b453fd..0e57bda 100644 --- a/src/space/services/subspace/subspace-device.service.ts +++ b/src/space/services/subspace/subspace-device.service.ts @@ -100,7 +100,7 @@ export class SubspaceDeviceService { if (!device.subspace || device.subspace.uuid !== subspace.uuid) { throw new HttpException( - 'Device is not associated with the specified subspace', + `Device ${deviceUuid} is not associated with the specified subspace ${subSpaceUuid} `, HttpStatus.BAD_REQUEST, ); } @@ -169,6 +169,7 @@ export class SubspaceDeviceService { private async findDevice(deviceUuid: string) { const device = await this.deviceRepository.findOne({ where: { uuid: deviceUuid }, + relations: ['subspace'], }); if (!device) { this.throwNotFound('Device', deviceUuid); From a39c28eedae81ee31fbfbe49467e17ea085d261d Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Tue, 5 Nov 2024 13:04:50 +0400 Subject: [PATCH 9/9] fixed showInHomePage --- src/scene/services/scene.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index ded9408..bd83ac1 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -172,7 +172,7 @@ export class SceneService { const scenesData = await this.sceneRepository.find({ where: { spaceUuid, - ...(showInHomePage !== undefined ? { showInHomePage } : {}), + ...(showInHomePage ? { showInHomePage } : {}), }, });