From d962d4a7b3fd0676e2e3bf45df4b960f8c16d710 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:03:30 +0300 Subject: [PATCH] finished schedule module --- .../src/helper/convertTimestampToDubaiTime.ts | 23 ++ libs/common/src/helper/getScheduleStatus.ts | 27 ++ src/app.module.ts | 2 + src/schedule/controllers/index.ts | 1 + .../controllers/schedule.controller.ts | 116 ++++++++ src/schedule/dtos/add.schedule.dto.ts | 84 ++++++ src/schedule/dtos/index.ts | 1 + .../interfaces/get.schedule.interface.ts | 10 + src/schedule/schedule.module.ts | 13 + src/schedule/services/index.ts | 1 + src/schedule/services/schedule.service.ts | 281 ++++++++++++++++++ 11 files changed, 559 insertions(+) create mode 100644 libs/common/src/helper/convertTimestampToDubaiTime.ts create mode 100644 libs/common/src/helper/getScheduleStatus.ts create mode 100644 src/schedule/controllers/index.ts create mode 100644 src/schedule/controllers/schedule.controller.ts create mode 100644 src/schedule/dtos/add.schedule.dto.ts create mode 100644 src/schedule/dtos/index.ts create mode 100644 src/schedule/interfaces/get.schedule.interface.ts create mode 100644 src/schedule/schedule.module.ts create mode 100644 src/schedule/services/index.ts create mode 100644 src/schedule/services/schedule.service.ts diff --git a/libs/common/src/helper/convertTimestampToDubaiTime.ts b/libs/common/src/helper/convertTimestampToDubaiTime.ts new file mode 100644 index 0000000..ade7c29 --- /dev/null +++ b/libs/common/src/helper/convertTimestampToDubaiTime.ts @@ -0,0 +1,23 @@ +export function convertTimestampToDubaiTime(timestamp) { + // Convert timestamp to milliseconds + const date = new Date(timestamp * 1000); + + // Convert to Dubai time (UTC+4) + const dubaiTimeOffset = 4 * 60; // 4 hours in minutes + const dubaiTime = new Date(date.getTime() + dubaiTimeOffset * 60 * 1000); + + // Format the date as YYYYMMDD + const year = dubaiTime.getUTCFullYear(); + const month = String(dubaiTime.getUTCMonth() + 1).padStart(2, '0'); // Months are zero-based + const day = String(dubaiTime.getUTCDate()).padStart(2, '0'); + + // Format the time as HH:MM (24-hour format) + const hours = String(dubaiTime.getUTCHours()).padStart(2, '0'); + const minutes = String(dubaiTime.getUTCMinutes()).padStart(2, '0'); + + // Return formatted date and time + return { + date: `${year}${month}${day}`, + time: `${hours}:${minutes}`, + }; +} diff --git a/libs/common/src/helper/getScheduleStatus.ts b/libs/common/src/helper/getScheduleStatus.ts new file mode 100644 index 0000000..e77496c --- /dev/null +++ b/libs/common/src/helper/getScheduleStatus.ts @@ -0,0 +1,27 @@ +export function getScheduleStatus(daysEnabled: string[]): string { + const daysMap: string[] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + const schedule: string[] = Array(7).fill('0'); + + daysEnabled.forEach((day) => { + const index: number = daysMap.indexOf(day); + if (index !== -1) { + schedule[index] = '1'; + } + }); + + return schedule.join(''); +} +export function getEnabledDays(schedule: string): string[] { + const daysMap: string[] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const enabledDays: string[] = []; + + // Iterate through the schedule string + for (let i = 0; i < schedule.length; i++) { + if (schedule[i] === '1') { + enabledDays.push(daysMap[i]); + } + } + + return enabledDays; +} diff --git a/src/app.module.ts b/src/app.module.ts index 2970185..29d07ac 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -24,6 +24,7 @@ import { AutomationModule } from './automation/automation.module'; import { RegionModule } from './region/region.module'; import { TimeZoneModule } from './timezone/timezone.module'; import { VisitorPasswordModule } from './vistor-password/visitor-password.module'; +import { ScheduleModule } from './schedule/schedule.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -50,6 +51,7 @@ import { VisitorPasswordModule } from './vistor-password/visitor-password.module RegionModule, TimeZoneModule, VisitorPasswordModule, + ScheduleModule, ], controllers: [AuthenticationController], providers: [ diff --git a/src/schedule/controllers/index.ts b/src/schedule/controllers/index.ts new file mode 100644 index 0000000..edd3a1d --- /dev/null +++ b/src/schedule/controllers/index.ts @@ -0,0 +1 @@ +export * from './schedule.controller'; diff --git a/src/schedule/controllers/schedule.controller.ts b/src/schedule/controllers/schedule.controller.ts new file mode 100644 index 0000000..94ad414 --- /dev/null +++ b/src/schedule/controllers/schedule.controller.ts @@ -0,0 +1,116 @@ +import { ScheduleService } from '../services/schedule.service'; +import { + Body, + Controller, + Get, + Post, + Param, + HttpException, + HttpStatus, + UseGuards, + Put, + Delete, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { AddScheduleDto, EnableScheduleDto } from '../dtos/add.schedule.dto'; + +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; + +@ApiTags('Schedule Module') +@Controller({ + version: '1', + path: 'schedule', +}) +export class ScheduleController { + constructor(private readonly scheduleService: ScheduleService) {} + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post(':deviceUuid') + async addDeviceSchedule( + @Param('deviceUuid') deviceUuid: string, + @Body() addScheduleDto: AddScheduleDto, + ) { + try { + const device = await this.scheduleService.addDeviceSchedule( + deviceUuid, + addScheduleDto, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'schedule added successfully', + data: device, + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':deviceUuid/category/:category') + async getDeviceScheduleByCategory( + @Param('deviceUuid') deviceUuid: string, + @Param('category') category: string, + ) { + try { + return await this.scheduleService.getDeviceScheduleByCategory( + deviceUuid, + category, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':deviceUuid/:scheduleId') + async deleteDeviceSchedule( + @Param('deviceUuid') deviceUuid: string, + @Param('scheduleId') scheduleId: string, + ) { + try { + await this.scheduleService.deleteDeviceSchedule(deviceUuid, scheduleId); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'schedule deleted successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('enable/:deviceUuid') + async enableDeviceSchedule( + @Param('deviceUuid') deviceUuid: string, + @Body() enableScheduleDto: EnableScheduleDto, + ) { + try { + await this.scheduleService.enableDeviceSchedule( + deviceUuid, + enableScheduleDto, + ); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'schedule updated successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/schedule/dtos/add.schedule.dto.ts b/src/schedule/dtos/add.schedule.dto.ts new file mode 100644 index 0000000..c51a027 --- /dev/null +++ b/src/schedule/dtos/add.schedule.dto.ts @@ -0,0 +1,84 @@ +import { WorkingDays } from '@app/common/constants/working-days'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsEnum, + IsNotEmpty, + IsString, + ValidateNested, +} from 'class-validator'; +export class FunctionDto { + @ApiProperty({ + description: 'code', + required: true, + }) + @IsString() + @IsNotEmpty() + public code: string; + + @ApiProperty({ + description: 'value', + required: true, + }) + @IsNotEmpty() + public value: any; +} + +// Update the main DTO class +export class AddScheduleDto { + @ApiProperty({ + description: 'category', + required: true, + }) + @IsString() + @IsNotEmpty() + public category: string; + + @ApiProperty({ + description: 'time', + required: true, + }) + @IsString() + @IsNotEmpty() + public time: string; + + @ApiProperty({ + description: 'function', + required: true, + type: FunctionDto, + }) + @ValidateNested() + @Type(() => FunctionDto) + public function: FunctionDto; + + @ApiProperty({ + description: 'days', + enum: WorkingDays, + isArray: true, + required: true, + }) + @IsArray() + @IsEnum(WorkingDays, { each: true }) + @IsNotEmpty() + public days: WorkingDays[]; +} + +export class EnableScheduleDto { + @ApiProperty({ + description: 'scheduleId', + required: true, + }) + @IsString() + @IsNotEmpty() + public scheduleId: string; + + @ApiProperty({ + description: 'enable', + required: true, + }) + @IsBoolean() + @IsNotEmpty() + public enable: boolean; +} diff --git a/src/schedule/dtos/index.ts b/src/schedule/dtos/index.ts new file mode 100644 index 0000000..e71549f --- /dev/null +++ b/src/schedule/dtos/index.ts @@ -0,0 +1 @@ +export * from './add.schedule.dto'; diff --git a/src/schedule/interfaces/get.schedule.interface.ts b/src/schedule/interfaces/get.schedule.interface.ts new file mode 100644 index 0000000..40c2011 --- /dev/null +++ b/src/schedule/interfaces/get.schedule.interface.ts @@ -0,0 +1,10 @@ +export interface getDeviceScheduleInterface { + success: boolean; + result: []; + msg: string; +} +export interface addScheduleDeviceInterface { + success: boolean; + result: boolean; + msg: string; +} diff --git a/src/schedule/schedule.module.ts b/src/schedule/schedule.module.ts new file mode 100644 index 0000000..f0f0a64 --- /dev/null +++ b/src/schedule/schedule.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ScheduleService } from './services/schedule.service'; +import { ScheduleController } from './controllers/schedule.controller'; +import { ConfigModule } from '@nestjs/config'; +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +@Module({ + imports: [ConfigModule, DeviceRepositoryModule], + controllers: [ScheduleController], + providers: [ScheduleService, DeviceRepository], + exports: [ScheduleService], +}) +export class ScheduleModule {} diff --git a/src/schedule/services/index.ts b/src/schedule/services/index.ts new file mode 100644 index 0000000..ad5aed0 --- /dev/null +++ b/src/schedule/services/index.ts @@ -0,0 +1 @@ +export * from './schedule.service'; diff --git a/src/schedule/services/schedule.service.ts b/src/schedule/services/schedule.service.ts new file mode 100644 index 0000000..f5a6417 --- /dev/null +++ b/src/schedule/services/schedule.service.ts @@ -0,0 +1,281 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { ConfigService } from '@nestjs/config'; +import { AddScheduleDto, EnableScheduleDto } from '../dtos/add.schedule.dto'; +import { + addScheduleDeviceInterface, + getDeviceScheduleInterface, +} from '../interfaces/get.schedule.interface'; + +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ProductType } from '@app/common/constants/product-type.enum'; +import { convertTimestampToDubaiTime } from '@app/common/helper/convertTimestampToDubaiTime'; +import { + getEnabledDays, + getScheduleStatus, +} from '@app/common/helper/getScheduleStatus'; + +@Injectable() +export class ScheduleService { + private tuya: TuyaContext; + constructor( + private readonly configService: ConfigService, + private readonly deviceRepository: DeviceRepository, + ) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + + async enableDeviceSchedule( + deviceUuid: string, + enableScheduleDto: EnableScheduleDto, + ) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + // Corrected condition for supported device types + if ( + deviceDetails.productDevice.prodType !== ProductType.THREE_G && + deviceDetails.productDevice.prodType !== ProductType.ONE_G && + deviceDetails.productDevice.prodType !== ProductType.TWO_G + ) { + throw new HttpException( + 'This device is not supported for schedule', + HttpStatus.BAD_REQUEST, + ); + } + return await this.enableScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + enableScheduleDto, + ); + } catch (error) { + throw new HttpException( + error.message || 'Error While Updating Schedule', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async enableScheduleDeviceInTuya( + deviceId: string, + enableScheduleDto: EnableScheduleDto, + ): Promise { + try { + const path = `/v2.0/cloud/timer/device/${deviceId}/state`; + const response = await this.tuya.request({ + method: 'PUT', + path, + body: { + enable: enableScheduleDto.enable, + timer_id: enableScheduleDto.scheduleId, + }, + }); + + return response as addScheduleDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error while updating schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async deleteDeviceSchedule(deviceUuid: string, scheduleId: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + // Corrected condition for supported device types + if ( + deviceDetails.productDevice.prodType !== ProductType.THREE_G && + deviceDetails.productDevice.prodType !== ProductType.ONE_G && + deviceDetails.productDevice.prodType !== ProductType.TWO_G + ) { + throw new HttpException( + 'This device is not supported for schedule', + HttpStatus.BAD_REQUEST, + ); + } + return await this.deleteScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + scheduleId, + ); + } catch (error) { + throw new HttpException( + error.message || 'Error While Deleting Schedule', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async deleteScheduleDeviceInTuya( + deviceId: string, + scheduleId: string, + ): Promise { + try { + const path = `/v2.0/cloud/timer/device/${deviceId}/batch?timer_ids=${scheduleId}`; + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + return response as addScheduleDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error while deleting schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addDeviceSchedule(deviceUuid: string, addScheduleDto: AddScheduleDto) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + + // Corrected condition for supported device types + if ( + deviceDetails.productDevice.prodType !== ProductType.THREE_G && + deviceDetails.productDevice.prodType !== ProductType.ONE_G && + deviceDetails.productDevice.prodType !== ProductType.TWO_G + ) { + throw new HttpException( + 'This device is not supported for schedule', + HttpStatus.BAD_REQUEST, + ); + } + await this.addScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + addScheduleDto, + ); + } catch (error) { + throw new HttpException( + error.message || 'Error While Adding Schedule', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async addScheduleDeviceInTuya( + deviceId: string, + addScheduleDto: AddScheduleDto, + ): Promise { + try { + const convertedTime = convertTimestampToDubaiTime(addScheduleDto.time); + const loops = getScheduleStatus(addScheduleDto.days); + + const path = `/v2.0/cloud/timer/device/${deviceId}`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + time: convertedTime.time, + timezone_id: 'Asia/Dubai', + loops: `${loops}`, + functions: [ + { + code: addScheduleDto.function.code, + value: addScheduleDto.function.value, + }, + ], + category: `category_${addScheduleDto.category}`, + }, + }); + + return response as addScheduleDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error adding schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDeviceScheduleByCategory(deviceUuid: string, category: string) { + try { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails || !deviceDetails.deviceTuyaUuid) { + throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND); + } + // Corrected condition for supported device types + if ( + deviceDetails.productDevice.prodType !== ProductType.THREE_G && + deviceDetails.productDevice.prodType !== ProductType.ONE_G && + deviceDetails.productDevice.prodType !== ProductType.TWO_G + ) { + throw new HttpException( + 'This device is not supported for schedule', + HttpStatus.BAD_REQUEST, + ); + } + const schedules = await this.getScheduleDeviceInTuya( + deviceDetails.deviceTuyaUuid, + category, + ); + const result = schedules.result.map((schedule: any) => { + return { + category: schedule.category.replace('category_', ''), + enable: schedule.enable, + function: { + code: schedule.functions[0].code, + value: schedule.functions[0].value, + }, + time: schedule.time, + schedule_id: schedule.timer_id, + timezone_id: schedule.timezone_id, + days: getEnabledDays(schedule.loops), + }; + }); + return convertKeysToCamelCase(result); + } catch (error) { + throw new HttpException( + error.message || 'Error While Adding Schedule', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getScheduleDeviceInTuya( + deviceId: string, + category: string, + ): Promise { + try { + const path = `/v2.0/cloud/timer/device/${deviceId}?category=category_${category}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + return response as getDeviceScheduleInterface; + } catch (error) { + console.error('Error fetching device schedule from Tuya:', error); + + throw new HttpException( + 'Error fetching device schedule from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getDeviceByDeviceUuid( + deviceUuid: string, + withProductDevice: boolean = true, + ) { + return await this.deviceRepository.findOne({ + where: { + uuid: deviceUuid, + }, + ...(withProductDevice && { relations: ['productDevice'] }), + }); + } +}