mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-11-26 07:54:53 +00:00
582 lines
18 KiB
TypeScript
582 lines
18 KiB
TypeScript
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
|
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import {
|
|
AddDoorLockOnlineInterface,
|
|
addDeviceObjectInterface,
|
|
createTickInterface,
|
|
deleteTemporaryPasswordInterface,
|
|
getPasswordInterface,
|
|
} 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 { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
|
|
|
|
@Injectable()
|
|
export class DoorLockService {
|
|
private tuya: TuyaContext;
|
|
constructor(
|
|
private readonly configService: ConfigService,
|
|
private readonly deviceRepository: DeviceRepository,
|
|
private readonly passwordEncryptionService: PasswordEncryptionService,
|
|
) {
|
|
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 deleteDoorLockPassword(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 deletePass = await this.deleteDoorLockPasswordTuya(
|
|
deviceDetails.deviceTuyaUuid,
|
|
passwordId,
|
|
);
|
|
|
|
if (!deletePass.success) {
|
|
throw new HttpException('PasswordId not found', HttpStatus.NOT_FOUND);
|
|
}
|
|
return deletePass;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.message || 'Error deleting temporary password',
|
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async deleteDoorLockPasswordTuya(
|
|
doorLockUuid: string,
|
|
passwordId: string,
|
|
): Promise<deleteTemporaryPasswordInterface> {
|
|
try {
|
|
const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-passwords/${passwordId}`;
|
|
|
|
const response = await this.tuya.request({
|
|
method: 'DELETE',
|
|
path,
|
|
});
|
|
|
|
return response as deleteTemporaryPasswordInterface;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
'Error deleting temporary password from Tuya',
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async getOfflineMultipleTimeTemporaryPasswords(doorLockUuid: 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 passwords = await this.getTemporaryPasswordsTuya(
|
|
deviceDetails.deviceTuyaUuid,
|
|
);
|
|
|
|
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);
|
|
}
|
|
|
|
return passwords;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.message ||
|
|
'Error getting offline multiple time temporary passwords',
|
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async getOfflineOneTimeTemporaryPasswords(doorLockUuid: 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 passwords = await this.getTemporaryPasswordsTuya(
|
|
deviceDetails.deviceTuyaUuid,
|
|
);
|
|
|
|
if (passwords.result.length > 0) {
|
|
const passwordFiltered = passwords.result.filter(
|
|
(item) =>
|
|
(!item.schedule_list || item.schedule_list.length === 0) &&
|
|
item.type === 1,
|
|
);
|
|
|
|
return convertKeysToCamelCase(passwordFiltered);
|
|
}
|
|
|
|
return passwords;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.message || 'Error getting offline one time temporary passwords',
|
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async getOnlineTemporaryPasswords(doorLockUuid: 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 passwords = await this.getTemporaryPasswordsTuya(
|
|
deviceDetails.deviceTuyaUuid,
|
|
);
|
|
|
|
if (passwords.result.length > 0) {
|
|
const passwordFiltered = passwords.result
|
|
.filter((item) => item.type === 1)
|
|
.map((password: any) => {
|
|
if (password.schedule_list?.length > 0) {
|
|
password.schedule_list = password.schedule_list.map(
|
|
(schedule) => {
|
|
schedule.working_day = this.getDaysFromWorkingDayValue(
|
|
schedule.working_day,
|
|
);
|
|
schedule.effective_time = this.minutesToTime(
|
|
schedule.effective_time,
|
|
);
|
|
schedule.invalid_time = this.minutesToTime(
|
|
schedule.invalid_time,
|
|
);
|
|
return schedule;
|
|
},
|
|
);
|
|
}
|
|
return password;
|
|
});
|
|
|
|
return convertKeysToCamelCase(passwordFiltered);
|
|
}
|
|
|
|
return passwords;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.message || 'Error getting online temporary passwords',
|
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async getTemporaryPasswordsTuya(
|
|
doorLockUuid: string,
|
|
): Promise<getPasswordInterface> {
|
|
try {
|
|
const path = `/v1.0/devices/${doorLockUuid}/door-lock/temp-passwords?valid=true`;
|
|
|
|
const response = await this.tuya.request({
|
|
method: 'GET',
|
|
path,
|
|
});
|
|
|
|
return response as getPasswordInterface;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
'Error getting temporary passwords from Tuya',
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async addOfflineMultipleTimeTemporaryPassword(
|
|
addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto,
|
|
doorLockUuid: string,
|
|
) {
|
|
try {
|
|
const createOnlinePass = await this.addOnlineTemporaryPassword(
|
|
addDoorLockOfflineTempDto,
|
|
doorLockUuid,
|
|
'multiple',
|
|
false,
|
|
);
|
|
if (!createOnlinePass) {
|
|
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
|
|
}
|
|
const createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya(
|
|
addDoorLockOfflineTempDto,
|
|
createOnlinePass.id,
|
|
createOnlinePass.deviceTuyaUuid,
|
|
'multiple',
|
|
);
|
|
if (!createOnceOfflinePass.success) {
|
|
throw new HttpException(
|
|
createOnceOfflinePass.msg,
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
return {
|
|
result: createOnceOfflinePass.result,
|
|
};
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.message || 'Error adding offline temporary password from Tuya',
|
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async addOfflineOneTimeTemporaryPassword(
|
|
addDoorLockOfflineTempDto: AddDoorLockOfflineTempDto,
|
|
doorLockUuid: string,
|
|
) {
|
|
try {
|
|
const createOnlinePass = await this.addOnlineTemporaryPassword(
|
|
addDoorLockOfflineTempDto,
|
|
doorLockUuid,
|
|
'once',
|
|
false,
|
|
);
|
|
if (!createOnlinePass) {
|
|
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
|
|
}
|
|
const createOnceOfflinePass = await this.addOfflineTemporaryPasswordTuya(
|
|
addDoorLockOfflineTempDto,
|
|
createOnlinePass.id,
|
|
createOnlinePass.deviceTuyaUuid,
|
|
'once',
|
|
);
|
|
if (!createOnceOfflinePass.success) {
|
|
throw new HttpException(
|
|
createOnceOfflinePass.msg,
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
return {
|
|
result: createOnceOfflinePass.result,
|
|
};
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.message || 'Error adding offline temporary password from Tuya',
|
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async addOfflineTemporaryPasswordTuya(
|
|
addDoorLockDto: AddDoorLockOnlineInterface,
|
|
onlinePassId: number,
|
|
doorLockUuid: string,
|
|
type: string,
|
|
): Promise<createTickInterface> {
|
|
try {
|
|
const path = `/v1.1/devices/${doorLockUuid}/door-lock/offline-temp-password`;
|
|
|
|
const response = await this.tuya.request({
|
|
method: 'POST',
|
|
path,
|
|
body: {
|
|
name: addDoorLockDto.name,
|
|
...(type === 'multiple' && {
|
|
effective_time: addDoorLockDto.effectiveTime,
|
|
invalid_time: addDoorLockDto.invalidTime,
|
|
}),
|
|
|
|
type,
|
|
password_id: onlinePassId,
|
|
},
|
|
});
|
|
|
|
return response as createTickInterface;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
'Error adding offline temporary password from Tuya',
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async addOnlineTemporaryPassword(
|
|
addDoorLockDto: AddDoorLockOnlineInterface,
|
|
doorLockUuid: string,
|
|
type: string = 'once',
|
|
isOnline: boolean = true,
|
|
) {
|
|
try {
|
|
const passwordData = await this.getTicketAndEncryptedPassword(
|
|
doorLockUuid,
|
|
addDoorLockDto.password,
|
|
);
|
|
if (
|
|
!passwordData.ticketKey ||
|
|
!passwordData.encryptedPassword ||
|
|
!passwordData.deviceTuyaUuid
|
|
) {
|
|
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
|
|
}
|
|
const addDeviceObj: addDeviceObjectInterface = {
|
|
...addDoorLockDto,
|
|
...passwordData,
|
|
};
|
|
const createPass = await this.addOnlineTemporaryPasswordTuya(
|
|
addDeviceObj,
|
|
passwordData.deviceTuyaUuid,
|
|
type,
|
|
addDeviceObj.scheduleList ? isOnline : false,
|
|
);
|
|
|
|
if (!createPass.success) {
|
|
throw new HttpException(createPass.msg, HttpStatus.BAD_REQUEST);
|
|
}
|
|
return {
|
|
id: createPass.result.id,
|
|
deviceTuyaUuid: passwordData.deviceTuyaUuid,
|
|
};
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.message || 'Error adding online temporary password from Tuya',
|
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
async getTicketAndEncryptedPassword(
|
|
doorLockUuid: string,
|
|
passwordPlan: 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 ticketDetails = await this.createDoorLockTicketTuya(
|
|
deviceDetails.deviceTuyaUuid,
|
|
);
|
|
|
|
if (!ticketDetails.result.ticket_id || !ticketDetails.result.ticket_key) {
|
|
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
|
|
}
|
|
const decrypted =
|
|
this.passwordEncryptionService.generateEncryptedPassword(
|
|
passwordPlan,
|
|
ticketDetails.result.ticket_key,
|
|
);
|
|
return {
|
|
ticketId: ticketDetails.result.ticket_id,
|
|
ticketKey: ticketDetails.result.ticket_key,
|
|
encryptedPassword: decrypted,
|
|
deviceTuyaUuid: deviceDetails.deviceTuyaUuid,
|
|
};
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.message || 'Error processing the request',
|
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
|
|
async createDoorLockTicketTuya(
|
|
deviceUuid: string,
|
|
): Promise<createTickInterface> {
|
|
try {
|
|
const path = `/v1.0/smart-lock/devices/${deviceUuid}/password-ticket`;
|
|
const response = await this.tuya.request({
|
|
method: 'POST',
|
|
path,
|
|
});
|
|
|
|
return response as createTickInterface;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
'Error creating door lock ticket from Tuya',
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
scheduleList = addDeviceObj.scheduleList.map((schedule) => ({
|
|
effective_time: this.timeToMinutes(schedule.effectiveTime),
|
|
invalid_time: this.timeToMinutes(schedule.invalidTime),
|
|
working_day: this.getWorkingDayValue(schedule.workingDay),
|
|
}));
|
|
}
|
|
|
|
const response = await this.tuya.request({
|
|
method: 'POST',
|
|
path,
|
|
body: {
|
|
name: addDeviceObj.name,
|
|
password: addDeviceObj.encryptedPassword,
|
|
effective_time: addDeviceObj.effectiveTime,
|
|
invalid_time: addDeviceObj.invalidTime,
|
|
password_type: 'ticket',
|
|
ticket_id: addDeviceObj.ticketId,
|
|
...(isOnline && {
|
|
schedule_list: scheduleList,
|
|
}),
|
|
type: type === 'multiple' ? '0' : '1',
|
|
},
|
|
});
|
|
|
|
return response as createTickInterface;
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error.msg || 'Error adding online temporary password from Tuya',
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
getWorkingDayValue(days) {
|
|
// Array representing the days of the week
|
|
const weekDays = ['Sat', 'Fri', 'Thu', 'Wed', 'Tue', 'Mon', 'Sun'];
|
|
|
|
// Initialize a binary string with 7 bits
|
|
let binaryString = '0000000';
|
|
|
|
// Iterate through the input array and update the binary string
|
|
days.forEach((day) => {
|
|
const index = weekDays.indexOf(day);
|
|
if (index !== -1) {
|
|
// Set the corresponding bit to '1'
|
|
binaryString =
|
|
binaryString.substring(0, index) +
|
|
'1' +
|
|
binaryString.substring(index + 1);
|
|
}
|
|
});
|
|
|
|
// Convert the binary string to an integer
|
|
const workingDayValue = parseInt(binaryString, 2);
|
|
|
|
return workingDayValue;
|
|
}
|
|
getDaysFromWorkingDayValue(workingDayValue) {
|
|
// Array representing the days of the week
|
|
const weekDays = ['Sat', 'Fri', 'Thu', 'Wed', 'Tue', 'Mon', 'Sun'];
|
|
|
|
// Convert the integer to a binary string and pad with leading zeros to ensure 7 bits
|
|
const binaryString = workingDayValue.toString(2).padStart(7, '0');
|
|
|
|
// Initialize an array to hold the days of the week
|
|
const days = [];
|
|
|
|
// Iterate through the binary string and weekDays array
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
if (binaryString[i] === '1') {
|
|
days.push(weekDays[i]);
|
|
}
|
|
}
|
|
|
|
return days;
|
|
}
|
|
timeToMinutes(timeStr) {
|
|
try {
|
|
// Special case for "24:00"
|
|
if (timeStr === '24:00') {
|
|
return 1440;
|
|
}
|
|
|
|
// Regular expression to validate the 24-hour time format (HH:MM)
|
|
const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
const match = timeStr.match(timePattern);
|
|
|
|
if (!match) {
|
|
throw new Error('Invalid time format');
|
|
}
|
|
|
|
// Extract hours and minutes from the matched groups
|
|
const hours = parseInt(match[1], 10);
|
|
const minutes = parseInt(match[2], 10);
|
|
|
|
// Calculate the total minutes
|
|
const totalMinutes = hours * 60 + minutes;
|
|
|
|
return totalMinutes;
|
|
} catch (error) {
|
|
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
minutesToTime(totalMinutes) {
|
|
try {
|
|
if (
|
|
typeof totalMinutes !== 'number' ||
|
|
totalMinutes < 0 ||
|
|
totalMinutes > 1440
|
|
) {
|
|
throw new Error('Invalid minutes value');
|
|
}
|
|
|
|
if (totalMinutes === 1440) {
|
|
return '24:00';
|
|
}
|
|
|
|
const hours = Math.floor(totalMinutes / 60);
|
|
const minutes = totalMinutes % 60;
|
|
|
|
const formattedHours = String(hours).padStart(2, '0');
|
|
const formattedMinutes = String(minutes).padStart(2, '0');
|
|
|
|
return `${formattedHours}:${formattedMinutes}`;
|
|
} catch (error) {
|
|
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
async getDeviceByDeviceUuid(
|
|
deviceUuid: string,
|
|
withProductDevice: boolean = true,
|
|
) {
|
|
try {
|
|
return await this.deviceRepository.findOne({
|
|
where: {
|
|
uuid: deviceUuid,
|
|
},
|
|
...(withProductDevice && { relations: ['productDevice'] }),
|
|
});
|
|
} catch (error) {
|
|
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
|
|
}
|
|
}
|
|
}
|