connectors templates

This commit is contained in:
Maksym Dudnik 2023-12-14 13:21:57 +02:00
parent bccb2e7e8c
commit 6ee6ccaa36
12 changed files with 584 additions and 179 deletions

View File

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-toolbar color="primary">
<h2 translate>gateway.rpc.save-template</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content style="width: 600px" class="mat-content" fxLayout="column">
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.rpc.template-name</mat-label>
<input matInput [formControl]="templateNameCtrl" required/>
<mat-error
*ngIf="templateNameCtrl.hasError('required')">
{{ 'gateway.rpc.template-name-required' | translate }}
</mat-error>
</mat-form-field>
<div class="mat-mdc-form-field-error"
style="margin-top: -15px; padding-left: 10px; font-size: 14px;"
*ngIf="validateDuplicateName(templateNameCtrl)">
{{ 'gateway.rpc.template-name-duplicate' | translate }}
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button
type="button"
(click)="close()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
[disabled]="!templateNameCtrl.valid"
(click)="save()">
{{ 'action.save' | translate }}
</button>
</div>

View File

@ -0,0 +1,62 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { FormBuilder, FormControl, UntypedFormControl, Validators } from '@angular/forms';
import { RPCTemplate, SaveRPCTemplateData } from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'gateway-service-rpc-connector-template-dialog',
templateUrl: './gateway-service-rpc-connector-template-dialog.html'
})
export class GatewayServiceRPCConnectorTemplateDialogComponent extends DialogComponent<GatewayServiceRPCConnectorTemplateDialogComponent, boolean> {
config: {
[key: string]: any;
};
templates: Array<RPCTemplate>;
templateNameCtrl: FormControl;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: SaveRPCTemplateData,
public dialogRef: MatDialogRef<GatewayServiceRPCConnectorTemplateDialogComponent, boolean>,
public fb: FormBuilder) {
super(store, router, dialogRef);
this.config = this.data.config;
this.templates = this.data.templates;
this.templateNameCtrl = this.fb.control('', [Validators.required])
}
validateDuplicateName(c: UntypedFormControl) {
const name = c.value.trim();
return !!this.templates.find((template) => template.name === name);
};
close(): void {
this.dialogRef.close();
}
save(): void {
this.dialogRef.close(this.templateNameCtrl.value);
}
}

View File

@ -22,13 +22,10 @@
{{template.name}}
</mat-panel-title>
<mat-panel-description>
<button mat-icon-button matTooltip="Delete" (click)="deleteTemplate($event)">
<button mat-icon-button matTooltip="Delete" (click)="deleteTemplate($event, template)">
<mat-icon class="material-icons">delete</mat-icon>
</button>
<button mat-icon-button matTooltip="Copy" (click)="copyTemplate($event)">
<mat-icon class="material-icons">content_copy</mat-icon>
</button>
<button mat-icon-button matTooltip="Use" (click)="useTemplate($event)">
<button mat-icon-button matTooltip="Use" (click)="applyTemplate($event, template)">
<mat-icon class="material-icons">play_arrow</mat-icon>
</button>
</mat-panel-description>
@ -37,13 +34,15 @@
<ng-container
*ngFor="let config of template.config | keyvalue : originalOrder"
[ngTemplateOutlet]="RPCTemplateRef"
[ngTemplateOutletContext]="{ $implicit: config, padding: false }">
[ngTemplateOutletContext]="{ $implicit: config, innerValue: false }">
</ng-container>
<ng-template #RPCTemplateRef let-config let-padding='padding'>
<ng-template #RPCTemplateRef let-config let-innerValue='innerValue'>
<div [fxLayout]="isObject(config.value) ? 'column': 'row'"
[fxLayoutAlign]="!isObject(config.value) ? 'space-between center' : ''"
[ngStyle]="{'padding-left': padding ? '16px': '0'}">
<div class="template-key">{{config.key}}</div>
[ngStyle]="{'padding-left': innerValue ? '16px': '0'}">
<div class="template-key">
{{!innerValue ? ('gateway.rpc.' + config.key | translate) : config.key}}
</div>
<div *ngIf="!isObject(config.value) else RPCObjectRow"
[ngClass]="{'boolean-true': config.value === true,
'boolean-false': config.value === false }">
@ -52,7 +51,7 @@
<ng-container
*ngFor="let subConfig of config.value | keyvalue : originalOrder"
[ngTemplateOutlet]="RPCTemplateRef"
[ngTemplateOutletContext]="{ $implicit: subConfig, padding: true }">
[ngTemplateOutletContext]="{ $implicit: subConfig, innerValue: true }">
</ng-container>
</ng-template>
</div>

