Skip to content

Copy device settings and append datatarget #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 12, 2025
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: 5 additions & 0 deletions src/app/applications/applications-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { IoTDeviceDetailComponent } from "./iot-devices/iot-device-detail/iot-de
import { IotDeviceEditComponent } from "./iot-devices/iot-device-edit/iot-device-edit.component";
import { MulticastDetailComponent } from "./multicast/multicast-detail/multicast-detail.component";
import { MulticastEditComponent } from "./multicast/multicast-edit/multicast-edit.component";
import { IotDeviceCopyComponent } from "./iot-devices/iot-device-copy/iot-device-copy.component";

const applicationRoutes: Routes = [
{
Expand Down Expand Up @@ -67,6 +68,10 @@ const applicationRoutes: Routes = [
},
],
},
{
path: "iot-device-copy/:deviceId",
component: IotDeviceCopyComponent,
},
{ path: "datatarget-new", component: DatatargetNewComponent },
{ path: "datatarget-edit", component: DatatargetEditComponent },
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<app-iot-device-edit [isDeviceCopy]="true"></app-iot-device-edit>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";

import { IotDeviceEditComponent } from "../iot-device-edit/iot-device-edit.component";

describe("IotDeviceEditComponent", () => {
let component: IotDeviceEditComponent;
let fixture: ComponentFixture<IotDeviceEditComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IotDeviceEditComponent],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(IotDeviceEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it("should create", () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Component, OnInit } from "@angular/core";

@Component({
selector: "app-iot-device-copy",
templateUrl: "./iot-device-copy.component.html",
styleUrls: ["./iot-device-copy.component.scss"],
})
export class IotDeviceCopyComponent implements OnInit {
constructor() {}

ngOnInit(): void {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { MeService } from "@shared/services/me.service";
import { OrganizationAccessScope } from "@shared/enums/access-scopes";
import { IotDeviceDetailsService } from "@applications/iot-devices/iot-device-details-service";
import { IoTDeviceChangeApplicationDialogComponent } from "../iot-device-change-application-dialog/iot-device-change-application-dialog.component";
import { ApplicationDialogModel, IoTDeviceApplicationDialogModel } from "@shared/models/dialog.model";
import { IoTDeviceApplicationDialogModel } from "@shared/models/dialog.model";

@Component({
selector: "app-iot-device",
Expand Down Expand Up @@ -56,6 +56,7 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy {
private deleteDialogSubscription: Subscription;
public dropdownButton: DropdownButton;
public canEdit = false;
private copyDeviceButtonId = "COPY-DEVICE";

private resetApiKeyId = "RESET-API-KEY";
private resetApiKeyOption: ExtraDropdownOption;
Expand Down Expand Up @@ -104,9 +105,10 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy {
"IOTDEVICE-TABLE-ROW.RESET-API-KEY",
"IOTDEVICE.GENERIC_HTTP.RESET-API-KEY",
"GEN.CANCEL",
"GEN.BACK"
])
.subscribe(translations => {
this.backButton.label = translations["NAV.APPLICATIONS"];
this.backButton.label = translations["GEN.BACK"];
this.dropdownButton.label = translations["IOTDEVICE-TABLE-ROW.SHOW-OPTIONS"];
this.titleService.setTitle(translations["TITLE.IOTDEVICE"]);

Expand Down Expand Up @@ -148,6 +150,14 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy {
onClick: () => this.onOpenChangeApplicationDialog(),
});
});

this.translate.get("IOTDEVICE.COPY-SETTINGS-TO-NEW-DEVICE").subscribe(translation => {
this.dropdownButton.extraOptions.push({
id: this.copyDeviceButtonId,
label: translation,
onClick: () => this.navigateToCopy(),
});
});
}
});
}
Expand Down Expand Up @@ -191,6 +201,10 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy {
});
}

navigateToCopy() {
this.router.navigate(["applications", this.application.id, "iot-device-copy", this.deviceId]);
}

