Merge branch 'rc'

This commit is contained in:
Igor Kulikov 2024-09-26 11:25:42 +03:00
commit a37ce176b2
24 changed files with 215 additions and 101 deletions

View File

@ -11,7 +11,7 @@
"resources": [], "resources": [],
"templateHtml": "<tb-gateway-connector [device]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-connector>", "templateHtml": "<tb-gateway-connector [device]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-connector>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}", "controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.gatewayConnectors?.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"settingsSchema": "{}", "settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway connectors\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway connectors\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"

View File

@ -360,6 +360,30 @@
"color": "#2196f3", "color": "#2196f3",
"settings": {}, "settings": {},
"_hash": 0.7454705362378311 "_hash": 0.7454705362378311
},
{
"name": "lastConnectTime",
"type": "attribute",
"label": "lastConnectTime",
"color": "#4caf50",
"settings": {},
"_hash": 0.7249585632235194
},
{
"name": "lastDisconnectTime",
"type": "attribute",
"label": "lastDisconnectTime",
"color": "#f44336",
"settings": {},
"_hash": 0.9812430092707332
},
{
"name": "active",
"type": "attribute",
"label": "active",
"color": "#ffc107",
"settings": {},
"_hash": 0.9216097189544408
} }
] ]
} }

View File

@ -14,16 +14,17 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Directive, inject, Input, OnDestroy, TemplateRef } from '@angular/core'; import { AfterViewInit, Directive, EventEmitter, inject, Input, OnDestroy, Output, TemplateRef } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, ValidationErrors, Validator } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, ValidationErrors, Validator } from '@angular/forms';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@Directive() @Directive()
export abstract class GatewayConnectorBasicConfigDirective<InputBasicConfig, OutputBasicConfig> export abstract class GatewayConnectorBasicConfigDirective<InputBasicConfig, OutputBasicConfig>
implements ControlValueAccessor, Validator, OnDestroy { implements AfterViewInit, ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<any>; @Input() generalTabContent: TemplateRef<any>;
@Output() initialized = new EventEmitter<void>();
basicFormGroup: FormGroup; basicFormGroup: FormGroup;
@ -45,6 +46,10 @@ export abstract class GatewayConnectorBasicConfigDirective<InputBasicConfig, Out
this.destroy$.complete(); this.destroy$.complete();
} }
ngAfterViewInit(): void {
this.initialized.emit();
}
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : { basicFormGroup: { valid: false } }; return this.basicFormGroup.valid ? null : { basicFormGroup: { valid: false } };
} }

View File

