Gateway fixes and added support for newer Gateway versions

This commit is contained in:
mpetrov 2024-10-02 14:34:52 +03:00
parent 97243cad43
commit 38b0e4fb02
12 changed files with 188 additions and 49 deletions

View File

@ -15,15 +15,17 @@
///
import { GatewayConnector, GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isNumber, isString } from '@core/utils';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
gatewayVersion: number;
configVersion: number;
protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector<BasicConfig>) {
this.gatewayVersion = this.parseVersion(this.gatewayVersionIn);
this.configVersion = this.parseVersion(this.connector.configVersion);
this.gatewayVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.gatewayVersionIn);
this.configVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.connector.configVersion);
}
getProcessedByVersion(): GatewayConnector<BasicConfig> {
@ -53,19 +55,13 @@ export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
}
private isVersionUpgradeNeeded(): boolean {
return this.gatewayVersionIn === GatewayVersion.Current && (!this.configVersion || this.configVersion < this.gatewayVersion);
return this.gatewayVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current)
&& (!this.configVersion || this.configVersion < this.gatewayVersion);
}
private isVersionDowngradeNeeded(): boolean {
return this.configVersion && this.connector.configVersion === GatewayVersion.Current && (this.configVersion > this.gatewayVersion);
}
private parseVersion(version: string | number): number {
if (isNumber(version)) {
return version as number;
}
return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0;
return this.configVersion && this.configVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current)
&& (this.configVersion > this.gatewayVersion);
}
protected abstract getDowngradedVersion(): GatewayConnector<BasicConfig>;

View File

@ -126,6 +126,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
if (!securityInfo.type) {
securityInfo.type = SecurityType.ANONYMOUS;
}
this.updateValidators(securityInfo.type);
this.securityFormGroup.reset(securityInfo, {emitEvent: false});
}
this.cdr.markForCheck();

View File