ngOnDestroy() {
// prevent memory leak by unsubscribing
if (this.iotDeviceSubscription) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
class="form-check-input"
required
[attr.disabled]="editmode ? '' : null"
(change)="isChecked($event)"
(change)="isDeviceTypeChecked($event)"
[checked]="iotDevice.type.toString().includes('LORAWAN')"
/>
<div class="image-container">
Expand All @@ -48,7 +48,7 @@
class="form-check-input"
required
[attr.disabled]="editmode ? '' : null"
(change)="isChecked($event)"
(change)="isDeviceTypeChecked($event)"
[checked]="iotDevice.type.toString().includes('GENERIC_HTTP')"
/>
<div class="image-container">
Expand All @@ -65,7 +65,7 @@
required
class="form-check-input"
[attr.disabled]="editmode ? '' : null"
(change)="isChecked($event)"
(change)="isDeviceTypeChecked($event)"
[checked]="iotDevice.type.toString().includes('MQTT')"
/>
<div class="image-container">
Expand All @@ -82,7 +82,7 @@
class="form-check-input"
required
[attr.disabled]="editmode ? '' : null"
(change)="isChecked($event)"
(change)="isDeviceTypeChecked($event)"
[checked]="iotDevice.type.toString().includes('SIGFOX')"
/>
<div class="image-container">
Expand Down Expand Up @@ -238,7 +238,7 @@ <h3>{{ "IOTDEVICE.LORAWANSETUP" | translate }}</h3>
[placeholder]="'QUESTION.DEVEUI-PLACEHOLDER' | translate"
class="form-control"
[(ngModel)]="iotDevice.lorawanSettings.devEUI"
[disabled]="editmode"
[disabled]="editmode && !isDeviceCopy"
[ngClass]="{
'is-invalid': formFailedSubmit && errorFields.includes('devEUI'),
'is-valid': formFailedSubmit && !errorFields.includes('devEUI')
Expand Down Expand Up @@ -417,6 +417,20 @@ <h3>{{ "QUESTION.ABP" | translate }}</h3>
<h3>{{ "QUESTION.METADATA" | translate }}</h3>
<app-form-key-value-list [(tags)]="metadataTags" [errorFieldId]="errorMetadataFieldId"> </app-form-key-value-list>
</div>
<div *ngIf="isDeviceCopy" class="form-group mt-5">
<label class="form-label" for="copyPayloadAndDatatarget">{{
"IOTDEVICE.COPY-DATATAGET-AND-PAYLOAD" | translate
}}</label>
<div>
<mat-checkbox
[(ngModel)]="copyPayloadAndDatatarget"
name="copyPayloadAndDatatarget"
id="copyPayloadAndDatatarget"
>
{{ "QUESTION.SKIPFCNTCHECK-YES" | translate }}
</mat-checkbox>
</div>
</div>
<div class="form-group mt-5">
<button (click)="routeBack()" class="btn btn-secondary" type="button">{{ "GEN.CANCEL" | translate }}</button>
<button class="btn btn-primary ml-2" type="submit">{{ "GEN.SAVE" | translate }}</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Location } from "@angular/common";
import { HttpErrorResponse } from "@angular/common/http";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
import { Title } from "@angular/platform-browser";
import { ActivatedRoute, Router } from "@angular/router";
import { Application } from "@app/applications/application.model";
Expand All @@ -17,18 +17,22 @@ import { jsonToList } from "@shared/helpers/json.helper";
import { ErrorMessage } from "@shared/models/error-message.model";
import { ScrollToTopService } from "@shared/services/scroll-to-top.service";
import { SharedVariableService } from "@shared/shared-variable/shared-variable.service";
import { Subscription } from "rxjs";
import { forkJoin, Subscription } from "rxjs";
import { IotDevice } from "../iot-device.model";
import { IoTDeviceService } from "../iot-device.service";
import { MeService } from "@shared/services/me.service";
import { OrganizationAccessScope } from "@shared/enums/access-scopes";
import { PayloadDeviceDatatargetService } from "@payload-decoder/payload-device-datatarget.service";
import { PayloadDeviceDatatargetGetManyResponse } from "@payload-decoder/payload-device-data.model";

@Component({
selector: "app-iot-device-edit",
templateUrl: "./iot-device-edit.component.html",
styleUrls: ["./iot-device-edit.component.scss"],
})
export class IotDeviceEditComponent implements OnInit, OnDestroy {
@Input() isDeviceCopy: boolean = false;
public copyPayloadAndDatatarget: boolean = false;
public errorMessages: any;
public errorFields: string[];
public formFailedSubmit = false;
Expand Down Expand Up @@ -57,6 +61,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
private deviceProfileService: DeviceProfileService,
private applicationService: ApplicationService,
private iotDeviceService: IoTDeviceService,
private datatargetPayloadService: PayloadDeviceDatatargetService,
private location: Location,
private shareVariable: SharedVariableService,
private deviceModelService: DeviceModelService,
Expand Down Expand Up @@ -113,14 +118,18 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
});
}