@ -23,14 +23,22 @@ export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector<BasicConfig>) { protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector<BasicConfig>) {
this.gatewayVersion = this.parseVersion(this.gatewayVersionIn); this.gatewayVersion = this.parseVersion(this.gatewayVersionIn);
this.configVersion = this.parseVersion(connector.configVersion); this.configVersion = this.parseVersion(this.connector.configVersion);
} }
getProcessedByVersion(): GatewayConnector<BasicConfig> { getProcessedByVersion(): GatewayConnector<BasicConfig> {
if (this.isVersionUpdateNeeded()) { if (!this.isVersionUpdateNeeded()) {
return this.isVersionUpgradeNeeded() return this.connector;
? this.getUpgradedVersion() }
: this.getDowngradedVersion();
return this.processVersionUpdate();
}
private processVersionUpdate(): GatewayConnector<BasicConfig> {
if (this.isVersionUpgradeNeeded()) {
return this.getUpgradedVersion();
} else if (this.isVersionDowngradeNeeded()) {
return this.getDowngradedVersion();
} }
return this.connector; return this.connector;
@ -48,6 +56,10 @@ export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
return this.gatewayVersionIn === GatewayVersion.Current && (!this.configVersion || this.configVersion < this.gatewayVersion); return this.gatewayVersionIn === 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 { private parseVersion(version: string | number): number {
if (isNumber(version)) { if (isNumber(version)) {
return version as number; return version as number;

View File

@ -38,7 +38,7 @@ export class OpcVersionProcessor extends GatewayConnectorVersionProcessor<OPCBas
...this.connector, ...this.connector,
configurationJson: { configurationJson: {
server: server ? OpcVersionMappingUtil.mapServerToUpgradedVersion(server) : {}, server: server ? OpcVersionMappingUtil.mapServerToUpgradedVersion(server) : {},
mapping: server.mapping ? OpcVersionMappingUtil.mapMappingToUpgradedVersion(server.mapping) : [], mapping: server?.mapping ? OpcVersionMappingUtil.mapMappingToUpgradedVersion(server.mapping) : [],
}, },
configVersion: this.gatewayVersionIn configVersion: this.gatewayVersionIn
} as GatewayConnector<OPCBasicConfig_v3_5_2>; } as GatewayConnector<OPCBasicConfig_v3_5_2>;

View File

@ -693,7 +693,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="flex"> <mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.inactivity-check-period-seconds</mat-label> <mat-label translate>gateway.inactivity-check-period-seconds</mat-label>
<input matInput formControlName="inactivityCheckPeriodSeconds"/> <input matInput type="number" min="0" formControlName="inactivityCheckPeriodSeconds"/>
<mat-error <mat-error
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')"> *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')">
{{ 'gateway.inactivity-check-period-seconds-required' | translate }} {{ 'gateway.inactivity-check-period-seconds-required' | translate }}

View File

@ -502,14 +502,14 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
} }
private addFileStorageValidators(group: FormGroup): void { private addFileStorageValidators(group: FormGroup): void {
['data_folder_path', 'max_file_count', 'max_read_records_count', 'max_records_per_file'].forEach(field => { ['max_file_count', 'max_read_records_count', 'max_records_per_file'].forEach(field => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]); group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false }); group.get(field).updateValueAndValidity({ emitEvent: false });
}); });
} }
private addSqliteStorageValidators(group: FormGroup): void { private addSqliteStorageValidators(group: FormGroup): void {
['data_file_path', 'messages_ttl_check_in_hours', 'messages_ttl_in_days'].forEach(field => { ['messages_ttl_check_in_hours', 'messages_ttl_in_days'].forEach(field => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]); group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false }); group.get(field).updateValueAndValidity({ emitEvent: false });
}); });

View File

