39 changed files with 3135 additions and 151 deletions
@ -0,0 +1,28 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-tab-group [formGroup]="basicFormGroup"> |
|||
<mat-tab label="{{ 'gateway.general' | translate }}"> |
|||
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container> |
|||
</mat-tab> |
|||
<mat-tab label="{{ 'gateway.master-connections' | translate }}"> |
|||
<tb-modbus-master-table formControlName="master"></tb-modbus-master-table> |
|||
</mat-tab> |
|||
<mat-tab label="{{ 'gateway.server-config' | translate }}"> |
|||
<tb-modbus-server-config formControlName="slave"></tb-modbus-server-config> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
@ -0,0 +1,126 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, TemplateRef } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormGroup, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
} from '@angular/forms'; |
|||
import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
import { Subject } from 'rxjs'; |
|||
|
|||
import { EllipsisChipListDirective } from '@shared/directives/public-api'; |
|||
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; |
|||
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-modbus-basic-config', |
|||
templateUrl: './modbus-basic-config.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ModbusBasicConfigComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => ModbusBasicConfigComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
standalone: true, |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
ModbusSlaveConfigComponent, |
|||
ModbusMasterTableComponent, |
|||
EllipsisChipListDirective, |
|||
], |
|||
styles: [` |
|||
:host { |
|||
height: 100%; |
|||
} |
|||
:host ::ng-deep { |
|||
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper { |
|||
height: 100%; |
|||
} |
|||
} |
|||
`]
|
|||
}) |
|||
|
|||
export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy { |
|||
|
|||
@Input() generalTabContent: TemplateRef<any>; |
|||
|
|||
basicFormGroup: FormGroup; |
|||
|
|||
onChange: (value: string) => void; |
|||
onTouched: () => void; |
|||
|
|||
protected readonly connectorType = ConnectorType; |
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
this.basicFormGroup = this.fb.group({ |
|||
master: [], |
|||
slave: [], |
|||
}); |
|||
|
|||
this.basicFormGroup.valueChanges |
|||
.pipe(takeUntil(this.destroy$)) |
|||
.subscribe(value => { |
|||
this.onChange(value); |
|||
this.onTouched(); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
registerOnChange(fn: (value: string) => void): void { |
|||
this.onChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: () => void): void { |
|||
this.onTouched = fn; |
|||
} |
|||
|
|||
writeValue(basicConfig: ModbusBasicConfig): void { |
|||
const editedBase = { |
|||
slave: basicConfig.slave || {}, |
|||
master: basicConfig.master || {}, |
|||
}; |
|||
|
|||
this.basicFormGroup.setValue(editedBase, {emitEvent: false}); |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.basicFormGroup.valid ? null : { |
|||
basicFormGroup: {valid: false} |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<div class="tb-modbus-keys-panel"> |
|||
<div class="tb-form-panel no-border no-padding"> |
|||
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div> |
|||
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys"> |
|||
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width" |
|||
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByKey; let $index = index; let last = last;"> |
|||
<div class="tb-form-panel stroked tb-flex"> |
|||
<ng-container [formGroup]="keyControl"> |
|||
<mat-expansion-panel class="tb-settings" [expanded]="last"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<mat-panel-title> |
|||
<div class="title-container"> |
|||
<span *ngIf="isMaster else tagName"> |
|||
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }} |
|||
</span> |
|||
<ng-template #tagName>{{ keyControl.get('tag').value }}</ng-template> |
|||
</div> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>gateway.platform-side</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate> |
|||
gateway.key |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="tag" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.key-required') | translate" |
|||
*ngIf="keyControl.get('tag').hasError('required') && |
|||
keyControl.get('tag').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>gateway.connector-side</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate> |
|||
gateway.type |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="type"> |
|||
<mat-option *ngFor="let type of modbusDataTypes" [value]="type">{{ type }}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="withFunctionCode" class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.function-code</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="functionCode"> |
|||
<mat-option *ngFor="let code of functionCodesMap.get(keyControl.get('id').value) || defaultFunctionCodes" [value]="code">{{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.objects-count</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input |
|||
matInput |
|||
type="number" |
|||
min="1" |
|||
name="value" |
|||
formControlName="objectsCount" |
|||
placeholder="{{ 'gateway.set' | translate }}" |
|||
[readonly]="!editableDataTypes.includes(keyControl.get('type').value)" |
|||
/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.address</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<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="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="value" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.value-required') | translate" |
|||
*ngIf="keyControl.get('value').hasError('required') && |
|||
keyControl.get('value').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</ng-container> |
|||
</div> |
|||
<button type="button" |
|||
mat-icon-button |
|||
(click)="deleteKey($event, $index)" |
|||
[matTooltip]="deleteKeyTitle | translate" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
<button type="button" mat-stroked-button color="primary" (click)="addKey()"> |
|||
{{ addKeyTitle | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<ng-template #noKeys> |
|||
<div class="tb-flex no-flex center align-center key-panel"> |
|||
<span class="tb-prompt" translate>{{ noKeysText }}</span> |
|||
</div> |
|||
</ng-template> |
|||
<div class="tb-flex flex-end"> |
|||
<button mat-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="applyKeysData()" |
|||
[disabled]="keysListFormArray.invalid || !keysListFormArray.dirty"> |
|||
{{ 'action.apply' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,44 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
.tb-modbus-keys-panel { |
|||
width: 77vw; |
|||
max-width: 700px; |
|||
|
|||
.title-container { |
|||
max-width: 11vw; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap |
|||
} |
|||
|
|||
.key-panel { |
|||
height: 500px; |
|||
overflow: auto; |
|||
} |
|||
|
|||
.tb-form-panel { |
|||
.mat-mdc-icon-button { |
|||
width: 56px; |
|||
height: 56px; |
|||
padding: 16px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,172 @@ |
|||
///
|
|||
/// 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 { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
|||
import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms'; |
|||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|||
import { |
|||
ModbusDataType, |
|||
ModbusFunctionCodeTranslationsMap, |
|||
ModbusObjectCountByDataType, |
|||
ModbusRegisterType, |
|||
ModbusValue, |
|||
ModbusValueKey, |
|||
noLeadTrailSpacesRegex, |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { GatewayHelpLinkPipe } from '@home/pipes/public-api'; |
|||
import { generateSecret } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-modbus-data-keys-panel', |
|||
templateUrl: './modbus-data-keys-panel.component.html', |
|||
styleUrls: ['./modbus-data-keys-panel.component.scss'], |
|||
standalone: true, |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
GatewayHelpLinkPipe, |
|||
] |
|||
}) |
|||
export class ModbusDataKeysPanelComponent implements OnInit { |
|||
|
|||
@Input() isMaster = false; |
|||
@Input() panelTitle: string; |
|||
@Input() addKeyTitle: string; |
|||
@Input() deleteKeyTitle: string; |
|||
@Input() noKeysText: string; |
|||
@Input() register: ModbusRegisterType; |
|||
@Input() keysType: ModbusValueKey; |
|||
@Input() values: ModbusValue[]; |
|||
@Input() popover: TbPopoverComponent<ModbusDataKeysPanelComponent>; |
|||
|
|||
@Output() keysDataApplied = new EventEmitter<Array<ModbusValue>>(); |
|||
|
|||
keysListFormArray: UntypedFormArray; |
|||
errorText = ''; |
|||
modbusDataTypes = Object.values(ModbusDataType); |
|||
withFunctionCode = true; |
|||
functionCodesMap = new Map(); |
|||
defaultFunctionCodes = []; |
|||
|
|||
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; |
|||
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; |
|||
readonly defaultReadFunctionCodes = [3, 4]; |
|||
readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; |
|||
|
|||
constructor(private fb: UntypedFormBuilder) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.keysListFormArray = this.prepareKeysFormArray(this.values); |
|||
this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES); |
|||
this.defaultFunctionCodes = this.getDefaultFunctionCodes(); |
|||
} |
|||
|
|||
trackByKey(_: number, keyControl: AbstractControl): AbstractControl { |
|||
return keyControl; |
|||
} |
|||
|
|||
addKey(): void { |
|||
const dataKeyFormGroup = this.fb.group({ |
|||
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
type: [ModbusDataType.STRING, [Validators.required]], |
|||
address: [0, [Validators.required]], |
|||
objectsCount: [1, [Validators.required]], |
|||
functionCode: [this.getDefaultFunctionCodes()[0]], |
|||
id: [{value: generateSecret(5), disabled: true}], |
|||
}); |
|||
this.observeKeyDataType(dataKeyFormGroup); |
|||
|
|||
this.keysListFormArray.push(dataKeyFormGroup); |
|||
} |
|||
|
|||
deleteKey($event: Event, index: number): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.keysListFormArray.removeAt(index); |
|||
this.keysListFormArray.markAsDirty(); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.popover?.hide(); |
|||
} |
|||
|
|||
applyKeysData(): void { |
|||
this.keysDataApplied.emit(this.keysListFormArray.value); |
|||
} |
|||
|
|||
private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray { |
|||
const keysControlGroups: Array<AbstractControl> = []; |
|||
if (values) { |
|||
values.forEach(keyData => { |
|||
const { tag, value, type, address, objectsCount, functionCode } = keyData; |
|||
const dataKeyFormGroup = this.fb.group({ |
|||
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
value: [{value, disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
type: [type, [Validators.required]], |
|||
address: [address, [Validators.required]], |
|||
objectsCount: [objectsCount, [Validators.required]], |
|||
functionCode: [functionCode, []], |
|||
id: [{value: generateSecret(5), disabled: true}], |
|||
}); |
|||
this.observeKeyDataType(dataKeyFormGroup); |
|||
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(type)); |
|||
|
|||
keysControlGroups.push(dataKeyFormGroup); |
|||
}); |
|||
} |
|||
return this.fb.array(keysControlGroups); |
|||
} |
|||
|
|||
private observeKeyDataType(keyFormGroup: FormGroup): void { |
|||
keyFormGroup.get('type').valueChanges.subscribe(dataType => { |
|||
const objectsCountControl = keyFormGroup.get('objectsCount'); |
|||
if (!this.editableDataTypes.includes(dataType)) { |
|||
objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]); |
|||
} |
|||
this.functionCodesMap.set(keyFormGroup.get('id').value, this.getFunctionCodes(dataType)); |
|||
}); |
|||
} |
|||
|
|||
private getFunctionCodes(dataType: ModbusDataType): number[] { |
|||
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { |
|||
return this.defaultWriteFunctionCodes; |
|||
} |
|||
const functionCodes = [...this.defaultReadFunctionCodes]; |
|||
if (dataType === ModbusDataType.BITS) { |
|||
const bitsFunctionCodes = [1, 2]; |
|||
bitsFunctionCodes.forEach(code => functionCodes.push(code)); |
|||
functionCodes.sort(); |
|||
} |
|||
if (this.keysType === ModbusValueKey.RPC_REQUESTS) { |
|||
this.defaultWriteFunctionCodes.forEach(code => functionCodes.push(code)); |
|||
} |
|||
return functionCodes; |
|||
} |
|||
|
|||
private getDefaultFunctionCodes(): number[] { |
|||
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { |
|||
return this.defaultWriteFunctionCodes; |
|||
} |
|||
if (this.keysType === ModbusValueKey.RPC_REQUESTS) { |
|||
return [...this.defaultReadFunctionCodes, ...this.defaultWriteFunctionCodes]; |
|||
} |
|||
return this.defaultReadFunctionCodes; |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<div class="tb-master-table tb-absolute-fill"> |
|||
<div fxFlex fxLayout="column" class="tb-master-table-content"> |
|||
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> |
|||
<div class="mat-toolbar-tools"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> |
|||
<span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span> |
|||
</div> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="manageSlave($event)" |
|||
matTooltip="{{ 'action.add' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>add</mat-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
(click)="enterFilterMode()" |
|||
matTooltip="{{ 'action.search' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>search</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-toolbar> |
|||
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode"> |
|||
<div class="mat-toolbar-tools"> |
|||
<button mat-icon-button |
|||
matTooltip="{{ 'action.search' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>search</mat-icon> |
|||
</button> |
|||
<mat-form-field fxFlex> |
|||
<mat-label> </mat-label> |
|||
<input #searchInput matInput |
|||
[formControl]="textSearch" |
|||
placeholder="{{ 'common.enter-search' | translate }}"/> |
|||
</mat-form-field> |
|||
<button mat-icon-button (click)="exitFilterMode()" |
|||
matTooltip="{{ 'action.close' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-toolbar> |
|||
<div class="table-container"> |
|||
<table mat-table [dataSource]="dataSource"> |
|||
<ng-container [matColumnDef]="'name'"> |
|||
<mat-header-cell *matHeaderCellDef class="table-value-column request-column"> |
|||
{{ 'gateway.name' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let mapping" class="table-value-column request-column"> |
|||
{{ mapping['name'] }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container [matColumnDef]="'type'"> |
|||
<mat-header-cell *matHeaderCellDef class="table-value-column request-column"> |
|||
{{ 'gateway.client-communication-type' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let mapping" class="table-value-column request-column"> |
|||
{{ ModbusClientTypeLabelsMap.get(mapping['type']) }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<mat-header-cell *matHeaderCellDef |
|||
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}"> |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let mapping; let i = index" |
|||
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}"> |
|||
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
|||
<button mat-icon-button |
|||
(click)="manageSlave($event, i)"> |
|||
<tb-icon>edit</tb-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
(click)="deleteMapping($event, i)"> |
|||
<tb-icon>delete</tb-icon> |
|||
</button> |
|||
</div> |
|||
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end"> |
|||
<button mat-icon-button |
|||
(click)="$event.stopPropagation()" |
|||
[matMenuTriggerFor]="cellActionsMenu"> |
|||
<mat-icon class="material-icons">more_vert</mat-icon> |
|||
</button> |
|||
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
|||
<button mat-icon-button |
|||
(click)="manageSlave($event, i)"> |
|||
<tb-icon>edit</tb-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
(click)="deleteMapping($event, i)"> |
|||
<tb-icon>delete</tb-icon> |
|||
</button> |
|||
</mat-menu> |
|||
</div> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row> |
|||
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions'];"></mat-row> |
|||
</table> |
|||
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center" |
|||
class="mat-headline-5 tb-absolute-fill tb-add-new"> |
|||
<button mat-button class="connector" |
|||
(click)="manageSlave($event)"> |
|||
<mat-icon class="tb-mat-96">add</mat-icon> |
|||
<span>{{ 'gateway.add-slave' | translate }}</span> |
|||
</button> |
|||
</section> |
|||
</div> |
|||
<span [fxShow]="textSearchMode && (dataSource.isEmpty() | async)" |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found" translate> |
|||
widget.no-data-found |
|||
</span> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,101 @@ |
|||
/** |
|||
* 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 '../scss/constants'; |
|||
|
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: block; |
|||
.tb-master-table { |
|||
.tb-master-table-content { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: #fff; |
|||
overflow: hidden; |
|||
|
|||
&.tb-outlined-border { |
|||
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); |
|||
border: solid 1px #e0e0e0; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.mat-toolbar-tools{ |
|||
min-height: auto; |
|||
} |
|||
|
|||
.title-container{ |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.tb-master-table-title { |
|||
padding-right: 20px; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.table-container { |
|||
overflow: auto; |
|||
.mat-mdc-table { |
|||
table-layout: fixed; |
|||
min-width: 450px; |
|||
|
|||
.table-value-column { |
|||
padding: 0 12px; |
|||
width: 23%; |
|||
|
|||
&.request-column { |
|||
width: 38%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.ellipsis { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.no-data-found { |
|||
height: calc(100% - 120px); |
|||
} |
|||
|
|||
@media #{$mat-xs} { |
|||
.mat-toolbar { |
|||
height: auto; |
|||
min-height: 100px; |
|||
|
|||
.tb-master-table-title{ |
|||
padding-bottom: 5px; |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
mat-cell.tb-value-cell { |
|||
cursor: pointer; |
|||
.mat-icon { |
|||
height: 24px; |
|||
width: 24px; |
|||
font-size: 24px; |
|||
color: #757575 |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,231 @@ |
|||
///
|
|||
/// 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 { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewChild, |
|||
} from '@angular/core'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { Subject } from 'rxjs'; |
|||
import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormArray, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
Validator, |
|||
} from '@angular/forms'; |
|||
import { |
|||
ModbusClientTypeLabelsMap, ModbusMasterConfig, SlaveConfig, |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { TbDatasource } from '@shared/abstract/datasource/datasource.abstract'; |
|||
import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-modbus-master-table', |
|||
templateUrl: './modbus-master-table.component.html', |
|||
styleUrls: ['./modbus-master-table.component.scss'], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ModbusMasterTableComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => ModbusMasterTableComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
standalone: true, |
|||
imports: [CommonModule, SharedModule] |
|||
}) |
|||
export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { |
|||
|
|||
@ViewChild('searchInput') searchInputField: ElementRef; |
|||
|
|||
textSearchMode = false; |
|||
dataSource: SlavesDatasource; |
|||
hidePageSize = false; |
|||
activeValue = false; |
|||
dirtyValue = false; |
|||
masterFormGroup: UntypedFormGroup; |
|||
textSearch = this.fb.control('', {nonNullable: true}); |
|||
|
|||
readonly ModbusClientTypeLabelsMap = ModbusClientTypeLabelsMap; |
|||
|
|||
private onChange: (value: string) => void = () => {}; |
|||
private onTouched: () => void = () => {}; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor( |
|||
public translate: TranslateService, |
|||
public dialog: MatDialog, |
|||
private dialogService: DialogService, |
|||
private fb: FormBuilder |
|||
) { |
|||
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([])}); |
|||
this.dirtyValue = !this.activeValue; |
|||
this.dataSource = new SlavesDatasource(); |
|||
} |
|||
|
|||
get slaves(): FormArray { |
|||
return this.masterFormGroup.get('slaves') as FormArray; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.masterFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value) => { |
|||
this.updateTableData(value.slaves); |
|||
this.onChange(value); |
|||
this.onTouched(); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
this.textSearch.valueChanges.pipe( |
|||
debounceTime(150), |
|||
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()), |
|||
takeUntil(this.destroy$) |
|||
).subscribe((text) => { |
|||
const searchText = text.trim(); |
|||
this.updateTableData(this.slaves.value, searchText.trim()); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: (value: string) => void): void { |
|||
this.onChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: () => void): void { |
|||
this.onTouched = fn; |
|||
} |
|||
|
|||
writeValue(master: ModbusMasterConfig): void { |
|||
this.slaves.clear(); |
|||
this.pushDataAsFormArrays(master.slaves); |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.slaves.controls.length ? null : { |
|||
slavesFormGroup: {valid: false} |
|||
}; |
|||
} |
|||
|
|||
enterFilterMode(): void { |
|||
this.textSearchMode = true; |
|||
setTimeout(() => { |
|||
this.searchInputField.nativeElement.focus(); |
|||
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
|||
}, 10); |
|||
} |
|||
|
|||
exitFilterMode(): void { |
|||
this.updateTableData(this.slaves.value); |
|||
this.textSearchMode = false; |
|||
this.textSearch.reset(); |
|||
} |
|||
|
|||
manageSlave($event: Event, index?: number): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const value = isDefinedAndNotNull(index) ? this.slaves.at(index).value : {}; |
|||
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
value, |
|||
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply' |
|||
} |
|||
}).afterClosed() |
|||
.pipe(take(1), takeUntil(this.destroy$)) |
|||
.subscribe(res => { |
|||
if (res) { |
|||
if (isDefinedAndNotNull(index)) { |
|||
this.slaves.at(index).patchValue(res); |
|||
} else { |
|||
this.slaves.push(this.fb.control(res)); |
|||
} |
|||
this.masterFormGroup.markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
deleteMapping($event: Event, index: number): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialogService.confirm( |
|||
this.translate.instant('gateway.delete-slave-title'), |
|||
'', |
|||
this.translate.instant('action.no'), |
|||
this.translate.instant('action.yes'), |
|||
true |
|||
).subscribe((result) => { |
|||
if (result) { |
|||
this.slaves.removeAt(index); |
|||
this.masterFormGroup.markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private updateTableData(data: SlaveConfig[], textSearch?: string): void { |
|||
let tableValue = data; |
|||
if (textSearch) { |
|||
tableValue = tableValue.filter(value => |
|||
Object.values(value).some(val => |
|||
val.toString().toLowerCase().includes(textSearch.toLowerCase()) |
|||
) |
|||
); |
|||
} |
|||
this.dataSource.loadData(tableValue); |
|||
} |
|||
|
|||
private pushDataAsFormArrays(slaves: SlaveConfig[]): void { |
|||
if (slaves?.length) { |
|||
slaves.forEach((slave: SlaveConfig) => this.slaves.push(this.fb.control(slave))); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export class SlavesDatasource extends TbDatasource<SlaveConfig> { |
|||
constructor() { |
|||
super(); |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<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="fixed-title-width" translate>gateway.client-cert-path</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="certfile" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.private-key-path</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="keyfile" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.password</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<div class="tb-flex no-gap align-center fill-height" matSuffix> |
|||
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="!isMaster" class="tb-form-row space-between tb-flex fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.server-hostname</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="server_hostname" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="isMaster" class="tb-form-row" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="reqclicert"> |
|||
<mat-label> |
|||
{{ 'gateway.request-client-certificate' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,139 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { ChangeDetectionStrategy, Component, forwardRef, Input, OnChanges, OnDestroy } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { |
|||
ModbusSecurity, |
|||
noLeadTrailSpacesRegex, |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-modbus-security-config', |
|||
templateUrl: './modbus-security-config.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ModbusSecurityConfigComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => ModbusSecurityConfigComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
standalone: true, |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
SecurityConfigComponent, |
|||
] |
|||
}) |
|||
export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { |
|||
|
|||
@Input() isMaster = false; |
|||
|
|||
securityConfigFormGroup: UntypedFormGroup; |
|||
|
|||
private onChange: (value: ModbusSecurity) => void; |
|||
private onTouched: () => void; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
this.securityConfigFormGroup = this.fb.group({ |
|||
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
}); |
|||
|
|||
this.observeValueChanges(); |
|||
} |
|||
|
|||
ngOnChanges(): void { |
|||
if (this.isMaster) { |
|||
this.securityConfigFormGroup = this.fb.group({ |
|||
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
reqclicert: [false, []], |
|||
}); |
|||
this.observeValueChanges(); |
|||
} |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
registerOnChange(fn: (value: ModbusSecurity) => void): void { |
|||
this.onChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: () => void): void { |
|||
this.onTouched = fn; |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.securityConfigFormGroup.valid ? null : { |
|||
serverConfigFormGroup: { valid: false } |
|||
}; |
|||
} |
|||
|
|||
writeValue(securityConfig: ModbusSecurity): void { |
|||
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 { |
|||
this.securityConfigFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value: ModbusSecurity) => { |
|||
this.onChange(value); |
|||
this.onTouched(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,256 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<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="fixed-title-width" translate>gateway.server-slave-config</div> |
|||
<tb-toggle-select formControlName="type" appearance="fill"> |
|||
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<div class="tb-form-panel no-border no-padding padding-top"> |
|||
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial" |
|||
class="tb-form-row column-xs" |
|||
fxLayoutAlign="space-between center" |
|||
> |
|||
<div class="fixed-title-width tb-required" translate>gateway.host</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.host-required') | translate" |
|||
*ngIf="slaveConfigFormGroup.get('host').hasError('required') |
|||
&& slaveConfigFormGroup.get('host').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial else serialPort" |
|||
class="tb-form-row column-xs" |
|||
fxLayoutAlign="space-between center" |
|||
> |
|||
<div class="fixed-title-width tb-required" translate>gateway.port</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}" |
|||
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip" |
|||
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') || |
|||
slaveConfigFormGroup.get('port').hasError('min') || |
|||
slaveConfigFormGroup.get('port').hasError('max')) && |
|||
slaveConfigFormGroup.get('port').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<ng-template #serialPort> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.port</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="slaveConfigFormGroup.get('serialPort') | getGatewayPortTooltip" |
|||
*ngIf="(slaveConfigFormGroup.get('serialPort').hasError('required') || |
|||
slaveConfigFormGroup.get('serialPort').hasError('min') || |
|||
slaveConfigFormGroup.get('serialPort').hasError('max')) && |
|||
slaveConfigFormGroup.get('serialPort').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate> |
|||
gateway.method |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="method"> |
|||
<mat-option *ngFor="let method of modbusMethodTypes" [value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div> |
|||
<div class="tb-flex no-gap"> |
|||
<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 }}"/> |
|||
<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> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.device-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="deviceName" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-name-required') | translate" |
|||
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') && |
|||
slaveConfigFormGroup.get('deviceName').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-profile-required') | translate" |
|||
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') && |
|||
slaveConfigFormGroup.get('deviceType').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.poll-period</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="this.slaveConfigFormGroup.get('type').value === ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.baudrate</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="baudrate" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard"> |
|||
<mat-label> |
|||
{{ 'gateway.send-data-TB' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<mat-expansion-panel> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title> |
|||
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="slaveConfigFormGroup"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.byte-order</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="byteOrder"> |
|||
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<mat-expansion-panel *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial" |
|||
[expanded]="showSecurityControl.value"> |
|||
<mat-expansion-panel-header class="nested-expansion-header"> |
|||
<mat-panel-title> |
|||
<mat-slide-toggle [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()"> |
|||
<mat-label> |
|||
{{ 'gateway.tls-connection' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<tb-modbus-security-config formControlName="security" [isMaster]="true"></tb-modbus-security-config> |
|||
</mat-expansion-panel> |
|||
<ng-container [formGroup]="slaveConfigFormGroup.get('identity')"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.vendor-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="vendorName" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.product-code</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="productCode" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.vendor-url</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="vendorUrl" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.product-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="productName" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.model-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="modelName" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>gateway.values</div> |
|||
<tb-modbus-values formControlName="values"></tb-modbus-values> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,229 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators, |
|||
} from '@angular/forms'; |
|||
import { |
|||
ModbusMethodLabelsMap, |
|||
ModbusMethodType, |
|||
ModbusOrderType, |
|||
ModbusProtocolLabelsMap, |
|||
ModbusProtocolType, |
|||
ModbusRegisterValues, |
|||
ModbusSlave, |
|||
noLeadTrailSpacesRegex, |
|||
PortLimits, |
|||
SlaveConfig, |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
import { GatewayPortTooltipPipe } from '@home/pipes/public-api'; |
|||
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; |
|||
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-modbus-server-config', |
|||
templateUrl: './modbus-slave-config.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ModbusSlaveConfigComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => ModbusSlaveConfigComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
standalone: true, |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
ModbusValuesComponent, |
|||
ModbusSecurityConfigComponent, |
|||
GatewayPortTooltipPipe, |
|||
], |
|||
styles: [` |
|||
:host { |
|||
.nested-expansion-header { |
|||
.mat-content { |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
`],
|
|||
}) |
|||
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { |
|||
|
|||
slaveConfigFormGroup: UntypedFormGroup; |
|||
showSecurityControl: UntypedFormControl; |
|||
ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; |
|||
ModbusMethodLabelsMap = ModbusMethodLabelsMap; |
|||
portLimits = PortLimits; |
|||
|
|||
readonly modbusProtocolTypes = Object.values(ModbusProtocolType); |
|||
readonly modbusMethodTypes = Object.values(ModbusMethodType); |
|||
readonly modbusOrderType = Object.values(ModbusOrderType); |
|||
readonly ModbusProtocolType = ModbusProtocolType; |
|||
readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; |
|||
readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; |
|||
|
|||
private onChange: (value: SlaveConfig) => void; |
|||
private onTouched: () => void; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
this.showSecurityControl = this.fb.control(false); |
|||
this.slaveConfigFormGroup = this.fb.group({ |
|||
type: [ModbusProtocolType.TCP, []], |
|||
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], |
|||
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
method: [ModbusMethodType.SOCKET, []], |
|||
unitId: [null, [Validators.required]], |
|||
baudrate: [null, []], |
|||
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
pollPeriod: [null, []], |
|||
sendDataToThingsBoard: [false, []], |
|||
byteOrder:[ModbusOrderType.BIG, []], |
|||
security: [], |
|||
identity: this.fb.group({ |
|||
vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
productCode: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
vendorUrl: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
productName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
modelName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
}), |
|||
values: [], |
|||
}); |
|||
|
|||
this.slaveConfigFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value: SlaveConfig) => { |
|||
if (value.type === ModbusProtocolType.Serial) { |
|||
value.port = value.serialPort; |
|||
delete value.serialPort; |
|||
} |
|||
this.onChange(value); |
|||
this.onTouched(); |
|||
}); |
|||
|
|||
this.observeTypeChange(); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
registerOnChange(fn: (value: SlaveConfig) => void): void { |
|||
this.onChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: () => void): void { |
|||
this.onTouched = fn; |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.slaveConfigFormGroup.valid ? null : { |
|||
serverConfigFormGroup: { valid: false } |
|||
}; |
|||
} |
|||
|
|||
writeValue(slaveConfig: ModbusSlave): void { |
|||
this.showSecurityControl.patchValue(!!slaveConfig.security); |
|||
this.updateSlaveConfig(slaveConfig); |
|||
this.updateControlsEnabling(slaveConfig.type); |
|||
} |
|||
|
|||
private observeTypeChange(): void { |
|||
this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => { |
|||
this.updateControlsEnabling(type); |
|||
}); |
|||
} |
|||
|
|||
private updateControlsEnabling(type: ModbusProtocolType): void { |
|||
if (type === ModbusProtocolType.Serial) { |
|||
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); |
|||
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); |
|||
} else { |
|||
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({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}); |
|||
} |
|||
} |
|||
@ -0,0 +1,354 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<div class="slaves-config-container"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ 'gateway.server-slave' | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<div [tb-help]="modbusHelpLink"></div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<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 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="tb-form-panel no-border no-padding padding-top"> |
|||
<div class="tb-flex row space-between align-center no-gap fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.server-connection</div> |
|||
<tb-toggle-select formControlName="type" appearance="fill"> |
|||
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<div class="tb-form-panel no-border no-padding padding-top"> |
|||
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial" |
|||
class="tb-form-row column-xs" |
|||
fxLayoutAlign="space-between center" |
|||
> |
|||
<div class="fixed-title-width tb-required" translate>gateway.host</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.host-required') | translate" |
|||
*ngIf="slaveConfigFormGroup.get('host').hasError('required') |
|||
&& slaveConfigFormGroup.get('host').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial else serialPort" |
|||
class="tb-form-row column-xs" |
|||
fxLayoutAlign="space-between center" |
|||
> |
|||
<div class="fixed-title-width tb-required" translate>gateway.port</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}" |
|||
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip" |
|||
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') || |
|||
slaveConfigFormGroup.get('port').hasError('min') || |
|||
slaveConfigFormGroup.get('port').hasError('max')) && |
|||
slaveConfigFormGroup.get('port').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<ng-template #serialPort> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.port</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="slaveConfigFormGroup.get('serialPort') | getGatewayPortTooltip" |
|||
*ngIf="(slaveConfigFormGroup.get('serialPort').hasError('required') || |
|||
slaveConfigFormGroup.get('serialPort').hasError('min') || |
|||
slaveConfigFormGroup.get('serialPort').hasError('max')) && |
|||
slaveConfigFormGroup.get('serialPort').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate> |
|||
gateway.method |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="method"> |
|||
<mat-option *ngFor="let method of modbusMethodTypes" [value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<ng-container *ngIf="this.slaveConfigFormGroup.get('type').value === ModbusProtocolType.Serial"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.baudrate</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="baudrate" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.bytesize</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="bytesize" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.stopbits</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="stopbits" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.parity</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="parity" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="strict"> |
|||
<mat-label> |
|||
{{ 'gateway.strict' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</ng-container> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div> |
|||
<div class="tb-flex no-gap"> |
|||
<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 }}"/> |
|||
<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> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.device-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="deviceName" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-name-required') | translate" |
|||
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') && |
|||
slaveConfigFormGroup.get('deviceName').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-profile-required') | translate" |
|||
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') && |
|||
slaveConfigFormGroup.get('deviceType').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange"> |
|||
<mat-label> |
|||
{{ 'gateway.send-data-on-change' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<mat-expansion-panel> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title> |
|||
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div class="tb-form-panel no-border no-padding padding-top"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.connection-timeout</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="timeout" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.byte-order</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="byteOrder"> |
|||
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.word-order</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="wordOrder"> |
|||
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<mat-expansion-panel *ngIf="this.slaveConfigFormGroup.get('security')" [expanded]="showSecurityControl.value"> |
|||
<mat-expansion-panel-header class="nested-expansion-header"> |
|||
<mat-panel-title> |
|||
<mat-slide-toggle [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()"> |
|||
<mat-label> |
|||
{{ 'gateway.tls-connection' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config> |
|||
</mat-expansion-panel> |
|||
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="retries"> |
|||
<mat-label> |
|||
{{ 'gateway.retries' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="retryOnEmpty"> |
|||
<mat-label> |
|||
{{ 'gateway.retries-on-empty' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="retryOnInvalid"> |
|||
<mat-label> |
|||
{{ 'gateway.retries-on-invalid' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.poll-period</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.connect-attempt-time</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.connect-attempt-count</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.wait-after-failed-attempts</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>gateway.values</div> |
|||
<tb-modbus-values [singleMode]="true" formControlName="values"></tb-modbus-values> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
cdkFocusInitial |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
(click)="add()" |
|||
[disabled]="slaveConfigFormGroup.invalid || !slaveConfigFormGroup.dirty"> |
|||
{{ this.data.buttonTitle | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,196 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { ChangeDetectionStrategy, Component, forwardRef, Inject, OnDestroy } from '@angular/core'; |
|||
import { |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
Validators, |
|||
} from '@angular/forms'; |
|||
import { |
|||
MappingInfo, |
|||
ModbusMethodLabelsMap, |
|||
ModbusMethodType, |
|||
ModbusOrderType, |
|||
ModbusProtocolLabelsMap, |
|||
ModbusProtocolType, |
|||
noLeadTrailSpacesRegex, |
|||
PortLimits, SlaveConfig, |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { Subject } from 'rxjs'; |
|||
import { ModbusValuesComponent } from '../modbus-values/modbus-values.component'; |
|||
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-modbus-slave-dialog', |
|||
templateUrl: './modbus-slave-dialog.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ModbusSlaveDialogComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => ModbusSlaveDialogComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
standalone: true, |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
ModbusValuesComponent, |
|||
ModbusSecurityConfigComponent, |
|||
GatewayPortTooltipPipe, |
|||
], |
|||
styles: [` |
|||
:host { |
|||
.slaves-config-container { |
|||
width: 900px; |
|||
} |
|||
.nested-expansion-header { |
|||
.mat-content { |
|||
height: 100%; |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
} |
|||
`],
|
|||
}) |
|||
export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialogComponent, SlaveConfig> implements OnDestroy { |
|||
|
|||
slaveConfigFormGroup: UntypedFormGroup; |
|||
showSecurityControl: UntypedFormControl; |
|||
portLimits = PortLimits; |
|||
|
|||
readonly modbusProtocolTypes = Object.values(ModbusProtocolType); |
|||
readonly modbusMethodTypes = Object.values(ModbusMethodType); |
|||
readonly modbusOrderType = Object.values(ModbusOrderType); |
|||
readonly ModbusProtocolType = ModbusProtocolType; |
|||
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; |
|||
readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap; |
|||
readonly modbusHelpLink = |
|||
'https://thingsboard.io/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters'; |
|||
readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict']; |
|||
readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor( |
|||
private fb: FormBuilder, |
|||
protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: MappingInfo, |
|||
public dialogRef: MatDialogRef<ModbusSlaveDialogComponent, SlaveConfig>, |
|||
) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.showSecurityControl = this.fb.control(false); |
|||
this.slaveConfigFormGroup = this.fb.group({ |
|||
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
type: [ModbusProtocolType.TCP, [Validators.required]], |
|||
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], |
|||
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
method: [ModbusMethodType.SOCKET, []], |
|||
baudrate: [null, []], |
|||
stopbits: [null, []], |
|||
bytesize: [null, []], |
|||
parity: [null, []], |
|||
strict: [false, []], |
|||
unitId: [null, [Validators.required]], |
|||
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
sendDataOnlyOnChange: [false, []], |
|||
timeout: [], |
|||
byteOrder: [ModbusOrderType.BIG, []], |
|||
wordOrder: [ModbusOrderType.BIG, []], |
|||
retries: [false, []], |
|||
retryOnEmpty: [false, []], |
|||
retryOnInvalid: [false, []], |
|||
pollPeriod: [null, []], |
|||
connectAttemptTimeMs: [null, []], |
|||
connectAttemptCount: [null, []], |
|||
waitAfterFailedAttemptsMs: [null, []], |
|||
values: [{}, []], |
|||
security: [{}], |
|||
}); |
|||
|
|||
this.slaveConfigFormGroup.patchValue({ |
|||
...this.data.value, |
|||
values: { |
|||
attributes: this.data.value.attributes ?? [], |
|||
timeseries: this.data.value.timeseries ?? [], |
|||
attributeUpdates: this.data.value.attributeUpdates ?? [], |
|||
rpc: this.data.value.rpc ?? [], |
|||
} |
|||
}); |
|||
this.showSecurityControl.patchValue(!!this.data.value.security); |
|||
this.updateControlsEnabling(this.data.value.type); |
|||
this.observeTypeChange(); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
add(): void { |
|||
if (this.slaveConfigFormGroup.valid) { |
|||
const slaveResult = {...this.slaveConfigFormGroup.value, ...this.slaveConfigFormGroup.value.values}; |
|||
delete slaveResult.values; |
|||
if (slaveResult.type === ModbusProtocolType.Serial) { |
|||
slaveResult.port = slaveResult.serialPort; |
|||
delete slaveResult.serialPort; |
|||
} |
|||
this.dialogRef.close(slaveResult); |
|||
} |
|||
} |
|||
|
|||
private observeTypeChange(): void { |
|||
this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => { |
|||
this.updateControlsEnabling(type); |
|||
}); |
|||
} |
|||
|
|||
private updateControlsEnabling(type: ModbusProtocolType): void { |
|||
if (type === ModbusProtocolType.Serial) { |
|||
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); |
|||
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); |
|||
} else { |
|||
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); |
|||
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); |
|||
} |
|||
}; |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
|
|||
<ng-container *ngIf="singleMode else multipleView"> |
|||
<div [formGroup]="valuesFormGroup" class="tb-form-panel no-border no-padding padding-top" fxLayout="column"> |
|||
<ng-container [ngTemplateOutlet]="singleView" [ngTemplateOutletContext]="{$implicit: null}"></ng-container> |
|||
</div> |
|||
</ng-container> |
|||
|
|||
<ng-template #multipleView> |
|||
<mat-tab-group [formGroup]="valuesFormGroup"> |
|||
<mat-tab *ngFor="let register of modbusRegisterTypes" label="{{ ModbusValuesTranslationsMap.get(register) | translate }}"> |
|||
<div [formGroup]="valuesFormGroup.get(register)" class="tb-form-panel no-border no-padding padding-top" fxLayout="column"> |
|||
<ng-container [ngTemplateOutlet]="singleView" [ngTemplateOutletContext]="{$implicit: register}"></ng-container> |
|||
</div> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
</ng-template> |
|||
|
|||
<ng-template #singleView let-register> |
|||
<div class="tb-form-row space-between tb-flex"> |
|||
<div class="fixed-title-width" translate>gateway.attributes</div> |
|||
<div class="tb-flex ellipsis-chips-container"> |
|||
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES, register)" class="tb-flex"> |
|||
<mat-chip *ngFor="let attribute of getValueGroup(ModbusValueKey.ATTRIBUTES, register)"> |
|||
{{ attribute.tag }} |
|||
</mat-chip> |
|||
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
|||
<label class="ellipsis-text"></label> |
|||
</mat-chip> |
|||
</mat-chip-listbox> |
|||
<button type="button" |
|||
mat-icon-button |
|||
color="primary" |
|||
#attributesButton |
|||
(click)="manageKeys($event, attributesButton, ModbusValueKey.ATTRIBUTES, register)"> |
|||
<tb-icon matButtonIcon>edit</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex"> |
|||
<div class="fixed-title-width" translate>gateway.timeseries</div> |
|||
<div class="tb-flex ellipsis-chips-container"> |
|||
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.TIMESERIES, register)"> |
|||
<mat-chip *ngFor="let telemetry of getValueGroup(ModbusValueKey.TIMESERIES, register)"> |
|||
{{ telemetry.tag }} |
|||
</mat-chip> |
|||
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
|||
<label class="ellipsis-text"></label> |
|||
</mat-chip> |
|||
</mat-chip-listbox> |
|||
<button type="button" |
|||
mat-icon-button |
|||
color="primary" |
|||
#telemetryButton |
|||
(click)="manageKeys($event, telemetryButton, ModbusValueKey.TIMESERIES, register)"> |
|||
<tb-icon matButtonIcon>edit</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex"> |
|||
<div class="fixed-title-width" translate>gateway.attribute-updates</div> |
|||
<div class="tb-flex ellipsis-chips-container"> |
|||
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register)" class="tb-flex"> |
|||
<mat-chip *ngFor="let attributeUpdate of getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register)"> |
|||
{{ attributeUpdate.tag }} |
|||
</mat-chip> |
|||
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
|||
<label class="ellipsis-text"></label> |
|||
</mat-chip> |
|||
</mat-chip-listbox> |
|||
<button type="button" |
|||
mat-icon-button |
|||
color="primary" |
|||
#attributesUpdatesButton |
|||
(click)="manageKeys($event, attributesUpdatesButton, ModbusValueKey.ATTRIBUTES_UPDATES, register)"> |
|||
<tb-icon matButtonIcon>edit</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex"> |
|||
<div class="fixed-title-width" translate>gateway.rpc-requests</div> |
|||
<div class="tb-flex ellipsis-chips-container"> |
|||
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.RPC_REQUESTS, register)" class="tb-flex"> |
|||
<mat-chip *ngFor="let rpcRequest of getValueGroup(ModbusValueKey.RPC_REQUESTS, register)"> |
|||
{{ rpcRequest.tag }} |
|||
</mat-chip> |
|||
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
|||
<label class="ellipsis-text"></label> |
|||
</mat-chip> |
|||
</mat-chip-listbox> |
|||
<button type="button" |
|||
mat-icon-button |
|||
color="primary" |
|||
#rpcRequestsButton |
|||
(click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)"> |
|||
<tb-icon matButtonIcon>edit</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
@ -0,0 +1,204 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
forwardRef, |
|||
Input, |
|||
OnChanges, |
|||
OnDestroy, |
|||
Renderer2, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormGroup, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
} from '@angular/forms'; |
|||
import { |
|||
ModbusKeysAddKeyTranslationsMap, |
|||
ModbusKeysDeleteKeyTranslationsMap, |
|||
ModbusKeysNoKeysTextTranslationsMap, |
|||
ModbusKeysPanelTitleTranslationsMap, |
|||
ModbusRegisterTranslationsMap, |
|||
ModbusRegisterType, |
|||
ModbusRegisterValues, |
|||
ModbusValueKey, |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
import { Subject } from 'rxjs'; |
|||
import { EllipsisChipListDirective } from '@shared/directives/public-api'; |
|||
import { MatButton } from '@angular/material/button'; |
|||
import { TbPopoverService } from '@shared/components/popover.service'; |
|||
import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-data-keys-panel.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-modbus-values', |
|||
templateUrl: './modbus-values.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ModbusValuesComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => ModbusValuesComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
standalone: true, |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
EllipsisChipListDirective, |
|||
], |
|||
styles: [` |
|||
:host { |
|||
.mat-mdc-tab-body-wrapper { |
|||
min-height: 320px; |
|||
} |
|||
} |
|||
`]
|
|||
}) |
|||
|
|||
export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { |
|||
|
|||
@Input() singleMode = false; |
|||
|
|||
modbusRegisterTypes: ModbusRegisterType[] = Object.values(ModbusRegisterType); |
|||
modbusValueKeys = Object.values(ModbusValueKey); |
|||
ModbusValuesTranslationsMap = ModbusRegisterTranslationsMap; |
|||
ModbusValueKey = ModbusValueKey; |
|||
valuesFormGroup: FormGroup; |
|||
|
|||
private onChange: (value: string) => void; |
|||
private onTouched: () => void; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private popoverService: TbPopoverService, |
|||
private renderer: Renderer2, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private cdr: ChangeDetectorRef, |
|||
) { |
|||
this.valuesFormGroup = this.fb.group(this.modbusRegisterTypes.reduce((registersAcc, register) => { |
|||
return { |
|||
...registersAcc, |
|||
[register]: this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})), |
|||
}; |
|||
}, {})); |
|||
|
|||
this.observeValuesChanges(); |
|||
} |
|||
|
|||
ngOnChanges(): void { |
|||
if (this.singleMode) { |
|||
this.valuesFormGroup = this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})); |
|||
this.observeValuesChanges(); |
|||
} |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
registerOnChange(fn: (value: string) => void): void { |
|||
this.onChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: () => void): void { |
|||
this.onTouched = fn; |
|||
} |
|||
|
|||
writeValue(values: ModbusRegisterValues): void { |
|||
this.valuesFormGroup.patchValue(values, {emitEvent: false}); |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.valuesFormGroup.valid ? null : { |
|||
valuesFormGroup: {valid: false} |
|||
}; |
|||
} |
|||
|
|||
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType) { |
|||
return register ? this.valuesFormGroup.get(register).get(valueKey).value : this.valuesFormGroup.get(valueKey).value; |
|||
} |
|||
|
|||
manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const trigger = matButton._elementRef.nativeElement; |
|||
if (this.popoverService.hasPopover(trigger)) { |
|||
this.popoverService.hidePopover(trigger); |
|||
} else { |
|||
const group = this.valuesFormGroup; |
|||
|
|||
const keysControl = register ? group.get(register).get(keysType) : group.get(keysType); |
|||
const ctx = { |
|||
values: keysControl.value, |
|||
isMaster: !this.singleMode, |
|||
keysType, |
|||
panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType), |
|||
addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType), |
|||
deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType), |
|||
noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType) |
|||
}; |
|||
const dataKeysPanelPopover = this.popoverService.displayPopover( |
|||
trigger, |
|||
this.renderer, |
|||
this.viewContainerRef, |
|||
ModbusDataKeysPanelComponent, |
|||
'leftBottom', |
|||
false, |
|||
null, |
|||
ctx, |
|||
{}, |
|||
{}, |
|||
{}, |
|||
true |
|||
); |
|||
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; |
|||
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData) => { |
|||
dataKeysPanelPopover.hide(); |
|||
keysControl.patchValue(keysData); |
|||
keysControl.markAsDirty(); |
|||
this.cdr.markForCheck(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private observeValuesChanges(): void { |
|||
this.valuesFormGroup.valueChanges |
|||
.pipe(takeUntil(this.destroy$)) |
|||
.subscribe(value => { |
|||
this.onChange(value); |
|||
this.onTouched(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
///
|
|||
/// 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.
|
|||
///
|
|||
|
|||
export * from './modbus-basic-config/modbus-basic-config.component'; |
|||
export * from './modbus-values/modbus-values.component'; |
|||
export * from './modbus-data-keys-panel/modbus-data-keys-panel.component'; |
|||
export * from './modbus-slave-config/modbus-slave-config.component'; |
|||
export * from './modbus-master-table/modbus-master-table.component'; |
|||
export * from './modbus-slave-dialog/modbus-slave-dialog.component'; |
|||
export * from './modbus-security-config/modbus-security-config.component'; |
|||
@ -0,0 +1,42 @@ |
|||
///
|
|||
/// 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 { PortLimits, } from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { AbstractControl } from '@angular/forms'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
@Pipe({ |
|||
name: 'getGatewayPortTooltip', |
|||
standalone: true, |
|||
}) |
|||
export class GatewayPortTooltipPipe implements PipeTransform { |
|||
|
|||
constructor(private translate: TranslateService) {} |
|||
|
|||
transform(portControl: AbstractControl): string { |
|||
if (portControl.hasError('required')) { |
|||
return this.translate.instant('gateway.port-required'); |
|||
} else if ( |
|||
portControl.hasError('min') || |
|||
portControl.hasError('max') |
|||
) { |
|||
return this.translate.instant('gateway.port-limits-error', |
|||
{min: PortLimits.MIN, max: PortLimits.MAX}); |
|||
} |
|||
return ''; |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { DataSource } from '@angular/cdk/collections'; |
|||
import { BehaviorSubject, Observable } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
export abstract class TbDatasource<T> implements DataSource<T> { |
|||
|
|||
protected dataSubject = new BehaviorSubject<Array<T>>([]); |
|||
|
|||
connect(): Observable<Array<T>> { |
|||
return this.dataSubject.asObservable(); |
|||
} |
|||
|
|||
disconnect(): void { |
|||
this.dataSubject.complete(); |
|||
} |
|||
|
|||
loadData(data: Array<T>): void { |
|||
this.dataSubject.next(data); |
|||
} |
|||
|
|||
isEmpty(): Observable<boolean> { |
|||
return this.dataSubject.pipe( |
|||
map((data: T[]) => !data.length) |
|||
); |
|||
} |
|||
|
|||
total(): Observable<number> { |
|||
return this.dataSubject.pipe( |
|||
map((data: T[]) => data.length) |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
///
|
|||
/// 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.
|
|||
///
|
|||
|
|||
export * from './datasource/datasource.abstract'; |
|||
Loading…
Reference in new issue