mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-14 18:05:48 +00:00
Merge branch 'dev' into SP-203-handle-user-profile
This commit is contained in:
@ -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;
|
||||
|
@ -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<DeviceDto> {
|
||||
|
||||
@ManyToOne(() => ProductEntity, (product) => product.devicesProductEntity, {
|
||||
nullable: false,
|
||||
lazy: true,
|
||||
})
|
||||
productDevice: ProductEntity;
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: false })
|
||||
uuid: string;
|
||||
|
||||
constructor(partial: Partial<DeviceEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -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';
|
||||
import { RegionModule } from './region/region.module';
|
||||
import { TimeZoneModule } from './timezone/timezone.module';
|
||||
@Module({
|
||||
@ -43,6 +44,7 @@ import { TimeZoneModule } from './timezone/timezone.module';
|
||||
UserNotificationModule,
|
||||
SeederModule,
|
||||
SceneModule,
|
||||
AutomationModule,
|
||||
DoorLockModule,
|
||||
RegionModule,
|
||||
TimeZoneModule,
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
||||
|
23
src/automation/automation.module.ts
Normal file
23
src/automation/automation.module.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
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';
|
||||
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: [AutomationController],
|
||||
providers: [
|
||||
AutomationService,
|
||||
SpaceRepository,
|
||||
DeviceService,
|
||||
DeviceRepository,
|
||||
ProductRepository,
|
||||
],
|
||||
exports: [AutomationService],
|
||||
})
|
||||
export class AutomationModule {}
|
150
src/automation/controllers/automation.controller.ts
Normal file
150
src/automation/controllers/automation.controller.ts
Normal file
@ -0,0 +1,150 @@
|
||||
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,
|
||||
UpdateAutomationDto,
|
||||
UpdateAutomationStatusDto,
|
||||
} 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 automation =
|
||||
await this.automationService.addAutomation(addAutomationDto);
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
success: true,
|
||||
message: 'Automation added successfully',
|
||||
data: automation,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Internal server error',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':unitUuid')
|
||||
async getAutomationByUnit(@Param('unitUuid') unitUuid: string) {
|
||||
try {
|
||||
const automation =
|
||||
await this.automationService.getAutomationByUnit(unitUuid);
|
||||
return automation;
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Internal server error',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('details/:automationId')
|
||||
async getAutomationDetails(@Param('automationId') automationId: string) {
|
||||
try {
|
||||
const automation =
|
||||
await this.automationService.getAutomationDetails(automationId);
|
||||
return automation;
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Internal server error',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
``;
|
||||
}
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':unitUuid/:automationId')
|
||||
async deleteAutomation(
|
||||
@Param('unitUuid') unitUuid: string,
|
||||
@Param('automationId') automationId: string,
|
||||
) {
|
||||
try {
|
||||
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: '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(
|
||||
error.message || 'Internal server error',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
1
src/automation/controllers/index.ts
Normal file
1
src/automation/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './automation.controller';
|
214
src/automation/dtos/automation.dto.ts
Normal file
214
src/automation/dtos/automation.dto.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
IsArray,
|
||||
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)',
|
||||
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,
|
||||
type: ExecutorProperty,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => ExecutorProperty)
|
||||
@IsOptional()
|
||||
public executorProperty?: ExecutorProperty;
|
||||
}
|
||||
|
||||
export class AddAutomationDto {
|
||||
@ApiProperty({ description: 'Unit ID', required: true })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public unitUuid: string;
|
||||
|
||||
@ApiProperty({ description: 'Automation name', required: true })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public automationName: string;
|
||||
|
||||
@ApiProperty({ description: 'Decision expression', required: true })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public decisionExpr: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Effective time',
|
||||
required: true,
|
||||
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<AddAutomationDto>) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateAutomationDto {
|
||||
@ApiProperty({ description: 'Automation name', required: true })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public automationName: string;
|
||||
|
||||
@ApiProperty({ description: 'Decision expression', required: true })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public decisionExpr: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Effective time',
|
||||
required: true,
|
||||
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<UpdateAutomationDto>) {
|
||||
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<UpdateAutomationStatusDto>) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
}
|
1
src/automation/dtos/index.ts
Normal file
1
src/automation/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './automation.dto';
|
45
src/automation/interface/automation.interface.ts
Normal file
45
src/automation/interface/automation.interface.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export interface AddAutomationInterface {
|
||||
success: boolean;
|
||||
msg?: string;
|
||||
result: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
export interface GetAutomationByUnitInterface {
|
||||
success: boolean;
|
||||
msg?: string;
|
||||
result: {
|
||||
list: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
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
|
||||
}
|
378
src/automation/services/automation.service.ts
Normal file
378
src/automation/services/automation.service.ts
Normal file
@ -0,0 +1,378 @@
|
||||
import {
|
||||
Injectable,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||
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';
|
||||
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
|
||||
import { DeviceService } from 'src/device/services';
|
||||
import {
|
||||
AddAutomationInterface,
|
||||
AutomationResponseData,
|
||||
DeleteAutomationInterface,
|
||||
GetAutomationByUnitInterface,
|
||||
} 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<string>('auth-config.ACCESS_KEY');
|
||||
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
|
||||
const tuyaEuUrl = this.configService.get<string>('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) =>
|
||||
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,
|
||||
false,
|
||||
);
|
||||
if (device) {
|
||||
action.entity_id = device.deviceTuyaUuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: AddAutomationInterface = await this.tuya.request({
|
||||
method: 'POST',
|
||||
path,
|
||||
body: {
|
||||
space_id: unitSpaceTuyaId,
|
||||
name: addAutomationDto.automationName,
|
||||
effective_time: {
|
||||
...addAutomationDto.effectiveTime,
|
||||
timezone_id: 'Asia/Dubai',
|
||||
},
|
||||
type: 'automation',
|
||||
decision_expr: addAutomationDto.decisionExpr,
|
||||
conditions: conditions,
|
||||
actions: actions,
|
||||
},
|
||||
});
|
||||
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 || 'Automation not found',
|
||||
err.status || HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
async getUnitByUuid(unitUuid: string): Promise<GetUnitByUuidInterface> {
|
||||
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 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=automation`;
|
||||
const response: GetAutomationByUnitInterface = 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: 'automation',
|
||||
};
|
||||
});
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
automationId: 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=${automationId}&space_id=${unitSpaceTuyaId}`;
|
||||
const response: DeleteAutomationInterface = await this.tuya.request({
|
||||
method: 'DELETE',
|
||||
path,
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateAutomation(
|
||||
updateAutomationDto: UpdateAutomationDto,
|
||||
automationId: string,
|
||||
) {
|
||||
try {
|
||||
const spaceTuyaId = await this.getAutomationDetails(automationId, true);
|
||||
if (!spaceTuyaId.spaceId) {
|
||||
throw new HttpException(
|
||||
"Automation doesn't exist",
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const addAutomation = {
|
||||
...updateAutomationDto,
|
||||
unitUuid: null,
|
||||
};
|
||||
const newAutomation = await this.addAutomation(
|
||||
addAutomation,
|
||||
spaceTuyaId.spaceId,
|
||||
);
|
||||
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 || '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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
src/automation/services/index.ts
Normal file
1
src/automation/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './automation.service';
|
@ -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<controlDeviceInterface> {
|
||||
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);
|
||||
|
@ -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(
|
||||
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(
|
||||
|
@ -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,
|
||||
|
12
src/door-lock/dtos/update.offline-temp.dto.ts
Normal file
12
src/door-lock/dtos/update.offline-temp.dto.ts
Normal file
@ -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;
|
||||
}
|
@ -55,3 +55,10 @@ export interface deleteTemporaryPasswordInterface {
|
||||
result: boolean;
|
||||
msg?: string;
|
||||
}
|
||||
export interface getPasswordOfflineInterface {
|
||||
success: boolean;
|
||||
result: {
|
||||
records: [];
|
||||
};
|
||||
msg?: string;
|
||||
}
|
||||
|
@ -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<getPasswordInterface> {
|
||||
try {
|
||||
@ -219,25 +211,45 @@ export class DoorLockService {
|
||||
);
|
||||
}
|
||||
}
|
||||
async getTemporaryOfflinePasswordsTuya(
|
||||
doorLockUuid: string,
|
||||
type: string,
|
||||
): Promise<getPasswordOfflineInterface> {
|
||||
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<createTickInterface> {
|
||||
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<createTickInterface> {
|
||||
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<createTickInterface> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
src/validators/password.validator.ts
Normal file
33
src/validators/password.validator.ts
Normal file
@ -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<string, any>, propertyName: string) {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsPasswordStrongConstraint,
|
||||
});
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user