fix commissioning

This commit is contained in:
hannathkadher
2025-04-21 10:58:14 +04:00
parent c677be400c
commit ed3c526efd
11 changed files with 214 additions and 64 deletions

View File

@ -3,6 +3,7 @@ import { RoleType } from './role.type.enum';
export const RolePermissions = {
[RoleType.SUPER_ADMIN]: [
'DEVICE_SINGLE_CONTROL',
'COMMISSION_DEVICE',
'DEVICE_VIEW',
'DEVICE_DELETE',
'DEVICE_UPDATE',
@ -58,6 +59,7 @@ export const RolePermissions = {
'PRODUCT_ADD',
],
[RoleType.ADMIN]: [
'COMMISSION_DEVICE',
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',
@ -127,6 +129,7 @@ export const RolePermissions = {
'SCENES_CONTROL',
],
[RoleType.SPACE_OWNER]: [
'COMMISSION_DEVICE',
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',

View File

@ -75,7 +75,7 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
@OneToMany(() => NewTagEntity, (tag) => tag.devices)
// @JoinTable({ name: 'device_tags' })
public tag: NewTagEntity[];
public tag: NewTagEntity;
constructor(partial: Partial<DeviceEntity>) {
super();

View File

@ -3,7 +3,6 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProductEntity } from '../../product/entities';
import { TagDto } from '../dtos';
import { TagModel } from '../../space-model/entities/tag-model.entity';
import { SpaceEntity } from './space.entity';
import { DeviceEntity } from '../../device/entities';
import { SubspaceEntity } from './subspace/subspace.entity';
@ -22,9 +21,6 @@ export class TagEntity extends AbstractEntity<TagDto> {
})
product: ProductEntity;
@ManyToOne(() => SpaceEntity, (space) => space.tags, { nullable: true })
space: SpaceEntity;
@ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, {
nullable: true,
})

View File

@ -19,6 +19,8 @@ import {
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
@Module({
imports: [ConfigModule, SpaceRepositoryModule],
@ -39,6 +41,8 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
SceneIconRepository,
SceneRepository,
AutomationRepository,
CommunityRepository,
SubspaceRepository,
],
exports: [],
})

View File

@ -22,9 +22,10 @@ import { ControllerRoute } from '@app/common/constants/controller-route';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { CommissionDeviceCsvDto } from '../dto';
import { CommunityParam } from '@app/common/dto/community-space.param';
import { DeviceCommissionService } from '../services';
import { ProjectParam } from '@app/common/dto/project-param.dto';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PermissionsGuard } from 'src/guards/permissions.guard';
@ApiTags('Commission Devices Module')
@Controller({
@ -34,6 +35,8 @@ import { ProjectParam } from '@app/common/dto/project-param.dto';
export class DeviceCommissionController {
constructor(private readonly commissionService: DeviceCommissionService) {}
@UseGuards(PermissionsGuard)
@Permissions('COMMISSION_DEVICE')
@ApiBearerAuth()
@Post()
@ApiConsumes('multipart/form-data')
@ -65,7 +68,7 @@ export class DeviceCommissionController {
@Param() param: ProjectParam,
@Req() req: any,
): Promise<BaseResponseDto> {
await this.commissionService.processCsv(file.path);
await this.commissionService.processCsv(param, file.path);
return {
message: 'CSV file received and processing started',
success: true,

View File

@ -2,44 +2,185 @@ import * as fs from 'fs';
import * as csv from 'csv-parser';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { DeviceService } from 'src/device/services';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { SpaceRepository } from '@app/common/modules/space';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { ProjectParam } from '@app/common/dto/project-param.dto';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
@Injectable()
export class DeviceCommissionService {
constructor(
private readonly tuyaService: TuyaService,
private readonly deviceService: DeviceService,
private readonly communityRepository: CommunityRepository,
private readonly spaceRepository: SpaceRepository,
private readonly subspaceRepository: SubspaceRepository,
private readonly deviceRepository: DeviceRepository,
private readonly projectRepository: ProjectRepository,
) {}
async processCsv(filePath: string): Promise<void> {
return new Promise((resolve, reject) => {
const results = [];
async processCsv(param: ProjectParam, filePath: string) {
const successCount = { value: 0 };
const failureCount = { value: 0 };
const projectId = param.projectUuid;
const project = await this.projectRepository.findOne({
where: { uuid: projectId },
});
if (!project) {
throw new HttpException('Project not found', HttpStatus.NOT_FOUND);
}
const rows: any[] = [];
try {
await new Promise<void>((resolve, reject) => {
fs.createReadStream(filePath)
.pipe(csv())
.on('data', async (row) => {
console.log(`Device: ${JSON.stringify(row)}`);
const deviceId = row.deviceId?.trim();
.on('data', (row) => rows.push(row))
.on('end', () => resolve())
.on('error', (error) => reject(error));
});
if (!deviceId) {
console.error('Missing deviceId or deviceName in row:', row);
return;
} else {
const device = await this.tuyaService.getDeviceDetails(
row.deviceId,
for (const row of rows) {
await this.processCsvRow(param, row, successCount, failureCount);
}
return new SuccessResponseDto({
message: `Successfully processed CSV file`,
data: {
successCount: successCount.value,
failureCount: failureCount.value,
},
statusCode: HttpStatus.ACCEPTED,
});
} catch (error) {
throw new HttpException(
'Failed to process CSV file',
HttpStatus.INTERNAL_SERVER_ERROR,
);
console.log(device);
}
})
.on('end', () => {
console.log(`Finished processing ${results.length} devices.`);
resolve();
})
.on('error', (error) => {
console.error('Error reading CSV', error);
reject(error);
}
private async processCsvRow(
param: ProjectParam,
row: any,
successCount: { value: number },
failureCount: { value: number },
) {
try {
const rawDeviceId = row['Device ID']?.trim();
const communityId = row['Community UUID']?.trim();
const spaceId = row['Space UUID']?.trim();
const subspaceId = row['Subspace UUID']?.trim();
const tagName = row['Tag']?.trim();
const productName = row['Product Name']?.trim();
const projectId = param.projectUuid;
if (!rawDeviceId) {
console.error('Missing Device ID in row:', row);
failureCount.value++;
return;
}
const device = await this.tuyaService.getDeviceDetails(rawDeviceId);
if (!device) {
console.error(`Device not found for Device ID: ${rawDeviceId}`);
failureCount.value++;
return;
}
const community = await this.communityRepository.findOne({
where: { uuid: communityId, project: { uuid: projectId } },
});
if (!community) {
console.error(`Community not found: ${communityId}`);
failureCount.value++;
return;
}
const tuyaSpaceId = community.externalId;
const space = await this.spaceRepository.findOne({
where: { uuid: spaceId },
relations: [
'productAllocations',
'productAllocations.tags',
'productAllocations.product',
],
});
if (!space) {
console.error(`Space not found: ${spaceId}`);
failureCount.value++;
return;
}
let subspace: SubspaceEntity | null = null;
if (subspaceId?.trim()) {
subspace = await this.subspaceRepository.findOne({
where: { uuid: subspaceId },
relations: [
'productAllocations',
'productAllocations.tags',
'productAllocations.product',
],
});
if (!subspace) {
console.error(`Subspace not found: ${subspaceId}`);
failureCount.value++;
return;
}
}
const allocations =
subspace?.productAllocations || space.productAllocations;
const match = allocations
.flatMap((pa) =>
(pa.tags || []).map((tag) => ({ product: pa.product, tag })),
)
.find(({ tag }) => tag.name === tagName);
if (!match) {
console.error(`No matching tag found for Device ID: ${rawDeviceId}`);
failureCount.value++;
return;
}
if (match.product.name !== productName) {
console.error(`Product name mismatch for Device ID: ${rawDeviceId}`);
failureCount.value++;
return;
}
const middlewareDevice = this.deviceRepository.create({
deviceTuyaUuid: rawDeviceId,
isActive: true,
spaceDevice: space,
subspace: subspace || null,
productDevice: match.product,
tag: match.tag,
});
await this.deviceRepository.save(middlewareDevice);
await this.deviceService.transferDeviceInSpacesTuya(
rawDeviceId,
tuyaSpaceId,
);
successCount.value++;
} catch (err) {
failureCount.value++;
}
}
}

View File

@ -5,6 +5,7 @@ import {
Delete,
Get,
Header,
HttpStatus,
Param,
Post,
Put,
@ -12,6 +13,7 @@ import {
Res,
UseGuards,
} from '@nestjs/common';
import { Response } from 'express';
import {
ApiBearerAuth,
ApiOperation,
@ -115,7 +117,13 @@ export class ProjectController {
@Param() params: GetProjectParam,
@Res() res: Response,
): Promise<void> {
try {
const csvStream = await this.projectService.exportToCsv(params);
csvStream.pipe(res as unknown as NodeJS.WritableStream);
} catch (error) {
res
.status(error.status || HttpStatus.INTERNAL_SERVER_ERROR)
.json({ message: error.message || 'Failed to generate CSV file.' });
}
}
}

View File

@ -254,17 +254,16 @@ export class ProjectService {
const stream = new PassThrough();
const csvStream = format({
headers: [
'Project Name',
'Project UUID',
'Device ID',
'Community Name',
'Community UUID',
'Space Location',
'Space Name',
'Space UUID',
'Space Location',
'Subspace Name',
'Subspace UUID',
'Tag',
'Tag Product Name',
'Product Name',
'Community UUID',
'Space UUID',
'Subspace UUID',
],
});
@ -306,17 +305,16 @@ export class ProjectService {
for (const productAllocation of subspace.productAllocations || []) {
for (const tag of productAllocation.tags || []) {
csvStream.write({
'Project Name': project.name,
'Project UUID': project.uuid,
'Device ID': '',
'Community Name': space.community?.name || '',
'Community UUID': space.community?.uuid || '',
'Space Location': spaceLocation,
'Space Name': space.spaceName,
'Space UUID': space.uuid,
'Space Location': spaceLocation,
'Subspace Name': subspace.subspaceName || '',
'Subspace UUID': subspace.uuid,
Tag: tag.name,
'Tag Product Name': productAllocation.product.name || '',
'Product Name': productAllocation.product.name || '',
'Community UUID': space.community?.uuid || '',
'Space UUID': space.uuid,
'Subspace UUID': subspace.uuid,
});
}
}
@ -325,17 +323,16 @@ export class ProjectService {
for (const productAllocation of space.productAllocations || []) {
for (const tag of productAllocation.tags || []) {
csvStream.write({
'Project Name': project.name,
'Project UUID': project.uuid,
'Device ID': '',
'Community Name': space.community?.name || '',
'Community UUID': space.community?.uuid || '',
'Space Location': spaceLocation,
'Space Name': space.spaceName,
'Space UUID': space.uuid,
'Space Location': spaceLocation,
'Subspace Name': '',
'Subspace UUID': '',
Tag: tag.name,
'Tag Product Name': productAllocation.product.name || '',
'Product Name': productAllocation.product.name || '',
'Community UUID': space.community?.uuid || '',
'Space UUID': space.uuid,
'Subspace UUID': '',
});
}
}

View File

@ -65,7 +65,7 @@ export class DisableSpaceHandler
}
}
const tagUuids = space.tags?.map((tag) => tag.uuid) || [];
const tagUuids = space.productAllocations?.map((tag) => tag.uuid) || [];
/* const subspaceDtos =
space.subspaces?.map((subspace) => ({
subspaceUuid: subspace.uuid,

View File

@ -582,8 +582,8 @@ export class SpaceService {
queryRunner: QueryRunner,
): Promise<void> {
try {
if (space.subspaces || space.tags) {
if (space.tags) {
if (space.subspaces || space.productAllocations) {
if (space.productAllocations) {
await this.spaceProductAllocationService.unlinkModels(
space,
queryRunner,

View File

@ -118,7 +118,7 @@ export class TagService {
await queryRunner.manager.update(
this.tagRepository.target,
{ uuid: tag.uuid },
{ subspace, space: null },
{ subspace },
);
tag.subspace = subspace;
}
@ -127,10 +127,9 @@ export class TagService {
await queryRunner.manager.update(
this.tagRepository.target,
{ uuid: tag.uuid },
{ subspace: null, space: space },
{ subspace: null },
);
tag.subspace = null;
tag.space = space;
}
return tag;
@ -367,7 +366,6 @@ export class TagService {
where: [
{
tag,
space: { uuid: spaceUuid },
product: { uuid: productUuid },
disabled: false,
},