Browse Source

functionality adjustments

pull/11235/head
mpetrov 2 years ago
parent
commit
62e2c8a6ad
  1. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  2. 25
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  3. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html
  4. 15
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts
  5. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  6. 64
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  7. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  8. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  9. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  10. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

9
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html

@ -27,7 +27,10 @@
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title> <mat-panel-title>
<div class="title-container"> <div class="title-container">
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }} <span *ngIf="isMaster else tagName">
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
</span>
<ng-template #tagName>{{ keyControl.get('tag').value }}</ng-template>
</div> </div>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -73,7 +76,7 @@
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="functionCode"> <mat-select formControlName="functionCode">
<mat-option *ngFor="let code of functionCodesMap.get(keyControl.get('tag').value + keyControl.get('address').value) || defaultFunctionCodes" [value]="code">{{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}</mat-option> <mat-option *ngFor="let code of functionCodesMap.get(keyControl.get('id').value) || defaultFunctionCodes" [value]="code">{{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -102,7 +105,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div *ngIf="isMaster" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value</div> <div class="fixed-title-width tb-required" translate>gateway.value</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">

25
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -18,7 +18,6 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms'; import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { import {
MappingKeysType,
ModbusDataType, ModbusDataType,
ModbusFunctionCodeTranslationsMap, ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType, ModbusObjectCountByDataType,
@ -30,6 +29,7 @@ import {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { GatewayHelpLinkPipe } from '@home/pipes/public-api'; import { GatewayHelpLinkPipe } from '@home/pipes/public-api';
import { generateSecret } from '@core/utils';
@Component({ @Component({
selector: 'tb-modbus-data-keys-panel', selector: 'tb-modbus-data-keys-panel',
@ -83,11 +83,12 @@ export class ModbusDataKeysPanelComponent implements OnInit {
addKey(): void { addKey(): void {
const dataKeyFormGroup = this.fb.group({ const dataKeyFormGroup = this.fb.group({
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusDataType.STRING, [Validators.required]], type: [ModbusDataType.STRING, [Validators.required]],
address: [0, [Validators.required]], address: [0, [Validators.required]],
objectsCount: [1, [Validators.required]], objectsCount: [1, [Validators.required]],
functionCode: [this.getDefaultFunctionCodes()[0]] functionCode: [this.getDefaultFunctionCodes()[0]],
id: [{value: generateSecret(5), disabled: true}],
}); });
this.observeKeyDataType(dataKeyFormGroup); this.observeKeyDataType(dataKeyFormGroup);
@ -117,14 +118,15 @@ export class ModbusDataKeysPanelComponent implements OnInit {
const { tag, value, type, address, objectsCount, functionCode } = keyData; const { tag, value, type, address, objectsCount, functionCode } = keyData;
const dataKeyFormGroup = this.fb.group({ const dataKeyFormGroup = this.fb.group({
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], value: [{value, disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, [Validators.required]], type: [type, [Validators.required]],
address: [address, [Validators.required]], address: [address, [Validators.required]],
objectsCount: [objectsCount, [Validators.required]], objectsCount: [objectsCount, [Validators.required]],
functionCode: [functionCode, []], functionCode: [functionCode, []],
id: [{value: generateSecret(5), disabled: true}],
}); });
this.observeKeyDataType(dataKeyFormGroup); this.observeKeyDataType(dataKeyFormGroup);
this.functionCodesMap.set(tag+address, this.getFunctionCodes(type)); this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(type));
keysControlGroups.push(dataKeyFormGroup); keysControlGroups.push(dataKeyFormGroup);
}); });
@ -138,8 +140,7 @@ export class ModbusDataKeysPanelComponent implements OnInit {
if (!this.editableDataTypes.includes(dataType)) { if (!this.editableDataTypes.includes(dataType)) {
objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]); objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]);
} }
const keyId = keyFormGroup.get('tag').value + keyFormGroup.get('address').value; this.functionCodesMap.set(keyFormGroup.get('id').value, this.getFunctionCodes(dataType));
this.functionCodesMap.set(keyId, this.getFunctionCodes(dataType));
}); });
} }
@ -147,7 +148,7 @@ export class ModbusDataKeysPanelComponent implements OnInit {
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return this.defaultWriteFunctionCodes; return this.defaultWriteFunctionCodes;
} }
const functionCodes = this.defaultReadFunctionCodes; const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) { if (dataType === ModbusDataType.BITS) {
const bitsFunctionCodes = [1, 2]; const bitsFunctionCodes = [1, 2];
bitsFunctionCodes.forEach(code => functionCodes.push(code)); bitsFunctionCodes.forEach(code => functionCodes.push(code));
@ -160,6 +161,12 @@ export class ModbusDataKeysPanelComponent implements OnInit {
} }
private getDefaultFunctionCodes(): number[] { private getDefaultFunctionCodes(): number[] {
return this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES ? this.defaultWriteFunctionCodes : this.defaultReadFunctionCodes; if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return this.defaultWriteFunctionCodes;
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
return [...this.defaultReadFunctionCodes, ...this.defaultWriteFunctionCodes];
}
return this.defaultReadFunctionCodes;
} }
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html

