added commissioning endpoint

This commit is contained in:
hannathkadher
2025-04-08 10:42:48 +04:00
parent d11c6a88f1
commit de09624db8
14 changed files with 256 additions and 2 deletions

View File

@ -482,6 +482,7 @@ export class ControllerRoute {
'This endpoint retrieves all devices in a specified group within a space, based on the group name and space UUID.'; 'This endpoint retrieves all devices in a specified group within a space, based on the group name and space UUID.';
}; };
}; };
static DEVICE = class { static DEVICE = class {
public static readonly ROUTE = 'devices'; public static readonly ROUTE = 'devices';
@ -574,7 +575,15 @@ export class ControllerRoute {
'This endpoint deletes all scenes associated with a specific switch device.'; 'This endpoint deletes all scenes associated with a specific switch device.';
}; };
}; };
static DEVICE_COMMISSION = class {
public static readonly ROUTE = '/projects/:projectUuid/devices/commission';
static ACTIONS = class {
public static readonly ADD_ALL_DEVICES_SUMMARY = 'Add all devices';
public static readonly ADD_ALL_DEVICES_DESCRIPTION =
'This endpoint add all devices in the system from tuya.';
};
};
static DEVICE_PROJECT = class { static DEVICE_PROJECT = class {
public static readonly ROUTE = '/projects/:projectUuid/devices'; public static readonly ROUTE = '/projects/:projectUuid/devices';
static ACTIONS = class { static ACTIONS = class {

View File

@ -53,6 +53,7 @@ export const RolePermissions = {
'VISITOR_PASSWORD_DELETE', 'VISITOR_PASSWORD_DELETE',
'USER_ADD', 'USER_ADD',
'SPACE_MEMBER_ADD', 'SPACE_MEMBER_ADD',
'COMMISSION_DEVICE',
], ],
[RoleType.ADMIN]: [ [RoleType.ADMIN]: [
'DEVICE_SINGLE_CONTROL', 'DEVICE_SINGLE_CONTROL',
@ -106,6 +107,7 @@ export const RolePermissions = {
'VISITOR_PASSWORD_DELETE', 'VISITOR_PASSWORD_DELETE',
'USER_ADD', 'USER_ADD',
'SPACE_MEMBER_ADD', 'SPACE_MEMBER_ADD',
'COMMISSION_DEVICE',
], ],
[RoleType.SPACE_MEMBER]: [ [RoleType.SPACE_MEMBER]: [
'DEVICE_SINGLE_CONTROL', 'DEVICE_SINGLE_CONTROL',
@ -163,5 +165,6 @@ export const RolePermissions = {
'VISITOR_PASSWORD_DELETE', 'VISITOR_PASSWORD_DELETE',
'USER_ADD', 'USER_ADD',
'SPACE_MEMBER_ADD', 'SPACE_MEMBER_ADD',
'COMMISSION_DEVICE',
], ],
}; };

View File

@ -0,0 +1,9 @@
import { File } from 'multer';
declare global {
namespace Express {
interface Request {
file?: File;
}
}
}

26
package-lock.json generated
View File

@ -26,6 +26,7 @@
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"csv-parser": "^3.2.0",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"firebase": "^10.12.5", "firebase": "^10.12.5",
"google-auth-library": "^9.14.1", "google-auth-library": "^9.14.1",
@ -47,8 +48,9 @@
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.21",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
@ -3105,6 +3107,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/multer": {
"version": "1.4.12",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
"integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.19", "version": "20.17.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz",
@ -5181,6 +5193,18 @@
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/csv-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
"integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==",
"license": "MIT",
"bin": {
"csv-parser": "bin/csv-parser"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/dashdash": { "node_modules/dashdash": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",

View File

@ -37,6 +37,7 @@
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"csv-parser": "^3.2.0",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"firebase": "^10.12.5", "firebase": "^10.12.5",
"google-auth-library": "^9.14.1", "google-auth-library": "^9.14.1",
@ -58,8 +59,9 @@
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.21",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",

View File

@ -30,6 +30,7 @@ import { TermsConditionsModule } from './terms-conditions/terms-conditions.modul
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module'; import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
import { TagModule } from './tags/tags.module'; import { TagModule } from './tags/tags.module';
import { ClientModule } from './client/client.module'; import { ClientModule } from './client/client.module';
import { DeviceCommissionModule } from './commission-device/commission-device.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
@ -63,6 +64,8 @@ import { ClientModule } from './client/client.module';
TermsConditionsModule, TermsConditionsModule,
PrivacyPolicyModule, PrivacyPolicyModule,
TagModule, TagModule,
DeviceCommissionModule,
], ],
providers: [ providers: [
{ {

View File

@ -0,0 +1,45 @@
import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DeviceCommissionController } from './controllers';
import { UserRepository } from '@app/common/modules/user/repositories';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { DeviceCommissionService } from './services';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { DeviceService } from 'src/device/services';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { SpaceRepository } from '@app/common/modules/space';
import { SceneService } from 'src/scene/services';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
@Module({
imports: [ConfigModule, SpaceRepositoryModule],
controllers: [DeviceCommissionController],
providers: [
UserRepository,
DeviceRepository,
DeviceCommissionService,
TuyaService,
DeviceService,
SceneDeviceRepository,
ProductRepository,
DeviceStatusFirebaseService,
SpaceRepository,
SceneService,
ProjectRepository,
DeviceStatusLogRepository,
SceneIconRepository,
SceneRepository,
AutomationRepository,
],
exports: [],
})
export class DeviceCommissionModule {}

View File

@ -0,0 +1,74 @@
import {
Controller,
Post,
Req,
UseGuards,
UploadedFile,
UseInterceptors,
UsePipes,
ValidationPipe,
Param,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiConsumes,
ApiOperation,
ApiTags,
ApiBody,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
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';
@ApiTags('Commission Devices Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.DEVICE_COMMISSION.ROUTE,
})
export class DeviceCommissionController {
constructor(private readonly commissionService: DeviceCommissionService) {}
@ApiBearerAuth()
@Post()
@ApiConsumes('multipart/form-data')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
},
}),
fileFilter: (req, file, cb) => {
if (!file.originalname.match(/\.(csv)$/)) {
return cb(new Error('Only CSV files are allowed!'), false);
}
cb(null, true);
},
}),
)
@ApiBody({ type: CommissionDeviceCsvDto })
@ApiOperation({
summary: ControllerRoute.DEVICE_COMMISSION.ACTIONS.ADD_ALL_DEVICES_SUMMARY,
description:
ControllerRoute.DEVICE_COMMISSION.ACTIONS.ADD_ALL_DEVICES_DESCRIPTION,
})
@UsePipes(new ValidationPipe({ transform: true }))
async addNewDevice(
@UploadedFile() file: Express.Multer.File,
@Param() param: ProjectParam,
@Req() req: any,
): Promise<BaseResponseDto> {
await this.commissionService.processCsv(file.path);
return {
message: 'CSV file received and processing started',
success: true,
};
}
}

