diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts index a781c52c71..93baf124a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts @@ -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 { gatewayVersion: number; configVersion: number; protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector) { - 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 { @@ -53,19 +55,13 @@ export abstract class GatewayConnectorVersionProcessor { } 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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts index 18d1a96e38..a6ee3ce93d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts @@ -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(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts index c8235cb697..1c0f74e8e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -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, 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); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index cad7af4392..a700dcc94b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -178,7 +178,7 @@ 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 8ca92d4ca7..00fd3e19c2 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 @@ -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 { - return this.dialog.open(AddConnectorDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - dataSourceData: this.dataSource.data, - gatewayVersion: this.gatewayVersion, - } - }).afterClosed(); + return this.ctx.ngZone.run(() => + this.dialog.open(AddConnectorDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataSourceData: this.dataSource.data, + gatewayVersion: this.gatewayVersion, + } + }).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(); } - 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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts index 06526ce08e..27b5285166 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts @@ -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'; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/latest-version-config.pipe.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/latest-version-config.pipe.ts new file mode 100644 index 0000000000..56484dde35 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/latest-version-config.pipe.ts @@ -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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts index 90c89ea316..fe507c1c46 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts @@ -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; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts index 9837f2f15d..f589dfea97 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts @@ -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': 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 9d08ee0324..1b044e787d 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 @@ -167,9 +167,10 @@ 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 { +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 { diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md new file mode 100644 index 0000000000..91cf23ce26 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md @@ -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` + +
diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json index 019d2062f8..cfa983710b 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json @@ -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",