@ -37,6 +37,7 @@ import { Observable, Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from 'rxjs/operators';
import { helpBaseUrl } from '@shared/models/constants';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
@Component({
selector: 'tb-add-connector-dialog',
@ -63,6 +64,7 @@ export class AddConnectorDialogComponent
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData,
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>,
private fb: FormBuilder,
private isLatestVersionConfig: LatestVersionConfigPipe,
private resourcesService: ResourcesService) {
super(store, router, dialogRef);
this.connectorForm = this.fb.group({
@ -103,9 +105,9 @@ export class AddConnectorDialogComponent
if (gatewayVersion) {
value.configVersion = gatewayVersion;
}
value.configurationJson = (gatewayVersion === GatewayVersion.Current
? defaultConfig[this.data.gatewayVersion]
: defaultConfig.legacy)
value.configurationJson = (this.isLatestVersionConfig.transform(gatewayVersion)
? defaultConfig[GatewayVersion.Current]
: defaultConfig[GatewayVersion.Legacy])
?? defaultConfig;
if (this.connectorForm.valid) {
this.dialogRef.close(value);

View File

@ -178,7 +178,7 @@
<ng-container [ngSwitch]="initialConnector.type">
<ng-container *ngSwitchCase="ConnectorType.MQTT">
<tb-mqtt-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -193,7 +193,7 @@
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.OPCUA">
<tb-opc-ua-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -208,7 +208,7 @@
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.MODBUS">
<tb-modbus-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
@ -316,7 +316,7 @@
</div>
<tb-report-strategy
[defaultValue]="ReportStrategyDefaultValue.Connector"
*ngIf="connectorForm.get('type').value === ConnectorType.MODBUS && connectorForm.get('configVersion').value === GatewayVersion.Current"
*ngIf="connectorForm.get('type').value === ConnectorType.MODBUS && (connectorForm.get('configVersion').value | isLatestVersionConfig)"
formControlName="reportStrategy"
/>
</section>

View File

@ -59,7 +59,6 @@ import {
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
noLeadTrailSpacesRegex,
GatewayVersion,
ReportStrategyDefaultValue,
ReportStrategyType,
} from './gateway-widget.models';
@ -71,6 +70,7 @@ import { PageData } from '@shared/models/page/page-data';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
export class ForceErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null): boolean {
@ -104,7 +104,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
readonly ConnectorConfigurationModes = ConfigurationModes;
readonly GatewayVersion = GatewayVersion;
readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue;
pageLink: PageLink;
@ -149,6 +148,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
private isLatestVersionConfig: LatestVersionConfigPipe,
private cd: ChangeDetectorRef) {
super(store);
@ -255,7 +255,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
delete value.class;
}
if (value.type === ConnectorType.MODBUS && value.configVersion === GatewayVersion.Current) {
if (value.type === ConnectorType.MODBUS && this.isLatestVersionConfig.transform(value.configVersion)) {
if (!value.reportStrategy) {
value.reportStrategy = {
type: ReportStrategyType.OnReportPeriod,
@ -508,6 +508,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
if (!connector.configurationJson) {
connector.configurationJson = {} as ConnectorBaseConfig;
}
if (this.gatewayVersion && !connector.configVersion) {
connector.configVersion = this.gatewayVersion;
}
connector.basicConfig = connector.configurationJson;
this.initialConnector = connector;
@ -517,7 +520,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.saveConnector(this.getUpdatedConnectorData(connector));
if (!previousType || previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
this.patchBasicConfigConnector(connector);
} else {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
@ -527,24 +530,26 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
private setInitialConnectorValues(connector: GatewayConnector): void {
const {basicConfig, mode, ...initialConnector} = connector;
this.toggleReportStrategy(connector.type);
this.connectorForm.get('mode').setValue(this.allowBasicConfig.has(connector.type)
? connector.mode ?? ConfigurationModes.BASIC
: null, {emitEvent: false}
);
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
this.connectorForm.get('type').setValue(connector.type, {emitEvent: false});
this.connectorForm.patchValue(initialConnector, {emitEvent: false});
}
private openAddConnectorDialog(): Observable<GatewayConnector> {
return this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
return this.ctx.ngZone.run(() =>
this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataSourceData: this.dataSource.data,
gatewayVersion: this.gatewayVersion,
}
}).afterClosed();
}).afterClosed()
);
}
uniqNameRequired(): ValidatorFn {
@ -650,13 +655,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private observeModeChange(): void {
this.connectorForm.get('mode').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((mode) => {
.subscribe(() => {
this.connectorForm.get('mode').markAsPristine();
if (mode === ConfigurationModes.BASIC) {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector({...this.initialConnector, mode: ConfigurationModes.BASIC});
});
}
});
}
@ -827,12 +827,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
...connector,
}, this.gatewayVersion);
if (this.gatewayVersion && !connectorState.configVersion) {
connectorState.configVersion = this.gatewayVersion;
}
connectorState.basicConfig = connectorState.configurationJson;
this.initialConnector = connectorState;
this.updateConnector(connectorState);
}
private updateConnector(connector: GatewayConnector): void {
this.jsonConfigSub?.unsubscribe();
switch (connector.type) {
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
@ -842,18 +847,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
default:
this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine();
}
this.createJsonConfigWatcher();
}
}
private updateBasicConfigConnector(connector: GatewayConnector): void {
this.basicConfigSub?.unsubscribe();
const previousType = this.connectorForm.get('type').value;
this.setInitialConnectorValues(connector);
if ((!connector.mode || connector.mode === ConfigurationModes.BASIC) && this.connectorForm.get('type').value !== connector.type) {
if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
this.patchBasicConfigConnector(connector);
} else {
this.basicConfigInitSubject.asObservable().pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector(connector);
});
} else {
this.patchBasicConfigConnector(connector);
}
}
@ -861,6 +869,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
this.createJsonConfigWatcher();
}
private toggleReportStrategy(type: ConnectorType): void {

View File

@ -33,6 +33,8 @@ export class GatewayHelpLinkPipe implements PipeTransform {
} else {
return;
}
} else if (field === 'attributes' || field === 'timeseries') {
return 'widget/lib/gateway/attributes_timeseries_expressions_fn';
}
return 'widget/lib/gateway/expressions_fn';
}

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.
///
import { Pipe, PipeTransform } from '@angular/core';
import { GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
@Pipe({
name: 'isLatestVersionConfig',
standalone: true,
})
export class LatestVersionConfigPipe implements PipeTransform {
transform(configVersion: number | string): boolean {
return GatewayConnectorVersionMappingUtil.parseVersion(configVersion)
>= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current);
}
}

