diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss index 2245de8b12..87fe898568 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss @@ -104,8 +104,3 @@ background-color: rgb(25, 128, 56); } } - -:host ::ng-deep tb-json-object-edit > div { - flex-grow: 1; -} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 8071a2f924..21d232f2f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/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 { EntityType } from '@shared/models/entity-type.models'; import { + ConnectorType, GatewayConnector, GatewayConnectorDefaultTypesTranslates, GatewayLogLevel @@ -213,10 +214,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie saveConnector(): void { const value = this.connectorForm.value; value.configuration = camelCase(value.name) + '.json'; - if (value.type !== 'grpc') { + if (value.type !== ConnectorType.GRPC) { delete value.key; } - if (value.type !== 'custom') { + if (value.type !== ConnectorType.CUSTOM) { delete value.class; } value.ts = new Date().getTime(); @@ -325,7 +326,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.initialConnector = null; this.connectorForm.setValue({ name: '', - type: 'mqtt', + type: ConnectorType.MQTT, logLevel: GatewayLogLevel.INFO, key: 'auto', class: '', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.html new file mode 100644 index 0000000000..d8b028361d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.html @@ -0,0 +1,54 @@ + + +

gateway.rpc.save-template

+ + +
+
+ + gateway.rpc.template-name + + + {{ 'gateway.rpc.template-name-required' | translate }} + + +
+ {{ 'gateway.rpc.template-name-duplicate' | translate }} +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-template-dialog.ts new file mode 100644 index 0000000000..5c70e1f233 --- /dev/null +++ b/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 { + + config: { + [key: string]: any; + }; + templates: Array; + templateNameCtrl: FormControl; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: SaveRPCTemplateData, + public dialogRef: MatDialogRef, + 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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.html new file mode 100644 index 0000000000..c0f6997a36 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.html @@ -0,0 +1,60 @@ + +
{{ 'gateway.rpc.templates-title' | translate }}
+ + + + {{template.name}} + + + + + + + + + + +
+
+ {{!innerValue ? ('gateway.rpc.' + config.key | translate) : config.key}} +
+
+ {{config.value}}
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.scss new file mode 100644 index 0000000000..7e5c4af1f1 --- /dev/null +++ b/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; + } + } + +} + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.ts new file mode 100644 index 0000000000..4392b35f2d --- /dev/null +++ b/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 = new EventEmitter(); + + @Output() + useTemplate: EventEmitter = new EventEmitter(); + + @Input() + rpcTemplates: Array; + + 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(() => { + }) + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html new file mode 100644 index 0000000000..2d992187f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html @@ -0,0 +1,542 @@ + +
+
{{ 'gateway.rpc.title' | translate: {type: gatewayConnectorDefaultTypesTranslates.get(connectorType)} }}
+ + + + + {{ 'gateway.rpc.methodFilter' | translate }} + + + + {{ 'gateway.rpc.requestTopicExpression' | translate }} + + + + {{ 'gateway.rpc.withResponse' | translate }} + + + {{ 'gateway.rpc.responseTopicExpression' | translate }} + + + + {{ 'gateway.rpc.responseTimeout' | translate }} + + + + {{ 'gateway.rpc.valueExpression' | translate }} + + + + + + {{ 'gateway.rpc.tag' | translate }} + + +
+ + {{ 'gateway.rpc.type' | translate }} + + + {{ type }} + + + + + {{ 'gateway.rpc.functionCode' | translate }} + + + {{ modbusCodesTranslate.get(code) | translate}} + + + +
+ + {{ 'gateway.rpc.value' | translate }} + + +
+ + {{ 'gateway.rpc.address' | translate }} + + + + {{ 'gateway.rpc.objectsCount' | translate }} + + +
+
+ + + {{ 'gateway.rpc.methodRPC' | translate }} + + + + {{ 'gateway.rpc.requestType' | translate }} + + + {{bACnetRequestTypesTranslates.get(type) | translate}} + + + + + {{ 'gateway.rpc.requestTimeout' | translate }} + + +
+ + {{ 'gateway.rpc.objectType' | translate }} + + + {{bACnetObjectTypesTranslates.get(type) | translate}} + + + + + {{ 'gateway.rpc.identifier' | translate }} + + +
+ + {{ 'gateway.rpc.propertyId' | translate }} + + +
+ + + {{ 'gateway.rpc.methodRPC' | translate }} + + + + {{ 'gateway.rpc.characteristicUUID' | translate }} + + + + {{ 'gateway.rpc.methodProcessing' | translate }} + + + {{bLEMethodsTranslates.get(type) | translate}} + + + + + {{ 'gateway.rpc.withResponse' | translate }} + + + + + {{ 'gateway.rpc.methodRPC' | translate }} + + + + {{ 'gateway.rpc.nodeID' | translate }} + + + + {{ 'gateway.rpc.isExtendedID' | translate }} + + + {{ 'gateway.rpc.isFD' | translate }} + + + {{ 'gateway.rpc.bitrateSwitch' | translate }} + +
+ + {{ 'gateway.rpc.dataLength' | translate }} + + + + {{ 'gateway.rpc.dataByteorder' | translate }} + + + {{ order | translate }} + + + +
+
+ + {{ 'gateway.rpc.dataBefore' | translate }} + + + + {{ 'gateway.rpc.dataAfter' | translate }} + + +
+ + {{ 'gateway.rpc.dataInHEX' | translate }} + + + + {{ 'gateway.rpc.dataExpression' | translate }} + + +
+ + + {{ 'gateway.rpc.methodFilter' | translate }} + + + + {{ 'gateway.rpc.valueExpression' | translate }} + + + + + + {{ 'gateway.rpc.methodRPC' | translate }} + + + + {{ 'gateway.rpc.valueExpression' | translate }} + + + + {{ 'gateway.rpc.withResponse' | translate }} + + + + + {{ 'gateway.rpc.methodRPC' | translate }} + + + + {{ 'gateway.rpc.methodProcessing' | translate }} + + + {{ SocketMethodProcessingsTranslates.get(method) | translate }} + + + + + {{ 'gateway.rpc.encoding' | translate }} + + + + {{ 'gateway.rpc.withResponse' | translate }} + + + + + {{ 'gateway.rpc.methodRPC' | translate }} + + + + {{ 'gateway.rpc.valueExpression' | translate }} + + + + {{ 'gateway.rpc.withResponse' | translate }} + + + + + {{ 'gateway.rpc.requestFilter' | translate }} + + + + {{ 'gateway.rpc.method' | translate }} + + + {{ SNMPMethodsTranslations.get(method) | translate }} + + + + + {{ 'gateway.rpc.withResponse' | translate }} + +
+ {{ 'gateway.rpc.oids' | translate }}* +
+ + + + delete + +
+ +
+
+ + + {{ 'gateway.rpc.methodFilter' | translate }} + + +
+ + {{ 'gateway.rpc.httpMethod' | translate }} + + + {{ method }} + + + + + {{ 'gateway.rpc.requestUrlExpression' | translate }} + + +
+
+ + {{ 'gateway.rpc.responseTimeout' | translate }} + + + + {{ 'gateway.rpc.timeout' | translate }} + + + + {{ 'gateway.rpc.tries' | translate }} + + +
+ + {{ 'gateway.rpc.valueExpression' | translate }} + + +
+ {{ 'gateway.rpc.httpHeaders' | translate }} +
+
+ {{ 'gateway.rpc.header-name' | translate }} + {{ 'gateway.rpc.value' | translate }} + +
+ +
+ + + + + + + + delete + + +
+
+ +
+
+ {{ 'gateway.rpc.security' | translate }} +
+
+ {{ 'gateway.rpc.security-name' | translate }} + {{ 'gateway.rpc.value' | translate }} + +
+ +
+ + + + + + + + delete + + +
+
+ +
+
+ + + {{ 'gateway.rpc.methodFilter' | translate }} + + +
+ + {{ 'gateway.rpc.httpMethod' | translate }} + + + {{ method }} + + + + + {{ 'gateway.rpc.requestUrlExpression' | translate }} + + +
+
+ + {{ 'gateway.rpc.responseTimeout' | translate }} + + + + {{ 'gateway.rpc.timeout' | translate }} + + + + {{ 'gateway.rpc.tries' | translate }} + + +
+ + {{ 'gateway.rpc.requestValueExpression' | translate }} + + + + {{ 'gateway.rpc.responseValueExpression' | translate }} + + +
+ {{ 'gateway.rpc.httpHeaders' | translate }} +
+
+ {{ 'gateway.rpc.header-name' | translate }} + {{ 'gateway.rpc.value' | translate }} + +
+ +
+ + + + + + + + delete + + +
+
+ +
+
+ + + + + + {{ 'gateway.rpc.method' | translate }} + + +
+ {{ 'gateway.rpc.arguments' | translate }} +
+ + + + delete + +
+ +
+
+ + + {{ 'gateway.statistics.command' | translate }} + + + + {{ 'widget-config.datasource-parameters' | translate }} + + edit + + + {{ 'gateway.rpc.json-value-invalid' | translate }} + + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.scss new file mode 100644 index 0000000000..a0af50aee8 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts new file mode 100644 index 0000000000..69b785d133 --- /dev/null +++ b/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]*(? GatewayServiceRPCConnectorComponent), + multi: true + } + ] +}) +export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValueAccessor { + + @Input() + connectorType: ConnectorType; + + @Output() + sendCommand: EventEmitter = new EventEmitter(); + + @Output() + saveTemplate: EventEmitter = new EventEmitter(); + + commandForm: FormGroup; + isMQTTWithResponse: FormControl; + codesArray: Array = [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, { + 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}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html index 33ddef3f81..8368698087 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html @@ -15,51 +15,46 @@ limitations under the License. --> -
- - - {{ 'gateway.statistics.command' | translate }} - - - {{ command }} - - - - - {{ 'gateway.statistics.timeout-ms' | translate }} - - - {{ 'gateway.statistics.timeout-min' | translate }} - - - - - - {{ 'gateway.statistics.command' | translate }} - - - - {{ 'widget-config.datasource-parameters' | translate }} - - edit - - - - +
+
+ + + {{ 'gateway.statistics.command' | translate }} + + + {{ command }} + + + + + {{ 'gateway.statistics.timeout-ms' | translate }} + + + {{ 'gateway.statistics.timeout-min' | translate }} + + + + + + + +
+
+ {{ 'gateway.rpc-command-result' | translate }} +
+ schedule + {{ resultTime | date: 'yyyy/MM/dd HH:mm:ss' }} +
+
+ +
-
- {{ 'gateway.rpc-command-result' | translate }} -
schedule - {{ resultTime | date: 'yyyy/MM/dd HH:mm:ss' }}
- -
- + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss index be5e215e81..f74097bbba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss @@ -16,10 +16,20 @@ :host { width: 100%; height: 100%; - overflow-x: auto; + overflow: auto; display: flex; - flex-direction: column; - padding: 0; + flex-direction: row; + padding: 0 5px; + + & > * { + height: 100%; + overflow: auto; + } + + & > tb-gateway-service-rpc-connector-templates:last-child { + margin-left: 10px; + } + .command-form { flex-wrap: nowrap; @@ -63,11 +73,11 @@ flex: 1; } } -} -:host ::ng-deep { - .tb-json-content { - height: 100%; + .border { + padding: 16px; + 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; } } - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts index b157a94044..b5cde980f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts @@ -14,23 +14,35 @@ /// 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 { 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, + 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', templateUrl: './gateway-service-rpc.component.html', styleUrls: ['./gateway-service-rpc.component.scss'] }) -export class GatewayServiceRPCComponent implements AfterViewInit { +export class GatewayServiceRPCComponent implements OnInit { @Input() ctx: WidgetContext; @@ -56,61 +68,158 @@ export class GatewayServiceRPCComponent implements AfterViewInit { 'Reboot' ]; - private connectorType: string; + public connectorType: ConnectorType; + public templates: Array = []; + + 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]], + command: [null, [Validators.required]], time: [60, [Validators.required, Validators.min(1)]], params: ['{}', [jsonRequired]], result: [null] }); } - ngAfterViewInit() { + ngOnInit() { this.isConnector = this.ctx.settings.isConnector; if (!this.isConnector) { 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; + }) } } - sendCommand() { + sendCommand(value?: RPCCommand) { this.resultTime = null; - const formValues = this.commandForm.value; + 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.resultTime = new Date().getTime(); this.commandForm.get('result').setValue(JSON.stringify(resp)) }, error: error => { - this.resultTime = new Date().getTime(); + this.resultTime = new Date().getTime(); console.error(error); this.commandForm.get('result').setValue(JSON.stringify(error.error)); } }); } - 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, { + } + + saveTemplate() { + this.dialog.open + (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(() => { + 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); + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 603b06cc01..cffa206c3b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -107,27 +107,190 @@ export interface GatewayConnector { 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([ - ['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([ + [ConnectorType.MQTT, 'MQTT'], + [ConnectorType.MODBUS, 'MODBUS'], + [ConnectorType.GRPC, 'GRPC'], + [ConnectorType.OPCUA, 'OPCUA'], + [ConnectorType.OPCUA_ASYNCIO, 'OPCUA ASYNCIO'], + [ConnectorType.BLE, 'BLE'], + [ConnectorType.REQUEST, 'REQUEST'], + [ConnectorType.CAN, 'CAN'], + [ConnectorType.BACNET, 'BACNET'], + [ConnectorType.ODBC, 'ODBC'], + [ConnectorType.REST, 'REST'], + [ConnectorType.SNMP, 'SNMP'], + [ConnectorType.FTP, 'FTP'], + [ConnectorType.SOCKET, 'SOCKET'], + [ConnectorType.XMPP, 'XMPP'], + [ConnectorType.OCPP, 'OCPP'], + [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([ + [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.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.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.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.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.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 +} + export interface LogLink { name: string; key: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 1cd099baac..36dc0db626 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/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 { GatewayStatisticsComponent } from '@home/components/widget/lib/gateway/gateway-statistics.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 { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/gateway-configuration.component'; import { @@ -70,6 +76,9 @@ import { RangeChartWidgetComponent } from '@home/components/widget/lib/chart/ran import { BarChartWithLabelsWidgetComponent } 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 { ActionButtonWidgetComponent } from '@home/components/widget/lib/button/action-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, GatewayStatisticsComponent, GatewayServiceRPCComponent, + GatewayServiceRPCConnectorComponent, + GatewayServiceRPCConnectorTemplatesComponent, DeviceGatewayCommandComponent, GatewayConfigurationComponent, GatewayRemoteConfigurationDialogComponent, + GatewayServiceRPCConnectorTemplateDialogComponent, ValueCardWidgetComponent, AggregatedValueCardWidgetComponent, CountWidgetComponent, @@ -153,11 +165,14 @@ import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-wi FlotWidgetComponent, GatewayConnectorComponent, GatewayLogsComponent, + GatewayServiceRPCConnectorComponent, + GatewayServiceRPCConnectorTemplatesComponent, GatewayStatisticsComponent, GatewayServiceRPCComponent, DeviceGatewayCommandComponent, GatewayConfigurationComponent, GatewayRemoteConfigurationDialogComponent, + GatewayServiceRPCConnectorTemplateDialogComponent, ValueCardWidgetComponent, AggregatedValueCardWidgetComponent, CountWidgetComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c71f33fca7..c717bf719a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2707,6 +2707,7 @@ "connectors-table-actions": "Actions", "connectors-table-key": "Key", "connectors-table-class": "Class", + "rpc-command-save-template": "Save Template", "rpc-command-send": "Send", "rpc-command-result": "Response", "rpc-command-edit-params": "Edit parameters", @@ -2803,6 +2804,96 @@ "remove-entry": "Remove configuration", "remote-shell": "Remote shell", "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", "save-tip": "Save configuration file", "security-type": "Security type",