Files
backend/src/space/services/space-device.service.ts
T

150 lines
5.1 KiB
TypeScript

import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { GetDeviceDetailsInterface } from 'src/device/interfaces/get.device.interface';
import { GetSpaceParam } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { ValidationService } from './space-validation.service';
import { ProductType } from '@app/common/constants/product-type.enum';
import { BatteryStatus } from '@app/common/constants/battery-status.enum';
@Injectable()
export class SpaceDeviceService {
constructor(
private readonly tuyaService: TuyaService,
private readonly validationService: ValidationService,
) {}
async listDevicesInSpace(params: GetSpaceParam): Promise<BaseResponseDto> {
const { spaceUuid, communityUuid, projectUuid } = params;
try {
// Validate community, project, and fetch space including devices in a single query
const space = await this.validationService.fetchSpaceDevices(spaceUuid);
if (!space || !space.devices?.length) {
throw new HttpException(
'The space does not contain any devices.',
HttpStatus.BAD_REQUEST,
);
}
const spaceHierarchy =
await this.validationService.getParentHierarchy(space);
const orderedHierarchy = [space, ...spaceHierarchy.reverse()];
// Fetch Tuya details for each device in parallel using Promise.allSettled
const deviceDetailsPromises = space.devices.map((device) =>
this.fetchDeviceDetails(device, orderedHierarchy),
);
const detailedDevices = (await Promise.allSettled(deviceDetailsPromises))
.filter((result) => result.status === 'fulfilled' && result.value)
.map((result) => (result as PromiseFulfilledResult<any>).value);
return new SuccessResponseDto({
data: detailedDevices,
message: 'Successfully retrieved list of devices.',
});
} catch (error) {
console.error('Error listing devices in space:', error);
throw new HttpException(
error.message || 'Failed to retrieve devices in space',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async fetchDeviceDetails(device: any, orderedHierarchy: any[]) {
try {
// Fetch Tuya details in parallel
const [tuyaDetails, tuyaDeviceStatusResponse] = await Promise.all([
this.getDeviceDetailsByDeviceIdTuya(device.deviceTuyaUuid),
this.tuyaService.getDevicesInstructionStatusTuya(device.deviceTuyaUuid),
]);
const tuyaStatusList =
tuyaDeviceStatusResponse?.result?.[0]?.status || [];
return {
spaces: orderedHierarchy.map((space) => ({
uuid: space.uuid,
spaceName: space.spaceName,
})),
uuid: device.uuid,
deviceTuyaUuid: device.deviceTuyaUuid,
productUuid: device.productDevice?.uuid || null,
productType: device.productDevice?.prodType || null,
isActive: device.isActive,
updatedAt: device.updatedAt,
deviceTag: device.tag,
subspace: device.subspace,
...tuyaDetails,
...(this.extractBatteryStatus(
device.productDevice?.prodType,
tuyaStatusList,
) !== null && {
battery: this.extractBatteryStatus(
device.productDevice?.prodType,
tuyaStatusList,
),
}),
status: tuyaStatusList,
};
} catch (error) {
console.warn(
`Skipping device ${device.deviceTuyaUuid} due to error: ${error.message}`,
);
return null;
}
}
private extractBatteryStatus(
deviceType: string,
tuyaStatus: any[],
): number | null {
const batteryCodes = {
[ProductType.DL]: BatteryStatus.RESIDUAL_ELECTRICITY,
[ProductType.DS]: BatteryStatus.BATTERY_PERCENTAGE,
[ProductType.WL]: BatteryStatus.BATTERY_PERCENTAGE,
[ProductType.SOS]: BatteryStatus.BATTERY_PERCENTAGE,
};
const batteryCode = batteryCodes[deviceType];
if (!batteryCode) return null;
const batteryStatus = tuyaStatus.find(
(status) => status.code === batteryCode,
);
return batteryStatus ? batteryStatus.value : null;
}
private async getDeviceDetailsByDeviceIdTuya(
deviceId: string,
): Promise<GetDeviceDetailsInterface> {
try {
const tuyaDeviceDetails =
await this.tuyaService.getDeviceDetails(deviceId);
// Convert keys to camel case
const camelCaseResponse = convertKeysToCamelCase(tuyaDeviceDetails);
// Exclude specific keys and add `productUuid`
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { uuid, ...rest } = camelCaseResponse;
return {
...rest,
} as GetDeviceDetailsInterface;
} catch (error) {
throw new HttpException(
`Error fetching device details from Tuya for device id ${deviceId}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}