View File

@ -24,6 +24,7 @@ import {
import { MqttVersionProcessor } from '@home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract';
import { OpcVersionProcessor } from '@home/components/widget/lib/gateway/abstract/opc-version-processor.abstract';
import { ModbusVersionProcessor } from '@home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract';
import { isNumber, isString } from '@core/utils';
export abstract class GatewayConnectorVersionMappingUtil {
@ -39,4 +40,12 @@ export abstract class GatewayConnectorVersionMappingUtil {
return connector;
}
}
static parseVersion(version: string | number): number {
if (isNumber(version)) {
return version as number;
}
return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0;
}
}

View File

@ -56,7 +56,7 @@ export class OpcVersionMappingUtil {
static mapMappingToUpgradedVersion(mapping: LegacyDeviceConnectorMapping[]): DeviceConnectorMapping[] {
return mapping.map((legacyMapping: LegacyDeviceConnectorMapping) => ({
...legacyMapping,
deviceNodeSource: this.getTypeSourceByValue(legacyMapping.deviceNodePattern),
deviceNodeSource: this.getDeviceNodeSourceByValue(legacyMapping.deviceNodePattern),
deviceInfo: {
deviceNameExpression: legacyMapping.deviceNamePattern,
deviceNameExpressionSource: this.getTypeSourceByValue(legacyMapping.deviceNamePattern),
@ -122,6 +122,14 @@ export class OpcVersionMappingUtil {
return OPCUaSourceType.CONST;
}
private static getDeviceNodeSourceByValue(value: string): OPCUaSourceType {
if (value.includes('${')) {
return OPCUaSourceType.IDENTIFIER;
} else {
return OPCUaSourceType.PATH;
}
}
private static getArgumentType(arg: unknown): string {
switch (typeof arg) {
case 'boolean':

View File

@ -170,6 +170,7 @@ import { RpcTemplateArrayViewPipe } from '@home/components/widget/lib/gateway/pi
import {
ReportStrategyComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
@NgModule({
declarations: [
@ -268,6 +269,7 @@ import {
ModbusRpcParametersComponent,
RpcTemplateArrayViewPipe,
ReportStrategyComponent,
LatestVersionConfigPipe,
],
exports: [
EntitiesTableWidgetComponent,
@ -337,7 +339,8 @@ import {
ScadaSymbolWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule}
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule},
{provide: LatestVersionConfigPipe}
]
})
export class WidgetComponentsModule {

View File

@ -0,0 +1,65 @@
### Expressions
#### JSON Path:
The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages:
- The JSONPath format can be used to extract data from the message body.
- The regular expression format can be used to extract data from the topic where the message will arrive.
- Slices can only be used in the expression fields of bytes converters.
JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured:
- `$`: The root element of the JSON document;
- `.`: Child operator used to select child elements. For example, $.store.book ;
- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object;
##### Examples:
For example, if we want to extract the device name from the following message, we can use the expression below:
MQTT message:
```
{
"sensorModelInfo": {
"sensorName": "AM-123",
"sensorType": "myDeviceType"
},
"data": {
"temp": 12.2,
"hum": 56,
"status": "ok"
}
}
{:copy-code}
```
Expression:
`${sensorModelInfo.sensorName}`
Converted data:
`AM-123`
If we want to extract all data from the message above, we can use the following expression:
`${data}`
Converted data:
`{"temp": 12.2, "hum": 56, "status": "ok"}`
Or if we want to extract specific data (for example “temperature”), you can use the following expression:
`${data.temp}`
And as a converted data we will get:
`12.2`
<br/>

View File

@ -20,13 +20,21 @@
"connectAttemptTimeMs": 5000,
"connectAttemptCount": 5,
"waitAfterFailedAttemptsMs": 300000,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 30000
},
"attributes": [
{
"tag": "string_read",
"type": "string",
"functionCode": 4,
"objectsCount": 4,
"address": 1
"address": 1,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 15000
}
},
{
"tag": "bits_read",
@ -86,7 +94,11 @@
"type": "8uint",
"functionCode": 4,
"objectsCount": 1,
"address": 17
"address": 17,
"reportStrategy": {
"type": "ON_REPORT_PERIOD",
"reportPeriod": 15000
}
},
{
"tag": "16uint_read",