Files
backend/src/space/services/subspace/subspace-device.service.ts
2025-03-10 10:57:27 +04:00

266 lines
8.1 KiB
TypeScript

import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface';
import { ValidationService } from '../space-validation.service';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import { In, QueryRunner } from 'typeorm';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { TagRepository } from '@app/common/modules/space';
@Injectable()
export class SubspaceDeviceService {
constructor(
private readonly subspaceRepository: SubspaceRepository,
private readonly deviceRepository: DeviceRepository,
private readonly tuyaService: TuyaService,
private readonly productRepository: ProductRepository,
private readonly validationService: ValidationService,
private readonly tagRepository: TagRepository,
) {}
async listDevicesInSubspace(
params: GetSubSpaceParam,
): Promise<BaseResponseDto> {
const { subSpaceUuid, spaceUuid, communityUuid, projectUuid } = params;
await this.validationService.checkCommunityAndProjectSpaceExistence(
communityUuid,
projectUuid,
spaceUuid,
);
const subspace = await this.findSubspaceWithDevices(subSpaceUuid);
const safeFetch = async (device: any) => {
try {
const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya(
device.deviceTuyaUuid,
);
return {
uuid: device.uuid,
deviceTuyaUuid: device.deviceTuyaUuid,
productUuid: device.productDevice.uuid,
productType: device.productDevice.prodType,
isActive: device.isActive,
createdAt: device.createdAt,
updatedAt: device.updatedAt,
...tuyaDetails,
};
} catch (error) {
return null;
}
};
const detailedDevices = await Promise.all(subspace.devices.map(safeFetch));
return new SuccessResponseDto({
data: detailedDevices.filter(Boolean), // Remove nulls
message: 'Successfully retrieved list of devices',
});
}
async associateDeviceToSubspace(
params: DeviceSubSpaceParam,
): Promise<BaseResponseDto> {
const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid, projectUuid } =
params;
try {
await this.validationService.checkCommunityAndProjectSpaceExistence(
communityUuid,
projectUuid,
spaceUuid,
);
const subspace = await this.findSubspace(subSpaceUuid);
const device = await this.findDevice(deviceUuid);
device.subspace = subspace;
const newDevice = await this.deviceRepository.save(device);
return new SuccessResponseDto({
data: newDevice,
message: `Successfully associated device to subspace`,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
} else {
throw new HttpException(
`Failed to associate device to subspace with error = ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async disassociateDeviceFromSubspace(
params: DeviceSubSpaceParam,
): Promise<BaseResponseDto> {
const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid, projectUuid } =
params;
try {
await this.validationService.checkCommunityAndProjectSpaceExistence(
communityUuid,
projectUuid,
spaceUuid,
);
const subspace = await this.findSubspace(subSpaceUuid);
const device = await this.findDeviceWithSubspaceAndTag(deviceUuid);
if (!device.subspace || device.subspace.uuid !== subspace.uuid) {
throw new HttpException(
`Device ${deviceUuid} is not associated with the specified subspace ${subSpaceUuid} `,
HttpStatus.BAD_REQUEST,
);
}
device.subspace = null;
const updatedDevice = await this.deviceRepository.save(device);
return new SuccessResponseDto({
data: updatedDevice,
message: 'Successfully dissociated device from subspace',
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
} else {
throw new HttpException(
`Failed to dissociate device from subspace error = ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
// Helper method to find subspace with devices relation
private async findSubspaceWithDevices(subSpaceUuid: string) {
const subspace = await this.subspaceRepository.findOne({
where: { uuid: subSpaceUuid },
relations: ['devices', 'devices.productDevice'],
});
if (!subspace) {
this.throwNotFound('Subspace', subSpaceUuid);
}
return subspace;
}
private async findSubspace(subSpaceUuid: string) {
const subspace = await this.subspaceRepository.findOne({
where: { uuid: subSpaceUuid },
});
if (!subspace) {
this.throwNotFound('Subspace', subSpaceUuid);
}
return subspace;
}
private async findDevice(deviceUuid: string) {
const device = await this.deviceRepository.findOne({
where: { uuid: deviceUuid },
relations: [
'subspace',
'tag',
'tag.space',
'tag.subspace',
'spaceDevice',
],
});
if (!device) {
this.throwNotFound('Device', deviceUuid);
}
return device;
}
async deleteSubspaceDevices(
devices: DeviceEntity[],
queryRunner: QueryRunner,
): Promise<void> {
const deviceUuids = devices.map((device) => device.uuid);
try {
if (deviceUuids.length === 0) {
return;
}
await queryRunner.manager.update(
this.deviceRepository.target,
{ uuid: In(deviceUuids) },
{ subspace: null },
);
} catch (error) {
throw new HttpException(
`Failed to delete devices with IDs ${deviceUuids.join(', ')}: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private throwNotFound(entity: string, uuid: string) {
throw new HttpException(
`${entity} with ID ${uuid} not found`,
HttpStatus.NOT_FOUND,
);
}
private async getDeviceDetailsByDeviceIdTuya(
deviceId: string,
): Promise<GetDeviceDetailsInterface> {
try {
const tuyaDeviceDetails =
await this.tuyaService.getDeviceDetails(deviceId);
const camelCaseResponse = convertKeysToCamelCase(tuyaDeviceDetails);
const product = await this.productRepository.findOne({
where: {
prodId: camelCaseResponse.productId,
},
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { uuid, ...rest } = camelCaseResponse;
return {
...rest,
productUuid: product?.uuid,
} as GetDeviceDetailsInterface;
} catch (error) {
throw new HttpException(
`Error fetching device details from Tuya for device uuid ${deviceId}.`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async findNextTag(): Promise<number> {
const tags = await this.tagRepository.find({ select: ['tag'] });
const tagNumbers = tags
.map((t) => t.tag.match(/^Tag (\d+)$/))
.filter((match) => match)
.map((match) => parseInt(match[1]))
.sort((a, b) => a - b);
const nextTagNumber = tagNumbers.length
? tagNumbers[tagNumbers.length - 1] + 1
: 1;
return nextTagNumber;
}
private async findDeviceWithSubspaceAndTag(deviceUuid: string) {
return await this.deviceRepository.findOne({
where: { uuid: deviceUuid },
relations: ['subspace', 'tag', 'spaceDevice'],
select: ['uuid', 'subspace', 'spaceDevice', 'productDevice', 'tag'],
});
}
}