View File

@ -50,6 +50,11 @@
background-color: rgba(25, 128, 56, 0.08);
}
mat-expansion-panel {
margin-top: 10px;
overflow: visible;
}
.mat-expansion-panel-header-description {
flex-direction: row-reverse;
align-items: center;

View File

@ -15,9 +15,16 @@
///
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ConnectorType, RPCTemplate } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { TranslateService } from '@ngx-translate/core';
import {
ConnectorType,
RPCTemplate
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { KeyValue } from '@angular/common';
import { EntityType } from '@shared/models/entity-type.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'tb-gateway-service-rpc-connector-templates',
@ -29,52 +36,52 @@ export class GatewayServiceRPCConnectorTemplatesComponent implements OnInit {
@Input()
connectorType: ConnectorType;
@Input()
ctx: WidgetContext;
@Output()
saveTemplate: EventEmitter<any> = new EventEmitter();
rpcTemplates: Array<RPCTemplate> = [];
@Output()
useTemplate: EventEmitter<any> = new EventEmitter();
constructor(private translate: TranslateService) {
this.rpcTemplates.push(
{
name: 'Test Template',
config: {
fieldString: 'string',
fieldNumber: 666,
fieldBool: true,
fieldArray: [111, 222, 333, "String", "444"],
fieldObj: {
subKey1: 'dasd',
subKey2: 666,
}
}
}
)
}
@Input()
rpcTemplates: Array<RPCTemplate>;
constructor(private attributeService: AttributeService) {
}
ngOnInit() {
}
public useTemplate($event: Event): void {
public applyTemplate($event: Event, template: RPCTemplate): void {
$event.stopPropagation();
console.log("useTemplate")
this.useTemplate.emit(template);
}
public copyTemplate($event: Event): void {
public deleteTemplate($event: Event, template: RPCTemplate): void {
$event.stopPropagation();
console.log("copyTemplate")
const index = this.rpcTemplates.findIndex(data => {
return data.name == template.name;
})
this.rpcTemplates.splice(index, 1);
const key = `${this.connectorType}_template`;
this.attributeService.saveEntityAttributes(
{
id: this.ctx.defaultSubscription.targetDeviceId,
entityType: EntityType.DEVICE
}
, AttributeScope.SERVER_SCOPE, [{
key,
value: this.rpcTemplates
}]).subscribe(() => {
})
}
public deleteTemplate($event: Event): void {
$event.stopPropagation();
console.log("deleteTemplate")
}
public originalOrder = (a: KeyValue<string, any>, b: KeyValue<string, any>): number => {
public originalOrder = (a, b): number => {
return 0;
}
public isObject(value: any) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

View File

@ -16,7 +16,8 @@
-->
<div fxLayout="column" class="command-form" [formGroup]="commandForm">
<div class="mat-subtitle-1 title">{{ 'gateway.rpc.title' | translate: {type: gatewayConnectorDefaultTypesTranslates.get(connectorType)} }}</div>
<div
class="mat-subtitle-1 title">{{ 'gateway.rpc.title' | translate: {type: gatewayConnectorDefaultTypesTranslates.get(connectorType)} }}</div>
<ng-template [ngIf]="connectorType">
<ng-container [ngSwitch]="connectorType">
<ng-template [ngSwitchCase]="ConnectorType.MQTT">
@ -26,27 +27,26 @@
placeholder="echo"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.request-topic-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.requestTopicExpression' | translate }}</mat-label>
<input matInput formControlName="requestTopicExpression"
placeholder="sensor/${deviceName}/request/${methodName}/${requestId}"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.response-topic-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.responseTopicExpression' | translate }}</mat-label>
<input matInput formControlName="responseTopicExpression"
placeholder="sensor/${deviceName}/response/${methodName}/${requestId}"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.response-timeout' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.responseTimeout' | translate }}</mat-label>
<input matInput formControlName="responseTimeout" type="number"
placeholder="10000" min="10" step="1"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.value-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.valueExpression' | translate }}</mat-label>
<input matInput formControlName="valueExpression"
placeholder="${params}"/>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.MODBUS">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.tag' | translate }}</mat-label>
@ -62,7 +62,7 @@
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.function-code' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.functionCode' | translate }}</mat-label>
<mat-select formControlName="functionCode">
<mat-option *ngFor="let code of codesArray" [value]="code">
{{ code }}
@ -77,20 +77,19 @@
placeholder="1" step="1"/>
</mat-form-field>
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.objects-count' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.objectsCount' | translate }}</mat-label>
<input matInput formControlName="objectsCount" type="number" min="0"
placeholder="31" step="1"/>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.BACNET">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-rpc' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
<input matInput formControlName="method" placeholder="set_state"/>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label>{{ 'gateway.rpc.request-type' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.requestType' | translate }}</mat-label>
<mat-select formControlName="requestType">
<mat-option *ngFor="let type of bACnetRequestTypes" [value]="type">
{{bACnetRequestTypesTranslates.get(type) | translate}}
@ -98,13 +97,13 @@
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.request-timeout' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.requestTimeout' | translate }}</mat-label>
<input matInput formControlName="requestTimeout" type="number"
min="10" step="1" placeholder="1000"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.object-type' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.objectType' | translate }}</mat-label>
<mat-select formControlName="objectType">
<mat-option *ngFor="let type of bACnetObjectTypes" [value]="type">
{{bACnetObjectTypesTranslates.get(type) | translate}}
@ -118,22 +117,21 @@
</mat-form-field>
</div>
<mat-form-field class="mat-block">
<mat-label>{{ 'gateway.rpc.property-id' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.propertyId' | translate }}</mat-label>
<input matInput formControlName="propertyId" placeholder="presentValue"/>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.BLE">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-rpc' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
<input matInput formControlName="methodRPC" placeholder="rpcMethod1"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.characteristic-uuid' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.characteristicUUID' | translate }}</mat-label>
<input matInput formControlName="characteristicUUID" placeholder="00002A00-0000-1000-8000-00805F9B34FB"/>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label>{{ 'gateway.rpc.method-processing' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodProcessing' | translate }}</mat-label>
<mat-select formControlName="methodProcessing">
<mat-option *ngFor="let type of bLEMethods" [value]="type">
{{bLEMethodsTranslates.get(type) | translate}}
@ -141,35 +139,34 @@
</mat-select>
</mat-form-field>
<mat-slide-toggle class="mat-slide" formControlName="withResponse">
{{ 'gateway.rpc.with-response' | translate }}
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.CAN">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-rpc' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
<input matInput formControlName="method" placeholder="sendSameData"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.node-id' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.nodeID' | translate }}</mat-label>
<input matInput formControlName="nodeID" type="number" placeholder="4" min="0" step="1"/>
</mat-form-field>
<mat-slide-toggle class="mat-slide margin" formControlName="isExtendedID">
{{ 'gateway.rpc.is-extended-id' | translate }}
{{ 'gateway.rpc.isExtendedID' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide margin" formControlName="isFD">
{{ 'gateway.rpc.is-fd' | translate }}
{{ 'gateway.rpc.isFD' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide margin" formControlName="bitrateSwitch">
{{ 'gateway.rpc.bitrate-switch' | translate }}
{{ 'gateway.rpc.bitrateSwitch' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.data-length' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.dataLength' | translate }}</mat-label>
<input matInput formControlName="dataLength" type="number" placeholder="2" min="1" step="1"/>
</mat-form-field>
<mat-form-field class="mat-block" fxFlex="50">
<mat-label>{{ 'gateway.rpc.data-byte-order' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.dataByteorder' | translate }}</mat-label>
<mat-select formControlName="dataByteorder">
<mat-option *ngFor="let order of cANByteOrders" [value]="order">
{{ order | translate }}
@ -179,59 +176,55 @@
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.data-before' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.dataBefore' | translate }}</mat-label>
<input matInput formControlName="dataBefore" placeholder="00AA"/>
</mat-form-field>
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.data-after' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.dataAfter' | translate }}</mat-label>
<input matInput formControlName="dataAfter" placeholder="0102"/>
</mat-form-field>
</div>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.data-in-hex' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.dataInHEX' | translate }}</mat-label>
<input matInput formControlName="dataInHEX"
placeholder="aa bb cc dd ee ff aa bb aa bb cc d ee ff"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.data-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.dataExpression' | translate }}</mat-label>
<input matInput formControlName="dataExpression"
placeholder="userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed"/>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.FTP">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-filter' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodFilter' | translate }}</mat-label>
<input matInput formControlName="methodFilter" placeholder="read"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.value-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.valueExpression' | translate }}</mat-label>
<input matInput formControlName="valueExpression" placeholder="${params}"/>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.OCPP">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-rpc' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
<input matInput formControlName="methodRPC" placeholder="rpc1"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.value-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.valueExpression' | translate }}</mat-label>
<input matInput formControlName="valueExpression" placeholder="${params}"/>
</mat-form-field>
<mat-slide-toggle class="mat-slide margin" formControlName="withResponse">
{{ 'gateway.rpc.with-response' | translate }}
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.SOCKET">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-rpc' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
<input matInput formControlName="methodRPC" placeholder="rpcMethod1"/>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label>{{ 'gateway.rpc.method-processing' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodProcessing' | translate }}</mat-label>
<mat-select formControlName="methodProcessing">
<mat-option *ngFor="let method of socketMethodProcessings" [value]="method">
{{ SocketMethodProcessingsTranslates.get(method) | translate }}
@ -247,27 +240,25 @@
</mat-select>
</mat-form-field>
<mat-slide-toggle class="mat-slide margin" formControlName="withResponse">
{{ 'gateway.rpc.with-response' | translate }}
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.XMPP">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-rpc' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
<input matInput formControlName="methodRPC" placeholder="rpc1"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.value-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.valueExpression' | translate }}</mat-label>
<input matInput formControlName="valueExpression" placeholder="${params}"/>
</mat-form-field>
<mat-slide-toggle class="mat-slide margin" formControlName="withResponse">
{{ 'gateway.rpc.with-response' | translate }}
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.SNMP">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.request-filter' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.requestFilter' | translate }}</mat-label>
<input matInput formControlName="requestFilter" placeholder="setData"/>
</mat-form-field>
<mat-form-field class="mat-block">
@ -298,15 +289,14 @@
</button>
</fieldset>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.REST">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-filter' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodFilter' | translate }}</mat-label>
<input matInput formControlName="methodFilter" placeholder="post_attributes"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field class="mat-block" fxFlex="33">
<mat-label>{{ 'gateway.rpc.http-method' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.HTTPMethod' | translate }}</mat-label>
<mat-select formControlName="HTTPMethod">
<mat-option *ngFor="let method of hTTPMethods" [value]="method">
{{ method }}
@ -314,14 +304,14 @@
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.rpc.request-url' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.requestUrlExpression' | translate }}</mat-label>
<input matInput formControlName="requestUrlExpression"
placeholder="http://127.0.0.1:5000/my_devices"/>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="33">
<mat-label>{{ 'gateway.rpc.response-timeout' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.responseTimeout' | translate }}</mat-label>
<input matInput formControlName="responseTimeout" type="number"
step="1" min="10" placeholder="10"/>
</mat-form-field>
@ -337,11 +327,11 @@
</mat-form-field>
</div>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.value-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.valueExpression' | translate }}</mat-label>
<input matInput formControlName="valueExpression" placeholder="${params}"/>
</mat-form-field>
<fieldset class="fields border" fxLayout="column" fxLayoutGap="10px" formArrayName="httpHeaders">
<span class="fields-label">{{ 'gateway.rpc.http-headers' | translate }}</span>
<span class="fields-label">{{ 'gateway.rpc.httpHeaders' | translate }}</span>
<div class="border" fxLayout="column" fxLayoutGap="10px" *ngIf="getFormArrayControls('httpHeaders').length">
<div fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center">
<span fxFlex class="title">{{ 'gateway.rpc.header-name' | translate }}</span>
@ -405,15 +395,14 @@
</button>
</fieldset>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.REQUEST">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-filter' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.methodFilter' | translate }}</mat-label>
<input matInput formControlName="methodFilter" placeholder="echo"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field class="mat-block" fxFlex="33">
<mat-label>{{ 'gateway.rpc.method' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.httpMethod' | translate }}</mat-label>
<mat-select formControlName="httpMethod">
<mat-option *ngFor="let method of hTTPMethods" [value]="method">
{{ method }}
@ -421,13 +410,13 @@
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.rpc.request-url' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.requestUrlExpression' | translate }}</mat-label>
<input matInput formControlName="requestUrlExpression" placeholder="http://127.0.0.1:5000/my_devices"/>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="33">
<mat-label>{{ 'gateway.rpc.response-timeout' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.responseTimeout' | translate }}</mat-label>
<input matInput formControlName="responseTimeout" type="number"
step="1" min="10" placeholder="10"/>
</mat-form-field>
@ -443,15 +432,15 @@
</mat-form-field>
</div>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.request-value-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.requestValueExpression' | translate }}</mat-label>
<input matInput formControlName="requestValueExpression" placeholder="${params}"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.response-value-expression' | translate }}</mat-label>
<mat-label>{{ 'gateway.rpc.responseValueExpression' | translate }}</mat-label>
<input matInput formControlName="responseValueExpression" placeholder="${temp}"/>
</mat-form-field>
<fieldset class="fields border" fxLayout="column" fxLayoutGap="10px" formArrayName="httpHeaders">
<span class="fields-label">{{ 'gateway.rpc.http-headers' | translate }}</span>
<span class="fields-label">{{ 'gateway.rpc.httpHeaders' | translate }}</span>
<div class="border" fxLayout="column" fxLayoutGap="10px" *ngIf="getFormArrayControls('httpHeaders').length">
<div fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center">
<span fxFlex class="title">{{ 'gateway.rpc.header-name' | translate }}</span>
@ -483,7 +472,6 @@
</button>
</fieldset>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.OPCUA_ASYNCIO">
<ng-container *ngTemplateOutlet="OPCUAForm"></ng-container>
</ng-template>
@ -513,11 +501,27 @@
</button>
</fieldset>
</ng-template>
<ng-template ngSwitchDefault>
{{'dsadas'}}
<mat-form-field>
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<input matInput formControlName="command"/>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'widget-config.datasource-parameters' | translate }}</mat-label>
<input matInput formControlName="params" type="JSON"/>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
matIconSuffix style="cursor:pointer;"
(click)="openEditJSONDialog($event)"
matTooltip="{{ 'gateway.rpc-command-edit-params' | translate }}">edit
</mat-icon>
</mat-form-field>
</ng-template>
</ng-container>
</ng-template>
<div class="template-actions" fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="10px">
<button mat-raised-button
(click)="sendCommand.emit()"
(click)="save()"
[disabled]="commandForm.invalid">
{{ 'gateway.rpc-command-save-template' | translate }}
</button>

View File

@ -14,27 +14,57 @@
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import {
BACnetObjectTypes, BACnetObjectTypesTranslates,
BACnetRequestTypes, BACnetRequestTypesTranslates, BLEMethods, BLEMethodsTranslates,
ControlValueAccessor,
FormArray,
FormBuilder,
FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
Validators
} from '@angular/forms';
import {
BACnetObjectTypes,
BACnetObjectTypesTranslates,
BACnetRequestTypes,
BACnetRequestTypesTranslates,
BLEMethods,
BLEMethodsTranslates,
CANByteOrders,
ConnectorType, GatewayConnectorDefaultTypesTranslates, HTTPMethods,
ConnectorType,
GatewayConnectorDefaultTypesTranslates,
HTTPMethods,
ModbusCommandTypes,
RPCCommand,
SNMPMethods, SNMPMethodsTranslations,
RPCTemplateConfig,
SNMPMethods,
SNMPMethodsTranslations,
SocketEncodings,
SocketMethodProcessings, SocketMethodProcessingsTranslates
SocketMethodProcessings,
SocketMethodProcessingsTranslates
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { TranslateService } from "@ngx-translate/core";
import { MatDialog } from '@angular/material/dialog';
import {
JsonObjectEditDialogComponent,
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-gateway-service-rpc-connector',
templateUrl: './gateway-service-rpc-connector.component.html',
styleUrls: ['./gateway-service-rpc-connector.component.scss']
styleUrls: ['./gateway-service-rpc-connector.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GatewayServiceRPCConnectorComponent),
multi: true
}
]
})
export class GatewayServiceRPCConnectorComponent implements OnInit {
export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValueAccessor {
@Input()
connectorType: ConnectorType;
@ -42,6 +72,9 @@ export class GatewayServiceRPCConnectorComponent implements OnInit {
@Output()
sendCommand: EventEmitter<RPCCommand> = new EventEmitter();
@Output()
saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter();
commandForm: FormGroup;
codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16];
@ -74,14 +107,41 @@ export class GatewayServiceRPCConnectorComponent implements OnInit {
'(\\#[-a-z\\d_]*)?$', // fragment locator
'i'
);
private propagateChange = (v: any) => {
}
constructor(private fb: FormBuilder,
private translate: TranslateService) {
private dialog: MatDialog,) {
}
ngOnInit() {
this.commandForm = this.connectorParamsFormGroupByType(this.connectorType)
this.commandForm = this.connectorParamsFormGroupByType(this.connectorType);
this.commandForm.valueChanges.subscribe(value => {
const httpHeaders = {};
const security = {};
switch (this.connectorType) {
case ConnectorType.REST:
value.httpHeaders.forEach(data => {
httpHeaders[data.headerName] = data.value;
})
value.httpHeaders = httpHeaders;
value.security.forEach(data => {
security[data.securityName] = data.value;
})
value.security = security;
break;
case ConnectorType.REQUEST:
value.httpHeaders.forEach(data => {
httpHeaders[data.headerName] = data.value;
})
value.httpHeaders = httpHeaders;
break;
}
if (this.commandForm.valid) {
this.propagateChange({...this.commandForm.value,...value});
}
})
}
connectorParamsFormGroupByType(type: ConnectorType): FormGroup {
@ -206,6 +266,13 @@ export class GatewayServiceRPCConnectorComponent implements OnInit {
method: [null, [Validators.required]],
arguments: this.fb.array([]),
})
break;
default:
formGroup = this.fb.group({
command: [null, [Validators.required]],
params: ['{}', [jsonRequired]],
})
}
return formGroup;
}
@ -213,7 +280,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit {
addSNMPoid(value: string = null) {
const oidsFA = this.commandForm.get('oid') as FormArray;
if (oidsFA) {
oidsFA.push(this.fb.control(value, [Validators.required]));
oidsFA.push(this.fb.control(value, [Validators.required]), {emitEvent: false});
}
}
@ -229,7 +296,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit {
value: [value.value, [Validators.required]]
})
if (headerFA) {
headerFA.push(formGroup);
headerFA.push(formGroup, {emitEvent: false});
}
}
@ -245,7 +312,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit {
value: [value.value, [Validators.required]]
})
if (securityFA) {
securityFA.push(formGroup);
securityFA.push(formGroup, {emitEvent: false});
}
}
@ -261,7 +328,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit {
addOCPUAArguments(value: string = null) {
const oidsFA = this.commandForm.get('arguments') as FormArray;
if (oidsFA) {
oidsFA.push(this.fb.control(value));
oidsFA.push(this.fb.control(value), {emitEvent: false});
}
}
@ -269,4 +336,86 @@ export class GatewayServiceRPCConnectorComponent implements OnInit {
const oidsFA = this.commandForm.get('arguments') as FormArray;
oidsFA.removeAt(index);
}
openEditJSONDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: JSON.parse(this.commandForm.get('params').value),
required: true
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.commandForm.get('params').setValue(JSON.stringify(res));
}
}
);
}
save() {
this.saveTemplate.emit();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
clearFromArrayByName(name: string) {
const formArray = this.commandForm.get(name) as FormArray;
while (formArray.length !== 0) {
formArray.removeAt(0)
}
}
writeValue(value: RPCTemplateConfig): void {
if (typeof value == "object") {
value = deepClone(value);
switch (this.connectorType) {
case ConnectorType.SNMP:
this.clearFromArrayByName("oids");
value.oids.forEach(value => {
this.addSNMPoid(value)
})
delete value.oids;
break;
case ConnectorType.REQUEST:
this.clearFromArrayByName("httpHeaders");
value.httpHeaders && Object.entries(value.httpHeaders).forEach(httpHeader => {
this.addHTTPHeader({headerName: httpHeader[0], value: httpHeader[1] as string})
})
delete value.httpHeaders;
break;
case ConnectorType.REST:
this.clearFromArrayByName("httpHeaders");
this.clearFromArrayByName("security");
value.security && Object.entries(value.security).forEach(securityHeader => {
this.addHTTPSecurity({securityName: securityHeader[0], value: securityHeader[1] as string})
})
delete value.security;
value.httpHeaders && Object.entries(value.httpHeaders).forEach(httpHeader => {
this.addHTTPHeader({headerName: httpHeader[0], value: httpHeader[1] as string})
})
delete value.httpHeaders;
break;
case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
this.clearFromArrayByName("arguments");
value.arguments.forEach(value => {
this.addOCPUAArguments(value)
})
delete value.arguments;
break;
}
this.commandForm.patchValue(value, {onlySelf: false});
}
}
}