@ -242,7 +242,7 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
consoleHandler: { consoleHandler: {
class: 'logging.StreamHandler', class: 'logging.StreamHandler',
formatter: 'LogFormatter', formatter: 'LogFormatter',
level: 'DEBUG', level: 0,
stream: 'ext://sys.stdout' stream: 'ext://sys.stdout'
}, },
databaseHandler: { databaseHandler: {

View File

@ -128,7 +128,7 @@ interface LogFormatterConfig {
interface StreamHandlerConfig { interface StreamHandlerConfig {
class: string; class: string;
formatter: string; formatter: string;
level: string; level: string | number;
stream: string; stream: string;
} }

View File

@ -26,10 +26,13 @@
<mat-expansion-panel class="tb-settings" [expanded]="last"> <mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title> <mat-panel-title>
<div class="title-container" *ngIf="keysType !== MappingKeysType.RPC_METHODS"> <ng-container *ngIf="keysType !== MappingKeysType.RPC_METHODS">
{{ keyControl.get('key').value }}{{ '-' }} <div tbTruncateWithTooltip class="title-container">
</div> {{ keyControl.get('key').value }}
<div class="title-container">{{ valueTitle(keyControl) }}</div> </div>
{{ '-' }}
</ng-container>
<div tbTruncateWithTooltip class="title-container">{{ valueTitle(keyControl) }}</div>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>

View File

@ -62,17 +62,25 @@
</mat-toolbar> </mat-toolbar>
<div class="table-container"> <div class="table-container">
<table mat-table [dataSource]="dataSource"> <table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="'name'"> <ng-container [matColumnDef]="'info'">
<mat-header-cell *matHeaderCellDef class="table-value-column"> <mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }} {{ 'gateway.info' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column"> <mat-cell *matCellDef="let slave" class="table-value-column">
{{ slave['name'] }} <div tbTruncateWithTooltip>{{ slave['host'] ?? slave['port'] }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'unitId'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.unit-id' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
<div tbTruncateWithTooltip>{{ slave['unitId'] }}</div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container [matColumnDef]="'type'"> <ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column"> <mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }} <div tbTruncateWithTooltip>{{ 'gateway.client-communication-type' | translate }}</div>
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column"> <mat-cell *matCellDef="let slave" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(slave['type']) }} {{ ModbusProtocolLabelsMap.get(slave['type']) }}
@ -113,8 +121,8 @@
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row> <mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['info', 'unitId', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let slave; columns: ['name', 'type', 'actions']"></mat-row> <mat-row *matRowDef="let slave; columns: ['info', 'unitId', 'type', 'actions']"></mat-row>
</table> </table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center" <section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new"> class="mat-headline-5 tb-absolute-fill tb-add-new">

View File

@ -113,7 +113,6 @@ export abstract class ModbusSlaveDialogAbstract<Component, Config> extends Dialo
private initializeSlaveFormGroup(): void { private initializeSlaveFormGroup(): void {
this.slaveConfigFormGroup = this.fb.group({ this.slaveConfigFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusProtocolType.TCP], type: [ModbusProtocolType.TCP],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],

View File

@ -27,23 +27,6 @@
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel"> <div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width slave-name-label tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.name-required') | translate"
*ngIf="slaveConfigFormGroup.get('name').hasError('required') &&
slaveConfigFormGroup.get('name').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="stroked tb-form-panel"> <div class="stroked tb-form-panel">
<div class="tb-form-panel no-border no-padding padding-top"> <div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-flex row space-between align-center no-gap fill-width"> <div class="tb-flex row space-between align-center no-gap fill-width">

View File

@ -17,11 +17,14 @@
import { Directive } from '@angular/core'; import { Directive } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { import {
BrokerConfig,
MappingType, MappingType,
MQTTBasicConfig, MQTTBasicConfig_v3_5_2, MQTTBasicConfig,
MQTTBasicConfig_v3_5_2,
RequestMappingData, RequestMappingData,
RequestMappingValue, RequestMappingValue,
RequestType RequestType,
WorkersConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isObject } from '@core/utils'; import { isObject } from '@core/utils';
import { import {
@ -73,6 +76,14 @@ export abstract class MqttBasicConfigDirective<BasicConfig>
}); });
} }
protected getBrokerMappedValue(broker: BrokerConfig, workers: WorkersConfig): BrokerConfig {
return {
...broker,
maxNumberOfWorkers: workers.maxNumberOfWorkers ?? 100,
maxMessageNumberPerWorker: workers.maxMessageNumberPerWorker ?? 10,
};
}
writeValue(basicConfig: BasicConfig): void { writeValue(basicConfig: BasicConfig): void {
this.basicFormGroup.setValue(this.mapConfigToFormValue(basicConfig), { emitEvent: false }); this.basicFormGroup.setValue(this.mapConfigToFormValue(basicConfig), { emitEvent: false });
} }

View File

@ -26,7 +26,6 @@ import {
import { import {
MqttBasicConfigDirective MqttBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract'; } from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract';
import { isDefinedAndNotNull } from '@core/utils';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { import {
@ -85,19 +84,14 @@ export class MqttBasicConfigComponent extends MqttBasicConfigDirective<MQTTBasic
} }
protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTBasicConfig_v3_5_2 { protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTBasicConfig_v3_5_2 {
let { broker, workers, mapping, requestsMapping } = basicConfig || {}; const { broker, workers, mapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) { return {
broker = { broker: this.getBrokerMappedValue(broker, workers),
...broker, mapping,
...workers, requestsMapping: (requestsMapping as RequestMappingData[])?.length
}; ? this.getRequestDataObject(requestsMapping as RequestMappingValue[])
} : {} as Record<RequestType, RequestMappingValue[]>
};
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingValue[]);
}
return { broker, mapping, requestsMapping };
} }
} }

View File

@ -102,23 +102,16 @@ export class MqttLegacyBasicConfigComponent extends MqttBasicConfigDirective<MQT
} }
protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTLegacyBasicConfig { protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTLegacyBasicConfig {
let { broker, workers, mapping, requestsMapping } = basicConfig || {}; const { broker, workers, mapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) { const updatedRequestMapping = (requestsMapping as RequestMappingData[])?.length
broker = { ? this.getRequestDataObject(requestsMapping as RequestMappingValue[])
...broker, : {} as Record<RequestType, RequestMappingData[]>;
...workers,
};
}
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingValue[]);
}
return { return {
broker, broker: this.getBrokerMappedValue(broker, workers),
mapping: MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping), mapping: MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping),
...(MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>)) ...(MqttVersionMappingUtil.mapRequestsToDowngradedVersion(updatedRequestMapping as Record<RequestType, RequestMappingData[]>))
}; };
} }
} }

View File

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

