Merge pull request #11718 from maxunbearable/task/4143-rpc-templates-update

RPC templates update for MQTT, MODBUS, OPC-UA
This commit is contained in:
Igor Kulikov 2024-09-25 18:01:40 +03:00 committed by GitHub
commit 61a0c65d3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 657 additions and 160 deletions

View File

@ -16,20 +16,9 @@
-->
<ng-container [formGroup]="rpcParametersFormGroup">
<div fxFlex fxLayout="row">
<mat-form-field fxFlex="100">
<mat-label>{{ 'gateway.key' | translate }}</mat-label>
<input matInput name="value" formControlName="tag" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="rpcParametersFormGroup.get('tag').hasError('required') &&
rpcParametersFormGroup.get('tag').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<div class="tb-form-hint tb-primary-fill tb-flex no-padding-top hint-container">
{{ 'gateway.rpc.hint.modbus-response-reading' | translate }}<br>
{{ 'gateway.rpc.hint.modbus-writing-functions' | translate }}
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50" class="mat-block">
@ -45,21 +34,6 @@
</mat-select>
</mat-form-field>
</div>
<div fxFlex fxLayout="row">
<mat-form-field fxFlex="100" *ngIf="writeFunctionCodes.includes(rpcParametersFormGroup.get('functionCode').value)">
<mat-label>{{ 'gateway.rpc.value' | translate }}</mat-label>
<input matInput name="value" formControlName="value" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="rpcParametersFormGroup.get('value').hasError('required') &&
rpcParametersFormGroup.get('value').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.address' | translate }}</mat-label>
@ -88,5 +62,20 @@
/>
</mat-form-field>
</div>
<div *ngIf="writeFunctionCodes.includes(rpcParametersFormGroup.get('functionCode').value)" fxFlex fxLayout="row">
<mat-form-field fxFlex="100">
<mat-label>{{ 'gateway.rpc.value' | translate }}</mat-label>
<input matInput name="value" formControlName="value" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="rpcParametersFormGroup.get('value').hasError('required') && rpcParametersFormGroup.get('value').touched"
class="tb-error"
>
warning
</mat-icon>
</mat-form-field>
</div>
</ng-container>

View File

