diff --git a/libs/common/src/constants/default.scene.icon.ts b/libs/common/src/constants/default.scene.icon.ts new file mode 100644 index 0000000..c082768 --- /dev/null +++ b/libs/common/src/constants/default.scene.icon.ts @@ -0,0 +1,2 @@ +export const defaultSceneIcon = + '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'; diff --git a/libs/common/src/constants/product-type.enum.ts b/libs/common/src/constants/product-type.enum.ts index 6442f87..2f518fb 100644 --- a/libs/common/src/constants/product-type.enum.ts +++ b/libs/common/src/constants/product-type.enum.ts @@ -15,4 +15,5 @@ export enum ProductType { WL = 'WL', GD = 'GD', CUR = 'CUR', + PC = 'PC', } diff --git a/libs/common/src/constants/secne-icon-type.enum.ts b/libs/common/src/constants/secne-icon-type.enum.ts new file mode 100644 index 0000000..1cbb693 --- /dev/null +++ b/libs/common/src/constants/secne-icon-type.enum.ts @@ -0,0 +1,4 @@ +export enum SceneIconType { + Default = 'default', + Other = 'other', +} diff --git a/libs/common/src/constants/source-type.enum.ts b/libs/common/src/constants/source-type.enum.ts new file mode 100644 index 0000000..781c6b7 --- /dev/null +++ b/libs/common/src/constants/source-type.enum.ts @@ -0,0 +1,7 @@ +export enum SourceType { + Device = 1, + Client = 2, + ThirdParty = 3, + Cloud = 4, + Unknown = -1, +} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index ae01e55..5d9d7f1 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -19,6 +19,8 @@ import { DeviceNotificationEntity } from '../modules/device/entities'; import { RegionEntity } from '../modules/region/entities'; import { TimeZoneEntity } from '../modules/timezone/entities'; import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; +import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; +import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; @Module({ imports: [ @@ -52,6 +54,9 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; RegionEntity, TimeZoneEntity, VisitorPasswordEntity, + DeviceStatusLogEntity, + SceneEntity, + SceneIconEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/firebase/devices-status/devices-status.module.ts b/libs/common/src/firebase/devices-status/devices-status.module.ts index ad13b29..54d5cfa 100644 --- a/libs/common/src/firebase/devices-status/devices-status.module.ts +++ b/libs/common/src/firebase/devices-status/devices-status.module.ts @@ -2,10 +2,15 @@ import { Module } from '@nestjs/common'; import { DeviceStatusFirebaseController } from './controllers/devices-status.controller'; import { DeviceStatusFirebaseService } from './services/devices-status.service'; import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories/device-status.repository'; @Module({ - providers: [DeviceStatusFirebaseService, DeviceRepository], + providers: [ + DeviceStatusFirebaseService, + DeviceRepository, + DeviceStatusLogRepository, + ], controllers: [DeviceStatusFirebaseController], - exports: [DeviceStatusFirebaseService], + exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository], }) export class DeviceStatusFirebaseModule {} diff --git a/libs/common/src/firebase/devices-status/dtos/add.devices-status.dto.ts b/libs/common/src/firebase/devices-status/dtos/add.devices-status.dto.ts index 9ad7a11..ded0771 100644 --- a/libs/common/src/firebase/devices-status/dtos/add.devices-status.dto.ts +++ b/libs/common/src/firebase/devices-status/dtos/add.devices-status.dto.ts @@ -35,4 +35,7 @@ export class AddDeviceStatusDto { @ValidateNested({ each: true }) @Type(() => StatusDto) status: StatusDto[]; + + @IsOptional() + log?: any; } diff --git a/libs/common/src/firebase/devices-status/services/devices-status.service.ts b/libs/common/src/firebase/devices-status/services/devices-status.service.ts index f075f34..162313b 100644 --- a/libs/common/src/firebase/devices-status/services/devices-status.service.ts +++ b/libs/common/src/firebase/devices-status/services/devices-status.service.ts @@ -11,6 +11,7 @@ import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { firebaseDataBase } from '../../firebase.config'; import { Database, DataSnapshot, get, ref, set } from 'firebase/database'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; @Injectable() export class DeviceStatusFirebaseService { private tuya: TuyaContext; @@ -18,6 +19,7 @@ export class DeviceStatusFirebaseService { constructor( private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, + private deviceStatusLogRepository: DeviceStatusLogRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -175,6 +177,7 @@ export class DeviceStatusFirebaseService { ); // Update or add status codes + for (const statusItem of addDeviceStatusDto.status) { statusMap.set(statusItem.code, statusItem.value); } @@ -184,7 +187,19 @@ export class DeviceStatusFirebaseService { code, value, })); - + const newLog = this.deviceStatusLogRepository.create({ + deviceId: addDeviceStatusDto.deviceUuid, + deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid, + productId: addDeviceStatusDto.log.productId, + log: addDeviceStatusDto.log, + code: existingData.status[0].code, + value: existingData.status[0].value, + eventId: addDeviceStatusDto.log.dataId, + eventTime: new Date( + addDeviceStatusDto.log.properties[0].time, + ).toISOString(), + }); + await this.deviceStatusLogRepository.save(newLog); // Save the updated data to Firebase await set(dataRef, existingData); diff --git a/libs/common/src/helper/services/tuya.web.socket.service.ts b/libs/common/src/helper/services/tuya.web.socket.service.ts index 5b8d89a..d117d62 100644 --- a/libs/common/src/helper/services/tuya.web.socket.service.ts +++ b/libs/common/src/helper/services/tuya.web.socket.service.ts @@ -16,7 +16,7 @@ export class TuyaWebSocketService { accessId: this.configService.get('tuya-config.TUYA_ACCESS_ID'), accessKey: this.configService.get('tuya-config.TUYA_ACCESS_KEY'), url: TuyaWebsocket.URL.EU, - env: TuyaWebsocket.env.TEST, + env: TuyaWebsocket.env.PROD, maxRetryTimes: 100, }); @@ -40,6 +40,7 @@ export class TuyaWebSocketService { await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ deviceTuyaUuid: message.payload.data.bizData.devId, status: message.payload.data.bizData.properties, + log: message.payload.data.bizData, }); this.client.ackMessage(message.messageId); diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts new file mode 100644 index 0000000..a7a998a --- /dev/null +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -0,0 +1,43 @@ +import { SourceType } from '@app/common/constants/source-type.enum'; +import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; + +@Entity('device-status-log') +@Unique('event_time_idx', ['eventTime']) +export class DeviceStatusLogEntity { + @Column({ type: 'int', generated: true, unsigned: true }) + id: number; + + @Column({ type: 'text' }) + eventId: string; + + @PrimaryColumn({ type: 'timestamptz' }) + eventTime: Date; + + @Column({ + type: 'enum', + enum: SourceType, + default: SourceType.Device, + }) + eventFrom: SourceType; + + @Column({ type: 'text' }) + deviceId: string; + + @Column({ type: 'text' }) + deviceTuyaId: string; + + @Column({ type: 'text' }) + productId: string; + + @Column({ type: 'text' }) + code: any; + + @Column({ type: 'text' }) + value: any; + + @Column({ type: 'jsonb' }) + log: any; + + @Column({ type: 'timestamptz', default: new Date() }) + ingestionTime: Date; +} diff --git a/libs/common/src/modules/device-status-log/entities/index.ts b/libs/common/src/modules/device-status-log/entities/index.ts new file mode 100644 index 0000000..07b1986 --- /dev/null +++ b/libs/common/src/modules/device-status-log/entities/index.ts @@ -0,0 +1 @@ +export * from './device-status-log.entity'; diff --git a/libs/common/src/modules/device-status-log/repositories/device-status.repository.ts b/libs/common/src/modules/device-status-log/repositories/device-status.repository.ts new file mode 100644 index 0000000..84f3d60 --- /dev/null +++ b/libs/common/src/modules/device-status-log/repositories/device-status.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { DeviceStatusLogEntity } from '../entities/device-status-log.entity'; + +@Injectable() +export class DeviceStatusLogRepository extends Repository { + constructor(private dataSource: DataSource) { + super(DeviceStatusLogEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/device-status-log/repositories/index.ts b/libs/common/src/modules/device-status-log/repositories/index.ts new file mode 100644 index 0000000..02628b5 --- /dev/null +++ b/libs/common/src/modules/device-status-log/repositories/index.ts @@ -0,0 +1 @@ +export * from './device-status.repository'; diff --git a/libs/common/src/modules/scene/dtos/index.ts b/libs/common/src/modules/scene/dtos/index.ts new file mode 100644 index 0000000..b1ca9c8 --- /dev/null +++ b/libs/common/src/modules/scene/dtos/index.ts @@ -0,0 +1 @@ +export * from './scene.dto'; diff --git a/libs/common/src/modules/scene/dtos/scene.dto.ts b/libs/common/src/modules/scene/dtos/scene.dto.ts new file mode 100644 index 0000000..c128a6a --- /dev/null +++ b/libs/common/src/modules/scene/dtos/scene.dto.ts @@ -0,0 +1,30 @@ +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +export class SceneDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public sceneTuyaUuid: string; + @IsString() + @IsNotEmpty() + public unitUuid: string; + @IsString() + @IsNotEmpty() + public iconUuid: string; + + @IsBoolean() + @IsNotEmpty() + public inHomePage: boolean; +} +export class SceneIconDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public icon: string; +} diff --git a/libs/common/src/modules/scene/entities/index.ts b/libs/common/src/modules/scene/entities/index.ts new file mode 100644 index 0000000..b7cd2fb --- /dev/null +++ b/libs/common/src/modules/scene/entities/index.ts @@ -0,0 +1 @@ +export * from './scene.entity'; diff --git a/libs/common/src/modules/scene/entities/scene.entity.ts b/libs/common/src/modules/scene/entities/scene.entity.ts new file mode 100644 index 0000000..2ade9ad --- /dev/null +++ b/libs/common/src/modules/scene/entities/scene.entity.ts @@ -0,0 +1,65 @@ +import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { SceneDto, SceneIconDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; + +// Define SceneIconEntity before SceneEntity +@Entity({ name: 'scene-icon' }) +export class SceneIconEntity extends AbstractEntity { + @Column({ + nullable: true, + type: 'text', + }) + public icon: string; + + @Column({ + type: 'enum', + enum: SceneIconType, + default: SceneIconType.Other, + }) + iconType: SceneIconType; + + @OneToMany( + () => SceneEntity, + (scenesIconEntity) => scenesIconEntity.sceneIcon, + ) + scenesIconEntity: SceneEntity[]; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} + +@Entity({ name: 'scene' }) +export class SceneEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + sceneTuyaUuid: string; + @Column({ + nullable: false, + }) + unitUuid: string; + @Column({ + nullable: false, + }) + showInHomePage: boolean; + + @ManyToOne(() => SceneIconEntity, (icon) => icon.scenesIconEntity, { + nullable: false, + }) + sceneIcon: SceneIconEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/scene/repositories/index.ts b/libs/common/src/modules/scene/repositories/index.ts new file mode 100644 index 0000000..75c83c9 --- /dev/null +++ b/libs/common/src/modules/scene/repositories/index.ts @@ -0,0 +1 @@ +export * from './scene.repository'; diff --git a/libs/common/src/modules/scene/repositories/scene.repository.ts b/libs/common/src/modules/scene/repositories/scene.repository.ts new file mode 100644 index 0000000..d077592 --- /dev/null +++ b/libs/common/src/modules/scene/repositories/scene.repository.ts @@ -0,0 +1,17 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { SceneEntity, SceneIconEntity } from '../entities'; + +@Injectable() +export class SceneRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SceneEntity, dataSource.createEntityManager()); + } +} + +@Injectable() +export class SceneIconRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SceneIconEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/scene/scene.repository.module.ts b/libs/common/src/modules/scene/scene.repository.module.ts new file mode 100644 index 0000000..8bda28a --- /dev/null +++ b/libs/common/src/modules/scene/scene.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SceneEntity, SceneIconEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([SceneEntity, SceneIconEntity])], +}) +export class SceneRepositoryModule {} diff --git a/libs/common/src/seed/seeder.module.ts b/libs/common/src/seed/seeder.module.ts index f63c05d..0c89c5c 100644 --- a/libs/common/src/seed/seeder.module.ts +++ b/libs/common/src/seed/seeder.module.ts @@ -18,6 +18,8 @@ import { RegionSeeder } from './services/regions.seeder'; import { RegionRepository } from '../modules/region/repositories'; import { TimeZoneSeeder } from './services/timezone.seeder'; import { TimeZoneRepository } from '../modules/timezone/repositories'; +import { SceneIconSeeder } from './services/scene.icon.seeder'; +import { SceneIconRepository } from '../modules/scene/repositories'; @Global() @Module({ providers: [ @@ -35,6 +37,8 @@ import { TimeZoneRepository } from '../modules/timezone/repositories'; RegionRepository, TimeZoneSeeder, TimeZoneRepository, + SceneIconSeeder, + SceneIconRepository, ], exports: [SeederService], controllers: [], diff --git a/libs/common/src/seed/services/scene.icon.seeder.ts b/libs/common/src/seed/services/scene.icon.seeder.ts new file mode 100644 index 0000000..3f363ce --- /dev/null +++ b/libs/common/src/seed/services/scene.icon.seeder.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { SceneIconRepository } from '@app/common/modules/scene/repositories'; +import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; +import { defaultSceneIcon } from '@app/common/constants/default.scene.icon'; + +@Injectable() +export class SceneIconSeeder { + constructor(private readonly sceneIconRepository: SceneIconRepository) {} + + async createSceneDefaultIconIfNotFound(): Promise { + try { + const defaultSceneIconData = await this.sceneIconRepository.find({ + where: { iconType: SceneIconType.Default }, + }); + + if (defaultSceneIconData.length <= 0) { + console.log('Creating default scene icon...'); + + await this.createDefaultSceneIcon(); + } + } catch (err) { + console.error('Error while checking default scene icon:', err); + throw err; + } + } + + private async createDefaultSceneIcon(): Promise { + try { + await this.sceneIconRepository.save({ + icon: defaultSceneIcon, + iconType: SceneIconType.Default, + }); + } catch (err) { + console.error('Error while creating default scene icon:', err); + throw err; + } + } +} diff --git a/libs/common/src/seed/services/seeder.service.ts b/libs/common/src/seed/services/seeder.service.ts index 47d899d..e627e44 100644 --- a/libs/common/src/seed/services/seeder.service.ts +++ b/libs/common/src/seed/services/seeder.service.ts @@ -5,6 +5,7 @@ import { SpaceTypeSeeder } from './space.type.seeder'; import { SuperAdminSeeder } from './supper.admin.seeder'; import { RegionSeeder } from './regions.seeder'; import { TimeZoneSeeder } from './timezone.seeder'; +import { SceneIconSeeder } from './scene.icon.seeder'; @Injectable() export class SeederService { constructor( @@ -14,6 +15,7 @@ export class SeederService { private readonly regionSeeder: RegionSeeder, private readonly timeZoneSeeder: TimeZoneSeeder, private readonly superAdminSeeder: SuperAdminSeeder, + private readonly sceneIconSeeder: SceneIconSeeder, ) {} async seed() { @@ -23,5 +25,6 @@ export class SeederService { await this.regionSeeder.addRegionDataIfNotFound(); await this.timeZoneSeeder.addTimeZoneDataIfNotFound(); await this.superAdminSeeder.createSuperAdminIfNotFound(); + await this.sceneIconSeeder.createSceneDefaultIconIfNotFound(); } } diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index bc9fc3c..e7074da 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, Get, @@ -16,6 +17,7 @@ import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; import { RefreshTokenGuard } from '@app/common/guards/jwt-refresh.auth.guard'; import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { OtpType } from '@app/common/constants/otp-type.enum'; @Controller({ version: EnableDisableStatusEnum.ENABLED, @@ -74,12 +76,27 @@ export class UserAuthController { @Post('user/forget-password') async forgetPassword(@Body() forgetPasswordDto: ForgetPasswordDto) { - await this.userAuthService.forgetPassword(forgetPasswordDto); - return { - statusCode: HttpStatus.OK, + const otpResult = await this.userAuthService.verifyOTP( + { + otpCode: forgetPasswordDto.otpCode, + email: forgetPasswordDto.email, + type: OtpType.PASSWORD, + }, + true, + ); + if (otpResult) { + await this.userAuthService.forgetPassword(forgetPasswordDto); + return { + statusCode: HttpStatus.OK, + data: {}, + message: 'Password changed successfully', + }; + } + throw new BadRequestException({ + statusCode: HttpStatus.BAD_REQUEST, data: {}, - message: 'Password changed successfully', - }; + message: 'Otp is incorrect', + }); } @ApiBearerAuth() diff --git a/src/auth/dtos/user-password.dto.ts b/src/auth/dtos/user-password.dto.ts index 9a93935..de5eb5b 100644 --- a/src/auth/dtos/user-password.dto.ts +++ b/src/auth/dtos/user-password.dto.ts @@ -22,4 +22,9 @@ export class ForgetPasswordDto { '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() + @IsString() + @IsNotEmpty() + public otpCode: string; } diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index 9fd9fe3..0c95297 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -18,7 +18,6 @@ import * as argon2 from 'argon2'; import { differenceInSeconds } from '@app/common/helper/differenceInSeconds'; import { LessThan, MoreThan } from 'typeorm'; import { ConfigService } from '@nestjs/config'; -import { UUID } from 'typeorm/driver/mongodb/bson.typings'; @Injectable() export class UserAuthService { @@ -211,7 +210,7 @@ export class UserAuthService { } const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); const expiryTime = new Date(); - expiryTime.setMinutes(expiryTime.getMinutes() + 1); + expiryTime.setMinutes(expiryTime.getMinutes() + 10); await this.otpRepository.save({ email: data.email, otpCode, @@ -233,7 +232,10 @@ export class UserAuthService { return { otpCode, cooldown }; } - async verifyOTP(data: VerifyOtpDto): Promise { + async verifyOTP( + data: VerifyOtpDto, + fromNewPassword: boolean = false, + ): Promise { const otp = await this.otpRepository.findOne({ where: { email: data.email, type: data.type }, }); @@ -258,7 +260,9 @@ export class UserAuthService { await this.otpRepository.delete(otp.uuid); throw new BadRequestException('OTP expired'); } - + if (fromNewPassword) { + await this.otpRepository.delete(otp.uuid); + } if (data.type == OtpType.VERIFICATION) { await this.userRepository.update( { email: data.email }, diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 5f1a7ae..7a2543e 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -191,4 +191,14 @@ export class DeviceController { batchFactoryResetDevicesDto, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':powerClampUuid/power-clamp/status') + async getPowerClampInstructionStatus( + @Param('powerClampUuid') powerClampUuid: string, + ) { + return await this.deviceService.getPowerClampInstructionStatus( + powerClampUuid, + ); + } } diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index 7422ed2..93438b9 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -70,3 +70,10 @@ export interface getDeviceLogsInterface { endTime: string; deviceUuid?: string; } +export interface GetPowerClampFunctionsStatusInterface { + result: { + properties: []; + }; + success: boolean; + msg: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 2b37df8..ebe73e2 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -14,6 +14,7 @@ import { GetDeviceDetailsFunctionsInterface, GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, + GetPowerClampFunctionsStatusInterface, controlDeviceInterface, getDeviceLogsInterface, updateDeviceFirmwareInterface, @@ -965,4 +966,81 @@ export class DeviceService { ); } } + async getPowerClampInstructionStatus(powerClampUuid: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(powerClampUuid); + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } else if (deviceDetails.productDevice.prodType !== ProductType.PC) { + throw new BadRequestException('This is not a power clamp device'); + } + const deviceStatus = await this.getPowerClampInstructionStatusTuya( + deviceDetails.deviceTuyaUuid, + ); + const statusList = deviceStatus.result.properties as { + code: string; + value: any; + }[]; + + const groupedStatus = statusList.reduce( + (acc, currentStatus) => { + const { code } = currentStatus; + + if (code.endsWith('A')) { + acc.phaseA.push(currentStatus); + } else if (code.endsWith('B')) { + acc.phaseB.push(currentStatus); + } else if (code.endsWith('C')) { + acc.phaseC.push(currentStatus); + } else { + acc.general.push(currentStatus); + } + return acc; + }, + { + phaseA: [] as { code: string; value: any }[], + phaseB: [] as { code: string; value: any }[], + phaseC: [] as { code: string; value: any }[], + general: [] as { code: string; value: any }[], + }, + ); + + return { + productUuid: deviceDetails.productDevice.uuid, + productType: deviceDetails.productDevice.prodType, + status: { + phaseA: groupedStatus.phaseA, + phaseB: groupedStatus.phaseB, + phaseC: groupedStatus.phaseC, + general: groupedStatus.general, + }, + }; + } catch (error) { + throw new HttpException( + error.message || 'Error fetching power clamp functions status', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getPowerClampInstructionStatusTuya( + deviceUuid: string, + ): Promise { + try { + const path = `/v2.0/cloud/thing/${deviceUuid}/shadow/properties`; + const response = await this.tuya.request({ + method: 'GET', + path, + query: { + device_ids: deviceUuid, + }, + }); + const camelCaseResponse = convertKeysToCamelCase(response); + return camelCaseResponse as GetPowerClampFunctionsStatusInterface; + } catch (error) { + throw new HttpException( + 'Error fetching power clamp functions status from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index b72fad9..d5f070e 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -10,6 +10,7 @@ import { DeviceService } from 'src/device/services'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -22,6 +23,7 @@ import { SpaceRepository } from '@app/common/modules/space/repositories'; ProductRepository, DeviceStatusFirebaseService, SpaceRepository, + DeviceStatusLogRepository, ], exports: [DoorLockService], }) diff --git a/src/scene/controllers/scene.controller.ts b/src/scene/controllers/scene.controller.ts index 1186433..4e986e2 100644 --- a/src/scene/controllers/scene.controller.ts +++ b/src/scene/controllers/scene.controller.ts @@ -8,10 +8,16 @@ import { Param, Post, Put, + Query, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } from '../dtos/scene.dto'; +import { + AddSceneIconDto, + AddSceneTapToRunDto, + GetSceneDto, + UpdateSceneTapToRunDto, +} from '../dtos/scene.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; @@ -39,9 +45,14 @@ export class SceneController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('tap-to-run/:unitUuid') - async getTapToRunSceneByUnit(@Param('unitUuid') unitUuid: string) { - const tapToRunScenes = - await this.sceneService.getTapToRunSceneByUnit(unitUuid); + async getTapToRunSceneByUnit( + @Param('unitUuid') unitUuid: string, + @Query() inHomePage: GetSceneDto, + ) { + const tapToRunScenes = await this.sceneService.getTapToRunSceneByUnit( + unitUuid, + inHomePage, + ); return tapToRunScenes; } @ApiBearerAuth() @@ -95,4 +106,23 @@ export class SceneController { data: tapToRunScene, }; } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('icon') + async addSceneIcon(@Body() addSceneIconDto: AddSceneIconDto) { + const tapToRunScene = await this.sceneService.addSceneIcon(addSceneIconDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Icon added successfully', + data: tapToRunScene, + }; + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('icon') + async getAllIcons() { + const icons = await this.sceneService.getAllIcons(); + return icons; + } } diff --git a/src/scene/dtos/scene.dto.ts b/src/scene/dtos/scene.dto.ts index ca39212..24334e9 100644 --- a/src/scene/dtos/scene.dto.ts +++ b/src/scene/dtos/scene.dto.ts @@ -6,8 +6,10 @@ import { ValidateNested, IsOptional, IsNumber, + IsBoolean, } from 'class-validator'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; +import { BooleanValues } from '@app/common/constants/boolean-values.enum'; class ExecutorProperty { @ApiProperty({ @@ -70,7 +72,20 @@ export class AddSceneTapToRunDto { @IsString() @IsNotEmpty() public unitUuid: string; - + @ApiProperty({ + description: 'Icon UUID', + required: false, + }) + @IsString() + @IsOptional() + public iconUuid: string; + @ApiProperty({ + description: 'show in home page', + required: true, + }) + @IsBoolean() + @IsNotEmpty() + public showInHomePage: boolean; @ApiProperty({ description: 'Scene name', required: true, @@ -109,7 +124,20 @@ export class UpdateSceneTapToRunDto { @IsString() @IsNotEmpty() public sceneName: string; - + @ApiProperty({ + description: 'Icon UUID', + required: true, + }) + @IsString() + @IsNotEmpty() + public iconUuid: string; + @ApiProperty({ + description: 'show in home page', + required: true, + }) + @IsBoolean() + @IsNotEmpty() + public showInHomePage: boolean; @ApiProperty({ description: 'Decision expression', required: true, @@ -132,3 +160,30 @@ export class UpdateSceneTapToRunDto { Object.assign(this, dto); } } +export class AddSceneIconDto { + @ApiProperty({ + description: 'Icon', + required: true, + }) + @IsString() + @IsNotEmpty() + public icon: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} +export class GetSceneDto { + @ApiProperty({ + example: true, + description: 'showInHomePage', + required: true, + default: false, + }) + @IsNotEmpty() + @IsBoolean() + @Transform((value) => { + return value.obj.showInHomePage === BooleanValues.TRUE; + }) + public showInHomePage: boolean = false; +} diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts index c673748..cdbf2d4 100644 --- a/src/scene/scene.module.ts +++ b/src/scene/scene.module.ts @@ -8,6 +8,10 @@ import { DeviceService } from 'src/device/services'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -18,6 +22,8 @@ import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/ DeviceService, DeviceRepository, ProductRepository, + SceneIconRepository, + SceneRepository, ], exports: [SceneService], }) diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 30e53aa..3278970 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -5,7 +5,12 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } from '../dtos'; +import { + AddSceneIconDto, + AddSceneTapToRunDto, + GetSceneDto, + UpdateSceneTapToRunDto, +} from '../dtos'; import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface'; import { ConfigService } from '@nestjs/config'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; @@ -14,12 +19,16 @@ import { DeviceService } from 'src/device/services'; import { AddTapToRunSceneInterface, DeleteTapToRunSceneInterface, - GetTapToRunSceneByUnitInterface, SceneDetailsResult, } from '../interface/scene.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { SpaceType } from '@app/common/constants/space-type.enum'; import { ActionExecutorEnum } from '@app/common/constants/automation.enum'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; @Injectable() export class SceneService { @@ -27,6 +36,8 @@ export class SceneService { constructor( private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, + private readonly sceneIconRepository: SceneIconRepository, + private readonly sceneRepository: SceneRepository, private readonly deviceService: DeviceService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -42,6 +53,7 @@ export class SceneService { async addTapToRunScene( addSceneTapToRunDto: AddSceneTapToRunDto, spaceTuyaId = null, + unitUuid?: string, ) { try { let unitSpaceTuyaId; @@ -89,6 +101,21 @@ export class SceneService { }); if (!response.success) { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } else { + const defaultSceneIcon = await this.sceneIconRepository.findOne({ + where: { iconType: SceneIconType.Default }, + }); + + await this.sceneRepository.save({ + sceneTuyaUuid: response.result.id, + sceneIcon: { + uuid: addSceneTapToRunDto.iconUuid + ? addSceneTapToRunDto.iconUuid + : defaultSceneIcon.uuid, + }, + showInHomePage: addSceneTapToRunDto.showInHomePage, + unitUuid: unitUuid ? unitUuid : addSceneTapToRunDto.unitUuid, + }); } return { id: response.result.id, @@ -134,33 +161,32 @@ export class SceneService { } } } - async getTapToRunSceneByUnit(unitUuid: string) { + async getTapToRunSceneByUnit(unitUuid: string, inHomePage: GetSceneDto) { try { - const unit = await this.getUnitByUuid(unitUuid); - if (!unit.spaceTuyaUuid) { - throw new BadRequestException('Invalid unit UUID'); - } + const showInHomePage = inHomePage?.showInHomePage; - const path = `/v2.0/cloud/scene/rule?space_id=${unit.spaceTuyaUuid}&type=scene`; - const response: GetTapToRunSceneByUnitInterface = await this.tuya.request( - { - method: 'GET', - path, + const scenesData = await this.sceneRepository.find({ + where: { + unitUuid, + ...(showInHomePage ? { showInHomePage } : {}), }, + }); + + const scenes = await Promise.all( + scenesData.map(async (scene) => { + const sceneData = await this.getTapToRunSceneDetails( + scene.sceneTuyaUuid, + false, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { actions, ...rest } = sceneData; + return { + ...rest, + }; + }), ); - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } - - return response.result.list.map((item) => { - return { - id: item.id, - name: item.name, - status: item.status, - type: 'tap_to_run', - }; - }); + return scenes; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException @@ -197,6 +223,8 @@ export class SceneService { if (!response.success) { throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); + } else { + await this.sceneRepository.delete({ sceneTuyaUuid: sceneId }); } return response; @@ -273,11 +301,17 @@ export class SceneService { } } } - + const scene = await this.sceneRepository.findOne({ + where: { sceneTuyaUuid: sceneId }, + relations: ['sceneIcon'], + }); return { id: responseData.id, name: responseData.name, status: responseData.status, + icon: scene.sceneIcon?.icon, + iconUuid: scene.sceneIcon?.uuid, + showInHome: scene.showInHomePage, type: 'tap_to_run', actions: actions, ...(withSpaceId && { spaceId: responseData.spaceId }), @@ -334,13 +368,32 @@ export class SceneService { const addSceneTapToRunDto: AddSceneTapToRunDto = { ...updateSceneTapToRunDto, unitUuid: null, + iconUuid: updateSceneTapToRunDto.iconUuid, + showInHomePage: updateSceneTapToRunDto.showInHomePage, }; + const scene = await this.sceneRepository.findOne({ + where: { sceneTuyaUuid: sceneId }, + }); + const newTapToRunScene = await this.addTapToRunScene( addSceneTapToRunDto, spaceTuyaId.spaceId, + scene.unitUuid, ); if (newTapToRunScene.id) { await this.deleteTapToRunScene(null, sceneId, spaceTuyaId.spaceId); + + await this.sceneRepository.update( + { sceneTuyaUuid: sceneId }, + { + sceneTuyaUuid: newTapToRunScene.id, + showInHomePage: addSceneTapToRunDto.showInHomePage, + sceneIcon: { + uuid: addSceneTapToRunDto.iconUuid, + }, + unitUuid: scene.unitUuid, + }, + ); return newTapToRunScene; } } catch (err) { @@ -354,4 +407,38 @@ export class SceneService { } } } + async addSceneIcon(addSceneIconDto: AddSceneIconDto) { + try { + const icon = await this.sceneIconRepository.save({ + icon: addSceneIconDto.icon, + }); + return icon; + } catch (err) { + throw new HttpException( + { + status: HttpStatus.INTERNAL_SERVER_ERROR, + error: 'Failed to add scene icon', + message: err.message || 'Unexpected error occurred', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getAllIcons() { + try { + const icons = await this.sceneIconRepository.find(); + return icons; + } catch (err) { + // Improved error handling + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + message: 'Failed to fetch icons', + error: + err.message || 'An unexpected error occurred while fetching icons', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 19e61ea..990e1e7 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -12,6 +12,7 @@ import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -25,6 +26,7 @@ import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/ SpaceRepository, DeviceRepository, VisitorPasswordRepository, + DeviceStatusLogRepository, ], exports: [VisitorPasswordService], })