Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/entities/dto/list-all-entities.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,8 @@ export class ListAllEntitiesDto {
| "owner"
| "contactPerson"
| "personalData"
| "openDataDkEnabled";
| "openDataDkEnabled"
| "deviceModel"
| "devices"
| "dataTargets";
}
11 changes: 7 additions & 4 deletions src/entities/lorawan-device.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import { IoTDeviceType } from "@enum/device-type.enum";

@ChildEntity(IoTDeviceType.LoRaWAN)
export class LoRaWANDevice extends IoTDevice {
/**
* This is used to identify the LoRaWAN device in Chirpstack,
* the remaining information is only stored in Chirpstack.
*/
@Column({ nullable: true })
@Length(16, 16, { message: "Must be 16 characters" })
deviceEUI: string;

@Column({ nullable: true })
@Length(32, 32, { message: "Must be 32 characters" })
OTAAapplicationKey: string;

@Column({ nullable: true })
deviceProfileName: string;

@Column({ nullable: true })
chirpstackApplicationId: number;

Expand Down
16 changes: 16 additions & 0 deletions src/migration/1700748970060-added-chirpstack-data-to-iot-device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddedChirpstackDataToIotDevice1700748970060 implements MigrationInterface {
name = 'AddedChirpstackDataToIotDevice1700748970060'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "iot_device" ADD "OTAAapplicationKey" character varying`);
await queryRunner.query(`ALTER TABLE "iot_device" ADD "deviceProfileName" character varying`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "iot_device" DROP COLUMN "deviceProfileName"`);
await queryRunner.query(`ALTER TABLE "iot_device" DROP COLUMN "OTAAapplicationKey"`);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { NetworkServerController } from "@admin-controller/chirpstack/network-se
ChirpstackGatewayController,
ServiceProfileController,
DeviceProfileController,
NetworkServerController
NetworkServerController,
],
imports: [HttpModule, ConfigModule.forRoot({ load: [configuration] })],
providers: [
Expand All @@ -29,6 +29,6 @@ import { NetworkServerController } from "@admin-controller/chirpstack/network-se
DeviceProfileService,
ChirpstackDeviceService,
],
exports: [ChirpstackDeviceService, ChirpstackGatewayService],
exports: [ChirpstackDeviceService, ChirpstackGatewayService, DeviceProfileService],
})
export class ChirpstackAdministrationModule {}
2 changes: 2 additions & 0 deletions src/modules/device-management/iot-device.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ReceiveDataModule } from "@modules/device-integrations/receive-data.mod
import { InternalMqttListenerModule } from "@modules/device-integrations/internal-mqtt-listener.module";
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";

