mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-10 15:17:41 +00:00
added commissioning endpoint
This commit is contained in:
@ -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 {
|
||||||
|
@ -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',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
9
libs/common/src/type/express/index.d.ts
vendored
Normal file
9
libs/common/src/type/express/index.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { File } from 'multer';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
file?: File;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
package-lock.json
generated
26
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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: [
|
||||||
{
|
{
|
||||||
|
45
src/commission-device/commission-device.module.ts
Normal file
45
src/commission-device/commission-device.module.ts
Normal 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 {}
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
1
src/commission-device/controllers/index.ts
Normal file
1
src/commission-device/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './commission-device.controller';
|
18
src/commission-device/dto/commission-device.dto.ts
Normal file
18
src/commission-device/dto/commission-device.dto.ts
Normal 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;
|
||||||
|
}
|
1
src/commission-device/dto/index.ts
Normal file
1
src/commission-device/dto/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './commission-device.dto';
|
45
src/commission-device/services/commission-device.service.ts
Normal file
45
src/commission-device/services/commission-device.service.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
1
src/commission-device/services/index.ts
Normal file
1
src/commission-device/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './commission-device.service';
|
19
src/validators/file.validator.ts
Normal file
19
src/validators/file.validator.ts
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user