View File

@ -41,7 +41,8 @@
</button>
</ng-container>
<ng-template #connectorForm>
<tb-gateway-service-rpc-connector [connectorType]="connectorType" (sendCommand)="sendCommand()"/>
<tb-gateway-service-rpc-connector formControlName="params" [connectorType]="connectorType"
(sendCommand)="sendCommand()" (saveTemplate)="saveTemplate()"/>
</ng-template>
</div>
<section class="result-block" [formGroup]="commandForm">
@ -52,4 +53,6 @@
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</section>
</div>
<tb-gateway-service-rpc-connector-templates fxFlex="30" *ngIf="isConnector" class="border"></tb-gateway-service-rpc-connector-templates>
<tb-gateway-service-rpc-connector-templates fxFlex="30" *ngIf="isConnector" class="border" [rpcTemplates]="templates"
[ctx]="ctx" [connectorType]="connectorType" (useTemplate)="useTemplate($event)">
</tb-gateway-service-rpc-connector-templates>

View File

@ -14,17 +14,28 @@
/// limitations under the License.
///
import { OnInit, Component, Input } from '@angular/core';
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants';
import {
JsonObjectEditDialogComponent,
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { ConnectorType, RPCCommand } from '@home/components/widget/lib/gateway/gateway-widget.models';
import {
ConnectorType,
RPCCommand,
RPCTemplate,
RPCTemplateConfig,
SaveRPCTemplateData
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import {
GatewayServiceRPCConnectorTemplateDialogComponent
} from '@home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
@Component({
selector: 'tb-gateway-service-rpc',
@ -58,9 +69,27 @@ export class GatewayServiceRPCComponent implements OnInit {
];
public connectorType: ConnectorType;
public templates: Array<RPCTemplate> = [];
private subscription: IWidgetSubscription;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
this.updateTemplates()
}),
onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => {
this.onDataUpdateError(e);
}),
dataLoading: () => {
}
}
};
constructor(private fb: FormBuilder,
private dialog: MatDialog) {
private dialog: MatDialog,
private utils: UtilsService,
private cd: ChangeDetectorRef,
private attributeService: AttributeService) {
this.commandForm = this.fb.group({
command: [null, [Validators.required]],
time: [60, [Validators.required, Validators.min(1)]],
@ -75,6 +104,17 @@ export class GatewayServiceRPCComponent implements OnInit {
this.commandForm.get('command').setValue(this.RPCCommands[0]);
} else {
this.connectorType = this.ctx.stateController.getStateParams().connector_rpc.value.type;
const subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: this.ctx.defaultSubscription.targetDeviceId,
entityName: 'Connector',
attributes: [{name: `${this.connectorType}_template`}]
}];
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo,
this.subscriptionOptions, false, true).subscribe(subscription => {
this.subscription = subscription;
})
}
}
@ -82,7 +122,9 @@ export class GatewayServiceRPCComponent implements OnInit {
this.resultTime = null;
const formValues = value || this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_';
this.ctx.controlApi.sendTwoWayCommand(commandPrefix + formValues.command.toLowerCase(), formValues.params, formValues.time).subscribe({
const command = !this.isConnector ? formValues.command.toLowerCase() : this.getCommandFromParamsByType(formValues.params);
const params = formValues.params;
this.ctx.controlApi.sendTwoWayCommand(commandPrefix + command, params, formValues.time).subscribe({
next: resp => {
this.resultTime = new Date().getTime();
this.commandForm.get('result').setValue(JSON.stringify(resp))
@ -95,23 +137,86 @@ export class GatewayServiceRPCComponent implements OnInit {
});
}
openEditJSONDialog($event: Event) {
if ($event) {
$event.stopPropagation();
getCommandFromParamsByType(params: RPCTemplateConfig) {
switch (this.connectorType) {
case ConnectorType.MQTT:
case ConnectorType.FTP:
case ConnectorType.SNMP:
case ConnectorType.REST:
case ConnectorType.REQUEST:
return params.methodFilter;
case ConnectorType.MODBUS:
return params.tag;
case ConnectorType.BACNET:
case ConnectorType.CAN:
case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
return params.method;
case ConnectorType.BLE:
case ConnectorType.OCPP:
case ConnectorType.SOCKET:
case ConnectorType.XMPP:
return params.methodRPC;
default:
return params.command;
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
}
saveTemplate() {
this.dialog.open<GatewayServiceRPCConnectorTemplateDialogComponent, SaveRPCTemplateData>
(GatewayServiceRPCConnectorTemplateDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: JSON.parse(this.commandForm.get('params').value),
required: true
}
data: {config: this.commandForm.value.params, templates: this.templates}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.commandForm.get('params').setValue(JSON.stringify(res));
const templateAttribute: RPCTemplate = {
name: res,
config: this.commandForm.value.params
}
const templatesArray = this.templates;
const existingIndex = templatesArray.findIndex(template=>{
return template.name == templateAttribute.name;
})
if (existingIndex > -1 ){
templatesArray.splice(existingIndex, 1)
}
templatesArray.push(templateAttribute)
const key = `${this.connectorType}_template`;
this.attributeService.saveEntityAttributes(
{
id: this.ctx.defaultSubscription.targetDeviceId,
entityType: EntityType.DEVICE
}
, AttributeScope.SERVER_SCOPE, [{
key,
value: templatesArray
}]).subscribe(() => {
})
}
);
}
useTemplate($event) {
this.commandForm.get('params').patchValue($event.config);
}
private updateTemplates() {
this.templates = this.subscription.data[0].data[0][1].length ?
JSON.parse(this.subscription.data[0].data[0][1]) : [];
if (this.templates.length && this.commandForm.get('params').value == "{}") {
this.commandForm.get('params').patchValue(this.templates[0].config);
}
this.cd.detectChanges();
}
private onDataUpdateError(e: any) {
const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name;
if (exceptionData.message) {
errorText += ': ' + exceptionData.message;
}
console.error(errorText);
}
}

View File

@ -261,10 +261,17 @@ export enum SocketEncodings {
}
export interface RPCTemplate {
name: string;
config: {
name?: string;
config: RPCTemplateConfig;
}
export interface RPCTemplateConfig {
[key: string]: any;
};
}
export interface SaveRPCTemplateData {
config: RPCTemplateConfig,
templates: Array<RPCTemplate>
}
export interface LogLink {

View File

@ -74,6 +74,9 @@ import { ValueChartCardWidgetComponent } from '@home/components/widget/lib/cards
import { ProgressBarWidgetComponent } from '@home/components/widget/lib/cards/progress-bar-widget.component';
import { LiquidLevelWidgetComponent } from '@home/components/widget/lib/indicator/liquid-level-widget.component';
import { DoughnutWidgetComponent } from '@home/components/widget/lib/chart/doughnut-widget.component';
import {
GatewayServiceRPCConnectorTemplateDialogComponent
} from '@home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog';
@NgModule({
declarations:
@ -107,6 +110,7 @@ import { DoughnutWidgetComponent } from '@home/components/widget/lib/chart/dough
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
@ -154,6 +158,7 @@ import { DoughnutWidgetComponent } from '@home/components/widget/lib/chart/dough
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,

View File

@ -2776,54 +2776,54 @@
"rpc": {
"title": "{{type}} Connector RPC parameters",
"templates-title": "Connector RPC Templates",
"method-filter": "Method filter",
"request-topic-expression": "Request topic expression",
"response-topic-expression": "Response topic expression",
"response-timeout": "Response Time",
"value-expression": "Value Expression",
"methodFilter": "Method filter",
"requestTopicExpression": "Request topic expression",
"responseTopicExpression": "Response topic expression",
"responseTimeout": "Response Time",
"valueExpression": "Value Expression",
"tag": "Tag",
"type": "Type",
"function-code": "Function Code",
"objects-count": "Objects Count",
"functionCode": "Function Code",
"objectsCount": "Objects Count",
"address": "Address",
"method": "Method",
"request-type": "Request Type",
"request-timeout": "Request Timeout",
"object-type": "Object type",
"requestType": "Request Type",
"requestTimeout": "Request Timeout",
"objectType": "Object type",
"identifier": "Identifier",
"property-id": "Property ID",
"method-rpc": "Method RPC name",
"with-response": "With Response",
"characteristic-uuid": "Characteristic UUID",
"method-processing": "Method Processing",
"node-id": "Node ID",
"is-extended-id": "Is Extended ID",
"is-fd": "Is FD",
"bitrate-switch": "Bitrate Switch",
"data-in-hex": "Data In HEX",
"data-length": "Data Length",
"data-byte-order": "Data Byte Order",
"data-before": "Data Before",
"data-after": "Data After",
"data-expression": "Data Expression",
"propertyId": "Property ID",
"methodRPC": "Method RPC name",
"withResponse": "With Response",
"characteristicUUID": "Characteristic UUID",
"methodProcessing": "Method Processing",
"nodeID": "Node ID",
"isExtendedID": "Is Extended ID",
"isFD": "Is FD",
"bitrateSwitch": "Bitrate Switch",
"dataInHEX": "Data In HEX",
"dataLength": "Data Length",
"dataByteorder": "Data Byte Order",
"dataBefore": "Data Before",
"dataAfter": "Data After",
"dataExpression": "Data Expression",
"encoding": "Encoding",
"oid": "OID",
"add-oid": "Add OID",
"add-header": "Add header",
"add-security": "Add security",
"remove": "Remove",
"request-filter": "Request Filter",
"request-url": "Request URL Expression",
"http-method": "HTTP Method",
"requestFilter": "Request Filter",
"requestUrlExpression": "Request URL Expression",
"HTTPMethod": "HTTP Method",
"timeout": "Timeout",
"tries": "Tries",
"http-headers": "HTTP Headers",
"httpHeaders": "HTTP Headers",
"header-name": "Header name",
"security-name": "Security name",
"value": "Value",
"security": "Security",
"response-value-expression": "Response Value Expression",
"request-value-expression": "Request Value Expression",
"responseValueExpression": "Response Value Expression",
"requestValueExpression": "Request Value Expression",
"arguments": "Arguments",
"add-argument": "Add argument",
"write-property": "Write property",
@ -2843,7 +2843,12 @@
"multi-get": "Multiget",
"get-next": "Get next",
"bulk-get": "Bulk get",
"walk": "Walk"
"walk": "Walk",
"save-template": "Save template",
"template-name": "Template name",
"template-name-required": "Template name is required.",
"template-name-duplicate": "The name is already used."
},
"other": "Other",