isChecked(event) {
isDeviceTypeChecked(event) {
if (event.target.checked) {
this.iotDevice.type = event.target.name;
} else if (!event.target.checked && this.iotDevice.type.toString().includes(event.target.name)) {
event.target.checked = true;
}
}

isCopyPayloadAndDatatargetChecked(event) {
this.copyPayloadAndDatatarget = event.target.checked;
}

getDevice(id: number): void {
this.deviceSubscription = this.iotDeviceService.getIoTDevice(id).subscribe((device: IotDevice) => {
this.iotDevice = device;
Expand All @@ -140,6 +149,44 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
if (device.metadata) {
this.metadataTags = jsonToList(device.metadata);
}

//If coming from copy, reset all these properties
if (this.isDeviceCopy) {
this.iotDevice.id = undefined;
this.iotDevice.name = undefined;
this.iotDevice.createdAt = undefined;
this.iotDevice.createdBy = undefined;
this.iotDevice.createdByName = undefined;
this.iotDevice.updatedAt = undefined;
this.iotDevice.updatedBy = undefined;
this.iotDevice.updatedByName = undefined;
this.copyPayloadAndDatatarget = true;

switch (this.iotDevice.type) {
case DeviceType.GENERIC_HTTP: {
this.iotDevice.apiKey = undefined;
break;
}
case DeviceType.LORAWAN: {
this.iotDevice.lorawanSettings.devEUI = undefined;
this.iotDevice.lorawanSettings.OTAAapplicationKey = undefined;
this.iotDevice.lorawanSettings.applicationSessionKey = undefined;
this.iotDevice.lorawanSettings.networkSessionKey = undefined;
this.iotDevice.lorawanSettings.devAddr = undefined;
this.iotDevice.lorawanSettings.fCntUp = undefined;
this.iotDevice.lorawanSettings.nFCntDown = undefined;
break;
}
case DeviceType.MQTT_INTERNAL_BROKER: {
this.iotDevice.mqttInternalBrokerSettings.caCertificate = undefined;
this.iotDevice.mqttInternalBrokerSettings.deviceCertificate = undefined;
this.iotDevice.mqttInternalBrokerSettings.deviceCertificateKey = undefined;
this.iotDevice.mqttInternalBrokerSettings.mqttpassword = undefined;
this.iotDevice.mqttInternalBrokerSettings.mqtttopicname = undefined;
this.iotDevice.mqttInternalBrokerSettings.mqttusername = undefined;
}
}
}
});
}

Expand Down Expand Up @@ -190,7 +237,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
}
}

if (this.deviceId !== 0) {
if (this.deviceId !== 0 && !this.isDeviceCopy) {
this.updateIoTDevice(this.deviceId);
} else {
this.postIoTDevice();
Expand Down Expand Up @@ -275,23 +322,49 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
}
}

private navigateToDeviceDetails(device: IotDevice) {
this.router.navigate(["applications/" + this.iotDevice.applicationId + "/iot-device/" + device.id + "/details"]);
}

postIoTDevice() {
// Sanitize devEUI for non-hex characters
// Sanitize devEUI
if (this.iotDevice.type === DeviceType.LORAWAN && this.iotDevice.lorawanSettings.devEUI) {
this.iotDevice.lorawanSettings.devEUI = this.iotDevice.lorawanSettings.devEUI.replace(/[^0-9A-Fa-f]/g, "");
}

this.iotDeviceService.createIoTDevice(this.iotDevice).subscribe(
(response: IotDevice) => {
this.router.navigate([
"applications/" + this.iotDevice.applicationId + "/iot-device/" + response.id + "/details",
]);
},
(error: HttpErrorResponse) => {
this.handleError(error);
this.formFailedSubmit = true;
//First create the device
this.iotDeviceService.createIoTDevice(this.iotDevice).subscribe((createdDevice: IotDevice) => {
if (!this.copyPayloadAndDatatarget) {
this.navigateToDeviceDetails(createdDevice);
return;
}
);

//If it's the copy device flow, then get all datatargets from the device that we want to copy.
this.datatargetPayloadService
.getByIoTDevice(this.deviceId)
.subscribe((result: PayloadDeviceDatatargetGetManyResponse) => {
//For each of these datatargets, append the copied device to that datatarget. First we make the observables
const appendToDatatargetObservables = result.data.map(element =>
this.datatargetPayloadService.appendCopiedIoTDevice(element.id, { deviceId: createdDevice.id })
);

if (appendToDatatargetObservables.length === 0) {
this.navigateToDeviceDetails(createdDevice);
return;
}

//Forkjoin is running all observables in parallel and when all are done it returns.
forkJoin(appendToDatatargetObservables).subscribe(
() => this.navigateToDeviceDetails(createdDevice),
this.formFailedSubmitHandleError
);
}, this.formFailedSubmitHandleError);
}, this.formFailedSubmitHandleError);
}

formFailedSubmitHandleError(error: HttpErrorResponse) {
this.handleError(error);
this.formFailedSubmit = true;
}

updateIoTDevice(id: number) {
Expand All @@ -301,8 +374,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
this.routeBack();
},
(error: HttpErrorResponse) => {
this.handleError(error);
this.formFailedSubmit = true;
this.formFailedSubmitHandleError(error);
}
);
}
Expand Down
Loading