diff --git a/package.json b/package.json index f2ac7144..1cbcb482 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@nestjs/swagger": "^6.1.2", "@nestjs/typeorm": "^9.0.1", "@types/bcryptjs": "^2.4.2", - "@types/geojson": "^7946.0.7", + "@types/geojson": "^7946.0.13", "@types/kafkajs": "^1.9.0", "@types/passport-saml": "^1.1.3", "@types/pem": "^1.9.6", @@ -92,7 +92,7 @@ "@types/cron": "^1.7.2", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.9", - "@types/geojson": "^7946.0.7", + "@types/geojson": "^7946.0.13", "@types/kafkajs": "^1.9.0", "@types/lodash": "^4.14.165", "@types/node": "^14.14.14", diff --git a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts index fa964ec9..b29f5066 100644 --- a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts +++ b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts @@ -12,13 +12,7 @@ import { Req, UseGuards, } from "@nestjs/common"; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiOperation, - ApiProduces, - ApiTags, -} from "@nestjs/swagger"; +import { ApiBadRequestResponse, ApiBearerAuth, ApiOperation, ApiProduces, ApiTags } from "@nestjs/swagger"; import { Read, GatewayAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; @@ -48,21 +42,15 @@ export class ChirpstackGatewayController { @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) @ApiBadRequestResponse() @GatewayAdmin() - async create( - @Req() req: AuthenticatedRequest, - @Body() dto: CreateGatewayDto - ): Promise { + async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateGatewayDto): Promise { checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.GatewayWrite); try { - const gateway = await this.chirpstackGatewayService.createNewGateway( - dto, - req.user.userId - ); + const gateway = await this.chirpstackGatewayService.createNewGateway(dto, req.user.userId); AuditLog.success( ActionType.CREATE, "ChirpstackGateway", req.user.userId, - dto.gateway.id, + dto.gateway.gatewayId, dto.gateway.name ); return gateway; @@ -71,7 +59,7 @@ export class ChirpstackGatewayController { ActionType.CREATE, "ChirpstackGateway", req.user.userId, - dto.gateway.id, + dto.gateway.gatewayId, dto.gateway.name ); if (err?.response?.data?.message == "object already exists") { @@ -94,9 +82,7 @@ export class ChirpstackGatewayController { @ApiProduces("application/json") @ApiOperation({ summary: "List all Chirpstack gateways" }) @Read() - async getOne( - @Param("gatewayId") gatewayId: string - ): Promise { + async getOne(@Param("gatewayId") gatewayId: string): Promise { if (gatewayId?.length != 16) { throw new BadRequestException(ErrorCodes.WrongLength); } @@ -119,30 +105,14 @@ export class ChirpstackGatewayController { @Body() dto: UpdateGatewayDto ): Promise { try { - if (dto.gateway.id) { + if (dto.gateway.gatewayId) { throw new BadRequestException(ErrorCodes.GatewayIdNotAllowedInUpdate); } - const gateway = await this.chirpstackGatewayService.modifyGateway( - gatewayId, - dto, - req - ); - AuditLog.success( - ActionType.UPDATE, - "ChirpstackGateway", - req.user.userId, - gatewayId, - dto.gateway.name - ); + const gateway = await this.chirpstackGatewayService.modifyGateway(gatewayId, dto, req); + AuditLog.success(ActionType.UPDATE, "ChirpstackGateway", req.user.userId, gatewayId, dto.gateway.name); return gateway; } catch (err) { - AuditLog.fail( - ActionType.UPDATE, - "ChirpstackGateway", - req.user.userId, - gatewayId, - dto.gateway.name - ); + AuditLog.fail(ActionType.UPDATE, "ChirpstackGateway", req.user.userId, gatewayId, dto.gateway.name); throw err; } } @@ -155,30 +125,18 @@ export class ChirpstackGatewayController { ): Promise { try { const gw = await this.chirpstackGatewayService.getOne(gatewayId); - if (gw.gateway.internalOrganizationId != null) { + if (gw.gateway.organizationId != null) { checkIfUserHasAccessToOrganization( req, - +gw.gateway.internalOrganizationId, + +gw.gateway.organizationId, OrganizationAccessScope.GatewayWrite ); } - const deleteResult = await this.chirpstackGatewayService.deleteGateway( - gatewayId - ); - AuditLog.success( - ActionType.DELETE, - "ChirpstackGateway", - req.user.userId, - gatewayId - ); + const deleteResult = await this.chirpstackGatewayService.deleteGateway(gatewayId); + AuditLog.success(ActionType.DELETE, "ChirpstackGateway", req.user.userId, gatewayId); return deleteResult; } catch (err) { - AuditLog.fail( - ActionType.DELETE, - "ChirpstackGateway", - req.user.userId, - gatewayId - ); + AuditLog.fail(ActionType.DELETE, "ChirpstackGateway", req.user.userId, gatewayId); throw err; } } diff --git a/src/entities/dto/chirpstack/detailed-gateway-response.dto.ts b/src/entities/dto/chirpstack/detailed-gateway-response.dto.ts deleted file mode 100644 index b10c7ac9..00000000 --- a/src/entities/dto/chirpstack/detailed-gateway-response.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; - -export class DetailedGatewayResponseDto extends GatewayResponseDto { - discoveryEnabled: boolean; - gatewayProfileID: string; - boards: any[]; - tags: { [id: string]: string | number }; - tagsString: string; - metadata: JSON; -} diff --git a/src/entities/dto/chirpstack/gateway-contents.dto.ts b/src/entities/dto/chirpstack/gateway-contents.dto.ts index 1876ff0d..f1876084 100644 --- a/src/entities/dto/chirpstack/gateway-contents.dto.ts +++ b/src/entities/dto/chirpstack/gateway-contents.dto.ts @@ -11,21 +11,9 @@ import { MinLength, ValidateNested, } from "class-validator"; - -import { ChirpstackBoardsDto } from "@dto/chirpstack/chirpstack-boards.dto"; import { CommonLocationDto } from "@dto/chirpstack/common-location.dto"; export class GatewayContentsDto { - @ApiProperty({ - required: false, - default: [], - type: [ChirpstackBoardsDto], - }) - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => ChirpstackBoardsDto) - boards?: ChirpstackBoardsDto[]; - @ApiProperty({ required: true }) @IsOptional() @IsString() @@ -36,14 +24,11 @@ export class GatewayContentsDto { @IsOptional() discoveryEnabled: boolean; - @ApiHideProperty() - gatewayProfileID?: string; - @ApiProperty({ required: true }) @IsString() @IsHexadecimal() @Length(16, 16) - id: string; + gatewayId: string; @ApiProperty({ required: false }) @ValidateNested({ each: true }) @@ -74,4 +59,10 @@ export class GatewayContentsDto { @ApiHideProperty() tags?: { [id: string]: string | number }; + + @ApiHideProperty() + gatewayProfileID?: string; + + @ApiHideProperty() + id: string; } diff --git a/src/entities/dto/chirpstack/gateway-response.dto.ts b/src/entities/dto/chirpstack/gateway-response.dto.ts index 1da87f4e..b91d3f58 100644 --- a/src/entities/dto/chirpstack/gateway-response.dto.ts +++ b/src/entities/dto/chirpstack/gateway-response.dto.ts @@ -1,22 +1,19 @@ import { CommonLocationDto } from "@dto/chirpstack/common-location.dto"; export class GatewayResponseDto { - id: string; + id: number; + gatewayId: string; name: string; - description: string; - organizationID: string; - networkServerID: string; + description?: string; + rxPacketsReceived: number; + txPacketsEmitted: number; + organizationId: number; + organizationName: string; location: CommonLocationDto; tags: { [id: string]: string | number }; - tagsString: string; - - networkServerName?: string; - createdAt?: string; - updatedAt?: string; - firstSeenAt?: string; - lastSeenAt?: string; - - internalOrganizationId: number; + createdAt?: Date; + updatedAt?: Date; + lastSeenAt?: Date; updatedBy?: number; createdBy?: number; } diff --git a/src/entities/dto/chirpstack/single-gateway-response.dto.ts b/src/entities/dto/chirpstack/single-gateway-response.dto.ts index ba28c684..d2cbae02 100644 --- a/src/entities/dto/chirpstack/single-gateway-response.dto.ts +++ b/src/entities/dto/chirpstack/single-gateway-response.dto.ts @@ -1,13 +1,7 @@ -import { DetailedGatewayResponseDto } from "@dto/chirpstack/detailed-gateway-response.dto"; import { GatewayStatsElementDto } from "@dto/chirpstack/gateway-stats.response.dto"; +import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; export class SingleGatewayResponseDto { - gateway: DetailedGatewayResponseDto; - - createdAt?: string; - updatedAt?: string; - firstSeenAt?: string; - lastSeenAt?: string; - + gateway: GatewayResponseDto; stats: GatewayStatsElementDto[]; } diff --git a/src/entities/dto/chirpstack/update-gateway.dto.ts b/src/entities/dto/chirpstack/update-gateway.dto.ts index bb6dae94..d8e4f1f0 100644 --- a/src/entities/dto/chirpstack/update-gateway.dto.ts +++ b/src/entities/dto/chirpstack/update-gateway.dto.ts @@ -3,9 +3,9 @@ import { ValidateNested } from "class-validator"; import { GatewayContentsDto } from "./gateway-contents.dto"; import { Type } from "class-transformer"; -export class UpdateGatewayContentsDto extends OmitType(GatewayContentsDto, ["id"]) { +export class UpdateGatewayContentsDto extends OmitType(GatewayContentsDto, ["gatewayId"]) { @ApiHideProperty() - id: string; + gatewayId: string; } export class UpdateGatewayDto { diff --git a/src/entities/gateway.entity.ts b/src/entities/gateway.entity.ts new file mode 100644 index 00000000..a5d5658b --- /dev/null +++ b/src/entities/gateway.entity.ts @@ -0,0 +1,42 @@ +import { DbBaseEntity } from "@entities/base.entity"; +import { Column, Entity, ManyToOne } from "typeorm"; +import { Organization } from "@entities/organization.entity"; +import { Point } from "geojson"; + +@Entity("gateway") +export class Gateway extends DbBaseEntity { + @Column() + name: string; + + @Column({ nullable: true }) + description?: string; + + @Column() + gatewayId: string; + + @ManyToOne(_ => Organization, organization => organization.gateways, { onDelete: "CASCADE" }) + organization: Organization; + + @Column() + rxPacketsReceived: number; + + @Column() + txPacketsEmitted: number; + + @Column() + tags: string; + + @Column({ + type: "geometry", + nullable: true, + spatialFeatureType: "Point", + srid: 4326, + }) + location?: Point; + + @Column({ type: "decimal", nullable: true }) + altitude?: number; + + @Column({ nullable: true }) + lastSeenAt?: Date; +} diff --git a/src/entities/organization.entity.ts b/src/entities/organization.entity.ts index 5fcb741c..54f4f85c 100644 --- a/src/entities/organization.entity.ts +++ b/src/entities/organization.entity.ts @@ -8,6 +8,7 @@ import { Permission } from "@entities/permissions/permission.entity"; import { SigFoxGroup } from "./sigfox-group.entity"; import { DeviceModel } from "./device-model.entity"; import { User } from "./user.entity"; +import { Gateway } from "@entities/gateway.entity"; @Entity("organization") @Unique(["name"]) @@ -60,4 +61,7 @@ export class Organization extends DbBaseEntity { nullable: true, }) awaitingUsers?: User[]; + + @OneToMany(_ => Gateway, gateway => gateway.organization, { nullable: true }) + gateways?: Gateway[]; } diff --git a/src/migration/1701090974275-created-database-entity-for-gateways.ts b/src/migration/1701090974275-created-database-entity-for-gateways.ts new file mode 100644 index 00000000..7fa4c3be --- /dev/null +++ b/src/migration/1701090974275-created-database-entity-for-gateways.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreatedDatabaseEntityForGateways1701090974275 implements MigrationInterface { + name = 'CreatedDatabaseEntityForGateways1701090974275' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "gateway" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "description" character varying, "gatewayId" character varying NOT NULL, "rxPacketsReceived" integer NOT NULL, "txPacketsEmitted" integer NOT NULL, "tags" character varying NOT NULL, "location" geometry(Point,4326), "lastSeenAt" TIMESTAMP, "createdById" integer, "updatedById" integer, "organizationId" integer, CONSTRAINT "PK_22c5b7ecdd6313de143815f9991" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "gateway" ADD CONSTRAINT "FK_a869dbb323b91ec9fa272654ce1" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gateway" ADD CONSTRAINT "FK_9916ae412fb9b02b981d094697b" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gateway" ADD CONSTRAINT "FK_66c3f3b7ccab40c8735e2cf07e7" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "gateway" DROP CONSTRAINT "FK_66c3f3b7ccab40c8735e2cf07e7"`); + await queryRunner.query(`ALTER TABLE "gateway" DROP CONSTRAINT "FK_9916ae412fb9b02b981d094697b"`); + await queryRunner.query(`ALTER TABLE "gateway" DROP CONSTRAINT "FK_a869dbb323b91ec9fa272654ce1"`); + await queryRunner.query(`DROP TABLE "gateway"`); + } + +} diff --git a/src/migration/1701158191841-added-altitude-to-gateway.ts b/src/migration/1701158191841-added-altitude-to-gateway.ts new file mode 100644 index 00000000..a41f107d --- /dev/null +++ b/src/migration/1701158191841-added-altitude-to-gateway.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddedAltitudeToGateway1701158191841 implements MigrationInterface { + name = 'AddedAltitudeToGateway1701158191841' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "gateway" ADD "altitude" numeric`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "gateway" DROP COLUMN "altitude"`); + } + +} diff --git a/src/modules/device-integrations/chirpstack-administration.module.ts b/src/modules/device-integrations/chirpstack-administration.module.ts index f0ff2a88..15dee41e 100644 --- a/src/modules/device-integrations/chirpstack-administration.module.ts +++ b/src/modules/device-integrations/chirpstack-administration.module.ts @@ -12,6 +12,8 @@ import { GenericChirpstackConfigurationService } from "@services/chirpstack/gene import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; import { ServiceProfileService } from "@services/chirpstack/service-profile.service"; import { NetworkServerController } from "@admin-controller/chirpstack/network-server.controller"; +import { SharedModule } from "@modules/shared.module"; +import { OrganizationModule } from "@modules/user-management/organization.module"; @Module({ controllers: [ @@ -20,7 +22,7 @@ import { NetworkServerController } from "@admin-controller/chirpstack/network-se DeviceProfileController, NetworkServerController, ], - imports: [HttpModule, ConfigModule.forRoot({ load: [configuration] })], + imports: [SharedModule, HttpModule, OrganizationModule, ConfigModule.forRoot({ load: [configuration] })], providers: [ GenericChirpstackConfigurationService, ChirpstackSetupNetworkServerService, diff --git a/src/modules/device-integrations/lorawan-gateway.module.ts b/src/modules/device-integrations/lorawan-gateway.module.ts index e5ce4972..f97fa332 100644 --- a/src/modules/device-integrations/lorawan-gateway.module.ts +++ b/src/modules/device-integrations/lorawan-gateway.module.ts @@ -6,10 +6,11 @@ import { GatewayStatusHistoryService } from "@services/chirpstack/gateway-status import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; import { GatewayBootstrapperService } from "@services/chirpstack/gateway-boostrapper.service"; import { HttpModule } from "@nestjs/axios"; +import { OrganizationModule } from "@modules/user-management/organization.module"; @Module({ controllers: [LoRaWANGatewayController], - imports: [SharedModule, HttpModule], + imports: [SharedModule, HttpModule, OrganizationModule], providers: [ ChirpstackGatewayService, ChirpstackSetupNetworkServerService, diff --git a/src/modules/device-management/iot-device.module.ts b/src/modules/device-management/iot-device.module.ts index 3ced280b..f208b1f5 100644 --- a/src/modules/device-management/iot-device.module.ts +++ b/src/modules/device-management/iot-device.module.ts @@ -19,6 +19,7 @@ import { InternalMqttListenerModule } from "@modules/device-integrations/interna 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"; @Module({ imports: [ @@ -28,6 +29,7 @@ import { LorawanDeviceDatabaseEnrichJob } from "@services/device-management/lora SigFoxGroupModule, SigfoxDeviceTypeModule, DeviceModelModule, + OrganizationModule, ReceiveDataModule, forwardRef(() => SigfoxDeviceModule), forwardRef(() => IoTLoRaWANDeviceModule), diff --git a/src/modules/shared.module.ts b/src/modules/shared.module.ts index b38429b8..7724881f 100644 --- a/src/modules/shared.module.ts +++ b/src/modules/shared.module.ts @@ -32,6 +32,7 @@ import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; import { OpenDataDkDataTarget } from "@entities/open-data-dk-push-data-target.entity"; import { MQTTInternalBrokerDevice } from "@entities/mqtt-internal-broker-device.entity"; import { MQTTExternalBrokerDevice } from "@entities/mqtt-external-broker-device.entity"; +import { Gateway } from "@entities/gateway.entity"; @Module({ imports: [ @@ -67,6 +68,7 @@ import { MQTTExternalBrokerDevice } from "@entities/mqtt-external-broker-device. GatewayStatusHistory, MQTTInternalBrokerDevice, MQTTExternalBrokerDevice, + Gateway, ]), ], providers: [AuditLog], diff --git a/src/services/chirpstack/chirpstack-device.service.ts b/src/services/chirpstack/chirpstack-device.service.ts index 44424bf4..03f6a435 100644 --- a/src/services/chirpstack/chirpstack-device.service.ts +++ b/src/services/chirpstack/chirpstack-device.service.ts @@ -1,8 +1,10 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { AxiosResponse } from "axios"; -import { ChirpstackDeviceActivationContentsDto } from "@dto/chirpstack/chirpstack-device-activation-response.dto"; -import { ChirpstackDeviceActivationDto } from "@dto/chirpstack/chirpstack-device-activation-response.dto"; +import { + ChirpstackDeviceActivationContentsDto, + ChirpstackDeviceActivationDto, +} from "@dto/chirpstack/chirpstack-device-activation-response.dto"; import { ChirpstackDeviceContentsDto } from "@dto/chirpstack/chirpstack-device-contents.dto"; import { ChirpstackDeviceKeysContentDto, diff --git a/src/services/chirpstack/chirpstack-gateway.service.ts b/src/services/chirpstack/chirpstack-gateway.service.ts index 6d4c46d4..4c0d650c 100644 --- a/src/services/chirpstack/chirpstack-gateway.service.ts +++ b/src/services/chirpstack/chirpstack-gateway.service.ts @@ -6,32 +6,35 @@ import { NotFoundException, } from "@nestjs/common"; import { AxiosResponse } from "axios"; -import * as BluebirdPromise from "bluebird"; import { ChirpstackErrorResponseDto } from "@dto/chirpstack/chirpstack-error-response.dto"; import { ChirpstackResponseStatus } from "@dto/chirpstack/chirpstack-response.dto"; import { CreateGatewayDto } from "@dto/chirpstack/create-gateway.dto"; import { GatewayStatsResponseDto } from "@dto/chirpstack/gateway-stats.response.dto"; import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways.dto"; import { SingleGatewayResponseDto } from "@dto/chirpstack/single-gateway-response.dto"; -import { - UpdateGatewayContentsDto, - UpdateGatewayDto, -} from "@dto/chirpstack/update-gateway.dto"; +import { UpdateGatewayContentsDto, UpdateGatewayDto } from "@dto/chirpstack/update-gateway.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; import { GenericChirpstackConfigurationService } from "@services/chirpstack/generic-chirpstack-configuration.service"; import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; import { GatewayContentsDto } from "@dto/chirpstack/gateway-contents.dto"; -import * as _ from "lodash"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; -import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; import { HttpService } from "@nestjs/axios"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Gateway } from "@entities/gateway.entity"; +import { Repository } from "typeorm"; +import { OrganizationService } from "@services/user-management/organization.service"; +import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; +import { CommonLocationDto } from "@dto/chirpstack/common-location.dto"; @Injectable() export class ChirpstackGatewayService extends GenericChirpstackConfigurationService { constructor( internalHttpService: HttpService, - private chirpstackSetupNetworkServerService: ChirpstackSetupNetworkServerService + private chirpstackSetupNetworkServerService: ChirpstackSetupNetworkServerService, + @InjectRepository(Gateway) + private gatewayRepository: Repository, + private organizationService: OrganizationService ) { super(internalHttpService); } @@ -41,33 +44,33 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ private readonly UPDATED_BY_KEY = "os2iot-updated-by"; private readonly CREATED_BY_KEY = "os2iot-created-by"; - async createNewGateway( - dto: CreateGatewayDto, - userId: number - ): Promise { + async createNewGateway(dto: CreateGatewayDto, userId: number): Promise { dto.gateway = await this.updateDtoContents(dto.gateway); dto.gateway.tags = this.addOrganizationToTags(dto); dto.gateway.tags = this.addUserToTags(dto, userId); + const gateway = this.mapContentsDtoToGateway(dto.gateway); + gateway.createdBy = userId; + gateway.updatedBy = userId; + gateway.rxPacketsReceived = 0; + gateway.txPacketsEmitted = 0; + + gateway.organization = await this.organizationService.findById(dto.organizationId); + const result = await this.post("gateways", dto); + await this.gatewayRepository.save(gateway); return this.handlePossibleError(result, dto); } - addUserToTags( - dto: CreateGatewayDto, - userId: number - ): { [id: string]: string | number } { + addUserToTags(dto: CreateGatewayDto, userId: number): { [id: string]: string | number } { const tags = dto.gateway.tags; tags[this.CREATED_BY_KEY] = `${userId}`; tags[this.UPDATED_BY_KEY] = `${userId}`; return tags; } - updateUpdatedByTag( - dto: UpdateGatewayDto, - userId: number - ): { [id: string]: string | number } { + updateUpdatedByTag(dto: UpdateGatewayDto, userId: number): { [id: string]: string | number } { const tags = dto.gateway.tags; tags[this.UPDATED_BY_KEY] = `${userId}`; return tags; @@ -80,90 +83,44 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } async getAll(organizationId?: number): Promise { - const limit = 1000; - let allResults: GatewayResponseDto[] = []; - let totalCount = 0; - let lastResults: ListAllGatewaysResponseDto; - do { - // Default parameters if not set - lastResults = await this.getAllWithPagination( - "gateways", - limit, - allResults.length - ); - allResults = _.union(allResults, lastResults.result); - totalCount = lastResults.totalCount; - } while (totalCount > allResults.length && lastResults.result.length > 0); - - await this.enrichWithOrganizationId(allResults); - if (organizationId !== undefined) { - const filteredResults = _.filter(allResults, x => { - return x.internalOrganizationId === +organizationId; - }); - return { - result: filteredResults, - totalCount: filteredResults.length, - }; + let query = this.gatewayRepository + .createQueryBuilder("gateway") + .innerJoinAndSelect("gateway.organization", "organization"); + + if (organizationId) { + query = query.where('"organizationId" = :organizationId', { organizationId }); } + const gateways = await query.getMany(); + return { - result: allResults, - totalCount: totalCount, + result: gateways.map(this.mapGatewayToResponseDto), + totalCount: gateways.length, }; } - /** - * Fetch gateways individually. This gives us the tags which contain the OS2 organization id. - * This is a very expensive operation, but it's the only way to retrieve gateway tags. - * @param results - */ - private async enrichWithOrganizationId(results: GatewayResponseDto[]) { - await BluebirdPromise.all( - BluebirdPromise.map( - results, - async x => { - try { - const gw = await this.getOne(x.id); - x.internalOrganizationId = gw.gateway.internalOrganizationId; - } catch (err) { - this.logger.error( - `Failed to fetch gateway details for id ${x.id}`, - err - ); - x.internalOrganizationId = null; - } - }, - { - concurrency: 50, - } - ) - ); - } - async getOne(gatewayId: string): Promise { if (gatewayId?.length != 16) { throw new BadRequestException("Invalid gateway id"); } try { - const result: SingleGatewayResponseDto = await this.get( - `gateways/${gatewayId}` - ); - result.gateway.internalOrganizationId = +result.gateway.tags[this.ORG_ID_KEY]; - result.gateway.createdBy = +result.gateway.tags[this.CREATED_BY_KEY]; - result.gateway.updatedBy = +result.gateway.tags[this.UPDATED_BY_KEY]; - result.gateway.tags[this.ORG_ID_KEY] = undefined; - result.gateway.tags[this.CREATED_BY_KEY] = undefined; - result.gateway.tags[this.UPDATED_BY_KEY] = undefined; + const result = new SingleGatewayResponseDto(); + const gateway = await this.gatewayRepository.findOne({ + where: { gatewayId }, + relations: { organization: true }, + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + const now = new Date(); + const statsFrom = new Date(new Date().setDate(now.getDate() - this.GATEWAY_STATS_INTERVAL_IN_DAYS)); - result.stats = (await this.getGatewayStats(gatewayId)).result; + result.stats = (await this.getGatewayStats(gatewayId, statsFrom, now)).result; + result.gateway = this.mapGatewayToResponseDto(gateway); return result; } catch (err) { - this.logger.error( - `Tried to find gateway with id: '${gatewayId}', got an error: ${JSON.stringify( - err - )}` - ); + this.logger.error(`Tried to find gateway with id: '${gatewayId}', got an error: ${JSON.stringify(err)}`); if (err?.message == "object does not exist") { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } @@ -171,12 +128,9 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } } - private async getGatewayStats(gatewayId: string): Promise { - const now = new Date(); - const to_time = now.toISOString(); - const from_time = new Date( - new Date().setDate(now.getDate() - this.GATEWAY_STATS_INTERVAL_IN_DAYS) - ).toISOString(); + async getGatewayStats(gatewayId: string, from: Date, to: Date): Promise { + const to_time = to.toISOString(); + const from_time = from.toISOString(); return await this.get( `gateways/${gatewayId}/stats?interval=DAY&startTimestamp=${from_time}&endTimestamp=${to_time}` @@ -191,10 +145,25 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ dto.gateway = await this.updateDtoContents(dto.gateway); dto.gateway.tags = await this.ensureOrganizationIdIsSet(gatewayId, dto, req); dto.gateway.tags = this.updateUpdatedByTag(dto, +req.user.userId); + + const gateway = this.mapContentsDtoToGateway(dto.gateway); + gateway.gatewayId = gatewayId; + gateway.updatedBy = req.user.userId; + const result = await this.put("gateways", dto, gatewayId); + await this.gatewayRepository.update({ gatewayId }, gateway); return this.handlePossibleError(result, dto); } + public async updateGatewayStats( + gatewayId: string, + rxPacketsReceived: number, + txPacketsEmitted: number, + lastSeenAt: Date | undefined + ) { + await this.gatewayRepository.update({ gatewayId }, { rxPacketsReceived, txPacketsEmitted, lastSeenAt }); + } + async ensureOrganizationIdIsSet( gatewayId: string, dto: UpdateGatewayDto, @@ -202,7 +171,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ ): Promise<{ [id: string]: string | number }> { const existing = await this.getOne(gatewayId); const tags = dto.gateway.tags; - tags[this.ORG_ID_KEY] = `${existing.gateway.internalOrganizationId}`; + tags[this.ORG_ID_KEY] = `${existing.gateway.organizationId}`; // TODO: Interpolated string will never be null? if (tags[this.ORG_ID_KEY] != null) { checkIfUserHasAccessToOrganization(req, +tags[this.ORG_ID_KEY], OrganizationAccessScope.GatewayWrite); @@ -212,14 +181,13 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ async deleteGateway(gatewayId: string): Promise { try { + await this.gatewayRepository.delete({ gatewayId }); await this.delete("gateways", gatewayId); return { success: true, }; } catch (err) { - this.logger.error( - `Got error from Chirpstack: ${JSON.stringify(err?.response?.data)}` - ); + this.logger.error(`Got error from Chirpstack: ${JSON.stringify(err?.response?.data)}`); return { success: false, chirpstackError: err?.response?.data as ChirpstackErrorResponseDto, @@ -233,9 +201,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ ): ChirpstackResponseStatus { if (result.status != 200) { this.logger.error( - `Error from Chirpstack: '${JSON.stringify( - dto - )}', got response: ${JSON.stringify(result.data)}` + `Error from Chirpstack: '${JSON.stringify(dto)}', got response: ${JSON.stringify(result.data)}` ); throw new BadRequestException({ success: false, @@ -264,9 +230,45 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } if (contentsDto?.tagsString) { - contentsDto.tags = JSON.parse(contentsDto.tagsString); + contentsDto.tags = JSON.parse(contentsDto.tagsString); // TODO: Updaze for new format when chirpstack 4 } + contentsDto.id = contentsDto.gatewayId; + return contentsDto; } + + public mapContentsDtoToGateway(dto: GatewayContentsDto) { + const gateway = new Gateway(); + gateway.name = dto.name; + gateway.gatewayId = dto.gatewayId; + gateway.description = dto.description; + gateway.altitude = dto.location.altitude; + gateway.location = { + type: "Point", + coordinates: [dto.location.longitude, dto.location.latitude], + }; + gateway.tags = JSON.stringify(dto.tags); + + return gateway; + } + + private mapGatewayToResponseDto(gateway: Gateway): GatewayResponseDto { + const responseDto = gateway as unknown as GatewayResponseDto; + responseDto.organizationId = gateway.organization.id; + responseDto.organizationName = gateway.organization.name; + responseDto.tags = JSON.parse(gateway.tags); + responseDto.tags["internalOrganizationId"] = undefined; + responseDto.tags["os2iot-updated-by"] = undefined; + responseDto.tags["os2iot-created-by"] = undefined; + + const commonLocation = new CommonLocationDto(); + commonLocation.latitude = gateway.location.coordinates[1]; + commonLocation.longitude = gateway.location.coordinates[0]; + commonLocation.altitude = gateway.altitude; + + responseDto.location = commonLocation; + + return responseDto; + } } diff --git a/src/services/chirpstack/gateway-boostrapper.service.ts b/src/services/chirpstack/gateway-boostrapper.service.ts index 3943d6f3..368d1223 100644 --- a/src/services/chirpstack/gateway-boostrapper.service.ts +++ b/src/services/chirpstack/gateway-boostrapper.service.ts @@ -28,33 +28,27 @@ export class GatewayBootstrapperService implements OnApplicationBootstrap { * @param gateways All chirpstack gateways * @param statusHistories Existing status histories to check against */ - private async seedGatewayStatus( - gateways: ListAllGatewaysResponseDto, - statusHistories: GatewayStatusHistory[] - ) { + private async seedGatewayStatus(gateways: ListAllGatewaysResponseDto, statusHistories: GatewayStatusHistory[]) { const now = new Date(); const errorTime = new Date(); errorTime.setSeconds(errorTime.getSeconds() - 150); // Don't overwrite ones which already have a status history - const newHistoriesForMissingGateways = gateways.result.reduce( - (res: GatewayStatusHistory[], gateway) => { - if (!statusHistories.some(history => history.mac === gateway.id)) { - // Best fit is to imitate the status logic from Chirpstack. - const lastSeenDate = new Date(gateway.lastSeenAt); - const wasOnline = errorTime.getTime() < lastSeenDate.getTime(); + const newHistoriesForMissingGateways = gateways.result.reduce((res: GatewayStatusHistory[], gateway) => { + if (!statusHistories.some(history => history.mac === gateway.gatewayId)) { + // Best fit is to imitate the status logic from Chirpstack. + const lastSeenDate = new Date(gateway.lastSeenAt); + const wasOnline = errorTime.getTime() < lastSeenDate.getTime(); - res.push({ - mac: gateway.id, - timestamp: now, - wasOnline, - } as GatewayStatusHistory); - } + res.push({ + mac: gateway.gatewayId, + timestamp: now, + wasOnline, + } as GatewayStatusHistory); + } - return res; - }, - [] - ); + return res; + }, []); if (newHistoriesForMissingGateways.length) { await this.statusHistoryService.createMany(newHistoriesForMissingGateways); diff --git a/src/services/chirpstack/gateway-status-history.service.ts b/src/services/chirpstack/gateway-status-history.service.ts index 4c5739c5..cbb7394d 100644 --- a/src/services/chirpstack/gateway-status-history.service.ts +++ b/src/services/chirpstack/gateway-status-history.service.ts @@ -4,17 +4,13 @@ import { } from "@dto/chirpstack/backend/gateway-all-status.dto"; import { GatewayStatus } from "@dto/chirpstack/backend/gateway-status.dto"; import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; -import { - GatewayStatusInterval, - gatewayStatusIntervalToDate, -} from "@enum/gateway-status-interval.enum"; +import { GatewayStatusInterval, gatewayStatusIntervalToDate } from "@enum/gateway-status-interval.enum"; import { Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { In, MoreThanOrEqual, Repository } from "typeorm"; import { ChirpstackGatewayService } from "./chirpstack-gateway.service"; import { nameof } from "@helpers/type-helper"; - -type GatewayId = { id: string; name: string }; +import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; @Injectable() export class GatewayStatusHistoryService { @@ -25,13 +21,11 @@ export class GatewayStatusHistoryService { ) {} private readonly logger = new Logger(GatewayStatusHistoryService.name); - public async findAllWithChirpstack( - query: ListAllGatewayStatusDto - ): Promise { + public async findAllWithChirpstack(query: ListAllGatewayStatusDto): Promise { // Very expensive operation. Since no gateway data is stored on the backend database, we need // to get them from Chirpstack. There's no filter by tags support so we must fetch all gateways. const gateways = await this.chirpstackGatewayService.getAll(query.organizationId); - const gatewayIds = gateways.result.map(gateway => gateway.id); + const gatewayIds = gateways.result.map(gateway => gateway.gatewayId); const fromDate = gatewayStatusIntervalToDate(query.timeInterval); if (!gatewayIds.length) { @@ -55,10 +49,7 @@ export class GatewayStatusHistoryService { latestStatusHistoryPerGatewayBeforePeriod ); - const data: GatewayStatus[] = this.mapStatusHistoryToGateways( - gateways.result, - statusHistories - ); + const data: GatewayStatus[] = this.mapStatusHistoryToGateways(gateways.result, statusHistories); return { data, @@ -66,20 +57,20 @@ export class GatewayStatusHistoryService { }; } - public async findOne( - gateway: Gateway, - timeInterval: GatewayStatusInterval - ): Promise { + public async findOne(gateway: GatewayResponseDto, timeInterval: GatewayStatusInterval): Promise { const fromDate = gatewayStatusIntervalToDate(timeInterval); const statusHistoriesInPeriod = await this.gatewayStatusHistoryRepository.find({ where: { - mac: gateway.id, + mac: gateway.gatewayId, timestamp: MoreThanOrEqual(fromDate), }, }); - const latestStatusHistoryPerGatewayBeforePeriod = await this.fetchLatestStatusBeforeDate([gateway.id], fromDate); + const latestStatusHistoryPerGatewayBeforePeriod = await this.fetchLatestStatusBeforeDate( + [gateway.gatewayId], + fromDate + ); const statusHistories = this.mergeStatusHistories( fromDate, @@ -101,9 +92,7 @@ export class GatewayStatusHistoryService { .getMany(); } - public createMany( - histories: GatewayStatusHistory[] - ): Promise { + public createMany(histories: GatewayStatusHistory[]): Promise { return this.gatewayStatusHistoryRepository.save(histories); } @@ -136,8 +125,8 @@ export class GatewayStatusHistoryService { return combinedHistories; } - private mapStatusHistoryToGateways( - gateways: Gateway[], + private mapStatusHistoryToGateways( + gateways: GatewayResponseDto[], statusHistories: GatewayStatusHistory[] ): GatewayStatus[] { return gateways.map(gateway => { @@ -145,26 +134,20 @@ export class GatewayStatusHistoryService { }); } - private mapStatusHistoryToGateway( - gateway: Gateway, - statusHistories: GatewayStatusHistory[] - ) { - const statusTimestamps = statusHistories.reduce( - (res: GatewayStatus["statusTimestamps"], history) => { - if (history.mac === gateway.id) { - res.push({ - timestamp: history.timestamp, - wasOnline: history.wasOnline, - }); - } - - return res; - }, - [] - ); + private mapStatusHistoryToGateway(gateway: GatewayResponseDto, statusHistories: GatewayStatusHistory[]) { + const statusTimestamps = statusHistories.reduce((res: GatewayStatus["statusTimestamps"], history) => { + if (history.mac === gateway.gatewayId) { + res.push({ + timestamp: history.timestamp, + wasOnline: history.wasOnline, + }); + } + + return res; + }, []); return { - id: gateway.id, + id: gateway.gatewayId, name: gateway.name, statusTimestamps, }; diff --git a/src/services/data-management/search.service.ts b/src/services/data-management/search.service.ts index e00930ed..1a8a9ea5 100644 --- a/src/services/data-management/search.service.ts +++ b/src/services/data-management/search.service.ts @@ -38,9 +38,7 @@ export class SearchService { const devicePromise = this.findDevicesAndMapType(req, trimmedQuery); const results = _.filter( - _.flatMap( - await Promise.all([applicationPromise, devicePromise, gatewayPromise]) - ), + _.flatMap(await Promise.all([applicationPromise, devicePromise, gatewayPromise])), x => x != null ); @@ -80,11 +78,7 @@ export class SearchService { }); } - private limitAndOrder( - data: SearchResultDto[], - limit: number, - offset: number - ): SearchResultDto[] { + private limitAndOrder(data: SearchResultDto[], limit: number, offset: number): SearchResultDto[] { const r = _.orderBy(data, ["updatedAt"], ["desc"]); const sliced = _.slice(r, offset, offset + limit); return sliced; @@ -100,19 +94,13 @@ export class SearchService { const mapped = await Promise.all( gateways.result.map(async x => { - const createdAt = new Date(Date.parse(x.createdAt)); - const updatedAt = new Date(Date.parse(x.updatedAt)); + const createdAt = new Date(x.createdAt); + const updatedAt = new Date(x.updatedAt); - const resultDto = new SearchResultDto( - x.name, - x.id, - createdAt, - updatedAt, - x.id - ); - const detailedInfo = await this.gatewayService.getOne(x.id); + const resultDto = new SearchResultDto(x.name, x.id, createdAt, updatedAt, x.gatewayId); + const detailedInfo = await this.gatewayService.getOne(x.gatewayId); - resultDto.organizationId = detailedInfo.gateway.internalOrganizationId; + resultDto.organizationId = detailedInfo.gateway.organizationId; return resultDto; }) ); @@ -127,10 +115,7 @@ export class SearchService { return data; } - private async findApplications( - req: AuthenticatedRequest, - trimmedQuery: string - ): Promise { + private async findApplications(req: AuthenticatedRequest, trimmedQuery: string): Promise { const qb = this.applicationRepository .createQueryBuilder("app") .where('"app"."name" ilike :name', { name: `%${trimmedQuery}%` }); @@ -138,10 +123,7 @@ export class SearchService { return await this.applySecuityAndSelect(req, qb, "app", "id"); } - private async findIoTDevices( - req: AuthenticatedRequest, - query: string - ): Promise { + private async findIoTDevices(req: AuthenticatedRequest, query: string): Promise { if (isHexadecimal(query)) { if (query.length == 16) { // LoRaWAN @@ -203,10 +185,7 @@ export class SearchService { return this.applySecuityAndSelect(req, qb, "device", "applicationId"); } - private async findIoTDevicesByName( - req: AuthenticatedRequest, - query: string - ): Promise { + private async findIoTDevicesByName(req: AuthenticatedRequest, query: string): Promise { const qb = this.getIoTDeviceQueryBuilder().where(`device.name ilike :name`, { name: `%${query}%`, }); @@ -229,20 +208,12 @@ export class SearchService { if (req.user.permissions.getAllApplicationsWithAtLeastRead().length == 0) { return []; } - qb = qb.andWhere( - `"${alias}"."${applicationIdColumn}" IN (:...allowedApplications)`, - { - allowedApplications: req.user.permissions.getAllApplicationsWithAtLeastRead(), - } - ); + qb = qb.andWhere(`"${alias}"."${applicationIdColumn}" IN (:...allowedApplications)`, { + allowedApplications: req.user.permissions.getAllApplicationsWithAtLeastRead(), + }); } - const toSelect = [ - `"${alias}"."id"`, - `"${alias}"."createdAt"`, - `"${alias}"."updatedAt"`, - `"${alias}"."name"`, - ]; + const toSelect = [`"${alias}"."id"`, `"${alias}"."createdAt"`, `"${alias}"."updatedAt"`, `"${alias}"."name"`]; const select = qb; if (alias == "device") { return select @@ -253,10 +224,7 @@ export class SearchService { .addSelect('"app"."belongsToId"', "organizationId") .getRawMany(); } else if (alias == "app") { - return select - .select(toSelect) - .addSelect('"app"."belongsToId"', "organizationId") - .getRawMany(); + return select.select(toSelect).addSelect('"app"."belongsToId"', "organizationId").getRawMany(); } } } diff --git a/src/services/device-management/application.service.ts b/src/services/device-management/application.service.ts index c6d34209..27d808d5 100644 --- a/src/services/device-management/application.service.ts +++ b/src/services/device-management/application.service.ts @@ -391,7 +391,6 @@ export class ApplicationService { .skip(query?.offset ? +query.offset : 0) .take(query?.limit ? +query.limit : 100) .orderBy(orderByColumn, direction) - .addOrderBy("name", "ASC") .getManyAndCount(); if (query.orderOn === "dataTargets") { diff --git a/src/services/device-management/iot-device.service.ts b/src/services/device-management/iot-device.service.ts index 7cdfedf5..c498152d 100644 --- a/src/services/device-management/iot-device.service.ts +++ b/src/services/device-management/iot-device.service.ts @@ -319,7 +319,6 @@ export class IoTDeviceService { return await this.loRaWANDeviceRepository .createQueryBuilder("iot_device") .where('"OTAAapplicationKey" is null') - .take(25) .getMany(); } @@ -327,6 +326,7 @@ export class IoTDeviceService { await this.loRaWANDeviceRepository.save(devices); } + async findMQTTDevice(id: number): Promise { return await this.mqttInternalBrokerDeviceRepository.findOne({ where: { id }, diff --git a/src/services/device-management/lorawan-device-database-enrich-job.ts b/src/services/device-management/lorawan-device-database-enrich-job.ts index e80fd00d..54d3dda9 100644 --- a/src/services/device-management/lorawan-device-database-enrich-job.ts +++ b/src/services/device-management/lorawan-device-database-enrich-job.ts @@ -1,23 +1,144 @@ -import { Injectable } from "@nestjs/common"; -import { Cron, CronExpression } from "@nestjs/schedule"; +import { Injectable, Logger } from "@nestjs/common"; +import { Cron, CronExpression, Timeout } from "@nestjs/schedule"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; +import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; +import * as BluebirdPromise from "bluebird"; +import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways.dto"; +import { OrganizationService } from "@services/user-management/organization.service"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Gateway } from "@entities/gateway.entity"; +import { Repository } from "typeorm"; @Injectable() export class LorawanDeviceDatabaseEnrichJob { - constructor(private chirpstackDeviceService: ChirpstackDeviceService, private iotDeviceService: IoTDeviceService) {} + constructor( + private chirpstackDeviceService: ChirpstackDeviceService, + private gatewayService: ChirpstackGatewayService, + private iotDeviceService: IoTDeviceService, + private organizationService: OrganizationService, + @InjectRepository(Gateway) + private gatewayRepository: Repository + ) {} - @Cron(CronExpression.EVERY_DAY_AT_5AM) // TODO: Finalize when to run + private readonly logger = new Logger(LorawanDeviceDatabaseEnrichJob.name, { timestamp: true }); + + @Cron(CronExpression.EVERY_MINUTE) + async fetchStatusForGateway() { + // Select all gateways from our database and chirpstack (Cheaper than individual calls) + const gateways = await this.gatewayService.getAll(); + const chirpStackGateways = await this.gatewayService.getAllWithPagination( + "gateways", + 1000, + 0 + ); + + // Setup batched fetching of status (Only for today) + await BluebirdPromise.all( + BluebirdPromise.map( + gateways.result, + async gateway => { + try { + const fromTime = new Date(); + const fromUtc = new Date( + Date.UTC(fromTime.getUTCFullYear(), fromTime.getUTCMonth(), fromTime.getUTCDate()) + ); + + const statsToday = await this.gatewayService.getGatewayStats( + gateway.gatewayId, + fromUtc, + new Date() + ); + // Save that to our database + const stats = statsToday.result[0]; + const chirpstackGateway = chirpStackGateways.result.find( + g => g.id.toString() === gateway.gatewayId + ); + + await this.gatewayService.updateGatewayStats( + gateway.gatewayId, + stats.rxPacketsReceived, + stats.txPacketsEmitted, + chirpstackGateway.lastSeenAt + ); + } catch (err) { + this.logger.error(`Gateway status fetch failed with: ${JSON.stringify(err)}`, err); + } + }, + { + concurrency: 20, + } + ) + ); + } + + @Timeout(30000) async enrichLoRaWANDeviceDatabase() { - // Select up to 25 lora devices without appId in the database + // Select lora devices without appId in the database const devices = await this.iotDeviceService.findNonEnrichedLoRaWANDevices(); - // Enrich from chirpstack - const enrichedDevices = await Promise.all( - devices.map(async device => await this.chirpstackDeviceService.enrichLoRaWANDevice(device)) + // Enrich from chirpstack batched + await BluebirdPromise.all( + BluebirdPromise.map( + devices, + async device => { + try { + const enrichedDevice = await this.chirpstackDeviceService.enrichLoRaWANDevice(device); + await this.iotDeviceService.updateLocalLoRaWANDevices([enrichedDevice]); + } catch (err) { + this.logger.error(`Database sync of lora devices failed with: ${JSON.stringify(err)}`, err); + } + }, + { + concurrency: 10, + } + ) ); + } + + // This is run once on startup and will create any gateways that exist in chirpstack but not our database + @Timeout(10000) + async importChirpstackGateways() { + // Get all chirpstack gateways + const chirpStackGateways = await this.gatewayService.getAllWithPagination( + "gateways", + 1000, + 0 + ); + + const dbGateways = await this.gatewayService.getAll(); - // Save to database - await this.iotDeviceService.updateLocalLoRaWANDevices(enrichedDevices); + // Filter for gateways not existing in our database + const unknownGateways = chirpStackGateways.result.filter( + g => dbGateways.result.findIndex(dbGateway => dbGateway.gatewayId === g.id.toString()) === -1 + ); + + await BluebirdPromise.all( + BluebirdPromise.map( + unknownGateways, + async x => { + try { + const gw = (await this.gatewayService.get(`gateways/${x.id}`)) as any; + const organizationId = gw.gateway.tags["internalOrganizationId"]; + + const gateway = this.gatewayService.mapContentsDtoToGateway(gw.gateway); + gateway.id = 0; + gateway.gatewayId = gw.gateway.id; + gateway.lastSeenAt = gw.lastSeenAt; + gateway.createdAt = new Date(Date.parse(gw.createdAt)); + gateway.rxPacketsReceived = 0; + gateway.txPacketsEmitted = 0; + gateway.createdBy = gw.gateway.tags["os2iot-created-by"]; + gateway.organization = await this.organizationService.findById(organizationId); + await this.gatewayRepository.save(gateway); + } catch (err) { + this.logger.error(`Database sync of gateways failed with: ${JSON.stringify(err)}`, err); + } + }, + { + concurrency: 10, + } + ) + ); } }