Browse Source

Merge pull request #10011 from iraznatovskyi/connectors-rpc-templates

Create gateway-service-rpc-connector and fix bugs
pull/10330/head
Igor Kulikov 3 years ago
committed by GitHub
parent
commit
7b1bdc4b54
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  2. 7
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  3. 54
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.html
  4. 63
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.ts
  5. 60
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.html
  6. 94
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.scss
  7. 82
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.ts
  8. 542
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html
  9. 67
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.scss
  10. 428
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  11. 89
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html
  12. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss
  13. 157
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  14. 199
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  15. 15
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  16. 91
      ui-ngx/src/assets/locale/locale.constant-en_US.json

5
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss

@ -104,8 +104,3 @@
background-color: rgb(25, 128, 56); background-color: rgb(25, 128, 56);
} }
} }
:host ::ng-deep tb-json-object-edit > div {
flex-grow: 1;
}

7
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -40,6 +40,7 @@ import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { import {
ConnectorType,
GatewayConnector, GatewayConnector,
GatewayConnectorDefaultTypesTranslates, GatewayConnectorDefaultTypesTranslates,
GatewayLogLevel GatewayLogLevel
@ -213,10 +214,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
saveConnector(): void { saveConnector(): void {
const value = this.connectorForm.value; const value = this.connectorForm.value;
value.configuration = camelCase(value.name) + '.json'; value.configuration = camelCase(value.name) + '.json';
if (value.type !== 'grpc') { if (value.type !== ConnectorType.GRPC) {
delete value.key; delete value.key;
} }
if (value.type !== 'custom') { if (value.type !== ConnectorType.CUSTOM) {
delete value.class; delete value.class;
} }
value.ts = new Date().getTime(); value.ts = new Date().getTime();
@ -325,7 +326,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.initialConnector = null; this.initialConnector = null;
this.connectorForm.setValue({ this.connectorForm.setValue({
name: '', name: '',
type: 'mqtt', type: ConnectorType.MQTT,
logLevel: GatewayLogLevel.INFO, logLevel: GatewayLogLevel.INFO,
key: 'auto', key: 'auto',
class: '', class: '',

54
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2024 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>

63
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.ts

@ -0,0 +1,63 @@
///
/// Copyright © 2016-2024 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.templateNameCtrl.setValue(this.templateNameCtrl.value.trim());
if (this.templateNameCtrl.valid) this.dialogRef.close(this.templateNameCtrl.value);
}
}

60
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.html

@ -0,0 +1,60 @@
<!--
Copyright © 2016-2024 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.
-->
<div class="mat-subtitle-1 title">{{ 'gateway.rpc.templates-title' | translate }}</div>
<mat-expansion-panel hideToggle *ngFor="let template of rpcTemplates">
<mat-expansion-panel-header>
<mat-panel-title>
{{template.name}}
</mat-panel-title>
<mat-panel-description>
<button mat-icon-button matTooltip="Delete" (click)="deleteTemplate($event, template)">
<mat-icon class="material-icons">delete</mat-icon>
</button>
<button mat-icon-button matTooltip="Use" (click)="applyTemplate($event, template)">
<mat-icon class="material-icons">play_arrow</mat-icon>
</button>
</mat-panel-description>
</mat-expansion-panel-header>
<ng-container
*ngFor="let config of template.config | keyvalue : originalOrder"
[ngTemplateOutlet]="RPCTemplateRef"
[ngTemplateOutletContext]="{ $implicit: config, innerValue: false }">
</ng-container>
<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': innerValue ? '16px': '0'}"
class="rpc-params-row">
<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 }">
{{config.value}}</div>
<ng-template #RPCObjectRow>
<ng-container
*ngFor="let subConfig of config.value | keyvalue : originalOrder"
[ngTemplateOutlet]="RPCTemplateRef"
[ngTemplateOutletContext]="{ $implicit: subConfig, innerValue: true }">
</ng-container>
</ng-template>
</div>
</ng-template>
</mat-expansion-panel>

94
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.scss

@ -0,0 +1,94 @@
/**
* Copyright © 2016-2024 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.
*/
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 0;
.title {
font-weight: 500;
}
.template-key {
color: rgba(0, 0, 0, 0.38);
height: 32px;
line-height: 32px;
}
.boolean-true, .boolean-false {
border-radius: 3px;
height: 32px;
line-height: 32px;
padding: 0 12px;
width: fit-content;
font-size: 14px;
text-transform: capitalize;
}
.boolean-false {
color: #d12730;
background-color: rgba(209, 39, 48, 0.08);
}
.boolean-true {
color: #198038;
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;
margin-right: 0;
& > mat-icon {
margin-left: 15px;
color: rgba(0, 0, 0, 0.38);
}
}
.mat-expansion-panel-header {
padding: 0 0 0 12px;
&.mat-expansion-panel-header.mat-expanded {
height: 48px;
}
.mat-content.mat-content-hide-toggle {
margin-right: 0;
}
}
.rpc-params-row {
overflow: hidden;
white-space: nowrap;
& :not(:first-child) {
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
}
}

82
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.ts

@ -0,0 +1,82 @@
///
/// Copyright © 2016-2024 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, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
ConnectorType,
RPCTemplate
} from '@home/components/widget/lib/gateway/gateway-widget.models';
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 { isLiteralObject } from '@app/core/utils';
@Component({
selector: 'tb-gateway-service-rpc-connector-templates',
templateUrl: './gateway-service-rpc-connector-templates.component.html',
styleUrls: ['./gateway-service-rpc-connector-templates.component.scss']
})
export class GatewayServiceRPCConnectorTemplatesComponent implements OnInit {
@Input()
connectorType: ConnectorType;
@Input()
ctx: WidgetContext;
@Output()
saveTemplate: EventEmitter<any> = new EventEmitter();
@Output()
useTemplate: EventEmitter<any> = new EventEmitter();
@Input()
rpcTemplates: Array<RPCTemplate>;
public readonly originalOrder = (): number => 0;
public readonly isObject = (value: any) => isLiteralObject(value);
constructor(private attributeService: AttributeService) {
}
ngOnInit() {
}
public applyTemplate($event: Event, template: RPCTemplate): void {
$event.stopPropagation();
this.useTemplate.emit(template);
}
public deleteTemplate($event: Event, template: RPCTemplate): void {
$event.stopPropagation();
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(() => {
})
}
}

542
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html

@ -0,0 +1,542 @@
<!--
Copyright © 2016-2024 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.
-->
<div fxLayout="column" class="command-form" [formGroup]="commandForm">
<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">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.methodFilter' | translate }}</mat-label>
<input matInput formControlName="methodFilter"
placeholder="echo"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.requestTopicExpression' | translate }}</mat-label>
<input matInput formControlName="requestTopicExpression"
placeholder="sensor/${deviceName}/request/${methodName}/${requestId}"/>
</mat-form-field>
<mat-slide-toggle class="margin" (click)="$event.stopPropagation()" [formControl]="isMQTTWithResponse">
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
<mat-form-field *ngIf="isMQTTWithResponse.value">
<mat-label>{{ 'gateway.rpc.responseTopicExpression' | translate }}</mat-label>
<input matInput formControlName="responseTopicExpression"
placeholder="sensor/${deviceName}/response/${methodName}/${requestId}"/>
</mat-form-field>
<mat-form-field *ngIf="isMQTTWithResponse.value">
<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.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>
<input matInput formControlName="tag" placeholder="setValue"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of modbusCommandTypes" [value]="type">
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.functionCode' | translate }}</mat-label>
<mat-select formControlName="functionCode">
<mat-option *ngFor="let code of codesArray" [value]="code">
{{ modbusCodesTranslate.get(code) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-form-field *ngIf="commandForm.get('functionCode').value>4">
<mat-label>{{ 'gateway.rpc.value' | translate }}</mat-label>
<input matInput formControlName="value" placeholder="value"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.address' | translate }}</mat-label>
<input matInput formControlName="address" type="number" min="0"
placeholder="1" step="1"/>
</mat-form-field>
<mat-form-field fxFlex="50">
<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.methodRPC' | translate }}</mat-label>
<input matInput formControlName="method" placeholder="set_state"/>
</mat-form-field>
<mat-form-field class="mat-block">
<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}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<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.objectType' | translate }}</mat-label>
<mat-select formControlName="objectType">
<mat-option *ngFor="let type of bACnetObjectTypes" [value]="type">
{{bACnetObjectTypesTranslates.get(type) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.identifier' | translate }}</mat-label>
<input matInput formControlName="identifier" type="number"
min="1" step="1" placeholder="1"/>
</mat-form-field>
</div>
<mat-form-field class="mat-block">
<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.methodRPC' | translate }}</mat-label>
<input matInput formControlName="methodRPC" placeholder="rpcMethod1"/>
</mat-form-field>
<mat-form-field>
<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.methodProcessing' | translate }}</mat-label>
<mat-select formControlName="methodProcessing">
<mat-option *ngFor="let type of bLEMethods" [value]="type">
{{bLEMethodsTranslates.get(type) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-slide-toggle class="mat-slide" formControlName="withResponse">
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.CAN">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
<input matInput formControlName="method" placeholder="sendSameData"/>
</mat-form-field>
<mat-form-field>
<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.isExtendedID' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide margin" formControlName="isFD">
{{ 'gateway.rpc.isFD' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide margin" formControlName="bitrateSwitch">
{{ 'gateway.rpc.bitrateSwitch' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<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.dataByteorder' | translate }}</mat-label>
<mat-select formControlName="dataByteorder">
<mat-option *ngFor="let order of cANByteOrders" [value]="order">
{{ order | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<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.dataAfter' | translate }}</mat-label>
<input matInput formControlName="dataAfter" placeholder="0102"/>
</mat-form-field>
</div>
<mat-form-field>
<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.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.methodFilter' | translate }}</mat-label>
<input matInput formControlName="methodFilter" placeholder="read"/>
</mat-form-field>
<mat-form-field>
<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.methodRPC' | translate }}</mat-label>
<input matInput formControlName="methodRPC" placeholder="rpc1"/>
</mat-form-field>
<mat-form-field>
<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.withResponse' | translate }}
</mat-slide-toggle>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.SOCKET">
<mat-form-field>
<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.methodProcessing' | translate }}</mat-label>
<mat-select formControlName="methodProcessing">
<mat-option *ngFor="let method of socketMethodProcessings" [value]="method">
{{ SocketMethodProcessingsTranslates.get(method) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label>{{ 'gateway.rpc.encoding' | translate }}</mat-label>
<input matInput formControlName="encoding" placeholder="{{socketEncodings[0]}}"/>
</mat-form-field>
<mat-slide-toggle class="mat-slide margin" formControlName="withResponse">
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.XMPP">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
<input matInput formControlName="methodRPC" placeholder="rpc1"/>
</mat-form-field>
<mat-form-field>
<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.withResponse' | translate }}
</mat-slide-toggle>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.SNMP">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.requestFilter' | translate }}</mat-label>
<input matInput formControlName="requestFilter" placeholder="setData"/>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label>{{ 'gateway.rpc.method' | translate }}</mat-label>
<mat-select formControlName="method">
<mat-option *ngFor="let method of sNMPMethods" [value]="method">
{{ SNMPMethodsTranslations.get(method) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-slide-toggle class="mat-slide margin" formControlName="withResponse">
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
<fieldset class="fields border" fxLayout="column" fxLayoutGap="10px" formArrayName="oid">
<span class="fields-label">{{ 'gateway.rpc.oids' | translate }}*</span>
<div fxFlex fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center"
*ngFor="let control of getFormArrayControls('oid'); let i = index">
<mat-form-field class="tb-inline-field" appearance="outline" fxFlex subscriptSizing="dynamic">
<input matInput [formControl]="control" required/>
</mat-form-field>
<mat-icon style="cursor:pointer;"
fxFlex="30px"
(click)="removeSNMPoid(i)"
matTooltip="{{ 'gateway.rpc.remove' | translate }}">delete
</mat-icon>
</div>
<button mat-raised-button
fxFlexAlign="start"
(click)="addSNMPoid()">
{{ 'gateway.rpc.add-oid' | translate }}
</button>
</fieldset>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.REST">
<mat-form-field>
<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.httpMethod' | translate }}</mat-label>
<mat-select formControlName="httpMethod">
<mat-option *ngFor="let method of hTTPMethods" [value]="method">
{{ method }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<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.responseTimeout' | translate }}</mat-label>
<input matInput formControlName="responseTimeout" type="number"
step="1" min="10" placeholder="10"/>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.rpc.timeout' | translate }}</mat-label>
<input matInput formControlName="timeout" type="number"
step="1" min="10" placeholder="1000"/>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.rpc.tries' | translate }}</mat-label>
<input matInput formControlName="tries" type="number"
step="1" min="1" placeholder="3"/>
</mat-form-field>
</div>
<mat-form-field>
<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.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>
<span fxFlex class="title">{{ 'gateway.rpc.value' | translate }}</span>
<span fxFlex="30px"></span>
</div>
<mat-divider></mat-divider>
<div fxFlex fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center"
*ngFor="let control of getFormArrayControls('httpHeaders'); let i = index">
<ng-container [formGroupName]="i">
<mat-form-field appearance="outline" fxFlex>
<input matInput formControlName="headerName"/>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex>
<input matInput formControlName="value" placeholder="application/json"/>
</mat-form-field>
<mat-icon style="cursor:pointer;"
fxFlex="30px"
(click)="removeHTTPHeader(i)"
matTooltip="{{ 'gateway.rpc.remove' | translate }}">delete
</mat-icon>
</ng-container>
</div>
</div>
<button mat-raised-button
fxFlexAlign="start"
(click)="addHTTPHeader()">
{{ 'gateway.rpc.add-header' | translate }}
</button>
</fieldset>
<fieldset class="fields border" fxLayout="column" fxLayoutGap="10px" formArrayName="security">
<span class="fields-label">{{ 'gateway.rpc.security' | translate }}</span>
<div class="border" fxLayout="column" fxLayoutGap="10px" *ngIf="getFormArrayControls('security').length">
<div fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center">
<span fxFlex class="title">{{ 'gateway.rpc.security-name' | translate }}</span>
<span fxFlex class="title">{{ 'gateway.rpc.value' | translate }}</span>
<span fxFlex="30px"></span>
</div>
<mat-divider></mat-divider>
<div fxFlex fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center"
*ngFor="let control of getFormArrayControls('security'); let i = index">
<ng-container [formGroupName]="i">
<mat-form-field appearance="outline" fxFlex>
<input matInput formControlName="securityName"/>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex>
<input matInput formControlName="value" placeholder="anonymous"/>
</mat-form-field>
<mat-icon style="cursor:pointer;"
fxFlex="30px"
(click)="removeHTTPSecurity(i)"
matTooltip="{{ 'gateway.rpc.remove' | translate }}">delete
</mat-icon>
</ng-container>
</div>
</div>
<button mat-raised-button
fxFlexAlign="start"
(click)="addHTTPSecurity()">
{{ 'gateway.rpc.add-security' | translate }}
</button>
</fieldset>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.REQUEST">
<mat-form-field>
<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.httpMethod' | translate }}</mat-label>
<mat-select formControlName="httpMethod">
<mat-option *ngFor="let method of hTTPMethods" [value]="method">
{{ method }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<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.responseTimeout' | translate }}</mat-label>
<input matInput formControlName="responseTimeout" type="number"
step="1" min="10" placeholder="10"/>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.rpc.timeout' | translate }}</mat-label>
<input matInput formControlName="timeout" type="number"
step="1" min="10" placeholder="10"/>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.rpc.tries' | translate }}</mat-label>
<input matInput formControlName="tries" type="number"
step="1" min="1" placeholder="1"/>
</mat-form-field>
</div>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.requestValueExpression' | translate }}</mat-label>
<input matInput formControlName="requestValueExpression" placeholder="${params}"/>
</mat-form-field>
<mat-form-field>
<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.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>
<span fxFlex class="title">{{ 'gateway.rpc.value' | translate }}</span>
<span fxFlex="30px"></span>
</div>
<mat-divider></mat-divider>
<div fxFlex fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center"
*ngFor="let control of getFormArrayControls('httpHeaders'); let i = index">
<ng-container [formGroupName]="i">
<mat-form-field appearance="outline" fxFlex>
<input matInput formControlName="headerName" placeholder="{{ 'gateway.rpc.set' | translate }}"/>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex>
<input matInput formControlName="value"/>
</mat-form-field>
<mat-icon style="cursor:pointer;"
fxFlex="30px"
(click)="removeHTTPHeader(i)"
matTooltip="{{ 'gateway.rpc.remove' | translate }}">delete
</mat-icon>
</ng-container>
</div>
</div>
<button mat-raised-button
fxFlexAlign="start"
(click)="addHTTPHeader()">
{{ 'gateway.rpc.add-header' | translate }}
</button>
</fieldset>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.OPCUA_ASYNCIO">
<ng-container *ngTemplateOutlet="OPCUAForm"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.OPCUA" #OPCUAForm>
<mat-form-field >
<mat-label>{{ 'gateway.rpc.method' | translate }}</mat-label>
<input matInput formControlName="method" placeholder="multiply"/>
</mat-form-field>
<fieldset class="fields border" fxLayout="column" fxLayoutGap="10px" formArrayName="arguments">
<span class="fields-label">{{ 'gateway.rpc.arguments' | translate }}</span>
<div fxFlex fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center"
*ngFor="let control of getFormArrayControls('arguments'); let i = index">
<mat-form-field appearance="outline" fxFlex>
<input matInput [formControl]="control" required/>
</mat-form-field>
<mat-icon style="cursor:pointer;"
fxFlex="30px"
(click)="removeOCPUAArguments(i)"
matTooltip="{{ 'gateway.rpc.remove' | translate }}">delete
</mat-icon>
</div>
<button mat-raised-button
fxFlexAlign="start"
(click)="addOCPUAArguments()">
{{ 'gateway.rpc.add-argument' | translate }}
</button>
</fieldset>
</ng-template>
<ng-template ngSwitchDefault>
<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" tb-json-to-string/>
<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-error *ngIf="commandForm.get('params').hasError('invalidJSON')">
{{ 'gateway.rpc.json-value-invalid' | translate }}
</mat-error>
</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)="save()"
[disabled]="commandForm.invalid">
{{ 'gateway.rpc-command-save-template' | translate }}
</button>
<button mat-raised-button
color="primary"
(click)="sendCommand.emit()"
[disabled]="commandForm.invalid">
{{ 'gateway.rpc-command-send' | translate }}
</button>
</div>
</div>

67
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.scss

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2024 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.
*/
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 0;
.title {
font-weight: 500;
}
.command-form {
flex-wrap: nowrap;
& > button {
margin-top: 10px;
}
}
.mat-mdc-slide-toggle.margin {
margin-bottom: 10px;
margin-left: 10px;
}
.fields{
.fields-label {
font-weight: 500;
}
}
.border {
padding: 16px;
margin-bottom: 10px;
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
.title {
color: rgba(0, 0, 0, .54);
}
.mat-icon {
color: rgba(0, 0, 0, .38);
}
.mat-divider {
margin-left: -16px;
margin-right: -16px;
margin-bottom: 16px;
}
}
}

428
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts

@ -0,0 +1,428 @@
///
/// Copyright © 2016-2024 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, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import {
ControlValueAccessor,
FormArray,
FormBuilder,
FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
Validators
} from '@angular/forms';
import {
BACnetObjectTypes,
BACnetObjectTypesTranslates,
BACnetRequestTypes,
BACnetRequestTypesTranslates,
BLEMethods,
BLEMethodsTranslates,
CANByteOrders,
ConnectorType,
GatewayConnectorDefaultTypesTranslates,
HTTPMethods,
ModbusCodesTranslate,
ModbusCommandTypes,
RPCCommand,
RPCTemplateConfig,
SNMPMethods,
SNMPMethodsTranslations,
SocketEncodings,
SocketMethodProcessings,
SocketMethodProcessingsTranslates
} from '@home/components/widget/lib/gateway/gateway-widget.models';
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';
export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?<! )$/;
@Component({
selector: 'tb-gateway-service-rpc-connector',
templateUrl: './gateway-service-rpc-connector.component.html',
styleUrls: ['./gateway-service-rpc-connector.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GatewayServiceRPCConnectorComponent),
multi: true
}
]
})
export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValueAccessor {
@Input()
connectorType: ConnectorType;
@Output()
sendCommand: EventEmitter<RPCCommand> = new EventEmitter();
@Output()
saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter();
commandForm: FormGroup;
isMQTTWithResponse: FormControl;
codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16];
ConnectorType = ConnectorType;
modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[];
bACnetRequestTypes = Object.values(BACnetRequestTypes) as BACnetRequestTypes[];
bACnetObjectTypes = Object.values(BACnetObjectTypes) as BACnetObjectTypes[];
bLEMethods = Object.values(BLEMethods) as BLEMethods[];
cANByteOrders = Object.values(CANByteOrders) as CANByteOrders[];
socketMethodProcessings = Object.values(SocketMethodProcessings) as SocketMethodProcessings[];
socketEncodings = Object.values(SocketEncodings) as SocketEncodings[];
sNMPMethods = Object.values(SNMPMethods) as SNMPMethods[];
hTTPMethods = Object.values(HTTPMethods) as HTTPMethods[];
bACnetRequestTypesTranslates = BACnetRequestTypesTranslates;
bACnetObjectTypesTranslates = BACnetObjectTypesTranslates;
bLEMethodsTranslates = BLEMethodsTranslates;
SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates;
SNMPMethodsTranslations = SNMPMethodsTranslations;
gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslates;
modbusCodesTranslate = ModbusCodesTranslate;
urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/;
numbersOnlyPattern = /^[0-9]*$/;
hexOnlyPattern = /^[0-9A-Fa-f ]+$/;
private propagateChange = (v: any) => {
}
constructor(private fb: FormBuilder,
private dialog: MatDialog,) {
}
ngOnInit() {
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});
}
});
this.isMQTTWithResponse = this.fb.control(false);
}
connectorParamsFormGroupByType(type: ConnectorType): FormGroup {
let formGroup: FormGroup;
switch (type) {
case ConnectorType.MQTT:
formGroup = this.fb.group({
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: [null, [Validators.pattern(noLeadTrailSpacesRegex)]],
responseTimeout: [null, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
})
break;
case ConnectorType.MODBUS:
formGroup = this.fb.group({
tag: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [null, [Validators.required]],
functionCode: [null, [Validators.required]],
value: [null, []],
address: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]],
objectsCount: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]]
})
const valueForm = formGroup.get('value');
formGroup.get('functionCode').valueChanges.subscribe(value => {
if (value > 4) {
valueForm.addValidators([Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]);
} else {
valueForm.clearValidators();
valueForm.setValue(null);
}
valueForm.updateValueAndValidity();
})
break;
case ConnectorType.BACNET:
formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestType: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTimeout: [null, [Validators.required, Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
objectType: [null, []],
identifier: [null, [Validators.required, Validators.min(1), Validators.pattern(this.numbersOnlyPattern)]],
propertyId: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
})
break;
case ConnectorType.BLE:
formGroup = this.fb.group({
methodRPC: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
characteristicUUID: ["00002A00-0000-1000-8000-00805F9B34FB", [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
methodProcessing: [null, [Validators.required]],
withResponse: [false, []]
})
break;
case ConnectorType.CAN:
formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
nodeID: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]],
isExtendedID: [false, []],
isFD: [false, []],
bitrateSwitch: [false, []],
dataLength: [null, [Validators.min(1), Validators.pattern(this.numbersOnlyPattern)]],
dataByteorder: [null, []],
dataBefore: [null, [Validators.pattern(noLeadTrailSpacesRegex), Validators.pattern(this.hexOnlyPattern)]],
dataAfter: [null, [Validators.pattern(noLeadTrailSpacesRegex), Validators.pattern(this.hexOnlyPattern)]],
dataInHEX: [null, [Validators.pattern(noLeadTrailSpacesRegex), Validators.pattern(this.hexOnlyPattern)]],
dataExpression: [null, [Validators.pattern(noLeadTrailSpacesRegex)]]
})
break;
case ConnectorType.FTP:
formGroup = this.fb.group({
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
})
break;
case ConnectorType.OCPP:
formGroup = this.fb.group({
methodRPC: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
withResponse: [false, []]
})
break;
case ConnectorType.SOCKET:
formGroup = this.fb.group({
methodRPC: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
methodProcessing: [null, [Validators.required]],
encoding: [SocketEncodings.UTF_8, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
withResponse: [false, []]
})
break;
case ConnectorType.XMPP:
formGroup = this.fb.group({
methodRPC: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
withResponse: [false, []]
})
break;
case ConnectorType.SNMP:
formGroup = this.fb.group({
requestFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [null, [Validators.required]],
withResponse: [false, []],
oid: this.fb.array([], [Validators.required])
})
break;
case ConnectorType.REST:
formGroup = this.fb.group({
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
httpMethod: [null, [Validators.required]],
requestUrlExpression: [null, [Validators.required, Validators.pattern(this.urlPattern)]],
responseTimeout: [null, [Validators.required, Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
timeout: [null, [Validators.required, Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
tries: [null, [Validators.required, Validators.min(1), Validators.pattern(this.numbersOnlyPattern)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
httpHeaders: this.fb.array([]),
security: this.fb.array([])
})
break;
case ConnectorType.REQUEST:
formGroup = this.fb.group({
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
httpMethod: [null, [Validators.required]],
requestUrlExpression: [null, [Validators.required, Validators.pattern(this.urlPattern)]],
responseTimeout: [null, [Validators.required, Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
timeout: [null, [Validators.required, Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
tries: [null, [Validators.required, Validators.min(1), Validators.pattern(this.numbersOnlyPattern)]],
requestValueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseValueExpression: [null, [Validators.pattern(noLeadTrailSpacesRegex)]],
httpHeaders: this.fb.array([]),
})
break;
case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
arguments: this.fb.array([]),
})
break;
default:
formGroup = this.fb.group({
command: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
params: [{}, [jsonRequired]],
})
}
return formGroup;
}
addSNMPoid(value: string = null) {
const oidsFA = this.commandForm.get('oid') as FormArray;
if (oidsFA) {
oidsFA.push(this.fb.control(value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]), {emitEvent: false});
}
}
removeSNMPoid(index: number) {
const oidsFA = this.commandForm.get('oid') as FormArray;
oidsFA.removeAt(index);
}
addHTTPHeader(value: { headerName: string, value: string } = {headerName: null, value: null}) {
const headerFA = this.commandForm.get('httpHeaders') as FormArray;
const formGroup = this.fb.group({
headerName: [value.headerName, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value.value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
})
if (headerFA) {
headerFA.push(formGroup, {emitEvent: false});
}
}
removeHTTPHeader(index: number) {
const oidsFA = this.commandForm.get('httpHeaders') as FormArray;
oidsFA.removeAt(index);
}
addHTTPSecurity(value: { securityName: string, value: string } = {securityName: null, value: null}) {
const securityFA = this.commandForm.get('security') as FormArray;
const formGroup = this.fb.group({
securityName: [value.securityName, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value.value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
})
if (securityFA) {
securityFA.push(formGroup, {emitEvent: false});
}
}
removeHTTPSecurity(index: number) {
const oidsFA = this.commandForm.get('security') as FormArray;
oidsFA.removeAt(index);
}
getFormArrayControls(path: string) {
return (this.commandForm.get(path) as FormArray).controls as FormControl[];
}
addOCPUAArguments(value: string = null) {
const oidsFA = this.commandForm.get('arguments') as FormArray;
if (oidsFA) {
oidsFA.push(this.fb.control(value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]), {emitEvent: false});
}
}
removeOCPUAArguments(index: number) {
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: this.commandForm.get('params').value,
required: true
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.commandForm.get('params').setValue(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});
}
}
}

89
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html

@ -15,51 +15,46 @@
limitations under the License. limitations under the License.
--> -->
<div fxLayout="row" fxLayout.lt-sm="column" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm"> <div fxLayout="column" fxFlex [ngClass]="{'border': isConnector}">
<ng-container *ngIf="!isConnector; else connectorForm"> <div fxLayout="row" fxLayout.lt-sm="column" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm">
<mat-form-field> <ng-container *ngIf="!isConnector; else connectorForm">
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label> <mat-form-field>
<mat-select formControlName="command"> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-option *ngFor="let command of RPCCommands" [value]="command"> <mat-select formControlName="command">
{{ command }} <mat-option *ngFor="let command of RPCCommands" [value]="command">
</mat-option> {{ command }}
</mat-select> </mat-option>
</mat-form-field> </mat-select>
<mat-form-field fxFlex> </mat-form-field>
<mat-label>{{ 'gateway.statistics.timeout-ms' | translate }}</mat-label> <mat-form-field fxFlex>
<input matInput formControlName="time" type="number" min="1"/> <mat-label>{{ 'gateway.statistics.timeout-ms' | translate }}</mat-label>
<mat-error *ngIf="commandForm.get('time').hasError('min')"> <input matInput formControlName="time" type="number" min="1"/>
{{ 'gateway.statistics.timeout-min' | translate }} <mat-error *ngIf="commandForm.get('time').hasError('min')">
</mat-error> {{ 'gateway.statistics.timeout-min' | translate }}
</mat-form-field> </mat-error>
</ng-container> </mat-form-field>
<ng-template #connectorForm> <button mat-raised-button
<mat-form-field> color="primary"
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label> (click)="sendCommand()"
<input matInput formControlName="command"/> [disabled]="commandForm.invalid">
</mat-form-field> {{ 'gateway.rpc-command-send' | translate }}
<mat-form-field fxFlex> </button>
<mat-label>{{ 'widget-config.datasource-parameters' | translate }}</mat-label> </ng-container>
<input matInput formControlName="params" type="JSON"/> <ng-template #connectorForm>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon" <tb-gateway-service-rpc-connector formControlName="params" [connectorType]="connectorType"
matIconSuffix style="cursor:pointer;" (sendCommand)="sendCommand()" (saveTemplate)="saveTemplate()"/>
(click)="openEditJSONDialog($event)" </ng-template>
matTooltip="{{ 'gateway.rpc-command-edit-params' | translate }}">edit </div>
</mat-icon> <section class="result-block" [formGroup]="commandForm">
</mat-form-field> <span>{{ 'gateway.rpc-command-result' | translate }}
</ng-template> <div *ngIf="resultTime" class="result-time" fxFlex fxLayout="row" fxLayoutAlign="center center">
<button mat-raised-button <mat-icon class="material-icons">schedule</mat-icon>
color="primary" <span>{{ resultTime | date: 'yyyy/MM/dd HH:mm:ss' }}</span>
(click)="sendCommand()" </div>
[disabled]="commandForm.invalid"> </span>
{{ 'gateway.rpc-command-send' | translate }} <tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</button> </section>
</div> </div>
<section class="result-block" [formGroup]="commandForm"> <tb-gateway-service-rpc-connector-templates fxFlex="30" *ngIf="isConnector" class="border" [rpcTemplates]="templates"
<span>{{ 'gateway.rpc-command-result' | translate }} [ctx]="ctx" [connectorType]="connectorType" (useTemplate)="useTemplate($event)">
<div *ngIf="resultTime" class="result-time" fxFlex fxLayout="row" fxLayoutAlign="center center"> <mat-icon </tb-gateway-service-rpc-connector-templates>
class="material-icons">schedule</mat-icon>
<span>{{ resultTime | date: 'yyyy/MM/dd HH:mm:ss' }}</span> </div></span>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</section>

26
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss

@ -16,10 +16,20 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-x: auto; overflow: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
padding: 0; padding: 0 5px;
& > * {
height: 100%;
overflow: auto;
}
& > tb-gateway-service-rpc-connector-templates:last-child {
margin-left: 10px;
}
.command-form { .command-form {
flex-wrap: nowrap; flex-wrap: nowrap;
@ -63,11 +73,11 @@
flex: 1; flex: 1;
} }
} }
}
:host ::ng-deep { .border {
.tb-json-content { padding: 16px;
height: 100%; box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
} }
} }

157
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts

@ -14,23 +14,35 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, Input } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants'; 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 { jsonRequired } from '@shared/components/json-object-edit.component';
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({ @Component({
selector: 'tb-gateway-service-rpc', selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html', templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss'] styleUrls: ['./gateway-service-rpc.component.scss']
}) })
export class GatewayServiceRPCComponent implements AfterViewInit { export class GatewayServiceRPCComponent implements OnInit {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -56,61 +68,158 @@ export class GatewayServiceRPCComponent implements AfterViewInit {
'Reboot' 'Reboot'
]; ];
private connectorType: string; 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, constructor(private fb: FormBuilder,
private dialog: MatDialog) { private dialog: MatDialog,
private utils: UtilsService,
private cd: ChangeDetectorRef,
private attributeService: AttributeService) {
this.commandForm = this.fb.group({ this.commandForm = this.fb.group({
command: [null,[Validators.required]], command: [null, [Validators.required]],
time: [60, [Validators.required, Validators.min(1)]], time: [60, [Validators.required, Validators.min(1)]],
params: ['{}', [jsonRequired]], params: ['{}', [jsonRequired]],
result: [null] result: [null]
}); });
} }
ngAfterViewInit() { ngOnInit() {
this.isConnector = this.ctx.settings.isConnector; this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) { if (!this.isConnector) {
this.commandForm.get('command').setValue(this.RPCCommands[0]); this.commandForm.get('command').setValue(this.RPCCommands[0]);
} else { } else {
this.connectorType = this.ctx.stateController.getStateParams().connector_rpc.value.type; 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;
})
} }
} }
sendCommand() { sendCommand(value?: RPCCommand) {
this.resultTime = null; this.resultTime = null;
const formValues = this.commandForm.value; const formValues = value || this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_'; 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 => { next: resp => {
this.resultTime = new Date().getTime(); this.resultTime = new Date().getTime();
this.commandForm.get('result').setValue(JSON.stringify(resp)) this.commandForm.get('result').setValue(JSON.stringify(resp))
}, },
error: error => { error: error => {
this.resultTime = new Date().getTime(); this.resultTime = new Date().getTime();
console.error(error); console.error(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error)); this.commandForm.get('result').setValue(JSON.stringify(error.error));
} }
}); });
} }
openEditJSONDialog($event: Event) { getCommandFromParamsByType(params: RPCTemplateConfig) {
if ($event) { switch (this.connectorType) {
$event.stopPropagation(); 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, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {config: this.commandForm.value.params, templates: this.templates}
jsonValue: JSON.parse(this.commandForm.get('params').value),
required: true
}
}).afterClosed().subscribe( }).afterClosed().subscribe(
(res) => { (res) => {
if (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(() => {
this.cd.detectChanges();
})
} }
} }
); );
} }
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);
}
} }

