From fc5bf77f87381b9a198b0ceec1295e646fdff854 Mon Sep 17 00:00:00 2001 From: Mads Apollo <121861974+MadsApollo@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:19:02 +0200 Subject: [PATCH] Feature/iot 118 move device (#263) * Added relations to organization for applications and deviceModels. Tbd, if this is the best approach or if another endpoint should be created. * Added endpoint to change iot devices, and all their connections. * Added exception for moving sigfox devices for the endpoint. * Pr fixes --------- Co-authored-by: Frederik Christ Vestergaard --- .../admin-controller/iot-device.controller.ts | 31 ++ .../dto/update-iot-device-application.dto.ts | 14 + .../device-management/iot-device.module.ts | 4 + .../payload-decoder.module.ts | 4 +- .../device-management/application.service.ts | 6 +- .../device-management/device-model.service.ts | 2 + .../device-management/iot-device.service.ts | 479 ++++++++++-------- 7 files changed, 327 insertions(+), 213 deletions(-) create mode 100644 src/entities/dto/update-iot-device-application.dto.ts diff --git a/src/controllers/admin-controller/iot-device.controller.ts b/src/controllers/admin-controller/iot-device.controller.ts index fa5a59f7..f3144859 100644 --- a/src/controllers/admin-controller/iot-device.controller.ts +++ b/src/controllers/admin-controller/iot-device.controller.ts @@ -58,6 +58,7 @@ import { MQTTInternalBrokerDeviceDTO } from "@dto/mqtt-internal-broker-device.dt import { MQTTExternalBrokerDeviceDTO } from "@dto/mqtt-external-broker-device.dto"; import { ApiAuth } from "@auth/swagger-auth-decorator"; import { DownlinkQueueDto } from "@dto/downlink.dto"; +import { UpdateIoTDeviceApplication } from "@dto/update-iot-device-application.dto"; @ApiTags("IoT Device") @Controller("iot-device") @@ -260,6 +261,36 @@ export class IoTDeviceController { return iotDevice; } + @Put("changeApplication/:id") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Update an existing IoT-Device" }) + @ApiBadRequestResponse() + async changeApplication( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() updateDto: UpdateIoTDeviceApplication + ): Promise { + // Old application + const dbIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, false); + + if (dbIotDevice.type === IoTDeviceType.SigFox) throw new BadRequestException(ErrorCodes.NotValidFormat); + + try { + checkIfUserHasAccessToApplication(req, dbIotDevice.application.id, ApplicationAccessScope.Write); + if (updateDto.applicationId !== dbIotDevice.application.id) { + // New application + checkIfUserHasAccessToApplication(req, updateDto.applicationId, ApplicationAccessScope.Write); + } + } catch (err) { + AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); + throw err; + } + + const iotDevice = await this.iotDeviceService.changeApplication(id, updateDto, req.user.userId); + AuditLog.success(ActionType.UPDATE, IoTDevice.name, req.user.userId, iotDevice.id, iotDevice.name); + return iotDevice; + } + @Post("createMany") @Header("Cache-Control", "none") @ApiOperation({ summary: "Create many IoT-Devices" }) diff --git a/src/entities/dto/update-iot-device-application.dto.ts b/src/entities/dto/update-iot-device-application.dto.ts new file mode 100644 index 00000000..25dedfa0 --- /dev/null +++ b/src/entities/dto/update-iot-device-application.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; + +interface DataTargetToPayloadDecoder { + dataTargetId: number; + payloadDecoderId: number; +} + +export class UpdateIoTDeviceApplication { + public deviceModelId: number; + public organizationId: number; + public applicationId: number; + @ApiProperty({ required: true }) + public dataTargetToPayloadDecoderIds: DataTargetToPayloadDecoder[]; +} diff --git a/src/modules/device-management/iot-device.module.ts b/src/modules/device-management/iot-device.module.ts index 70c1f6db..e88381cd 100644 --- a/src/modules/device-management/iot-device.module.ts +++ b/src/modules/device-management/iot-device.module.ts @@ -20,6 +20,8 @@ import { EncryptionHelperService } from "@services/encryption-helper.service"; import { CsvGeneratorService } from "@services/csv-generator.service"; import { LorawanDeviceDatabaseEnrichJob } from "@services/device-management/lorawan-device-database-enrich-job"; import { OrganizationModule } from "@modules/user-management/organization.module"; +import { DataTargetModule } from "./data-target.module"; +import { PayloadDecoderModule } from "./payload-decoder.module"; @Module({ imports: [ @@ -34,6 +36,8 @@ import { OrganizationModule } from "@modules/user-management/organization.module forwardRef(() => SigfoxDeviceModule), forwardRef(() => IoTLoRaWANDeviceModule), InternalMqttListenerModule, + DataTargetModule, + forwardRef(() => PayloadDecoderModule), ], exports: [MqttService, IoTDeviceService, IoTDeviceDownlinkService], controllers: [IoTDeviceController, IoTDevicePayloadDecoderController], diff --git a/src/modules/device-management/payload-decoder.module.ts b/src/modules/device-management/payload-decoder.module.ts index c14c0a68..586d9644 100644 --- a/src/modules/device-management/payload-decoder.module.ts +++ b/src/modules/device-management/payload-decoder.module.ts @@ -1,4 +1,4 @@ -import { Module } from "@nestjs/common"; +import { forwardRef, Module } from "@nestjs/common"; import { PayloadDecoderController } from "@admin-controller/payload-decoder.controller"; import { DataTargetModule } from "@modules/device-management/data-target.module"; @@ -8,7 +8,7 @@ import { OrganizationModule } from "@modules/user-management/organization.module import { PayloadDecoderService } from "@services/data-management/payload-decoder.service"; @Module({ - imports: [SharedModule, IoTDeviceModule, DataTargetModule, OrganizationModule], + imports: [SharedModule, forwardRef(() => IoTDeviceModule), DataTargetModule, OrganizationModule], exports: [PayloadDecoderService], controllers: [PayloadDecoderController], providers: [PayloadDecoderService], diff --git a/src/services/device-management/application.service.ts b/src/services/device-management/application.service.ts index f248db46..bd3c5175 100644 --- a/src/services/device-management/application.service.ts +++ b/src/services/device-management/application.service.ts @@ -61,7 +61,7 @@ export class ApplicationService { where: orgCondition, take: query.limit, skip: query.offset, - relations: ["iotDevices", "dataTargets", "controlledProperties", "deviceTypes"], + relations: ["iotDevices", "dataTargets", "controlledProperties", "deviceTypes", nameof("belongsTo")], order: sorting, }); @@ -83,7 +83,7 @@ export class ApplicationService { : { id: In(allowedApplications) }, take: query.limit, skip: query.offset, - relations: ["iotDevices"], + relations: ["iotDevices", nameof("belongsTo")], order: { id: query.sort }, }); @@ -102,7 +102,7 @@ export class ApplicationService { where: allowedOrganisations != null ? { belongsTo: In(allowedOrganisations) } : {}, take: +query.limit, skip: +query.offset, - relations: ["iotDevices", "dataTargets", "controlledProperties", "deviceTypes"], + relations: ["iotDevices", "dataTargets", "controlledProperties", "deviceTypes", nameof("belongsTo")], order: sorting, }); diff --git a/src/services/device-management/device-model.service.ts b/src/services/device-management/device-model.service.ts index eae58b10..d9922b42 100644 --- a/src/services/device-management/device-model.service.ts +++ b/src/services/device-management/device-model.service.ts @@ -9,6 +9,7 @@ import * as AJV from "ajv"; import { deviceModelSchema } from "@resources/device-model-schema"; import { UpdateDeviceModelDto } from "@dto/update-device-model.dto"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { nameof } from "@helpers/type-helper"; @Injectable() export class DeviceModelService { @@ -52,6 +53,7 @@ export class DeviceModelService { take: query?.limit ? +query.limit : 100, skip: query?.offset ? +query.offset : 0, order: this.getSorting(query), + relations: [nameof("belongsTo")], }); return { diff --git a/src/services/device-management/iot-device.service.ts b/src/services/device-management/iot-device.service.ts index a64ce476..dd093e7e 100644 --- a/src/services/device-management/iot-device.service.ts +++ b/src/services/device-management/iot-device.service.ts @@ -36,8 +36,8 @@ import { filterValidIotDeviceMaps, isValidIoTDeviceMap, mapAllDevicesByProcessed, - validateMQTTInternalBroker, validateMQTTExternalBroker, + validateMQTTInternalBroker, } from "@helpers/iot-device.helper"; import { BadRequestException, @@ -71,6 +71,12 @@ import * as fs from "fs"; import { caCertPath } from "@resources/resource-paths"; import { DeviceProfileService } from "@services/chirpstack/device-profile.service"; import { ApplicationChirpstackService } from "@services/chirpstack/chirpstack-application.service"; +import { UpdateIoTDeviceApplication } from "@dto/update-iot-device-application.dto"; +import { DataTargetService } from "@services/data-targets/data-target.service"; +import { PayloadDecoderService } from "@services/data-management/payload-decoder.service"; +import { DataTarget } from "@entities/data-target.entity"; +import { PayloadDecoder } from "@entities/payload-decoder.entity"; +import { IoTDevicePayloadDecoderDataTargetConnection } from "@entities/iot-device-payload-decoder-data-target-connection.entity"; type IoTDeviceOrSpecialized = | IoTDevice @@ -80,6 +86,8 @@ type IoTDeviceOrSpecialized = @Injectable() export class IoTDeviceService { + private readonly logger = new Logger(IoTDeviceService.name); + constructor( @InjectRepository(GenericHTTPDevice) private genericHTTPDeviceRepository: Repository, @@ -93,6 +101,8 @@ export class IoTDeviceService { private mqttInternalBrokerDeviceRepository: Repository, @InjectRepository(MQTTExternalBrokerDevice) private mqttExternalBrokerDeviceRepository: Repository, + @InjectRepository(IoTDevicePayloadDecoderDataTargetConnection) + private deviceConnectionsRepository: Repository, private entityManager: EntityManager, private applicationService: ApplicationService, private chirpstackDeviceService: ChirpstackDeviceService, @@ -107,11 +117,11 @@ export class IoTDeviceService { private internalMqttClientListenerService: InternalMqttClientListenerService, private encryptionHelperService: EncryptionHelperService, private csvGeneratorService: CsvGeneratorService, - private deviceProfileService: DeviceProfileService + private deviceProfileService: DeviceProfileService, + private dataTargetService: DataTargetService, + private payloadDecoderService: PayloadDecoderService ) {} - private readonly logger = new Logger(IoTDeviceService.name); - async findOne(id: number): Promise { return await this.iotDeviceRepository.findOneOrFail({ where: { id }, @@ -218,85 +228,6 @@ export class IoTDeviceService { }; } - private async mapToIoTDeviceMinimal( - data: Promise, - req: AuthenticatedRequest - ): Promise { - const applications = req.user.permissions.getAllApplicationsWithAtLeastRead(); - const organizations = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); - return (await data).map(x => { - return { - id: x.id, - name: x.name, - lastActiveTime: x.sentTime != null ? x.sentTime : null, - organizationId: x.organizationId, - applicationId: x.applicationId, - canRead: this.hasAccessToIoTDevice(x, applications, organizations, req), - }; - }); - } - - private hasAccessToIoTDevice( - x: IoTDeviceMinimalRaw, - apps: number[], - orgs: number[], - req: AuthenticatedRequest - ): boolean { - if (req.user.permissions.isGlobalAdmin) { - return true; - } else if (orgs.some(orgId => orgId == x.organizationId)) { - return true; - } else if (apps.some(appId => appId == x.applicationId)) { - return true; - } - return false; - } - - private getQueryForFindAllByPayloadDecoder(payloadDecoderId: number): SelectQueryBuilder { - return this.iotDeviceRepository - .createQueryBuilder("device") - .innerJoin("device.application", "application") - .innerJoin("device.connections", "connection") - .leftJoin("device.latestReceivedMessage", "receivedMessage") - .where('"connection"."payloadDecoderId" = :id', { id: payloadDecoderId }) - .orderBy("device.id") - .select(['"device"."id"', '"device"."name"', '"receivedMessage"."sentTime"']); - } - - /** - * Avoid calling the endpoint /devices/:id at SigFox - * https://support.sigfox.com/docs/api-rate-limiting - */ - private async getDataFromSigFoxAboutDevice(sigfoxGroup: SigFoxGroup, sigfoxDevice: SigFoxDeviceWithBackendDataDto) { - const allDevices = await this.sigfoxApiDeviceService.getAllByGroupIds(sigfoxGroup, [sigfoxDevice.groupId]); - - const thisDevice = allDevices.data.find(x => x.id == sigfoxDevice.deviceId); - return thisDevice; - } - - private buildIoTDeviceWithRelationsQuery(): SelectQueryBuilder { - return this.iotDeviceRepository - .createQueryBuilder("iot_device") - .loadAllRelationIds({ relations: ["createdBy", "updatedBy"] }) - .innerJoinAndSelect("iot_device.application", "application", 'application.id = iot_device."applicationId"') - .leftJoinAndSelect("iot_device.receivedMessagesMetadata", "metadata", 'metadata."deviceId" = iot_device.id') - .leftJoinAndSelect( - "iot_device.latestReceivedMessage", - "receivedMessage", - '"receivedMessage"."deviceId" = iot_device.id' - ) - .leftJoinAndSelect("iot_device.deviceModel", "device_model", 'device_model.id = iot_device."deviceModelId"') - .orderBy('metadata."sentTime"', "DESC"); - } - - private async queryDatabaseForIoTDevice(id: number) { - return await this.buildIoTDeviceWithRelationsQuery().where("iot_device.id = :id", { id: id }).getOne(); - } - - private queryDatabaseForIoTDevices(ids: number[]) { - return this.buildIoTDeviceWithRelationsQuery().where("iot_device.id IN (:...ids)", { ids }).getMany(); - } - async findGenericHttpDeviceByApiKey(key: string): Promise { return await this.genericHTTPDeviceRepository.findOneBy({ apiKey: key }); } @@ -431,6 +362,59 @@ export class IoTDeviceService { return res; } + // eslint-disable-next-line max-lines-per-function + async changeApplication(id: number, updateDto: UpdateIoTDeviceApplication, userId: number): Promise { + const existingIoTDevice = await this.iotDeviceRepository.findOneOrFail({ + where: { id }, + relations: { application: { belongsTo: true }, deviceModel: true, connections: true }, + }); + + try { + const application = await this.applicationService.findOne(updateDto.applicationId); + const deviceModel = await this.deviceModelService.getById(updateDto.deviceModelId); + + const dataTargets = await this.dataTargetService.findDataTargetsByApplicationId(updateDto.applicationId); + const payloadDecoders = await this.payloadDecoderService.findAndCountWithPagination({}, null); + + const dataTargetMatches: DataTarget[] = updateDto.dataTargetToPayloadDecoderIds.map(dtAndpl => + dataTargets.find(dt => dt.id === dtAndpl.dataTargetId) + ); + + const payloadMatches: (PayloadDecoder | undefined)[] = updateDto.dataTargetToPayloadDecoderIds.map(dtAndpl => + payloadDecoders.data.find(pl => pl.id === dtAndpl.payloadDecoderId) + ); + + if (!application || dataTargetMatches.some(dt => dt === undefined)) { + throw new BadRequestException(ErrorCodes.NotValidFormat); + } + + const connectionArray: IoTDevicePayloadDecoderDataTargetConnection[] = []; + for (let i = 0; i < dataTargetMatches.length; ++i) { + const connection = new IoTDevicePayloadDecoderDataTargetConnection(); + connection.dataTarget = dataTargetMatches[i]; + connection.payloadDecoder = payloadMatches[i]; + connection.iotDevices = [existingIoTDevice]; + connectionArray.push(connection); + } + + existingIoTDevice.application = application; + existingIoTDevice.deviceModel = deviceModel; + existingIoTDevice.updatedBy = userId; + + const res = await this.iotDeviceRepository.save(existingIoTDevice); + + for (const connection of existingIoTDevice.connections) { + await this.deviceConnectionsRepository.delete(connection.id); + } + + const savedConnections = await this.deviceConnectionsRepository.save(connectionArray); + + return res; + } catch (e) { + throw new BadRequestException(e); + } + } + async updateMany(updateDto: UpdateIoTDeviceBatchDto, userId: number): Promise { // Fetch existing devices from db and map them const existingDevices = await this.iotDeviceRepository.findBy({ @@ -495,27 +479,6 @@ export class IoTDeviceService { await this.mqttExternalBrokerDeviceRepository.save(device); } - private async deleteMulticastsFromDevice(manager: EntityManager, device: IoTDevice) { - const multicastRepository = manager.getRepository(Multicast); - // Take only the multicasts with references to the device. - // Filtering by device id won't return multicasts with all devices - const _multicasts = await multicastRepository - .createQueryBuilder("multicast") - .innerJoinAndSelect("multicast.iotDevices", "iot_device") - .getMany(); - - // Filter multicasts without the device out to avoid updating them - const multicastsWithDevice = _multicasts.filter(multicast => - multicast.iotDevices.some(iotDevice => iotDevice.id === device.id) - ); - // Remove device from the mappings. It's important that each existing mapping has been fetched - // as the new mappings will replace the existing ones. - multicastsWithDevice.forEach( - multicast => (multicast.iotDevices = multicast.iotDevices?.filter(iotDevice => iotDevice.id !== device.id)) - ); - await multicastRepository.save(multicastsWithDevice); - } - async deleteMany(ids: number[]): Promise { return this.iotDeviceRepository.delete(ids); } @@ -552,6 +515,209 @@ export class IoTDeviceService { } } + async mapDeviceModels(iotDevicesDtoMap: CreateIoTDeviceMapDto[]): Promise { + // Pre-fetch device models + const deviceModelIds = iotDevicesDtoMap.reduce((ids: number[], dto) => { + if (dto.iotDeviceDto.deviceModelId) { + ids.push(dto.iotDeviceDto.deviceModelId); + } + + return ids; + }, []); + + const deviceModels = await this.deviceModelService.getByIdsWithRelations(deviceModelIds); + + const applicationIds = iotDevicesDtoMap.reduce((ids: number[], dto) => { + if (dto.iotDeviceDto.applicationId) { + ids.push(dto.iotDeviceDto.applicationId); + } + return ids; + }, []); + + const applications = await this.applicationService.findManyWithOrganisation(applicationIds); + + // Ensure that each device model is assignable + this.setDeviceModel(iotDevicesDtoMap, applications, deviceModels); + } + + resetHttpDeviceApiKey(httpDevice: GenericHTTPDevice): Promise { + httpDevice.apiKey = uuidv4(); + return this.iotDeviceRepository.save(httpDevice); + } + + public async mapChildDtoToIoTDevice(iotDevicesDtoMap: CreateIoTDeviceMapDto[], isUpdate: boolean): Promise { + // Pre-fetch lorawan settings, if any + const loraDeviceEuis = await this.getLorawanDeviceEuis(iotDevicesDtoMap); + const loraOrganizationId = await this.chirpstackDeviceService.getDefaultOrganizationId(); + const loraApplications = await this.chirpstackDeviceService.getAllApplicationsWithPagination(loraOrganizationId); + + // Populate each IoT device with the specific device type metadata + for (const map of iotDevicesDtoMap) { + try { + if (map.iotDevice.constructor.name === LoRaWANDevice.name) { + const cast = map.iotDevice as LoRaWANDevice; + map.iotDevice = await this.mapAndCreateLoRaWANDevice( + map.iotDeviceDto, + cast, + isUpdate, + loraDeviceEuis, + loraApplications + ); + } else if (map.iotDevice.constructor.name === SigFoxDevice.name) { + const cast = map.iotDevice as SigFoxDevice; + map.iotDevice = await this.mapSigFoxDevice(map.iotDeviceDto, cast); + } else if (map.iotDevice.constructor.name === MQTTInternalBrokerDevice.name) { + const cast = map.iotDevice as MQTTInternalBrokerDevice; + map.iotDevice = await this.mapMQTTInternalBrokerDevice(map.iotDeviceDto, cast); + } else if (map.iotDevice.constructor.name === MQTTExternalBrokerDevice.name) { + const cast = map.iotDevice as MQTTExternalBrokerDevice; + map.iotDevice = await this.mapMQTTExternalBrokerDevice(map.iotDeviceDto, cast, isUpdate); + } + } catch (error) { + map.error = { + message: (error as Error)?.message ?? ErrorCodes.FailedToCreateOrUpdateIotDevice, + }; + } + } + } + + async getAllSigfoxDevicesByGroup(group: SigFoxGroup, removeExisting: boolean): Promise { + const devices = await this.sigfoxApiDeviceService.getAllByGroupIds(group, [group.sigfoxGroupId]); + + if (removeExisting) { + const sigfoxDeviceIdsInUse = await this.sigfoxRepository.find({ + select: ["deviceId"], + }); + const filtered = devices.data.filter(x => { + return !sigfoxDeviceIdsInUse.some(y => y.deviceId == x.id); + }); + return { + data: filtered, + }; + } + + return devices; + } + + async getDevicesMetadataCsv(applicationId: number) { + const iotDevices = await this.iotDeviceRepository + .createQueryBuilder("device") + .leftJoinAndSelect("device.deviceModel", "deviceModel") + .where("device.applicationId = :applicationId", { applicationId }) + .getMany(); + + for (const d of iotDevices) { + if (d.type !== IoTDeviceType.LoRaWAN) { + continue; + } + await this.chirpstackDeviceService.enrichLoRaWANDevice(d); + } + + const csvString = this.csvGeneratorService.generateDeviceMetadataCsv(iotDevices); + return Buffer.from(csvString); + } + + private async mapToIoTDeviceMinimal( + data: Promise, + req: AuthenticatedRequest + ): Promise { + const applications = req.user.permissions.getAllApplicationsWithAtLeastRead(); + const organizations = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); + return (await data).map(x => { + return { + id: x.id, + name: x.name, + lastActiveTime: x.sentTime != null ? x.sentTime : null, + organizationId: x.organizationId, + applicationId: x.applicationId, + canRead: this.hasAccessToIoTDevice(x, applications, organizations, req), + }; + }); + } + + private hasAccessToIoTDevice( + x: IoTDeviceMinimalRaw, + apps: number[], + orgs: number[], + req: AuthenticatedRequest + ): boolean { + if (req.user.permissions.isGlobalAdmin) { + return true; + } else if (orgs.some(orgId => orgId == x.organizationId)) { + return true; + } else if (apps.some(appId => appId == x.applicationId)) { + return true; + } + return false; + } + + private getQueryForFindAllByPayloadDecoder(payloadDecoderId: number): SelectQueryBuilder { + return this.iotDeviceRepository + .createQueryBuilder("device") + .innerJoin("device.application", "application") + .innerJoin("device.connections", "connection") + .leftJoin("device.latestReceivedMessage", "receivedMessage") + .where('"connection"."payloadDecoderId" = :id', { id: payloadDecoderId }) + .orderBy("device.id") + .select(['"device"."id"', '"device"."name"', '"receivedMessage"."sentTime"']); + } + + /** + * Avoid calling the endpoint /devices/:id at SigFox + * https://support.sigfox.com/docs/api-rate-limiting + */ + private async getDataFromSigFoxAboutDevice(sigfoxGroup: SigFoxGroup, sigfoxDevice: SigFoxDeviceWithBackendDataDto) { + const allDevices = await this.sigfoxApiDeviceService.getAllByGroupIds(sigfoxGroup, [sigfoxDevice.groupId]); + + const thisDevice = allDevices.data.find(x => x.id == sigfoxDevice.deviceId); + return thisDevice; + } + + private buildIoTDeviceWithRelationsQuery(): SelectQueryBuilder { + return this.iotDeviceRepository + .createQueryBuilder("iot_device") + .loadAllRelationIds({ relations: ["createdBy", "updatedBy"] }) + .innerJoinAndSelect("iot_device.application", "application", 'application.id = iot_device."applicationId"') + .innerJoinAndSelect("application.belongsTo", "belongsTo", 'belongsTo.id = application."belongsToId"') + .leftJoinAndSelect("iot_device.receivedMessagesMetadata", "metadata", 'metadata."deviceId" = iot_device.id') + .leftJoinAndSelect( + "iot_device.latestReceivedMessage", + "receivedMessage", + '"receivedMessage"."deviceId" = iot_device.id' + ) + .leftJoinAndSelect("iot_device.deviceModel", "device_model", 'device_model.id = iot_device."deviceModelId"') + .orderBy('metadata."sentTime"', "DESC"); + } + + private async queryDatabaseForIoTDevice(id: number) { + return await this.buildIoTDeviceWithRelationsQuery().where("iot_device.id = :id", { id: id }).getOne(); + } + + private queryDatabaseForIoTDevices(ids: number[]) { + return this.buildIoTDeviceWithRelationsQuery().where("iot_device.id IN (:...ids)", { ids }).getMany(); + } + + private async deleteMulticastsFromDevice(manager: EntityManager, device: IoTDevice) { + const multicastRepository = manager.getRepository(Multicast); + // Take only the multicasts with references to the device. + // Filtering by device id won't return multicasts with all devices + const _multicasts = await multicastRepository + .createQueryBuilder("multicast") + .innerJoinAndSelect("multicast.iotDevices", "iot_device") + .getMany(); + + // Filter multicasts without the device out to avoid updating them + const multicastsWithDevice = _multicasts.filter(multicast => + multicast.iotDevices.some(iotDevice => iotDevice.id === device.id) + ); + // Remove device from the mappings. It's important that each existing mapping has been fetched + // as the new mappings will replace the existing ones. + multicastsWithDevice.forEach( + multicast => (multicast.iotDevices = multicast.iotDevices?.filter(iotDevice => iotDevice.id !== device.id)) + ); + await multicastRepository.save(multicastsWithDevice); + } + private averageStatsForSameDay(stats: DeviceStatsResponseDto[]) { const statsSummed = stats.reduce( (res: Record, item) => { @@ -636,31 +802,6 @@ export class IoTDeviceService { await this.mapChildDtoToIoTDevice(filterValidIotDeviceMaps(iotDeviceMaps), isUpdate); } - async mapDeviceModels(iotDevicesDtoMap: CreateIoTDeviceMapDto[]): Promise { - // Pre-fetch device models - const deviceModelIds = iotDevicesDtoMap.reduce((ids: number[], dto) => { - if (dto.iotDeviceDto.deviceModelId) { - ids.push(dto.iotDeviceDto.deviceModelId); - } - - return ids; - }, []); - - const deviceModels = await this.deviceModelService.getByIdsWithRelations(deviceModelIds); - - const applicationIds = iotDevicesDtoMap.reduce((ids: number[], dto) => { - if (dto.iotDeviceDto.applicationId) { - ids.push(dto.iotDeviceDto.applicationId); - } - return ids; - }, []); - - const applications = await this.applicationService.findManyWithOrganisation(applicationIds); - - // Ensure that each device model is assignable - this.setDeviceModel(iotDevicesDtoMap, applications, deviceModels); - } - private setDeviceModel( iotDevicesDtoMap: CreateIoTDeviceMapDto[], applications: Application[], @@ -693,58 +834,16 @@ export class IoTDeviceService { } map.iotDevice.deviceModel = deviceModelMatch; - } - else { + } else { map.iotDevice.deviceModel = null; } } } - resetHttpDeviceApiKey(httpDevice: GenericHTTPDevice): Promise { - httpDevice.apiKey = uuidv4(); - return this.iotDeviceRepository.save(httpDevice); - } - private async getApplicationsByIds(applicationIds: number[]) { return applicationIds.length ? await this.applicationService.findManyByIds(applicationIds) : []; } - public async mapChildDtoToIoTDevice(iotDevicesDtoMap: CreateIoTDeviceMapDto[], isUpdate: boolean): Promise { - // Pre-fetch lorawan settings, if any - const loraDeviceEuis = await this.getLorawanDeviceEuis(iotDevicesDtoMap); - const loraOrganizationId = await this.chirpstackDeviceService.getDefaultOrganizationId(); - const loraApplications = await this.chirpstackDeviceService.getAllApplicationsWithPagination(loraOrganizationId); - - // Populate each IoT device with the specific device type metadata - for (const map of iotDevicesDtoMap) { - try { - if (map.iotDevice.constructor.name === LoRaWANDevice.name) { - const cast = map.iotDevice as LoRaWANDevice; - map.iotDevice = await this.mapAndCreateLoRaWANDevice( - map.iotDeviceDto, - cast, - isUpdate, - loraDeviceEuis, - loraApplications - ); - } else if (map.iotDevice.constructor.name === SigFoxDevice.name) { - const cast = map.iotDevice as SigFoxDevice; - map.iotDevice = await this.mapSigFoxDevice(map.iotDeviceDto, cast); - } else if (map.iotDevice.constructor.name === MQTTInternalBrokerDevice.name) { - const cast = map.iotDevice as MQTTInternalBrokerDevice; - map.iotDevice = await this.mapMQTTInternalBrokerDevice(map.iotDeviceDto, cast); - } else if (map.iotDevice.constructor.name === MQTTExternalBrokerDevice.name) { - const cast = map.iotDevice as MQTTExternalBrokerDevice; - map.iotDevice = await this.mapMQTTExternalBrokerDevice(map.iotDeviceDto, cast, isUpdate); - } - } catch (error) { - map.error = { - message: (error as Error)?.message ?? ErrorCodes.FailedToCreateOrUpdateIotDevice, - }; - } - } - } - private async getLorawanDeviceEuis(iotDevicesDtoMap: CreateIoTDeviceMapDto[]): Promise { const iotLorawanDevices = iotDevicesDtoMap.reduce((res: string[], { iotDevice, iotDeviceDto }) => { if (iotDevice.constructor.name === LoRaWANDevice.name && iotDeviceDto.lorawanSettings) { @@ -796,42 +895,6 @@ export class IoTDeviceService { } } - async getAllSigfoxDevicesByGroup(group: SigFoxGroup, removeExisting: boolean): Promise { - const devices = await this.sigfoxApiDeviceService.getAllByGroupIds(group, [group.sigfoxGroupId]); - - if (removeExisting) { - const sigfoxDeviceIdsInUse = await this.sigfoxRepository.find({ - select: ["deviceId"], - }); - const filtered = devices.data.filter(x => { - return !sigfoxDeviceIdsInUse.some(y => y.deviceId == x.id); - }); - return { - data: filtered, - }; - } - - return devices; - } - - async getDevicesMetadataCsv(applicationId: number) { - const iotDevices = await this.iotDeviceRepository - .createQueryBuilder("device") - .leftJoinAndSelect("device.deviceModel", "deviceModel") - .where("device.applicationId = :applicationId", { applicationId }) - .getMany(); - - for (const d of iotDevices) { - if (d.type !== IoTDeviceType.LoRaWAN) { - continue; - } - await this.chirpstackDeviceService.enrichLoRaWANDevice(d); - } - - const csvString = this.csvGeneratorService.generateDeviceMetadataCsv(iotDevices); - return Buffer.from(csvString); - } - private async doEditInSigFoxBackend( currentSigFoxSettings: SigFoxApiDeviceContent, dto: CreateIoTDeviceDto,