@ -15,7 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="securityConfigFormGroup"> <div class="tb-form-panel no-border no-padding" [formGroup]="securityConfigFormGroup">
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.hints.path-in-os' | translate }}</div>
<div class="tb-form-row space-between tb-flex fill-width"> <div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.client-cert-path</div> <div class="fixed-title-width" translate>gateway.client-cert-path</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">

15
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts

@ -112,7 +112,20 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali
} }
writeValue(securityConfig: ModbusSecurity): void { writeValue(securityConfig: ModbusSecurity): void {
this.securityConfigFormGroup.patchValue(securityConfig, {emitEvent: false}); const { certfile, password, keyfile, server_hostname } = securityConfig;
let securityState = {
certfile: certfile ?? '',
password: password ?? '',
keyfile: keyfile ?? '',
server_hostname: server_hostname?? '',
reqclicert: !!securityConfig.reqclicert,
};
if (this.isMaster) {
securityState = { ...securityState, reqclicert: !!securityConfig.reqclicert };
} else {
securityState = { ...securityState, server_hostname: server_hostname ?? '' };
}
this.securityConfigFormGroup.reset(securityState, {emitEvent: false});
} }
private observeValueChanges(): void { private observeValueChanges(): void {

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html

@ -16,6 +16,7 @@
--> -->
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="slaveConfigFormGroup"> <div class="tb-form-panel no-border no-padding padding-top" [formGroup]="slaveConfigFormGroup">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<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">
<div class="fixed-title-width" translate>gateway.server-slave-config</div> <div class="fixed-title-width" translate>gateway.server-slave-config</div>
<tb-toggle-select formControlName="type" appearance="fill"> <tb-toggle-select formControlName="type" appearance="fill">
@ -105,6 +106,15 @@
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.unit-id-required') | translate"
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') &&
slaveConfigFormGroup.get('unitId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

64
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts

@ -23,7 +23,8 @@ import {
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
ValidationErrors, ValidationErrors,
Validator, Validators, Validator,
Validators,
} from '@angular/forms'; } from '@angular/forms';
import { import {
ModbusMethodLabelsMap, ModbusMethodLabelsMap,
@ -31,8 +32,11 @@ import {
ModbusOrderType, ModbusOrderType,
ModbusProtocolLabelsMap, ModbusProtocolLabelsMap,
ModbusProtocolType, ModbusProtocolType,
ModbusRegisterValues,
ModbusSlave,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
PortLimits, SlaveConfig, PortLimits,
SlaveConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -104,7 +108,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET, []], method: [ModbusMethodType.SOCKET, []],
unitId: [null, []], unitId: [null, [Validators.required]],
baudrate: [null, []], baudrate: [null, []],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -155,13 +159,9 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
}; };
} }
writeValue(slaveConfig: SlaveConfig): void { writeValue(slaveConfig: ModbusSlave): void {
if (slaveConfig.type === ModbusProtocolType.Serial) {
slaveConfig.serialPort = slaveConfig.port as string;
delete slaveConfig.port;
}
this.slaveConfigFormGroup.patchValue(slaveConfig, {emitEvent: false});
this.showSecurityControl.patchValue(!!slaveConfig.security); this.showSecurityControl.patchValue(!!slaveConfig.security);
this.updateSlaveConfig(slaveConfig);
this.updateControlsEnabling(slaveConfig.type); this.updateControlsEnabling(slaveConfig.type);
} }
@ -180,4 +180,50 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
} }
}; };
private updateSlaveConfig(slaveConfig: ModbusSlave): void {
const {
type,
method,
unitId,
deviceName,
deviceType,
pollPeriod,
sendDataToThingsBoard,
byteOrder,
security,
identity,
values,
baudrate,
host,
port,
} = slaveConfig;
let slaveState: ModbusSlave = {
host: host ?? '',
type: type ?? ModbusProtocolType.TCP,
method: method ?? ModbusMethodType.SOCKET,
unitId: unitId ?? null,
deviceName: deviceName ?? '',
deviceType: deviceType ?? '',
pollPeriod: pollPeriod ?? null,
sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder: byteOrder ?? ModbusOrderType.BIG,
security: security ?? {},
identity: identity ?? {
vendorName: '',
productCode: '',
vendorUrl: '',
productName: '',
modelName: '',
},
values: values ?? {} as ModbusRegisterValues,
port: port ?? null,
};
if (slaveConfig.type === ModbusProtocolType.Serial) {
slaveState = { ...slaveState, baudrate, serialPort: port, host: '', port: null } as ModbusSlave;
} else {
slaveState = { ...slaveState, serialPort: '', baudrate: null } as ModbusSlave;
}
this.slaveConfigFormGroup.setValue(slaveState, {emitEvent: false});
}
} }

