Fixed automation bugs

This commit is contained in:
hannathkadher
2024-11-05 00:48:54 +04:00
parent aad794b2cd
commit a3eaa0fa3c
10 changed files with 227 additions and 151 deletions

View File

@ -0,0 +1,9 @@
import { TuyaResponseInterface } from './tuya.response.interface';
export interface AddTuyaResponseInterface extends TuyaResponseInterface {
result: {
id: string;
};
t?: number;
tid?: string;
}

View File

@ -1,2 +1,3 @@
export * from './tuya.response.interface'; export * from './tuya.response.interface';
export * from './tap-to-run-action.interface'; export * from './tap-to-run-action.interface';
export * from './automation.interface';

View File

@ -1,5 +1,5 @@
export interface TuyaResponseInterface { export interface TuyaResponseInterface {
success: boolean; success: boolean;
msg?: string; msg?: string;
result: boolean; result: boolean | { id: string };
} }

View File

@ -1,7 +1,11 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConvertedAction, TuyaResponseInterface } from '../interfaces'; import {
AddTuyaResponseInterface,
ConvertedAction,
TuyaResponseInterface,
} from '../interfaces';
@Injectable() @Injectable()
export class TuyaService { export class TuyaService {
@ -144,32 +148,72 @@ export class TuyaService {
) { ) {
const path = `/v2.0/cloud/scene/rule`; 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 { try {
const response = await this.tuya.request({ const response: TuyaResponseInterface = await this.tuya.request({
method: 'POST', method: 'PUT',
path, path,
body: { body: {
space_id: spaceId, ids: automationUuid,
name: automationName, is_enable: isEnable,
effective_time: {
...effectiveTime,
timezone_id: 'Asia/Dubai',
},
type: 'automation',
decision_expr: decisionExpr,
conditions: conditions,
actions: actions,
}, },
}); });
if (!response.success) { 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) { } catch (error) {
throw new HttpException( throw new HttpException(
error.message || 'Failed to create automation in Tuya', error.message || 'Failed to update automation state',
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }

View File

@ -18,11 +18,7 @@ import {
} from '../dtos/automation.dto'; } from '../dtos/automation.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { import { AutomationParamDto, SpaceParamDto } from '../dtos';
AutomationParamDto,
DeleteAutomationParamDto,
SpaceParamDto,
} from '../dtos';
@ApiTags('Automation Module') @ApiTags('Automation Module')
@Controller({ @Controller({
@ -45,6 +41,7 @@ export class AutomationController {
data: automation, data: automation,
}; };
} }
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get(':spaceUuid') @Get(':spaceUuid')
@ -67,14 +64,15 @@ export class AutomationController {
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Delete(':unitUuid/:automationId') @Delete(':automationUuid')
async deleteAutomation(@Param() param: DeleteAutomationParamDto) { async deleteAutomation(@Param() param: AutomationParamDto) {
await this.automationService.deleteAutomation(param); await this.automationService.deleteAutomation(param);
return { return {
statusCode: HttpStatus.OK, statusCode: HttpStatus.OK,
message: 'Automation Deleted Successfully', message: 'Automation Deleted Successfully',
}; };
} }
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put(':automationUuid') @Put(':automationUuid')

View File

@ -10,7 +10,7 @@ import {
} from 'class-validator'; } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
class EffectiveTime { export class EffectiveTime {
@ApiProperty({ description: 'Start time', required: true }) @ApiProperty({ description: 'Start time', required: true })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()

View File

@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator'; import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
export class DeleteAutomationParamDto { export class DeleteAutomationParamDto {
@ApiProperty({ @ApiProperty({
@ -10,9 +10,10 @@ export class DeleteAutomationParamDto {
spaceUuid: string; spaceUuid: string;
@ApiProperty({ @ApiProperty({
description: 'UUID of the Automation', description: 'TuyaId of the automation',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', example: 'SfFi2Tbn09btes84',
}) })
@IsUUID() @IsString()
@IsNotEmpty()
automationUuid: string; automationUuid: string;
} }

View File

@ -1,3 +1,5 @@
import { EffectiveTime } from '../dtos';
export interface AddAutomationInterface { export interface AddAutomationInterface {
success: boolean; success: boolean;
msg?: string; msg?: string;
@ -48,3 +50,12 @@ export interface AutomationDetailsResult {
name: string; name: string;
type: string; type: string;
} }
export interface AddAutomationParams {
actions: Action[];
conditions: Condition[];
automationName: string;
effectiveTime: EffectiveTime;
decisionExpr: string;
spaceTuyaId: string;
}

View File

@ -7,7 +7,7 @@ import {
import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories';
import { import {
AddAutomationDto, AddAutomationDto,
DeleteAutomationParamDto, AutomationParamDto,
UpdateAutomationDto, UpdateAutomationDto,
UpdateAutomationStatusDto, UpdateAutomationStatusDto,
} from '../dtos'; } from '../dtos';
@ -17,11 +17,10 @@ import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { DeviceService } from 'src/device/services'; import { DeviceService } from 'src/device/services';
import { import {
Action, Action,
AddAutomationInterface, AddAutomationParams,
AutomationDetailsResult, AutomationDetailsResult,
AutomationResponseData, AutomationResponseData,
Condition, Condition,
DeleteAutomationInterface,
GetAutomationBySpaceInterface, GetAutomationBySpaceInterface,
} from '../interface/automation.interface'; } from '../interface/automation.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
@ -30,6 +29,7 @@ import {
EntityTypeEnum, EntityTypeEnum,
} from '@app/common/constants/automation.enum'; } from '@app/common/constants/automation.enum';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
@Injectable() @Injectable()
export class AutomationService { export class AutomationService {
@ -50,39 +50,26 @@ export class AutomationService {
}); });
} }
async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { async addAutomation(addAutomationDto: AddAutomationDto) {
try { try {
const { automationName, effectiveTime, decisionExpr } = addAutomationDto; const {
const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid);
const actions = await this.processEntities<Action>(
addAutomationDto.actions,
'actionExecutor',
{ [ActionExecutorEnum.DEVICE_ISSUE]: true },
this.deviceService,
);
const conditions = await this.processEntities<Condition>(
addAutomationDto.conditions,
'entityType',
{ [EntityTypeEnum.DEVICE_REPORT]: true },
this.deviceService,
);
const response = (await this.tuyaService.createAutomation(
space.spaceTuyaUuid,
automationName, automationName,
effectiveTime, effectiveTime,
decisionExpr, decisionExpr,
conditions,
actions, actions,
)) as AddAutomationInterface; conditions,
} = addAutomationDto;
return { const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid);
id: response?.result.id, const response = await this.add({
}; automationName,
effectiveTime,
decisionExpr,
actions,
conditions,
spaceTuyaId: space.spaceTuyaUuid,
});
return response;
} catch (err) { } catch (err) {
console.log(err);
if (err instanceof BadRequestException) { if (err instanceof BadRequestException) {
throw err; throw err;
} else { } 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) { async getSpaceByUuid(spaceUuid: string) {
try { try {
const space = await this.spaceRepository.findOne({ const space = await this.spaceRepository.findOne({
@ -120,6 +134,7 @@ export class AutomationService {
} }
} }
} }
async getAutomationBySpace(spaceUuid: string) { async getAutomationBySpace(spaceUuid: string) {
try { try {
const space = await this.getSpaceByUuid(spaceUuid); const space = await this.getSpaceByUuid(spaceUuid);
@ -277,34 +292,45 @@ export class AutomationService {
} }
} }
async deleteAutomation(param: DeleteAutomationParamDto, spaceTuyaId = null) { async deleteAutomation(param: AutomationParamDto) {
try { try {
const { automationUuid, spaceUuid } = param; const { automationUuid } = param;
let tuyaSpaceId;
if (!spaceTuyaId) { const automation = await this.getAutomationDetails(automationUuid, true);
const space = await this.getSpaceByUuid(spaceUuid);
tuyaSpaceId = space.spaceTuyaUuid; if (!automation && !automation.spaceId) {
if (!tuyaSpaceId) { throw new HttpException(
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`); `Invalid automationid ${automationUuid}`,
} HttpStatus.BAD_REQUEST,
} else { );
tuyaSpaceId = spaceTuyaId;
} }
const response = this.tuyaService.deleteAutomation(
const path = `/v2.0/cloud/scene/rule?ids=${automationUuid}&space_id=${tuyaSpaceId}`; automation.spaceId,
const response: DeleteAutomationInterface = await this.tuya.request({ automationUuid,
method: 'DELETE', );
path,
});
if (!response.success) {
throw new HttpException('Automation not found', HttpStatus.NOT_FOUND);
}
return response; return response;
} catch (err) { } catch (err) {
if (err instanceof BadRequestException) { if (err instanceof HttpException) {
throw err; // Re-throw BadRequestException 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 { } else {
throw new HttpException( throw new HttpException(
err.message || 'Automation not found', err.message || 'Automation not found',
@ -318,28 +344,28 @@ export class AutomationService {
updateAutomationDto: UpdateAutomationDto, updateAutomationDto: UpdateAutomationDto,
automationUuid: string, automationUuid: string,
) { ) {
const { actions, conditions, automationName, effectiveTime, decisionExpr } =
updateAutomationDto;
try { try {
const spaceTuyaId = await this.getAutomationDetails(automationUuid, true); const automation = await this.getAutomationDetails(automationUuid, true);
if (!spaceTuyaId.spaceId) { if (!automation.spaceId) {
throw new HttpException( throw new HttpException(
"Automation doesn't exist", "Automation doesn't exist",
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
); );
} }
const addAutomation = {
...updateAutomationDto, const newAutomation = await this.add({
spaceUuid: null, actions,
}; conditions,
const newAutomation = await this.addAutomation( automationName,
addAutomation, effectiveTime,
spaceTuyaId.spaceId, decisionExpr,
); spaceTuyaId: automation.spaceId,
const params: DeleteAutomationParamDto = { });
spaceUuid: spaceTuyaId.spaceId,
automationUuid: automationUuid,
};
if (newAutomation.id) { if (newAutomation.id) {
await this.deleteAutomation(null, params); await this.delete(automation.spaceId, automationUuid);
return newAutomation; return newAutomation;
} }
} catch (err) { } catch (err) {
@ -357,29 +383,21 @@ export class AutomationService {
updateAutomationStatusDto: UpdateAutomationStatusDto, updateAutomationStatusDto: UpdateAutomationStatusDto,
automationUuid: string, automationUuid: string,
) { ) {
const { isEnable, spaceUuid } = updateAutomationStatusDto;
try { try {
const space = await this.getSpaceByUuid( const space = await this.getSpaceByUuid(spaceUuid);
updateAutomationStatusDto.spaceUuid,
);
if (!space.spaceTuyaUuid) { if (!space.spaceTuyaUuid) {
throw new BadRequestException( throw new HttpException(
`Invalid space UUID ${updateAutomationStatusDto.spaceUuid}`, `Invalid space UUID ${spaceUuid}`,
HttpStatus.NOT_FOUND,
); );
} }
const path = `/v2.0/cloud/scene/rule/state?space_id=${space.spaceTuyaUuid}`; const response = await this.tuyaService.updateAutomationState(
const response: DeleteAutomationInterface = await this.tuya.request({ space.spaceTuyaUuid,
method: 'PUT', automationUuid,
path, isEnable,
body: { );
ids: automationUuid,
is_enable: updateAutomationStatusDto.isEnable,
},
});
if (!response.success) {
throw new HttpException('Automation not found', HttpStatus.NOT_FOUND);
}
return response; return response;
} catch (err) { } catch (err) {
@ -394,41 +412,41 @@ export class AutomationService {
} }
} }
async processEntities<T extends Action | Condition>( private async prepareActions(actions: Action[]): Promise<ConvertedAction[]> {
entities: T[], // Accepts either Action[] or Condition[] const convertedData = convertKeysToSnakeCase(actions) as ConvertedAction[];
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<T[]> {
// 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;
// Check if entity needs device UUID lookup await Promise.all(
const key = processedEntity[lookupKey]; convertedData.map(async (action) => {
if ( if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) {
entityTypeOrExecutorMap[key as ActionExecutorEnum | EntityTypeEnum] const device = await this.deviceService.getDeviceByDeviceUuid(
) { action.entity_id,
const device = await deviceService.getDeviceByDeviceUuid(
processedEntity.entityId,
false, false,
); );
if (device) { 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;
} }
} }

View File

@ -70,16 +70,10 @@ export class SubspaceDeviceService {
device.subspace = subspace; device.subspace = subspace;
console.log(
'Starting to save device to subspace:',
new Date(),
device.subspace,
);
const newDevice = await this.deviceRepository.save(device); const newDevice = await this.deviceRepository.save(device);
console.log('Device saved to subspace:', new Date(), newDevice);
return new SuccessResponseDto({ return new SuccessResponseDto({
data: device, data: newDevice,
message: 'Successfully associated device to subspace', message: 'Successfully associated device to subspace',
}); });
} catch (error) { } catch (error) {