View File

@ -0,0 +1 @@
export * from './commission-device.controller';

View File

@ -0,0 +1,18 @@
// dto/commission-device.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, Validate } from 'class-validator';
import { FileValidator } from 'src/validators/file.validator';
export class CommissionDeviceCsvDto {
@ApiProperty({
type: 'string',
format: 'binary',
description: 'CSV file containing device data',
})
@IsNotEmpty({ message: 'CSV file is required' })
@Validate(FileValidator, ['text/csv', 'application/vnd.ms-excel'], {
message: 'Only CSV files are allowed',
})
file: any;
}

View File

@ -0,0 +1 @@
export * from './commission-device.dto';

View File

@ -0,0 +1,45 @@
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 { DeviceService } from 'src/device/services';
@Injectable()
export class DeviceCommissionService {
constructor(
private readonly tuyaService: TuyaService,
private readonly deviceService: DeviceService,
) {}
async processCsv(filePath: string): Promise<void> {
return new Promise((resolve, reject) => {
const results = [];
fs.createReadStream(filePath)
.pipe(csv())
.on('data', async (row) => {
console.log(`Device: ${JSON.stringify(row)}`);
const deviceId = row.deviceId?.trim();
if (!deviceId) {
console.error('Missing deviceId or deviceName in row:', row);
return;
} else {
const device = await this.tuyaService.getDeviceDetails(
row.deviceId,
);
console.log(device);
}
})
.on('end', () => {
console.log(`Finished processing ${results.length} devices.`);
resolve();
})
.on('error', (error) => {
console.error('Error reading CSV', error);
reject(error);
});
});
}
}

View File

@ -0,0 +1 @@
export * from './commission-device.service';

View File

@ -0,0 +1,19 @@
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
@ValidatorConstraint({ name: 'fileValidator', async: false })
export class FileValidator implements ValidatorConstraintInterface {
constructor(private readonly allowedMimeTypes: string[]) {}
validate(file: Express.Multer.File, _args: ValidationArguments) {
if (!file || !file.mimetype) return false;
return this.allowedMimeTypes.includes(file.mimetype);
}
defaultMessage(_args: ValidationArguments) {
return 'Invalid file type';
}
}