@Module({
imports: [
Expand All @@ -38,6 +39,7 @@ import { CsvGeneratorService } from "@services/csv-generator.service";
PeriodicSigFoxCleanupService,
IoTDeviceDownlinkService,
SigFoxMessagesService,
LorawanDeviceDatabaseEnrichJob,
MqttService,
IoTDeviceService,
EncryptionHelperService,
Expand Down
132 changes: 39 additions & 93 deletions src/services/chirpstack/chirpstack-device.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ import { groupBy } from "lodash";
import { LoRaWANStatsResponseDto } from "@dto/chirpstack/device/lorawan-stats.response.dto";
import { ConfigService } from "@nestjs/config";
import { HttpService } from "@nestjs/axios";
import { DeviceProfileService } from "@services/chirpstack/device-profile.service";

@Injectable()
export class ChirpstackDeviceService extends GenericChirpstackConfigurationService {
constructor(internalHttpService: HttpService, private configService: ConfigService) {
constructor(
internalHttpService: HttpService,
private configService: ConfigService,
private deviceProfileService: DeviceProfileService
) {
super(internalHttpService);

this.deviceStatsIntervalInDays = configService.get<number>(
"backend.deviceStatsIntervalInDays"
);
this.deviceStatsIntervalInDays = configService.get<number>("backend.deviceStatsIntervalInDays");
}

private readonly logger = new Logger(ChirpstackDeviceService.name);
Expand All @@ -65,17 +68,12 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
// if default exist use it
let applicationId = applications.result.find(
element =>
element.serviceProfileID.toLowerCase() ===
dto.device.serviceProfileID.toLowerCase() &&
element.serviceProfileID.toLowerCase() === dto.device.serviceProfileID.toLowerCase() &&
element.name.startsWith(this.defaultApplicationName)
)?.id;
// otherwise create default
if (!applicationId) {
applicationId = await this.createDefaultApplication(
applicationId,
dto,
organizationID
);
applicationId = await this.createDefaultApplication(applicationId, dto, organizationID);
}

return +applicationId;
Expand All @@ -88,9 +86,7 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
) {
applicationId = await this.createApplication({
application: {
name: `${
this.defaultApplicationName
}-${dto.device.serviceProfileID.toLowerCase()}`.substring(0, 50),
name: `${this.defaultApplicationName}-${dto.device.serviceProfileID.toLowerCase()}`.substring(0, 50),
description: this.DEFAULT_DESCRIPTION,
organizationID: organizationID,
serviceProfileID: dto.device.serviceProfileID,
Expand All @@ -99,10 +95,7 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
return applicationId;
}

makeCreateChirpstackDeviceDto(
dto: CreateLoRaWANSettingsDto,
name: string
): CreateChirpstackDeviceDto {
makeCreateChirpstackDeviceDto(dto: CreateLoRaWANSettingsDto, name: string): CreateChirpstackDeviceDto {
const csDto = new ChirpstackDeviceContentsDto();
csDto.name = `${this.DEVICE_NAME_PREFIX}${name}`.toLowerCase();
csDto.description = this.DEFAULT_DESCRIPTION;
Expand All @@ -116,9 +109,7 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
return { device: csDto };
}

async overwriteDownlink(
dto: CreateChirpstackDeviceQueueItemDto
): Promise<CreateChirpstackDeviceQueueItemResponse> {
async overwriteDownlink(dto: CreateChirpstackDeviceQueueItemDto): Promise<CreateChirpstackDeviceQueueItemResponse> {
await this.deleteDownlinkQueue(dto.deviceQueueItem.devEUI);
try {
const res = await this.post<CreateChirpstackDeviceQueueItemDto>(
Expand All @@ -127,12 +118,9 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
);
return res.data;
} catch (err) {
const fcntError =
"enqueue downlink payload error: get next downlink fcnt for deveui error";
const fcntError = "enqueue downlink payload error: get next downlink fcnt for deveui error";
if (err?.response?.data?.error?.startsWith(fcntError)) {
throw new BadRequestException(
ErrorCodes.DeviceIsNotActivatedInChirpstack
);
throw new BadRequestException(ErrorCodes.DeviceIsNotActivatedInChirpstack);
}

throw err;
Expand All @@ -148,9 +136,7 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
}

async getDownlinkQueue(deviceEUI: string): Promise<DeviceDownlinkQueueResponseDto> {
const res = await this.get<DeviceDownlinkQueueResponseDto>(
`devices/${deviceEUI}/queue`
);
const res = await this.get<DeviceDownlinkQueueResponseDto>(`devices/${deviceEUI}/queue`);
return res;
}

Expand All @@ -177,20 +163,14 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
isUpdate
);
if (res.status != 200) {
this.logger.warn(
`Could not ABP activate Chirpstack Device using body: ${JSON.stringify(
dto
)}`
);
this.logger.warn(`Could not ABP activate Chirpstack Device using body: ${JSON.stringify(dto)}`);
return false;
}
return res.status == 200;
}

async getAllDevicesStatus(): Promise<ChirpstackManyDeviceResponseDto> {
return await this.get<ChirpstackManyDeviceResponseDto>(
`devices?limit=10000&offset=0`
);
return await this.get<ChirpstackManyDeviceResponseDto>(`devices?limit=10000&offset=0`);
}

private async createOrUpdateABPActivation(
Expand Down Expand Up @@ -223,11 +203,7 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
return { res, dto };
}

async activateDeviceWithOTAA(
deviceEUI: string,
nwkKey: string,
isUpdate: boolean
): Promise<boolean> {
async activateDeviceWithOTAA(deviceEUI: string, nwkKey: string, isUpdate: boolean): Promise<boolean> {
// http://localhost:8080/api/devices/0011223344557188/keys
// {"deviceKeys":{"nwkKey":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","devEUI":"0011223344557188"}}

Expand All @@ -244,9 +220,7 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
res = await this.post(`devices/${deviceEUI}/keys`, dto);
}
if (res.status != 200) {
this.logger.warn(
`Could not activate Chirpstack Device using body: ${JSON.stringify(dto)}`
);
this.logger.warn(`Could not activate Chirpstack Device using body: ${JSON.stringify(dto)}`);
return false;
}
return res.status == 200;
Expand All @@ -264,22 +238,16 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
}

if (res.status !== 200) {
this.logger.warn(
`Could not create Chirpstack Device using body: ${JSON.stringify(dto)}`
);
this.logger.warn(`Could not create Chirpstack Device using body: ${JSON.stringify(dto)}`);

return false;
}

return true;
}

async getChirpstackApplication(
id: string
): Promise<ChirpstackSingleApplicationResponseDto> {
return await this.get<ChirpstackSingleApplicationResponseDto>(
`applications/${id}`
);
async getChirpstackApplication(id: string): Promise<ChirpstackSingleApplicationResponseDto> {
return await this.get<ChirpstackSingleApplicationResponseDto>(`applications/${id}`);
}

async getChirpstackDevice(id: string): Promise<ChirpstackDeviceContentsDto> {
Expand All @@ -291,23 +259,17 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi

async getKeys(deviceId: string): Promise<ChirpstackDeviceKeysContentDto> {
try {
const res = await this.get<ChirpstackDeviceKeysResponseDto>(
`devices/${deviceId}/keys`
);
const res = await this.get<ChirpstackDeviceKeysResponseDto>(`devices/${deviceId}/keys`);
return res.deviceKeys;
} catch (err) {
// Chirpstack returns 404 if keys are not saved ..
return new ChirpstackDeviceKeysContentDto();
}
}

async getActivation(
deviceId: string
): Promise<ChirpstackDeviceActivationContentsDto> {
async getActivation(deviceId: string): Promise<ChirpstackDeviceActivationContentsDto> {
try {
const res = await this.get<ChirpstackDeviceActivationDto>(
`devices/${deviceId}/activation`
);
const res = await this.get<ChirpstackDeviceActivationDto>(`devices/${deviceId}/activation`);
return res.deviceActivation;
} catch (err) {
return new ChirpstackDeviceActivationContentsDto();
Expand Down Expand Up @@ -336,17 +298,17 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
loraDevice.lorawanSettings.deviceStatusBattery = csData.deviceStatusBattery;
loraDevice.lorawanSettings.deviceStatusMargin = csData.deviceStatusMargin;

const deviceProfile = await this.deviceProfileService.findOneDeviceProfileById(csData.deviceProfileID);
loraDevice.deviceProfileName = deviceProfile.deviceProfile.name;

const appMatch = applications.find(app => app.id === csData.applicationID);
loraDevice.lorawanSettings.serviceProfileID = appMatch
? appMatch.serviceProfileID
: loraDevice.lorawanSettings.serviceProfileID;

if (!loraDevice.lorawanSettings.serviceProfileID) {
const csAppliation = await this.getChirpstackApplication(
csData.applicationID
);
loraDevice.lorawanSettings.serviceProfileID =
csAppliation.application.serviceProfileID;
const csAppliation = await this.getChirpstackApplication(csData.applicationID);
loraDevice.lorawanSettings.serviceProfileID = csAppliation.application.serviceProfileID;
}

return loraDevice;
Expand All @@ -358,6 +320,7 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
// OTAA
loraDevice.lorawanSettings.activationType = ActivationType.OTAA;
loraDevice.lorawanSettings.OTAAapplicationKey = keys.nwkKey;
loraDevice.OTAAapplicationKey = keys.nwkKey;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do this when we have the otaaapplicationkey on the lorawansettings? :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lorawan settings are not set in the database prior to this, and if the device is not otaa it should not be set

} else {
const activation = await this.getActivation(loraDevice.deviceEUI);
if (activation.devAddr != null) {
Expand All @@ -374,25 +337,16 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
}
}

async isDeviceAlreadyCreated(
deviceEUI: string,
chirpstackIds: ChirpstackDeviceId[] = null
): Promise<boolean> {
const devices = !chirpstackIds
? await this.getAllChirpstackDevices()
: chirpstackIds;
const alreadyExists = devices.some(
x => x.devEUI.toLowerCase() === deviceEUI.toLowerCase()
);
async isDeviceAlreadyCreated(deviceEUI: string, chirpstackIds: ChirpstackDeviceId[] = null): Promise<boolean> {
const devices = !chirpstackIds ? await this.getAllChirpstackDevices() : chirpstackIds;
const alreadyExists = devices.some(x => x.devEUI.toLowerCase() === deviceEUI.toLowerCase());
return alreadyExists;
}

getStats(deviceEUI: string): Promise<LoRaWANStatsResponseDto> {
const now = new Date();
const to_time = now.toISOString();
const from_time = new Date(
new Date().setDate(now.getDate() - this.deviceStatsIntervalInDays)
).toISOString();
const from_time = new Date(new Date().setDate(now.getDate() - this.deviceStatsIntervalInDays)).toISOString();

return this.get<LoRaWANStatsResponseDto>(
`devices/${deviceEUI}/stats?interval=DAY&startTimestamp=${from_time}&endTimestamp=${to_time}`
Expand All @@ -408,10 +362,7 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
public async getLoRaWANApplications(
devices: LoRaWANDeviceWithChirpstackDataDto[]
): Promise<ChirpstackSingleApplicationResponseDto[]> {
const loraDevicesByAppId = groupBy(
devices,
device => device.chirpstackApplicationId
);
const loraDevicesByAppId = groupBy(devices, device => device.chirpstackApplicationId);

const res: ChirpstackSingleApplicationResponseDto[] = [];

Expand All @@ -423,16 +374,11 @@ export class ChirpstackDeviceService extends GenericChirpstackConfigurationServi
return res;
}

private async getAllChirpstackDevices(
limit = 1000
): Promise<ChirpstackDeviceContentsDto[]> {
return (await this.get<ListAllDevicesResponseDto>(`devices?limit=${limit}`))
.result;
private async getAllChirpstackDevices(limit = 1000): Promise<ChirpstackDeviceContentsDto[]> {
return (await this.get<ListAllDevicesResponseDto>(`devices?limit=${limit}`)).result;
}

private async createApplication(
dto: CreateChirpstackApplicationDto
): Promise<string> {
private async createApplication(dto: CreateChirpstackApplicationDto): Promise<string> {
return (await this.post("applications", dto)).data.id;
}
}
Loading