mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-15 18:27:05 +00:00
fix commissioning
This commit is contained in:
@ -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',
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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: [],
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
console.log(device);
|
||||
for (const row of rows) {
|
||||
await this.processCsvRow(param, row, successCount, failureCount);
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
console.log(`Finished processing ${results.length} devices.`);
|
||||
resolve();
|
||||
})
|
||||
.on('error', (error) => {
|
||||
console.error('Error reading CSV', error);
|
||||
reject(error);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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': '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
|
Reference in New Issue
Block a user