9
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html

@ -176,6 +176,15 @@
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.unit-id-required') | translate"
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') &&
slaveConfigFormGroup.get('unitId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts

@ -125,7 +125,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
bytesize: [null, []], bytesize: [null, []],
parity: [null, []], parity: [null, []],
strict: [false, []], strict: [false, []],
unitId: [null, []], unitId: [null, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
sendDataOnlyOnChange: [false, []], sendDataOnlyOnChange: [false, []],
@ -140,7 +140,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
connectAttemptCount: [null, []], connectAttemptCount: [null, []],
waitAfterFailedAttemptsMs: [null, []], waitAfterFailedAttemptsMs: [null, []],
values: [{}, []], values: [{}, []],
security: [], security: [{}],
}); });
this.slaveConfigFormGroup.patchValue({ this.slaveConfigFormGroup.patchValue({

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -957,6 +957,7 @@ export interface ModbusSecurity {
keyfile?: string; keyfile?: string;
password?: string; password?: string;
server_hostname?: string; server_hostname?: string;
reqclicert?: boolean;
} }
export interface ModbusSlave { export interface ModbusSlave {
@ -964,6 +965,7 @@ export interface ModbusSlave {
type: ModbusProtocolType; type: ModbusProtocolType;
method: ModbusMethodType; method: ModbusMethodType;
unitId: number; unitId: number;
serialPort?: string;
baudrate?: number; baudrate?: number;
deviceName: string; deviceName: string;
deviceType: string; deviceType: string;

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3313,6 +3313,7 @@
"update-config": "Add/update configuration JSON", "update-config": "Add/update configuration JSON",
"username": "Username", "username": "Username",
"username-required": "Username is required.", "username-required": "Username is required.",
"unit-id-required": "Unit ID is required.",
"read-coils": "Read Coils", "read-coils": "Read Coils",
"read-discrete-inputs": "Read Discrete Inputs", "read-discrete-inputs": "Read Discrete Inputs",
"read-multiple-holding-registers": "Read Multiple Holding Register", "read-multiple-holding-registers": "Read Multiple Holding Register",
@ -3322,6 +3323,7 @@
"write-register": "Write Register", "write-register": "Write Register",
"write-registers": "Write Registers", "write-registers": "Write Registers",
"hints": { "hints": {
"modbus-server": "Starting with version 3.0, Gateway can run as a Modbus slave.",
"remote-configuration": "Enables remote configuration and management of the gateway", "remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or IP address of platform server", "host": "Hostname or IP address of platform server",
@ -3360,6 +3362,7 @@
"grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.", "grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.",
"grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.", "grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.",
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.", "permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.",
"path-in-os": "Path in gateway os.",
"memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.", "memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.",
"framer-type": "Type of framer.", "framer-type": "Type of framer.",
"file": "Your data will be stored in separated files and will be saved even after the gateway restart.", "file": "Your data will be stored in separated files and will be saved even after the gateway restart.",

Loading…
Cancel
Save