View File

@ -181,9 +181,14 @@
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy" *ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig" formControlName="basicConfig"
[generalTabContent]="generalTabContent" [generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/> />
<ng-template #legacy> <ng-template #legacy>
<tb-mqtt-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/> <tb-mqtt-legacy-basic-config
(initialized)="basicConfigInitSubject.next()"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="ConnectorType.OPCUA"> <ng-container *ngSwitchCase="ConnectorType.OPCUA">
@ -191,9 +196,14 @@
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy" *ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig" formControlName="basicConfig"
[generalTabContent]="generalTabContent" [generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/> />
<ng-template #legacy> <ng-template #legacy>
<tb-opc-ua-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/> <tb-opc-ua-legacy-basic-config
(initialized)="basicConfigInitSubject.next()"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="ConnectorType.MODBUS"> <ng-container *ngSwitchCase="ConnectorType.MODBUS">
@ -201,9 +211,14 @@
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy" *ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig" formControlName="basicConfig"
[generalTabContent]="generalTabContent" [generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/> />
<ng-template #legacy> <ng-template #legacy>
<tb-modbus-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/> <tb-modbus-legacy-basic-config
formControlName="basicConfig"
(initialized)="basicConfigInitSubject.next()"
[generalTabContent]="generalTabContent"
/>
</ng-template> </ng-template>
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -229,7 +244,7 @@
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="button" type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid" [disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()"> (click)="saveConnector(false)">
{{ 'action.save' | translate }} {{ 'action.save' | translate }}
</button> </button>
</div> </div>

View File

@ -31,7 +31,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Observable, of, Subject, Subscription } from 'rxjs'; import { forkJoin, Observable, of, Subject, Subscription } from 'rxjs';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
@ -110,8 +110,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
activeConnectors: Array<string>; activeConnectors: Array<string>;
mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC; mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC;
initialConnector: GatewayConnector; initialConnector: GatewayConnector;
basicConfigInitSubject = new Subject<void>();
private gatewayVersion: string; private gatewayVersion: string;
private isGatewayActive: boolean;
private inactiveConnectors: Array<string>; private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource; private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource; private inactiveConnectorsDataSource: AttributeDatasource;
@ -124,7 +126,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private subscriptionOptions: WidgetSubscriptionOptions = { private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: { callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => { onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated(); this.onErrorsUpdated();
}), }),
onDataUpdateError: (_, e) => this.ctx.ngZone.run(() => { onDataUpdateError: (_, e) => this.ctx.ngZone.run(() => {
this.onDataUpdateError(e); this.onDataUpdateError(e);
@ -155,8 +157,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = this.getSortingDataAccessor(); this.dataSource.sortingDataAccessor = this.getSortingDataAccessor();
this.ctx.$scope.gatewayConnectors = this;
this.loadConnectors(); this.loadConnectors();
this.loadGatewayState();
this.observeModeChange(); this.observeModeChange();
} }
@ -166,9 +170,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
super.ngOnDestroy(); super.ngOnDestroy();
} }
saveConnector(): void { saveConnector(isNew = true): void {
const value = this.getConnectorData(); const value = this.getConnectorData();
const scope = (!this.initialConnector || this.activeConnectors.includes(this.initialConnector.name)) const scope = (isNew || this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE ? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE; : AttributeScope.SERVER_SCOPE;
@ -275,7 +279,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
isConnectorSynced(attribute: GatewayAttributeData): boolean { isConnectorSynced(attribute: GatewayAttributeData): boolean {
const connectorData = attribute.value; const connectorData = attribute.value;
if (!connectorData.ts || attribute.skipSync) { if (!connectorData.ts || attribute.skipSync || !this.isGatewayActive) {
return false; return false;
} }
const clientIndex = this.activeData.findIndex(data => { const clientIndex = this.activeData.findIndex(data => {
@ -479,7 +483,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
filter(Boolean), filter(Boolean),
) )
.subscribe(value => { .subscribe(value => {
this.initialConnector = null;
if (this.connectorForm.disabled) { if (this.connectorForm.disabled) {
this.connectorForm.enable(); this.connectorForm.enable();
} }
@ -487,9 +490,16 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
value.configurationJson = {} as ConnectorBaseConfig; value.configurationJson = {} as ConnectorBaseConfig;
} }
value.basicConfig = value.configurationJson; value.basicConfig = value.configurationJson;
this.updateConnector(value); this.initialConnector = value;
this.connectorForm.patchValue(value, {emitEvent: false});
this.generate('basicConfig.broker.clientId'); this.generate('basicConfig.broker.clientId');
setTimeout(() => this.saveConnector()); if (this.connectorForm.get('type').value === value.type || !this.allowBasicConfig.has(value.type)) {
this.saveConnector();
} else {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.saveConnector();
});
}
}); });
} }
@ -590,6 +600,19 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
private loadGatewayState(): void {
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE)
.pipe(takeUntil(this.destroy$))
.subscribe((attributes: AttributeData[]) => {
const active = attributes.find(data => data.key === 'active').value;
const lastDisconnectedTime = attributes.find(data => data.key === 'lastDisconnectTime')?.value;
const lastConnectedTime = attributes.find(data => data.key === 'lastConnectTime').value;
this.isGatewayActive = this.getGatewayStatus(active, lastConnectedTime, lastDisconnectedTime);
});
}
private parseConnectors(attribute: GatewayAttributeData[]): string[] { private parseConnectors(attribute: GatewayAttributeData[]): string[] {
const connectors = attribute?.[0]?.value || []; const connectors = attribute?.[0]?.value || [];
return isString(connectors) ? JSON.parse(connectors) : connectors; return isString(connectors) ? JSON.parse(connectors) : connectors;
@ -598,7 +621,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private observeModeChange(): void { private observeModeChange(): void {
this.connectorForm.get('mode').valueChanges this.connectorForm.get('mode').valueChanges
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(() => this.connectorForm.get('mode').markAsPristine()); .subscribe((mode) => {
this.connectorForm.get('mode').markAsPristine();
if (mode === ConfigurationModes.BASIC) {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector({...this.initialConnector, mode: ConfigurationModes.BASIC});
});
}
});
} }
private observeAttributeChange(): void { private observeAttributeChange(): void {
@ -664,10 +694,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText); console.error(errorText);
} }
private onDataUpdated(): void { private onErrorsUpdated(): void {
this.cd.detectChanges(); this.cd.detectChanges();
} }
private onDataUpdated(): void {
const dataSources = this.ctx.defaultSubscription.data;
const active = dataSources.find(data => data.dataKey.name === 'active').data[0][1];
const lastDisconnectedTime = dataSources.find(data => data.dataKey.name === 'lastDisconnectTime').data[0][1];
const lastConnectedTime = dataSources.find(data => data.dataKey.name === 'lastConnectTime').data[0][1];
this.isGatewayActive = this.getGatewayStatus(active, lastConnectedTime, lastDisconnectedTime);
this.cd.detectChanges();
}
private getGatewayStatus(active: boolean, lastConnectedTime: number, lastDisconnectedTime: number): boolean {
if (!active) {
return false;
}
return !lastDisconnectedTime || lastConnectedTime > lastDisconnectedTime;
}
private generateSubscription(): void { private generateSubscription(): void {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
@ -760,13 +809,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
case ConnectorType.MQTT: case ConnectorType.MQTT:
case ConnectorType.OPCUA: case ConnectorType.OPCUA:
case ConnectorType.MODBUS: case ConnectorType.MODBUS:
this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false}); this.updateBasicConfigConnector(connector);
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
setTimeout(() => {
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
});
break; break;
default: default:
this.connectorForm.patchValue({...connector, mode: null}); this.connectorForm.patchValue({...connector, mode: null});
@ -775,6 +818,24 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.createJsonConfigWatcher(); this.createJsonConfigWatcher();
} }
private updateBasicConfigConnector(connector: GatewayConnector): void {
this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false});
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
if ((!connector.mode || connector.mode === ConfigurationModes.BASIC) && this.connectorForm.get('type').value !== connector.type) {
this.basicConfigInitSubject.asObservable().pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector(connector);
});
} else {
this.patchBasicConfigConnector(connector);
}
}
private patchBasicConfigConnector(connector: GatewayConnector): void {
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
}
private toggleReportStrategy(type: ConnectorType): void { private toggleReportStrategy(type: ConnectorType): void {
const reportStrategyControl = this.connectorForm.get('reportStrategy'); const reportStrategyControl = this.connectorForm.get('reportStrategy');
if (type === ConnectorType.MODBUS) { if (type === ConnectorType.MODBUS) {

View File

@ -711,7 +711,7 @@ export const HelpLinkByMappingTypeMap = new Map<MappingType, string>(
[ [
[MappingType.DATA, helpBaseUrl + '/docs/iot-gateway/config/mqtt/#section-mapping'], [MappingType.DATA, helpBaseUrl + '/docs/iot-gateway/config/mqtt/#section-mapping'],
[MappingType.OPCUA, helpBaseUrl + '/docs/iot-gateway/config/opc-ua/#section-mapping'], [MappingType.OPCUA, helpBaseUrl + '/docs/iot-gateway/config/opc-ua/#section-mapping'],
[MappingType.REQUESTS, helpBaseUrl + '/docs/iot-gateway/config/mqtt/#section-mapping'] [MappingType.REQUESTS, helpBaseUrl + '/docs/iot-gateway/config/mqtt/#requests-mapping']
] ]
); );

View File

@ -45,7 +45,7 @@ export class OpcVersionMappingUtil {
static mapServerToDowngradedVersion(config: OPCBasicConfig_v3_5_2): LegacyServerConfig { static mapServerToDowngradedVersion(config: OPCBasicConfig_v3_5_2): LegacyServerConfig {
const { mapping, server } = config; const { mapping, server } = config;
const { enableSubscriptions, ...restServer } = server; const { enableSubscriptions, ...restServer } = server ?? {} as ServerConfig;
return { return {
...restServer, ...restServer,
mapping: mapping ? this.mapMappingToDowngradedVersion(mapping) : [], mapping: mapping ? this.mapMappingToDowngradedVersion(mapping) : [],

View File

@ -464,6 +464,12 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
'.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] mat-icon {\n' + '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] mat-icon {\n' +
'color: ' + mdDarkDisabled + ';\n' + 'color: ' + mdDarkDisabled + ';\n' +
'}\n' + '}\n' +
'.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button tb-icon {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] tb-icon {\n' +
'color: ' + mdDarkDisabled + ';\n' +
'}\n' +
'.mat-divider {\n' + '.mat-divider {\n' +
'border-top-color: ' + mdDarkDivider + ';\n' + 'border-top-color: ' + mdDarkDivider + ';\n' +
'}\n' + '}\n' +

View File

@ -3036,6 +3036,7 @@
"grpc-max-pings-without-data-required": "Max pings without data is required", "grpc-max-pings-without-data-required": "Max pings without data is required",
"grpc-max-pings-without-data-min": "Max pings without data can not be less then 1", "grpc-max-pings-without-data-min": "Max pings without data can not be less then 1",
"grpc-max-pings-without-data-pattern": "Max pings without data is not valid", "grpc-max-pings-without-data-pattern": "Max pings without data is not valid",
"info": "Info",
"identity": "Identity", "identity": "Identity",
"inactivity-check-period-seconds": "Inactivity check period (in sec)", "inactivity-check-period-seconds": "Inactivity check period (in sec)",
"inactivity-check-period-seconds-required": "Inactivity check period is required", "inactivity-check-period-seconds-required": "Inactivity check period is required",
@ -3317,7 +3318,7 @@
"check-connectors-configuration-min": "Check connectors configuration can not be less then 1", "check-connectors-configuration-min": "Check connectors configuration can not be less then 1",
"check-connectors-configuration-pattern": "Check connectors configuration is not valid", "check-connectors-configuration-pattern": "Check connectors configuration is not valid",
"add": "Add command", "add": "Add command",
"timeout": "Timeout", "timeout": "Timeout (in sec)",
"timeout-ms": "Timeout (in ms)", "timeout-ms": "Timeout (in ms)",
"timeout-required": "Timeout is required", "timeout-required": "Timeout is required",
"timeout-min": "Timeout can not be less then 1", "timeout-min": "Timeout can not be less then 1",

View File

@ -3,7 +3,6 @@
"master": { "master": {
"slaves": [ "slaves": [
{ {
"name": "Slave 1",
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 5021, "port": 5021,
"type": "tcp", "type": "tcp",
@ -231,10 +230,10 @@
"attributes": [ "attributes": [
{ {
"address": 5, "address": 5,
"type": "string", "type": "8int",
"tag": "sm", "tag": "coil",
"objectsCount": 1, "objectsCount": 1,
"value": "12" "value": 0
} }
], ],
"timeseries": [], "timeseries": [],