@ -0,0 +1,20 @@
/**
* 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 {
.hint-container {
margin-bottom: 12px;
}
}

View File

@ -39,13 +39,14 @@ import {
ModbusEditableDataTypes,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusValue,
noLeadTrailSpacesRegex,
RPCTemplateConfigModbus,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'tb-modbus-rpc-parameters',
templateUrl: './modbus-rpc-parameters.component.html',
styleUrls: ['./modbus-rpc-parameters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
@ -80,14 +81,13 @@ export class ModbusRpcParametersComponent implements ControlValueAccessor, Valid
private readonly readFunctionCodes = [1, 2, 3, 4];
private readonly bitsFunctionCodes = [...this.readFunctionCodes, ...this.writeFunctionCodes];
private onChange: (value: ModbusValue) => void;
private onChange: (value: RPCTemplateConfigModbus) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.rpcParametersFormGroup = this.fb.group({
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusDataType.BYTES, [Validators.required]],
functionCode: [this.defaultFunctionCodes[0], [Validators.required]],
value: [{value: '', disabled: true}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -106,7 +106,7 @@ export class ModbusRpcParametersComponent implements ControlValueAccessor, Valid
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusValue) => void): void {
registerOnChange(fn: (value: RPCTemplateConfigModbus) => void): void {
this.onChange = fn;
}
@ -120,7 +120,7 @@ export class ModbusRpcParametersComponent implements ControlValueAccessor, Valid
};
}
writeValue(value: ModbusValue): void {
writeValue(value: RPCTemplateConfigModbus): void {
this.rpcParametersFormGroup.patchValue(value, {emitEvent: false});
}

View File

@ -0,0 +1,48 @@
<!--
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.
-->
<ng-container [formGroup]="rpcParametersFormGroup">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-name' | 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()" formControlName="withResponse">
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
<mat-form-field *ngIf="rpcParametersFormGroup.get('withResponse')?.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="rpcParametersFormGroup.get('withResponse')?.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-container>

View File

@ -0,0 +1,24 @@
/**
* 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 {
display: flex;
flex-direction: column;
.mat-mdc-slide-toggle.margin {
margin-bottom: 10px;
margin-left: 10px;
}
}

View File

@ -0,0 +1,139 @@
///
/// 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 {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator, Validators,
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil, tap } from 'rxjs/operators';
import {
integerRegex,
noLeadTrailSpacesRegex,
RPCTemplateConfigMQTT
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'tb-mqtt-rpc-parameters',
templateUrl: './mqtt-rpc-parameters.component.html',
styleUrls: ['./mqtt-rpc-parameters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttRpcParametersComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttRpcParametersComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class MqttRpcParametersComponent implements ControlValueAccessor, Validator, OnDestroy {
rpcParametersFormGroup: UntypedFormGroup;
private onChange: (value: RPCTemplateConfigMQTT) => void = (_) => {};
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.rpcParametersFormGroup = this.fb.group({
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: [{ value: null, disabled: true }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTimeout: [{ value: null, disabled: true }, [Validators.min(10), Validators.pattern(integerRegex)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
withResponse: [false, []],
});
this.observeValueChanges();
this.observeWithResponse();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: RPCTemplateConfigMQTT) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.rpcParametersFormGroup.valid ? null : {
rpcParametersFormGroup: { valid: false }
};
}
writeValue(value: RPCTemplateConfigMQTT): void {
this.rpcParametersFormGroup.patchValue(value, {emitEvent: false});
this.toggleResponseFields(value.withResponse);
}
private observeValueChanges(): void {
this.rpcParametersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
private observeWithResponse(): void {
this.rpcParametersFormGroup.get('withResponse').valueChanges.pipe(
tap((isActive: boolean) => this.toggleResponseFields(isActive)),
takeUntil(this.destroy$),
).subscribe();
}
private toggleResponseFields(enabled: boolean): void {
const responseTopicControl = this.rpcParametersFormGroup.get('responseTopicExpression');
const responseTimeoutControl = this.rpcParametersFormGroup.get('responseTimeout');
if (enabled) {
responseTopicControl.enable();
responseTimeoutControl.enable();
} else {
responseTopicControl.disable();
responseTimeoutControl.disable();
}
}
}

View File

@ -0,0 +1,93 @@
<!--
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.
-->
<ng-container [formGroup]="rpcParametersFormGroup">
<div class="tb-form-hint tb-primary-fill tb-flex no-padding-top hint-container">
{{ 'gateway.rpc.hint.opc-method' | translate }}
</div>
<mat-form-field class="tb-flex">
<mat-label>{{ 'gateway.rpc.method' | translate }}</mat-label>
<input matInput formControlName="method" placeholder="multiply"/>
</mat-form-field>
<fieldset class="tb-form-panel stroked arguments-container" fxLayout="column" formArrayName="arguments">
<strong>
<span class="fields-label">{{ 'gateway.rpc.arguments' | translate }}</span>
</strong>
<div fxFlex fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center"
*ngFor="let argumentFormGroup of rpcParametersFormGroup.get('arguments')['controls']; let i = index" [formGroup]="argumentFormGroup">
<div class="tb-form-row column-xs type-container" fxLayoutAlign="space-between center">
<div class="tb-required" translate>gateway.type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-select-trigger>
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(argumentFormGroup.get('type').value)?.icon">
</mat-icon>
<span>{{ valueTypes.get(argumentFormGroup.get('type').value)?.name | translate }}</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}">
</mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs value-container" fxLayoutAlign="space-between center">
<div class="tb-required" translate>gateway.value</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex tb-suffix-absolute">
<ng-container [ngSwitch]="argumentFormGroup.get('type').value">
<input *ngSwitchCase="MappingValueType.STRING" matInput required formControlName="string"
placeholder="{{ 'gateway.set' | translate }}" />
<input *ngSwitchCase="MappingValueType.INTEGER" matInput required formControlName="integer" type="number"
placeholder="{{ 'gateway.set' | translate }}" />
<input *ngSwitchCase="MappingValueType.DOUBLE" matInput required formControlName="double" type="number"
placeholder="{{ 'gateway.set' | translate }}" />
<mat-select *ngSwitchCase="MappingValueType.BOOLEAN" formControlName="boolean">
<mat-option [value]="true">true</mat-option>
<mat-option [value]="false">false</mat-option>
</mat-select>
</ng-container>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="argumentFormGroup.get(argumentFormGroup.get('type').value).hasError('required')
&& argumentFormGroup.get(argumentFormGroup.get('type').value).touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<button mat-icon-button (click)="removeArgument(i)"
class="tb-box-button"
matTooltip="{{ 'gateway.rpc.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
<button mat-raised-button
fxFlexAlign="start"
(click)="addArgument()">
{{ 'gateway.rpc.add-argument' | translate }}
</button>
</fieldset>
</ng-container>

View File

@ -0,0 +1,32 @@
/**
* 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 {
.arguments-container {
margin-bottom: 10px;
}
.type-container {
width: 40%;
}
.value-container {
width: 50%;
}
.hint-container {
margin-bottom: 12px;
}
}

View File

@ -0,0 +1,169 @@
///
/// 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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormArray,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator, Validators,
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
integerRegex,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex,
OPCTypeValue,
RPCTemplateConfigOPC
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isDefinedAndNotNull, isEqual } from '@core/utils';
@Component({
selector: 'tb-opc-rpc-parameters',
templateUrl: './opc-rpc-parameters.component.html',
styleUrls: ['./opc-rpc-parameters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpcRpcParametersComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcRpcParametersComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class OpcRpcParametersComponent implements ControlValueAccessor, Validator, OnDestroy {
rpcParametersFormGroup: UntypedFormGroup;
readonly valueTypeKeys: MappingValueType[] = Object.values(MappingValueType);
readonly MappingValueType = MappingValueType;
readonly valueTypes = mappingValueTypesMap;
private onChange: (value: RPCTemplateConfigOPC) => void = (_) => {} ;
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
this.rpcParametersFormGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
arguments: this.fb.array([]),
});
this.observeValueChanges();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: RPCTemplateConfigOPC) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.rpcParametersFormGroup.valid ? null : {
rpcParametersFormGroup: { valid: false }
};
}
writeValue(params: RPCTemplateConfigOPC): void {
this.clearArguments();
params.arguments?.map(({type, value}) => ({type, [type]: value }))
.forEach(argument => this.addArgument(argument as OPCTypeValue));
this.cdr.markForCheck();
this.rpcParametersFormGroup.get('method').patchValue(params.method);
}
private observeValueChanges(): void {
this.rpcParametersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(params => {
const updatedArguments = params.arguments.map(({type, ...config}) => ({type, value: config[type]}));
this.onChange({method: params.method, arguments: updatedArguments});
this.onTouched();
});
}
removeArgument(index: number): void {
(this.rpcParametersFormGroup.get('arguments') as FormArray).removeAt(index);
}
addArgument(value: OPCTypeValue = {} as OPCTypeValue): void {
const fromGroup = this.fb.group({
type: [value.type ?? MappingValueType.STRING],
string: [
value.string ?? { value: '', disabled: !(isEqual(value, {}) || value.string)},
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]
],
integer: [
{value: value.integer ?? 0, disabled: !isDefinedAndNotNull(value.integer)},
[Validators.required, Validators.pattern(integerRegex)]
],
double: [{value: value.double ?? 0, disabled: !isDefinedAndNotNull(value.double)}, [Validators.required]],
boolean: [{value: value.boolean ?? false, disabled: !isDefinedAndNotNull(value.boolean)}, [Validators.required]],
});
this.observeTypeChange(fromGroup);
(this.rpcParametersFormGroup.get('arguments') as FormArray).push(fromGroup, {emitEvent: false});
}
clearArguments(): void {
const formArray = this.rpcParametersFormGroup.get('arguments') as FormArray;
while (formArray.length !== 0) {
formArray.removeAt(0);
}
}
private observeTypeChange(dataKeyFormGroup: FormGroup): void {
dataKeyFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
dataKeyFormGroup.disable({emitEvent: false});
dataKeyFormGroup.get('type').enable({emitEvent: false});
dataKeyFormGroup.get(type).enable({emitEvent: false});
});
}
}

View File

@ -70,8 +70,8 @@
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get(keyControl.get('type').value).hasError('required')
&& keyControl.get(keyControl.get('type').value).touched"
*ngIf="keyControl.get(keyControl.get('value').value).hasError('required')
&& keyControl.get(keyControl.get('value').value).touched"
class="tb-error">
warning
</mat-icon>

View File

@ -104,7 +104,7 @@ export class TypeValuePanelComponent implements ControlValueAccessor, Validator,
dataKeyFormGroup.disable({emitEvent: false});
dataKeyFormGroup.get('type').enable({emitEvent: false});
dataKeyFormGroup.get(type).enable({emitEvent: false});
})
});
}
deleteKey($event: Event, index: number): void {

View File

@ -44,7 +44,11 @@
<div class="template-key">
{{!innerValue ? ('gateway.rpc.' + config.key | translate) : config.key}}
</div>
<div *ngIf="!isObject(config.value) else RPCObjectRow"
<div *ngIf="isArray(config.value)" tbTruncateWithTooltip class="array-value">
{{ config.value | getRpcTemplateArrayView }}
</div>
<ng-container *ngIf="isObject(config.value)" [ngTemplateOutlet]="RPCObjectRow"></ng-container>
<div *ngIf="!isObject(config.value) && !isArray(config.value)"
[ngClass]="{'boolean-true': config.value === true,
'boolean-false': config.value === false }">
<ng-container *ngIf="config.key === 'method' else value" [ngTemplateOutlet]="SNMPMethod"></ng-container>

View File

@ -104,6 +104,10 @@
flex: 1;
margin: 0;
}
.array-value {
margin-left: 10px;
}
}

View File

@ -49,7 +49,8 @@ export class GatewayServiceRPCConnectorTemplatesComponent implements OnInit {
rpcTemplates: Array<RPCTemplate>;
public readonly originalOrder = (): number => 0;
public readonly isObject = (value: any) => isLiteralObject(value);
public readonly isObject = (value: unknown) => isLiteralObject(value);
public readonly isArray = (value: unknown) => Array.isArray(value);
public readonly SNMPMethodsTranslations = SNMPMethodsTranslations;
constructor(private attributeService: AttributeService) {

View File

@ -20,36 +20,6 @@
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()" formControlName="withResponse">
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
<mat-form-field *ngIf="commandForm.get('withResponse')?.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="commandForm.get('withResponse')?.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.BACNET">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>
@ -407,31 +377,6 @@
</button>
</fieldset>
</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>

View File

@ -51,7 +51,6 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
import { takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs";
@Component({
@ -129,7 +128,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
this.propagateChange({...this.commandForm.value, ...value});
}
});
this.observeMQTTWithResponse();
}
ngOnDestroy(): void {
@ -141,16 +139,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
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: [{ value: null, disabled: true }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTimeout: [{ value: null, disabled: true }, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
withResponse: [false, []],
});
break;
case ConnectorType.BACNET:
formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -246,12 +234,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
httpHeaders: this.fb.array([]),
})
break;
case ConnectorType.OPCUA:
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)]],
@ -293,18 +275,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
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();
@ -368,34 +338,8 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
})
delete value.httpHeaders;
break;
case ConnectorType.OPCUA:
this.clearFromArrayByName("arguments");
value.arguments.forEach(value => {
this.addOCPUAArguments(value)
})
delete value.arguments;
break;
}
this.commandForm.patchValue(value, {onlySelf: false});
}
}
private observeMQTTWithResponse(): void {
if (this.connectorType === ConnectorType.MQTT) {
this.commandForm.get('withResponse').valueChanges.pipe(
tap((isActive: boolean) => {
const responseTopicControl = this.commandForm.get('responseTopicExpression');
const responseTimeoutControl = this.commandForm.get('responseTimeout');
if (isActive) {
responseTopicControl.enable();
responseTimeoutControl.enable();
} else {
responseTopicControl.disable();
responseTimeoutControl.disable();
}
}),
takeUntil(this.destroy$),
).subscribe();
}
}
}

View File

@ -42,16 +42,20 @@
</ng-container>
<ng-template #connectorForm>
<tb-gateway-service-rpc-connector
*ngIf="connectorType !== ConnectorType.MODBUS else modbusParameters"
*ngIf="!typesWithUpdatedParams.has(connectorType) else updatedParameters"
formControlName="params"
[connectorType]="connectorType"
(sendCommand)="sendCommand()"
(saveTemplate)="saveTemplate()"
/>
<ng-template #modbusParameters>
<ng-template #updatedParameters>
<div fxLayout="column" class="rpc-parameters">
<div class="mat-subtitle-1 tb-form-panel-title">{{ 'gateway.rpc.title' | translate: {type: gatewayConnectorDefaultTypesTranslates.get(connectorType)} }}</div>
<tb-modbus-rpc-parameters formControlName="params"/>
<ng-container [ngSwitch]="connectorType">
<tb-modbus-rpc-parameters *ngSwitchCase="ConnectorType.MODBUS" formControlName="params"/>
<tb-mqtt-rpc-parameters *ngSwitchCase="ConnectorType.MQTT" formControlName="params"/>
<tb-opc-rpc-parameters *ngSwitchCase="ConnectorType.OPCUA" formControlName="params"/>
</ng-container>
<div class="template-actions" fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="10px">
<button mat-raised-button
(click)="saveTemplate()"

View File

@ -74,6 +74,11 @@ export class GatewayServiceRPCComponent implements OnInit {
readonly ConnectorType = ConnectorType;
readonly gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
readonly typesWithUpdatedParams = new Set<ConnectorType>([
ConnectorType.MQTT,
ConnectorType.OPCUA,
ConnectorType.MODBUS,
]);
private subscription: IWidgetSubscription;
private subscriptionOptions: WidgetSubscriptionOptions = {

View File

@ -297,7 +297,7 @@ export interface LegacyTimeseries {
export interface RpcArgument {
type: string;
value: number;
value: number | string | boolean;
}
export interface RpcMethod {
@ -542,6 +542,37 @@ export interface RPCTemplateConfig {
[key: string]: any;
}
export interface RPCTemplateConfigMQTT {
methodFilter: string;
requestTopicExpression: string;
responseTopicExpression?: string;
responseTimeout?: number;
valueExpression: string;
withResponse: boolean;
}
export interface RPCTemplateConfigModbus {
tag: string;
type: ModbusDataType;
functionCode?: number;
objectsCount: number;
address: number;
value?: string;
}
export interface RPCTemplateConfigOPC {
method: string;
arguments: RpcArgument[];
}
export interface OPCTypeValue {
type: MappingValueType;
boolean?: boolean;
double?: number;
integer?: number;
string?: string;
}
export interface SaveRPCTemplateData {
config: RPCTemplateConfig;
templates: Array<RPCTemplate>;

View File

@ -0,0 +1,28 @@
///
/// 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 { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'getRpcTemplateArrayView',
standalone: true,
})
export class RpcTemplateArrayViewPipe implements PipeTransform {
transform(values: {value: string | boolean | number}[]): string {
return values.map(({value}) => value.toString()).join(', ');
}
}

View File

@ -141,9 +141,6 @@ import {
import {
TypeValuePanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component';
import {
ModbusRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component';
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
import {
MqttLegacyBasicConfigComponent
@ -160,6 +157,16 @@ import {
import {
ModbusLegacyBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-legacy-basic-config.component';
import {
MqttRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/mqtt-rpc-parameters/mqtt-rpc-parameters.component';
import {
OpcRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/opc-rpc-parameters/opc-rpc-parameters.component';
import {
ModbusRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component';
import { RpcTemplateArrayViewPipe } from '@home/components/widget/lib/gateway/pipes/rpc-template-array-view.pipe';
import {
ReportStrategyComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component';
@ -256,6 +263,10 @@ import {
GatewayAdvancedConfigurationComponent,
OpcUaLegacyBasicConfigComponent,
ModbusLegacyBasicConfigComponent,
MqttRpcParametersComponent,
OpcRpcParametersComponent,
ModbusRpcParametersComponent,
RpcTemplateArrayViewPipe,
ReportStrategyComponent,
],
exports: [

View File

@ -3142,10 +3142,11 @@
"title": "{{type}} Connector RPC parameters",
"templates-title": "Connector RPC Templates",
"methodFilter": "Method filter",
"method-name": "Method name",
"requestTopicExpression": "Request topic expression",
"responseTopicExpression": "Response topic expression",
"responseTimeout": "Response Time",
"valueExpression": "Value Expression",
"responseTimeout": "Response timeout",
"valueExpression": "Value expression",
"tag": "Tag",
"type": "Type",
"functionCode": "Function Code",
@ -3184,6 +3185,11 @@
"tries": "Tries",
"httpHeaders": "HTTP Headers",
"header-name": "Header name",
"hint": {
"modbus-response-reading": "RPC response will return all subtracted values from all connected devices when the reading functions are selected.",
"modbus-writing-functions": "RPC will write a filled value to all connected devices when the writing functions are selected.",
"opc-method": "A filled method name is the OPC-UA method that will processed on the server side (make sure your node has the requested method)."
},
"security-name": "Security name",
"value": "Value",
"security": "Security",