199
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -107,27 +107,190 @@ export interface GatewayConnector {
key?: string; key?: string;
} }
export enum ConnectorType {
MQTT = 'mqtt',
MODBUS = 'modbus',
GRPC = 'grpc',
OPCUA = 'opcua',
OPCUA_ASYNCIO = 'opcua_asyncio',
BLE = 'ble',
REQUEST = 'request',
CAN = 'can',
BACNET = 'bacnet',
ODBC = 'odbc',
REST = 'rest',
SNMP = 'snmp',
FTP = 'ftp',
SOCKET = 'socket',
XMPP = 'xmpp',
OCPP = 'ocpp',
CUSTOM = 'custom'
}
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([ export const GatewayConnectorDefaultTypesTranslates = new Map<ConnectorType, string>([
['mqtt', 'MQTT'], [ConnectorType.MQTT, 'MQTT'],
['modbus', 'MODBUS'], [ConnectorType.MODBUS, 'MODBUS'],
['grpc', 'GRPC'], [ConnectorType.GRPC, 'GRPC'],
['opcua', 'OPCUA'], [ConnectorType.OPCUA, 'OPCUA'],
['opcua_asyncio', 'OPCUA ASYNCIO'], [ConnectorType.OPCUA_ASYNCIO, 'OPCUA ASYNCIO'],
['ble', 'BLE'], [ConnectorType.BLE, 'BLE'],
['request', 'REQUEST'], [ConnectorType.REQUEST, 'REQUEST'],
['can', 'CAN'], [ConnectorType.CAN, 'CAN'],
['bacnet', 'BACNET'], [ConnectorType.BACNET, 'BACNET'],
['odbc', 'ODBC'], [ConnectorType.ODBC, 'ODBC'],
['rest', 'REST'], [ConnectorType.REST, 'REST'],
['snmp', 'SNMP'], [ConnectorType.SNMP, 'SNMP'],
['ftp', 'FTP'], [ConnectorType.FTP, 'FTP'],
['socket', 'SOCKET'], [ConnectorType.SOCKET, 'SOCKET'],
['xmpp', 'XMPP'], [ConnectorType.XMPP, 'XMPP'],
['ocpp', 'OCPP'], [ConnectorType.OCPP, 'OCPP'],
['custom', 'CUSTOM'] [ConnectorType.CUSTOM, 'CUSTOM']
]); ]);
export interface RPCCommand {
command: string,
params: any,
time: number
}
export enum ModbusCommandTypes {
Bits = 'bits',
Bit = 'bit',
String = 'string',
Bytes = 'bytes',
Int8 = '8int',
Uint8 = '8uint',
Int16 = '16int',
Uint16 = '16uint',
Float16 = '16float',
Int32 = '32int',
Uint32 = '32uint',
Float32 = '32float',
Int64 = '64int',
Uint64 = '64uint',
Float64 = '64float'
}
export const ModbusCodesTranslate = new Map<number, string>([
[1, 'gateway.rpc.read-coils'],
[2, 'gateway.rpc.read-discrete-inputs'],
[3, 'gateway.rpc.read-multiple-holding-registers'],
[4, 'gateway.rpc.read-input-registers'],
[5, 'gateway.rpc.write-single-coil'],
[6, 'gateway.rpc.write-single-holding-register'],
[15, 'gateway.rpc.write-multiple-coils'],
[16, 'gateway.rpc.write-multiple-holding-registers']
])
export enum BACnetRequestTypes {
WriteProperty = 'writeProperty',
ReadProperty = 'readProperty'
}
export const BACnetRequestTypesTranslates = new Map<BACnetRequestTypes, string>([
[BACnetRequestTypes.WriteProperty, 'gateway.rpc.write-property'],
[BACnetRequestTypes.ReadProperty, "gateway.rpc.read-property"]
])
export enum BACnetObjectTypes {
BinaryInput = 'binaryInput',
BinaryOutput = 'binaryOutput',
AnalogInput = 'analogInput',
AnalogOutput = 'analogOutput',
BinaryValue = 'binaryValue',
AnalogValue = 'analogValue'
}
export const BACnetObjectTypesTranslates = new Map<BACnetObjectTypes, string>([
[BACnetObjectTypes.AnalogOutput, 'gateway.rpc.analog-output'],
[BACnetObjectTypes.AnalogInput, 'gateway.rpc.analog-input'],
[BACnetObjectTypes.BinaryOutput, 'gateway.rpc.binary-output'],
[BACnetObjectTypes.BinaryInput, 'gateway.rpc.binary-input'],
[BACnetObjectTypes.BinaryValue, 'gateway.rpc.binary-value'],
[BACnetObjectTypes.AnalogValue, 'gateway.rpc.analog-value']
])
export enum BLEMethods {
WRITE = 'write',
READ = 'read',
SCAN = 'scan'
}
export const BLEMethodsTranslates = new Map<BLEMethods, string>([
[BLEMethods.WRITE, 'gateway.rpc.write'],
[BLEMethods.READ, 'gateway.rpc.read'],
[BLEMethods.SCAN, 'gateway.rpc.scan'],
])
export enum CANByteOrders {
LITTLE = 'LITTLE',
BIG = 'BIG'
}
export enum SocketMethodProcessings {
WRITE = 'write'
}
export const SocketMethodProcessingsTranslates = new Map<SocketMethodProcessings, string>([
[SocketMethodProcessings.WRITE, 'gateway.rpc.write']
])
export enum SNMPMethods {
SET = 'set',
MULTISET = "multiset",
GET = "get",
BULKWALK = "bulkwalk",
TABLE = "table",
MULTIGET = "multiget",
GETNEXT = "getnext",
BULKGET = "bulkget",
WALKS = "walk"
}
export const SNMPMethodsTranslations = new Map<SNMPMethods, string>([
[SNMPMethods.SET, 'gateway.rpc.set'],
[SNMPMethods.MULTISET, 'gateway.rpc.multiset'],
[SNMPMethods.GET, 'gateway.rpc.get'],
[SNMPMethods.BULKWALK, 'gateway.rpc.bulk-walk'],
[SNMPMethods.TABLE, 'gateway.rpc.table'],
[SNMPMethods.MULTIGET, 'gateway.rpc.multi-get'],
[SNMPMethods.GETNEXT, 'gateway.rpc.get-next'],
[SNMPMethods.BULKGET, 'gateway.rpc.bul-kget'],
[SNMPMethods.WALKS, 'gateway.rpc.walk']
])
export enum HTTPMethods {
CONNECT = 'CONNECT',
DELETE = 'DELETE',
GET = 'GET',
HEAD = 'HEAD',
OPTIONS = 'OPTIONS',
PATCH = 'PATCH',
POST = 'POST',
PUT = 'PUT',
TRACE = 'TRACE'
}
export enum SocketEncodings {
UTF_8 = 'utf-8'
}
export interface RPCTemplate {
name?: string;
config: RPCTemplateConfig;
}
export interface RPCTemplateConfig {
[key: string]: any;
}
export interface SaveRPCTemplateData {
config: RPCTemplateConfig,
templates: Array<RPCTemplate>
}
export interface LogLink { export interface LogLink {
name: string; name: string;
key: string; key: string;

15
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -47,6 +47,12 @@ import { GatewayConnectorComponent } from '@home/components/widget/lib/gateway/g
import { GatewayLogsComponent } from '@home/components/widget/lib/gateway/gateway-logs.component'; import { GatewayLogsComponent } from '@home/components/widget/lib/gateway/gateway-logs.component';
import { GatewayStatisticsComponent } from '@home/components/widget/lib/gateway/gateway-statistics.component'; import { GatewayStatisticsComponent } from '@home/components/widget/lib/gateway/gateway-statistics.component';
import { GatewayServiceRPCComponent } from '@home/components/widget/lib/gateway/gateway-service-rpc.component'; import { GatewayServiceRPCComponent } from '@home/components/widget/lib/gateway/gateway-service-rpc.component';
import {
GatewayServiceRPCConnectorComponent
} from '@home/components/widget/lib/gateway/gateway-service-rpc-connector.component';
import {
GatewayServiceRPCConnectorTemplatesComponent
} from '@home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component';
import { DeviceGatewayCommandComponent } from '@home/components/widget/lib/gateway/device-gateway-command.component'; import { DeviceGatewayCommandComponent } from '@home/components/widget/lib/gateway/device-gateway-command.component';
import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/gateway-configuration.component'; import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import { import {
@ -70,6 +76,9 @@ import { RangeChartWidgetComponent } from '@home/components/widget/lib/chart/ran
import { import {
BarChartWithLabelsWidgetComponent BarChartWithLabelsWidgetComponent
} from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.component'; } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.component';
import {
GatewayServiceRPCConnectorTemplateDialogComponent
} from '@home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog';
import { SingleSwitchWidgetComponent } from '@home/components/widget/lib/rpc/single-switch-widget.component'; import { SingleSwitchWidgetComponent } from '@home/components/widget/lib/rpc/single-switch-widget.component';
import { ActionButtonWidgetComponent } from '@home/components/widget/lib/button/action-button-widget.component'; import { ActionButtonWidgetComponent } from '@home/components/widget/lib/button/action-button-widget.component';
import { CommandButtonWidgetComponent } from '@home/components/widget/lib/button/command-button-widget.component'; import { CommandButtonWidgetComponent } from '@home/components/widget/lib/button/command-button-widget.component';
@ -103,9 +112,12 @@ import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-wi
GatewayLogsComponent, GatewayLogsComponent,
GatewayStatisticsComponent, GatewayStatisticsComponent,
GatewayServiceRPCComponent, GatewayServiceRPCComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
DeviceGatewayCommandComponent, DeviceGatewayCommandComponent,
GatewayConfigurationComponent, GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
ValueCardWidgetComponent, ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent, AggregatedValueCardWidgetComponent,
CountWidgetComponent, CountWidgetComponent,
@ -153,11 +165,14 @@ import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-wi
FlotWidgetComponent, FlotWidgetComponent,
GatewayConnectorComponent, GatewayConnectorComponent,
GatewayLogsComponent, GatewayLogsComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
GatewayStatisticsComponent, GatewayStatisticsComponent,
GatewayServiceRPCComponent, GatewayServiceRPCComponent,
DeviceGatewayCommandComponent, DeviceGatewayCommandComponent,
GatewayConfigurationComponent, GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
ValueCardWidgetComponent, ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent, AggregatedValueCardWidgetComponent,
CountWidgetComponent, CountWidgetComponent,

91
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2707,6 +2707,7 @@
"connectors-table-actions": "Actions", "connectors-table-actions": "Actions",
"connectors-table-key": "Key", "connectors-table-key": "Key",
"connectors-table-class": "Class", "connectors-table-class": "Class",
"rpc-command-save-template": "Save Template",
"rpc-command-send": "Send", "rpc-command-send": "Send",
"rpc-command-result": "Response", "rpc-command-result": "Response",
"rpc-command-edit-params": "Edit parameters", "rpc-command-edit-params": "Edit parameters",
@ -2803,6 +2804,96 @@
"remove-entry": "Remove configuration", "remove-entry": "Remove configuration",
"remote-shell": "Remote shell", "remote-shell": "Remote shell",
"remote-configuration": "Remote Configuration", "remote-configuration": "Remote Configuration",
"rpc": {
"title": "{{type}} Connector RPC parameters",
"templates-title": "Connector RPC Templates",
"methodFilter": "Method filter",
"requestTopicExpression": "Request topic expression",
"responseTopicExpression": "Response topic expression",
"responseTimeout": "Response Time",
"valueExpression": "Value Expression",
"tag": "Tag",
"type": "Type",
"functionCode": "Function Code",
"objectsCount": "Objects Count",
"address": "Address",
"method": "Method",
"requestType": "Request Type",
"requestTimeout": "Request Timeout",
"objectType": "Object type",
"identifier": "Identifier",
"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",
"requestFilter": "Request Filter",
"requestUrlExpression": "Request URL Expression",
"httpMethod": "HTTP Method",
"timeout": "Timeout",
"tries": "Tries",
"httpHeaders": "HTTP Headers",
"header-name": "Header name",
"security-name": "Security name",
"value": "Value",
"security": "Security",
"responseValueExpression": "Response Value Expression",
"requestValueExpression": "Request Value Expression",
"arguments": "Arguments",
"add-argument": "Add argument",
"write-property": "Write property",
"read-property": "Read property",
"analog-output": "Analog output",
"analog-input": "Analog input",
"binary-output": "Binary output",
"binary-input": "Binary input",
"binary-value": "Binary value",
"analog-value": "Analog value",
"write": "Write",
"read": "Read",
"scan": "Scan",
"oids": "OIDS",
"set": "Set",
"multiset": "Multiset",
"get": "Get",
"bulk-walk": "Bulk walk",
"table": "Table",
"multi-get": "Multiget",
"get-next": "Get next",
"bulk-get": "Bulk get",
"walk": "Walk",
"save-template": "Save template",
"template-name": "Template name",
"template-name-required": "Template name is required.",
"template-name-duplicate": "Template with such name already exists, it will be updated.",
"command": "Command",
"params": "Params",
"read-coils": "01: Read Coils",
"read-discrete-inputs": "02: Read Discrete Inputs",
"read-multiple-holding-registers": "03: Read Multiple Holding Registers",
"read-input-registers": "04: Read Input Registers",
"write-single-coil": "05: Write Single Coil",
"write-single-holding-register": "06: Write Single Holding Register",
"write-multiple-coils": "15: Write Multiple Coils",
"write-multiple-holding-registers": "16: Write Multiple Holding Registers",
"json-value-invalid": "JSON value has an invalid format"
},
"other": "Other", "other": "Other",
"save-tip": "Save configuration file", "save-tip": "Save configuration file",
"security-type": "Security type", "security-type": "Security type",

Loading…
Cancel
Save