finished schedule module

This commit is contained in:
faris Aljohari
2024-09-15 18:03:30 +03:00
parent 0df4af175f
commit d962d4a7b3
11 changed files with 559 additions and 0 deletions

View File

@ -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}`,
};
}

View File

@ -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;
}

View File

@ -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: [

View File

@ -0,0 +1 @@
export * from './schedule.controller';

View File

@ -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,
);
}
}
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from './add.schedule.dto';

View File

@ -0,0 +1,10 @@
export interface getDeviceScheduleInterface {
success: boolean;
result: [];
msg: string;
}
export interface addScheduleDeviceInterface {
success: boolean;
result: boolean;
msg: string;
}

View File

@ -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 {}

View File

@ -0,0 +1 @@
export * from './schedule.service';

View File

@ -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<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 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<addScheduleDeviceInterface> {
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<addScheduleDeviceInterface> {
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<addScheduleDeviceInterface> {
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<getDeviceScheduleInterface> {
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'] }),
});
}
}