104 changed files with 4453 additions and 638 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-slave-config formControlName="slave"></tb-modbus-slave-config> |
||||
|
</mat-tab> |
||||
|
</mat-tab-group> |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-mdc-tab-body-content { |
||||
|
overflow: hidden !important; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
///
|
||||
|
/// 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/ellipsis-chip-list.directive'; |
||||
|
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, |
||||
|
], |
||||
|
styleUrls: ['./modbus-basic-config.component.scss'], |
||||
|
}) |
||||
|
|
||||
|
export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy { |
||||
|
|
||||
|
@Input() generalTabContent: TemplateRef<any>; |
||||
|
|
||||
|
basicFormGroup: FormGroup; |
||||
|
|
||||
|
onChange: (value: ModbusBasicConfig) => void; |
||||
|
onTouched: () => void; |
||||
|
|
||||
|
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: ModbusBasicConfig) => 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,180 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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: trackByControlId; 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" |
||||
|
max="50000" |
||||
|
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 tb-required" 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" max="50000" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.address-required') | translate" |
||||
|
*ngIf="keyControl.get('address').hasError('required') && |
||||
|
keyControl.get('address').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</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,208 @@ |
|||||
|
///
|
||||
|
/// 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, OnDestroy, OnInit, Output } from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
FormArray, |
||||
|
FormGroup, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormGroup, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { |
||||
|
ModbusDataType, |
||||
|
ModbusFunctionCodeTranslationsMap, |
||||
|
ModbusObjectCountByDataType, |
||||
|
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/components/widget/lib/gateway/pipes/gateway-help-link.pipe'; |
||||
|
import { generateSecret } from '@core/utils'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
|
||||
|
@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, OnDestroy { |
||||
|
|
||||
|
@coerceBoolean() |
||||
|
@Input() isMaster = false; |
||||
|
@Input() panelTitle: string; |
||||
|
@Input() addKeyTitle: string; |
||||
|
@Input() deleteKeyTitle: string; |
||||
|
@Input() noKeysText: string; |
||||
|
@Input() keysType: ModbusValueKey; |
||||
|
@Input() values: ModbusValue[]; |
||||
|
@Input() popover: TbPopoverComponent<ModbusDataKeysPanelComponent>; |
||||
|
|
||||
|
@Output() keysDataApplied = new EventEmitter<Array<ModbusValue>>(); |
||||
|
|
||||
|
keysListFormArray: FormArray<UntypedFormGroup>; |
||||
|
modbusDataTypes = Object.values(ModbusDataType); |
||||
|
withFunctionCode = true; |
||||
|
functionCodesMap = new Map(); |
||||
|
defaultFunctionCodes = []; |
||||
|
|
||||
|
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; |
||||
|
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
private readonly defaultReadFunctionCodes = [3, 4]; |
||||
|
private readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; |
||||
|
private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16]; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES); |
||||
|
this.keysListFormArray = this.prepareKeysFormArray(this.values); |
||||
|
this.defaultFunctionCodes = this.getDefaultFunctionCodes(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
trackByControlId(_: number, keyControl: AbstractControl): string { |
||||
|
return keyControl.value.id; |
||||
|
} |
||||
|
|
||||
|
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.BYTES, [Validators.required]], |
||||
|
address: [null, [Validators.required]], |
||||
|
objectsCount: [1, [Validators.required]], |
||||
|
functionCode: [{ value: this.getDefaultFunctionCodes()[0], disabled: !this.withFunctionCode }, [Validators.required]], |
||||
|
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(value => { |
||||
|
const dataKeyFormGroup = this.createDataKeyFormGroup(value); |
||||
|
this.observeKeyDataType(dataKeyFormGroup); |
||||
|
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type)); |
||||
|
|
||||
|
keysControlGroups.push(dataKeyFormGroup); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return this.fb.array(keysControlGroups); |
||||
|
} |
||||
|
|
||||
|
private createDataKeyFormGroup(modbusValue: ModbusValue): FormGroup { |
||||
|
const { tag, value, type, address, objectsCount, functionCode } = modbusValue; |
||||
|
|
||||
|
return 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: [{ value: functionCode, disabled: !this.withFunctionCode }, [Validators.required]], |
||||
|
id: [{ value: generateSecret(5), disabled: true }], |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private observeKeyDataType(keyFormGroup: FormGroup): void { |
||||
|
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { |
||||
|
if (!this.editableDataTypes.includes(dataType)) { |
||||
|
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); |
||||
|
} |
||||
|
this.updateFunctionCodes(keyFormGroup, dataType); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateFunctionCodes(keyFormGroup: FormGroup, dataType: ModbusDataType): void { |
||||
|
const functionCodes = this.getFunctionCodes(dataType); |
||||
|
this.functionCodesMap.set(keyFormGroup.get('id').value, functionCodes); |
||||
|
if (!functionCodes.includes(keyFormGroup.get('functionCode').value)) { |
||||
|
keyFormGroup.get('functionCode').patchValue(functionCodes[0], {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getFunctionCodes(dataType: ModbusDataType): number[] { |
||||
|
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { |
||||
|
return dataType === ModbusDataType.STRING |
||||
|
? this.stringAttrUpdatesWriteFunctionCodes |
||||
|
: this.defaultWriteFunctionCodes; |
||||
|
} |
||||
|
|
||||
|
const functionCodes = [...this.defaultReadFunctionCodes]; |
||||
|
if (dataType === ModbusDataType.BITS) { |
||||
|
const bitsFunctionCodes = [1, 2]; |
||||
|
functionCodes.push(...bitsFunctionCodes); |
||||
|
functionCodes.sort(); |
||||
|
} |
||||
|
if (this.keysType === ModbusValueKey.RPC_REQUESTS) { |
||||
|
functionCodes.push(...this.defaultWriteFunctionCodes); |
||||
|
} |
||||
|
|
||||
|
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"> |
||||
|
{{ 'gateway.name' | translate }} |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let mapping" class="table-value-column"> |
||||
|
{{ mapping['name'] }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container [matColumnDef]="'type'"> |
||||
|
<mat-header-cell *matHeaderCellDef class="table-value-column"> |
||||
|
{{ 'gateway.client-communication-type' | translate }} |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let mapping" class="table-value-column"> |
||||
|
{{ ModbusProtocolLabelsMap.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,90 @@ |
|||||
|
/** |
||||
|
* 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; |
||||
|
|
||||
|
.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: 38%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.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,228 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
ChangeDetectorRef, |
||||
|
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 { |
||||
|
ModbusMasterConfig, |
||||
|
ModbusProtocolLabelsMap, |
||||
|
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 { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; |
||||
|
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; |
||||
|
|
||||
|
@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; |
||||
|
masterFormGroup: UntypedFormGroup; |
||||
|
textSearch = this.fb.control('', {nonNullable: true}); |
||||
|
|
||||
|
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; |
||||
|
|
||||
|
private onChange: (value: ModbusMasterConfig) => void = () => {}; |
||||
|
private onTouched: () => void = () => {}; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor( |
||||
|
public translate: TranslateService, |
||||
|
public dialog: MatDialog, |
||||
|
private dialogService: DialogService, |
||||
|
private fb: FormBuilder, |
||||
|
private cdr: ChangeDetectorRef, |
||||
|
) { |
||||
|
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([]) }); |
||||
|
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 => this.updateTableData(this.slaves.value, text.trim())); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (value: ModbusMasterConfig) => 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; |
||||
|
this.cdr.detectChanges(); |
||||
|
const searchInput = this.searchInputField.nativeElement; |
||||
|
searchInput.focus(); |
||||
|
searchInput.setSelectionRange(0, 0); |
||||
|
} |
||||
|
|
||||
|
exitFilterMode(): void { |
||||
|
this.updateTableData(this.slaves.value); |
||||
|
this.textSearchMode = false; |
||||
|
this.textSearch.reset(); |
||||
|
} |
||||
|
|
||||
|
manageSlave($event: Event, index?: number): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const withIndex = isDefinedAndNotNull(index); |
||||
|
const value = withIndex ? this.slaves.at(index).value : {}; |
||||
|
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
value, |
||||
|
buttonTitle: withIndex ? 'action.add' : 'action.apply' |
||||
|
} |
||||
|
}).afterClosed() |
||||
|
.pipe(take(1), takeUntil(this.destroy$)) |
||||
|
.subscribe(res => { |
||||
|
if (res) { |
||||
|
if (withIndex) { |
||||
|
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 |
||||
|
).pipe(take(1), takeUntil(this.destroy$)).subscribe((result) => { |
||||
|
if (result) { |
||||
|
this.slaves.removeAt(index); |
||||
|
this.masterFormGroup.markAsDirty(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateTableData(data: SlaveConfig[], textSearch?: string): void { |
||||
|
if (textSearch) { |
||||
|
data = data.filter(item => |
||||
|
Object.values(item).some(value => |
||||
|
value.toString().toLowerCase().includes(textSearch.toLowerCase()) |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
this.dataSource.loadData(data); |
||||
|
} |
||||
|
|
||||
|
private pushDataAsFormArrays(slaves: SlaveConfig[]): void { |
||||
|
if (slaves?.length) { |
||||
|
slaves.forEach((slave: SlaveConfig) => this.slaves.push(this.fb.control(slave))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class SlavesDatasource extends TbTableDatasource<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,163 @@ |
|||||
|
///
|
||||
|
/// 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 |
||||
|
} 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 { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
|
||||
|
@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, |
||||
|
] |
||||
|
}) |
||||
|
export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { |
||||
|
|
||||
|
@coerceBoolean() |
||||
|
@Input() isMaster = false; |
||||
|
|
||||
|
securityConfigFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private disabled = false; |
||||
|
|
||||
|
private onChange: (value: ModbusSecurity) => void; |
||||
|
private onTouched: () => void; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { |
||||
|
this.securityConfigFormGroup = this.fb.group({ |
||||
|
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
reqclicert: [{value: false, disabled: true}], |
||||
|
}); |
||||
|
|
||||
|
this.observeValueChanges(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(): void { |
||||
|
this.updateMasterEnabling(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (value: ModbusSecurity) => void): void { |
||||
|
this.onChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: () => void): void { |
||||
|
this.onTouched = fn; |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.securityConfigFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.securityConfigFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
this.updateMasterEnabling(); |
||||
|
this.cdr.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.securityConfigFormGroup.valid ? null : { |
||||
|
securityConfigFormGroup: { valid: false } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(securityConfig: ModbusSecurity): void { |
||||
|
const { certfile, password, keyfile, server_hostname } = securityConfig; |
||||
|
const securityState = { |
||||
|
certfile: certfile ?? '', |
||||
|
password: password ?? '', |
||||
|
keyfile: keyfile ?? '', |
||||
|
server_hostname: server_hostname ?? '', |
||||
|
reqclicert: !!securityConfig.reqclicert, |
||||
|
}; |
||||
|
|
||||
|
this.securityConfigFormGroup.reset(securityState, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
private updateMasterEnabling(): void { |
||||
|
if (this.isMaster) { |
||||
|
if (!this.disabled) { |
||||
|
this.securityConfigFormGroup.get('reqclicert').enable({emitEvent: false}); |
||||
|
} |
||||
|
this.securityConfigFormGroup.get('server_hostname').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
if (!this.disabled) { |
||||
|
this.securityConfigFormGroup.get('server_hostname').enable({emitEvent: false}); |
||||
|
} |
||||
|
this.securityConfigFormGroup.get('reqclicert').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private observeValueChanges(): void { |
||||
|
this.securityConfigFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value: ModbusSecurity) => { |
||||
|
this.onChange(value); |
||||
|
this.onTouched(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,263 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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 [formGroup]="slaveConfigFormGroup" class="slave-container"> |
||||
|
<div class="tb-form-panel no-border no-padding padding-top"> |
||||
|
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div> |
||||
|
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.enable' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="slave-content 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-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="protocolType !== 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="protocolType !== 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]="'gateway.port-required' | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('port').hasError('required') && slaveConfigFormGroup.get('port').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 protocolType === ModbusProtocolType.Serial ? modbusSerialMethodTypes : 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="protocolType === 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"> |
||||
|
<mat-select formControlName="baudrate"> |
||||
|
<mat-option *ngFor="let rate of modbusBaudrates" [value]="rate">{{ rate }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings"> |
||||
|
<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.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 *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle fxLayoutAlign="center" [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> |
||||
|
<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> |
||||
|
<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> |
||||
|
</div> |
||||
@ -0,0 +1,27 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
$server-config-header-height: 132px; |
||||
|
|
||||
|
:host { |
||||
|
.slave-content { |
||||
|
height: calc(100% - #{$server-config-header-height}); |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
.slave-container { |
||||
|
display: inherit; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,287 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
FormControl, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators, |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
ModbusBaudrates, |
||||
|
ModbusMethodLabelsMap, |
||||
|
ModbusMethodType, |
||||
|
ModbusOrderType, |
||||
|
ModbusProtocolLabelsMap, |
||||
|
ModbusProtocolType, |
||||
|
ModbusRegisterValues, |
||||
|
ModbusSerialMethodType, |
||||
|
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 { startWith, takeUntil } from 'rxjs/operators'; |
||||
|
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; |
||||
|
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; |
||||
|
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; |
||||
|
import { isEqual } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-modbus-slave-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, |
||||
|
], |
||||
|
styleUrls: ['./modbus-slave-config.component.scss'], |
||||
|
}) |
||||
|
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { |
||||
|
|
||||
|
slaveConfigFormGroup: UntypedFormGroup; |
||||
|
showSecurityControl: FormControl<boolean>; |
||||
|
ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; |
||||
|
ModbusMethodLabelsMap = ModbusMethodLabelsMap; |
||||
|
portLimits = PortLimits; |
||||
|
|
||||
|
readonly modbusProtocolTypes = Object.values(ModbusProtocolType); |
||||
|
readonly modbusMethodTypes = Object.values(ModbusMethodType); |
||||
|
readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType); |
||||
|
readonly modbusOrderType = Object.values(ModbusOrderType); |
||||
|
readonly ModbusProtocolType = ModbusProtocolType; |
||||
|
readonly modbusBaudrates = ModbusBaudrates; |
||||
|
|
||||
|
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; |
||||
|
private 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: [0, [Validators.required]], |
||||
|
baudrate: [this.modbusBaudrates[0]], |
||||
|
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
pollPeriod: [5000], |
||||
|
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.observeValueChanges(); |
||||
|
this.observeTypeChange(); |
||||
|
this.observeFormEnable(); |
||||
|
this.observeShowSecurity(); |
||||
|
} |
||||
|
|
||||
|
get isSlaveEnabled(): boolean { |
||||
|
return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value; |
||||
|
} |
||||
|
|
||||
|
get protocolType(): ModbusProtocolType { |
||||
|
return this.slaveConfigFormGroup.get('type').value; |
||||
|
} |
||||
|
|
||||
|
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 : { |
||||
|
slaveConfigFormGroup: { valid: false } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(slaveConfig: ModbusSlave): void { |
||||
|
this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {})); |
||||
|
this.updateSlaveConfig(slaveConfig); |
||||
|
this.updateFormEnableState(slaveConfig.sendDataToThingsBoard); |
||||
|
} |
||||
|
|
||||
|
private observeValueChanges(): void { |
||||
|
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(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private observeTypeChange(): void { |
||||
|
this.slaveConfigFormGroup.get('type').valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(type => { |
||||
|
this.updateFormEnableState(this.isSlaveEnabled); |
||||
|
this.updateMethodType(type); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateMethodType(type: ModbusProtocolType): void { |
||||
|
if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) { |
||||
|
this.slaveConfigFormGroup.get('method').patchValue( |
||||
|
type === ModbusProtocolType.Serial |
||||
|
? ModbusSerialMethodType.ASCII |
||||
|
: ModbusMethodType.SOCKET, |
||||
|
{emitEvent: false} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private observeFormEnable(): void { |
||||
|
this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges |
||||
|
.pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$)) |
||||
|
.subscribe(value => this.updateFormEnableState(value)); |
||||
|
} |
||||
|
|
||||
|
private updateFormEnableState(enabled: boolean): void { |
||||
|
if (enabled) { |
||||
|
this.slaveConfigFormGroup.enable({emitEvent: false}); |
||||
|
this.showSecurityControl.enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.slaveConfigFormGroup.disable({emitEvent: false}); |
||||
|
this.showSecurityControl.disable({emitEvent: false}); |
||||
|
this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false}); |
||||
|
} |
||||
|
this.updateEnablingByProtocol(this.protocolType); |
||||
|
this.updateSecurityEnable(this.showSecurityControl.value); |
||||
|
} |
||||
|
|
||||
|
private observeShowSecurity(): void { |
||||
|
this.showSecurityControl.valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(value => this.updateSecurityEnable(value)); |
||||
|
} |
||||
|
|
||||
|
private updateSecurityEnable(securityEnabled: boolean): void { |
||||
|
if (securityEnabled && this.isSlaveEnabled && this.protocolType !== ModbusProtocolType.Serial) { |
||||
|
this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateEnablingByProtocol(type: ModbusProtocolType): void { |
||||
|
const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; |
||||
|
const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; |
||||
|
|
||||
|
if (this.isSlaveEnabled) { |
||||
|
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); |
||||
|
} |
||||
|
|
||||
|
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false })); |
||||
|
} |
||||
|
|
||||
|
private updateSlaveConfig(slaveConfig: ModbusSlave): void { |
||||
|
const { |
||||
|
type = ModbusProtocolType.TCP, |
||||
|
method = ModbusMethodType.RTU, |
||||
|
unitId = 0, |
||||
|
deviceName = '', |
||||
|
deviceType = '', |
||||
|
pollPeriod = 5000, |
||||
|
sendDataToThingsBoard = false, |
||||
|
byteOrder = ModbusOrderType.BIG, |
||||
|
security = {}, |
||||
|
identity = { |
||||
|
vendorName: '', |
||||
|
productCode: '', |
||||
|
vendorUrl: '', |
||||
|
productName: '', |
||||
|
modelName: '', |
||||
|
}, |
||||
|
values = {} as ModbusRegisterValues, |
||||
|
baudrate = this.modbusBaudrates[0], |
||||
|
host = '', |
||||
|
port = null, |
||||
|
} = slaveConfig; |
||||
|
|
||||
|
const slaveState: ModbusSlave = { |
||||
|
type, |
||||
|
method, |
||||
|
unitId, |
||||
|
deviceName, |
||||
|
deviceType, |
||||
|
pollPeriod, |
||||
|
sendDataToThingsBoard: !!sendDataToThingsBoard, |
||||
|
byteOrder, |
||||
|
security, |
||||
|
identity, |
||||
|
values, |
||||
|
baudrate, |
||||
|
host: type === ModbusProtocolType.Serial ? '' : host, |
||||
|
port: type === ModbusProtocolType.Serial ? null : port, |
||||
|
serialPort: (type === ModbusProtocolType.Serial ? port : '') as string, |
||||
|
}; |
||||
|
|
||||
|
this.slaveConfigFormGroup.setValue(slaveState, { emitEvent: false }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,362 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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="protocolType !== 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="protocolType !== 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]="'gateway.port-required' | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('serialPort').hasError('required') && |
||||
|
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 protocolType === ModbusProtocolType.Serial ? modbusSerialMethodTypes : modbusMethodTypes" |
||||
|
[value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-container *ngIf="protocolType === 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"> |
||||
|
<mat-select formControlName="baudrate"> |
||||
|
<mat-option *ngFor="let rate of modbusBaudrates" [value]="rate">{{ rate }}</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.bytesize</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="bytesize"> |
||||
|
<mat-option *ngFor="let size of modbusByteSizes" [value]="size">{{ size }}</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.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"> |
||||
|
<mat-select formControlName="parity"> |
||||
|
<mat-option *ngFor="let parity of modbusParities" [value]="parity">{{ ModbusParityLabelsMap.get(parity) }}</mat-option> |
||||
|
</mat-select> |
||||
|
</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> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings"> |
||||
|
<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 *ngIf="protocolType !== ModbusProtocolType.Serial" 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> |
||||
|
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle fxLayoutAlign="center" [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> |
||||
|
<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-230" 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-230" 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-230" 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-230" 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> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<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"> |
||||
|
{{ data.buttonTitle | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,21 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
.slaves-config-container { |
||||
|
width: 80vw; |
||||
|
max-width: 900px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,243 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
FormControl, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormGroup, |
||||
|
Validators, |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
ModbusBaudrates, |
||||
|
ModbusByteSizes, |
||||
|
ModbusMethodLabelsMap, |
||||
|
ModbusMethodType, |
||||
|
ModbusOrderType, |
||||
|
ModbusParity, |
||||
|
ModbusParityLabelsMap, |
||||
|
ModbusProtocolLabelsMap, |
||||
|
ModbusProtocolType, |
||||
|
ModbusSerialMethodType, |
||||
|
ModbusSlaveInfo, |
||||
|
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/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { isEqual } from '@core/utils'; |
||||
|
|
||||
|
@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, |
||||
|
], |
||||
|
styleUrls: ['./modbus-slave-dialog.component.scss'], |
||||
|
}) |
||||
|
export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialogComponent, SlaveConfig> implements OnDestroy { |
||||
|
|
||||
|
slaveConfigFormGroup: UntypedFormGroup; |
||||
|
showSecurityControl: FormControl<boolean>; |
||||
|
portLimits = PortLimits; |
||||
|
|
||||
|
readonly modbusProtocolTypes = Object.values(ModbusProtocolType); |
||||
|
readonly modbusMethodTypes = Object.values(ModbusMethodType); |
||||
|
readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType); |
||||
|
readonly modbusParities = Object.values(ModbusParity); |
||||
|
readonly modbusByteSizes = ModbusByteSizes; |
||||
|
readonly modbusBaudrates = ModbusBaudrates; |
||||
|
readonly modbusOrderType = Object.values(ModbusOrderType); |
||||
|
readonly ModbusProtocolType = ModbusProtocolType; |
||||
|
readonly ModbusParityLabelsMap = ModbusParityLabelsMap; |
||||
|
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; |
||||
|
readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap; |
||||
|
readonly modbusHelpLink = |
||||
|
'https://thingsboard.io/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters'; |
||||
|
|
||||
|
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict']; |
||||
|
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host', 'wordOrder']; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor( |
||||
|
private fb: FormBuilder, |
||||
|
protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo, |
||||
|
public dialogRef: MatDialogRef<ModbusSlaveDialogComponent, SlaveConfig>, |
||||
|
) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.showSecurityControl = this.fb.control(false); |
||||
|
this.initializeSlaveFormGroup(); |
||||
|
this.updateSlaveFormGroup(); |
||||
|
this.updateControlsEnabling(this.data.value.type); |
||||
|
this.observeTypeChange(); |
||||
|
this.observeShowSecurity(); |
||||
|
this.showSecurityControl.patchValue(!!this.data.value.security && !isEqual(this.data.value.security, {})); |
||||
|
} |
||||
|
|
||||
|
get protocolType(): ModbusProtocolType { |
||||
|
return this.slaveConfigFormGroup.get('type').value; |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
add(): void { |
||||
|
if (!this.slaveConfigFormGroup.valid) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const { values, type, serialPort, ...rest } = this.slaveConfigFormGroup.value; |
||||
|
const slaveResult = { ...rest, type, ...values }; |
||||
|
|
||||
|
if (type === ModbusProtocolType.Serial) { |
||||
|
slaveResult.port = serialPort; |
||||
|
} |
||||
|
|
||||
|
this.dialogRef.close(slaveResult); |
||||
|
} |
||||
|
|
||||
|
private initializeSlaveFormGroup(): void { |
||||
|
this.slaveConfigFormGroup = this.fb.group({ |
||||
|
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
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, [Validators.required]], |
||||
|
baudrate: [this.modbusBaudrates[0]], |
||||
|
stopbits: [1], |
||||
|
bytesize: [ModbusByteSizes[0]], |
||||
|
parity: [ModbusParity.None], |
||||
|
strict: [true], |
||||
|
unitId: [0, [Validators.required]], |
||||
|
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
sendDataOnlyOnChange: [false], |
||||
|
timeout: [35], |
||||
|
byteOrder: [ModbusOrderType.BIG], |
||||
|
wordOrder: [ModbusOrderType.BIG], |
||||
|
retries: [true], |
||||
|
retryOnEmpty: [true], |
||||
|
retryOnInvalid: [true], |
||||
|
pollPeriod: [5000], |
||||
|
connectAttemptTimeMs: [5000], |
||||
|
connectAttemptCount: [5], |
||||
|
waitAfterFailedAttemptsMs: [300000], |
||||
|
values: [{}], |
||||
|
security: [{}], |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateSlaveFormGroup(): void { |
||||
|
this.slaveConfigFormGroup.patchValue({ |
||||
|
...this.data.value, |
||||
|
port: this.data.value.type === ModbusProtocolType.Serial ? null : this.data.value.port, |
||||
|
serialPort: this.data.value.type === ModbusProtocolType.Serial ? this.data.value.port : '', |
||||
|
values: { |
||||
|
attributes: this.data.value.attributes ?? [], |
||||
|
timeseries: this.data.value.timeseries ?? [], |
||||
|
attributeUpdates: this.data.value.attributeUpdates ?? [], |
||||
|
rpc: this.data.value.rpc ?? [], |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private observeTypeChange(): void { |
||||
|
this.slaveConfigFormGroup.get('type').valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(type => { |
||||
|
this.updateControlsEnabling(type); |
||||
|
this.updateMethodType(type); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateMethodType(type: ModbusProtocolType): void { |
||||
|
if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) { |
||||
|
this.slaveConfigFormGroup.get('method').patchValue( |
||||
|
type === ModbusProtocolType.Serial |
||||
|
? ModbusSerialMethodType.ASCII |
||||
|
: ModbusMethodType.SOCKET, |
||||
|
{emitEvent: false} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateControlsEnabling(type: ModbusProtocolType): void { |
||||
|
const [enableKeys, disableKeys] = type === ModbusProtocolType.Serial |
||||
|
? [this.serialSpecificControlKeys, this.tcpUdpSpecificControlKeys] |
||||
|
: [this.tcpUdpSpecificControlKeys, this.serialSpecificControlKeys]; |
||||
|
|
||||
|
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); |
||||
|
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false })); |
||||
|
|
||||
|
this.updateSecurityEnabling(this.showSecurityControl.value); |
||||
|
} |
||||
|
|
||||
|
private observeShowSecurity(): void { |
||||
|
this.showSecurityControl.valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(value => this.updateSecurityEnabling(value)); |
||||
|
} |
||||
|
|
||||
|
private updateSecurityEnabling(isEnabled: boolean): void { |
||||
|
if (isEnabled && this.protocolType !== ModbusProtocolType.Serial) { |
||||
|
this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,121 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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).value" class="tb-flex"> |
||||
|
<mat-chip *ngFor="let attribute of getValueGroup(ModbusValueKey.ATTRIBUTES, register).value"> |
||||
|
{{ 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" |
||||
|
[disabled]="disabled" |
||||
|
#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).value"> |
||||
|
<mat-chip *ngFor="let telemetry of getValueGroup(ModbusValueKey.TIMESERIES, register).value"> |
||||
|
{{ 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" |
||||
|
[disabled]="disabled" |
||||
|
#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).value" class="tb-flex"> |
||||
|
<mat-chip *ngFor="let attributeUpdate of getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register).value"> |
||||
|
{{ 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 |
||||
|
[disabled]="disabled" |
||||
|
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).value" class="tb-flex"> |
||||
|
<mat-chip *ngFor="let rpcRequest of getValueGroup(ModbusValueKey.RPC_REQUESTS, register).value"> |
||||
|
{{ 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" |
||||
|
[disabled]="disabled" |
||||
|
#rpcRequestsButton |
||||
|
(click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)"> |
||||
|
<tb-icon matButtonIcon>edit</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
|
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
.mat-mdc-tab-body-wrapper { |
||||
|
min-height: 320px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
::ng-deep .mdc-evolution-chip-set__chips { |
||||
|
align-items: center; |
||||
|
} |
||||
@ -0,0 +1,236 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
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, |
||||
|
ModbusValue, |
||||
|
ModbusValueKey, |
||||
|
ModbusValues, |
||||
|
ModbusValuesState, |
||||
|
} 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/ellipsis-chip-list.directive'; |
||||
|
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'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
|
||||
|
@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, |
||||
|
], |
||||
|
styleUrls: ['./modbus-values.component.scss'] |
||||
|
}) |
||||
|
|
||||
|
export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { |
||||
|
|
||||
|
@coerceBoolean() |
||||
|
@Input() singleMode = false; |
||||
|
|
||||
|
disabled = false; |
||||
|
modbusRegisterTypes: ModbusRegisterType[] = Object.values(ModbusRegisterType); |
||||
|
modbusValueKeys = Object.values(ModbusValueKey); |
||||
|
ModbusValuesTranslationsMap = ModbusRegisterTranslationsMap; |
||||
|
ModbusValueKey = ModbusValueKey; |
||||
|
valuesFormGroup: FormGroup; |
||||
|
|
||||
|
private onChange: (value: ModbusValuesState) => 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, |
||||
|
) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.initializeValuesFormGroup(); |
||||
|
this.observeValuesChanges(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (value: ModbusValuesState) => void): void { |
||||
|
this.onChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: () => void): void { |
||||
|
this.onTouched = fn; |
||||
|
} |
||||
|
|
||||
|
writeValue(values: ModbusValuesState): void { |
||||
|
if (this.singleMode) { |
||||
|
this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), { emitEvent: false }); |
||||
|
} else { |
||||
|
const { holding_registers, coils_initializer, input_registers, discrete_inputs } = values as ModbusRegisterValues; |
||||
|
this.valuesFormGroup.setValue({ |
||||
|
holding_registers: this.getSingleRegisterState(holding_registers), |
||||
|
coils_initializer: this.getSingleRegisterState(coils_initializer), |
||||
|
input_registers: this.getSingleRegisterState(input_registers), |
||||
|
discrete_inputs: this.getSingleRegisterState(discrete_inputs), |
||||
|
}, { emitEvent: false }); |
||||
|
} |
||||
|
this.cdr.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.valuesFormGroup.valid ? null : { |
||||
|
valuesFormGroup: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
this.cdr.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType): FormGroup { |
||||
|
return register |
||||
|
? this.valuesFormGroup.get(register).get(valueKey) as FormGroup |
||||
|
: this.valuesFormGroup.get(valueKey) as FormGroup; |
||||
|
} |
||||
|
|
||||
|
manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void { |
||||
|
$event.stopPropagation(); |
||||
|
const trigger = matButton._elementRef.nativeElement; |
||||
|
if (this.popoverService.hasPopover(trigger)) { |
||||
|
this.popoverService.hidePopover(trigger); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const keysControl = this.getValueGroup(keysType, register); |
||||
|
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: ModbusValue[]) => { |
||||
|
dataKeysPanelPopover.hide(); |
||||
|
keysControl.patchValue(keysData); |
||||
|
keysControl.markAsDirty(); |
||||
|
this.cdr.markForCheck(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private initializeValuesFormGroup(): void { |
||||
|
const getValuesFormGroup = () => this.fb.group(this.modbusValueKeys.reduce((acc, key) => { |
||||
|
acc[key] = this.fb.control([[], []]); |
||||
|
return acc; |
||||
|
}, {})); |
||||
|
|
||||
|
if (this.singleMode) { |
||||
|
this.valuesFormGroup = getValuesFormGroup(); |
||||
|
} else { |
||||
|
this.valuesFormGroup = this.fb.group( |
||||
|
this.modbusRegisterTypes.reduce((registersAcc, register) => { |
||||
|
registersAcc[register] = getValuesFormGroup(); |
||||
|
return registersAcc; |
||||
|
}, {}) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private observeValuesChanges(): void { |
||||
|
this.valuesFormGroup.valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(value => { |
||||
|
this.onChange(value); |
||||
|
this.onTouched(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private getSingleRegisterState(values: ModbusValues): ModbusValues { |
||||
|
return { |
||||
|
attributes: values?.attributes ?? [], |
||||
|
timeseries: values?.timeseries ?? [], |
||||
|
attributeUpdates: values?.attributeUpdates ?? [], |
||||
|
rpc: values?.rpc ?? [], |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="serverConfigFormGroup"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" tbTruncateWithTooltip translate>gateway.server-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="url" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.server-url-required') | translate" |
||||
|
*ngIf="serverConfigFormGroup.get('url').hasError('required') && |
||||
|
serverConfigFormGroup.get('url').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-hint-tooltip-icon="{{ 'gateway.hints.opcua-timeout' | translate }}"> |
||||
|
<div tbTruncateWithTooltip>{{ 'gateway.timeout' | translate }}</div> |
||||
|
</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="timeoutInMillis" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'gateway.timeout-error' | translate: {min: 1000}" |
||||
|
*ngIf="(serverConfigFormGroup.get('timeoutInMillis').hasError('required') || |
||||
|
serverConfigFormGroup.get('timeoutInMillis').hasError('min')) && |
||||
|
serverConfigFormGroup.get('timeoutInMillis').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" tbTruncateWithTooltip translate>gateway.security</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="security"> |
||||
|
<mat-option *ngFor="let version of securityPolicyTypes" [value]="version.value">{{ version.name }}</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" tb-hint-tooltip-icon="{{ 'gateway.hints.scan-period' | translate }}"> |
||||
|
<div tbTruncateWithTooltip>{{ 'gateway.scan-period' | translate }}</div> |
||||
|
</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="scanPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'gateway.scan-period-error' | translate: {min: 1000}" |
||||
|
*ngIf="(serverConfigFormGroup.get('scanPeriodInMillis').hasError('required') || |
||||
|
serverConfigFormGroup.get('scanPeriodInMillis').hasError('min')) && |
||||
|
serverConfigFormGroup.get('scanPeriodInMillis').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-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}"> |
||||
|
<div tbTruncateWithTooltip>{{ 'gateway.sub-check-period' | translate }}</div> |
||||
|
</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="subCheckPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'gateway.sub-check-period-error' | translate: {min: 10}" |
||||
|
*ngIf="(serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('required') || |
||||
|
serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('min')) && |
||||
|
serverConfigFormGroup.get('subCheckPeriodInMillis').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="enableSubscriptions"> |
||||
|
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.enable-subscription' | translate }}"> |
||||
|
<div tbTruncateWithTooltip>{{ 'gateway.enable-subscription' | translate }}</div> |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showMap"> |
||||
|
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.show-map' | translate }}"> |
||||
|
{{ 'gateway.show-map' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<tb-security-config formControlName="identity" |
||||
|
[extendCertificatesModel]="true"> |
||||
|
</tb-security-config> |
||||
|
</div> |
||||
@ -1,26 +0,0 @@ |
|||||
///
|
|
||||
/// 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 './mapping-table/mapping-table.component'; |
|
||||
export * from './device-info-table/device-info-table.component'; |
|
||||
export * from './security-config/security-config.component'; |
|
||||
export * from './server-config/server-config.component'; |
|
||||
export * from './mapping-data-keys-panel/mapping-data-keys-panel.component'; |
|
||||
export * from './type-value-panel/type-value-panel.component'; |
|
||||
export * from './broker-config-control/broker-config-control.component'; |
|
||||
export * from './workers-config-control/workers-config-control.component'; |
|
||||
export * from './opc-ua-basic-config/opc-ua-basic-config.component'; |
|
||||
export * from './mqtt-basic-config/mqtt-basic-config.component'; |
|
||||
@ -1,127 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
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]="serverConfigFormGroup"> |
|
||||
<div class="server-settings"> |
|
||||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|
||||
<div class="server-conf-field-title" translate>gateway.server-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="url" placeholder="{{ 'gateway.set' | translate }}"/> |
|
||||
<mat-icon matSuffix |
|
||||
matTooltipPosition="above" |
|
||||
matTooltipClass="tb-error-tooltip" |
|
||||
[matTooltip]="('gateway.server-url-required') | translate" |
|
||||
*ngIf="serverConfigFormGroup.get('url').hasError('required') && |
|
||||
serverConfigFormGroup.get('url').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="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.opcua-timeout' | translate }}" translate> |
|
||||
gateway.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="timeoutInMillis" placeholder="{{ 'gateway.set' | translate }}"/> |
|
||||
<mat-icon matSuffix |
|
||||
matTooltipPosition="above" |
|
||||
matTooltipClass="tb-error-tooltip" |
|
||||
[matTooltip]="'gateway.timeout-error' | translate: {min: 1000}" |
|
||||
*ngIf="(serverConfigFormGroup.get('timeoutInMillis').hasError('required') || |
|
||||
serverConfigFormGroup.get('timeoutInMillis').hasError('min')) && |
|
||||
serverConfigFormGroup.get('timeoutInMillis').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="server-conf-field-title" translate>gateway.security</div> |
|
||||
<div class="tb-flex no-gap"> |
|
||||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|
||||
<mat-select formControlName="security"> |
|
||||
<mat-option *ngFor="let version of securityPolicyTypes" [value]="version.value">{{ version.name }}</mat-option> |
|
||||
</mat-select> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|
||||
<div class="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.scan-period' | translate }}" translate> |
|
||||
gateway.scan-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="scanPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/> |
|
||||
<mat-icon matSuffix |
|
||||
matTooltipPosition="above" |
|
||||
matTooltipClass="tb-error-tooltip" |
|
||||
[matTooltip]="'gateway.scan-period-error' | translate: {min: 1000}" |
|
||||
*ngIf="(serverConfigFormGroup.get('scanPeriodInMillis').hasError('required') || |
|
||||
serverConfigFormGroup.get('scanPeriodInMillis').hasError('min')) && |
|
||||
serverConfigFormGroup.get('scanPeriodInMillis').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="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}" translate> |
|
||||
gateway.sub-check-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="subCheckPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/> |
|
||||
<mat-icon matSuffix |
|
||||
matTooltipPosition="above" |
|
||||
matTooltipClass="tb-error-tooltip" |
|
||||
[matTooltip]="'gateway.sub-check-period-error' | translate: {min: 10}" |
|
||||
*ngIf="(serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('required') || |
|
||||
serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('min')) && |
|
||||
serverConfigFormGroup.get('subCheckPeriodInMillis').touched" |
|
||||
class="tb-error"> |
|
||||
warning |
|
||||
</mat-icon> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
|
||||
<mat-slide-toggle class="mat-slide" formControlName="enableSubscriptions"> |
|
||||
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.enable-subscription' | translate }}"> |
|
||||
{{ 'gateway.enable-subscription' | translate }} |
|
||||
</mat-label> |
|
||||
</mat-slide-toggle> |
|
||||
</div> |
|
||||
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
|
||||
<mat-slide-toggle class="mat-slide" formControlName="showMap"> |
|
||||
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.show-map' | translate }}"> |
|
||||
{{ 'gateway.show-map' | translate }} |
|
||||
</mat-label> |
|
||||
</mat-slide-toggle> |
|
||||
</div> |
|
||||
<tb-security-config formControlName="identity" |
|
||||
[extendCertificatesModel]="true"> |
|
||||
</tb-security-config> |
|
||||
</div> |
|
||||
@ -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'); |
||||
|
} |
||||
|
if (portControl.hasError('min') || portControl.hasError('max')) { |
||||
|
return this.translate.instant('gateway.port-limits-error', { |
||||
|
min: PortLimits.MIN, |
||||
|
max: PortLimits.MAX, |
||||
|
}); |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
} |
||||
@ -1,17 +0,0 @@ |
|||||
///
|
|
||||
/// 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 './gateway-help-link/gateway-help-link.pipe'; |
|
||||
@ -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 TbTableDatasource<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,116 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
Directive, |
||||
|
ElementRef, |
||||
|
Input, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
} from '@angular/core'; |
||||
|
import { fromEvent, Subject } from 'rxjs'; |
||||
|
import { filter, takeUntil, tap } from 'rxjs/operators'; |
||||
|
import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
|
||||
|
@Directive({ |
||||
|
standalone: true, |
||||
|
selector: '[tbTruncateWithTooltip]', |
||||
|
providers: [MatTooltip], |
||||
|
}) |
||||
|
export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDestroy { |
||||
|
|
||||
|
@Input('tbTruncateWithTooltip') |
||||
|
text: string; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
tooltipEnabled = true; |
||||
|
|
||||
|
@Input() |
||||
|
position: TooltipPosition = 'above'; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor( |
||||
|
private elementRef: ElementRef, |
||||
|
private renderer: Renderer2, |
||||
|
private tooltip: MatTooltip |
||||
|
) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.observeMouseEvents(); |
||||
|
this.applyTruncationStyles(); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
if (!this.text) { |
||||
|
this.text = this.elementRef.nativeElement.innerText; |
||||
|
} |
||||
|
|
||||
|
this.tooltip.position = this.position; |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
if (this.tooltip._isTooltipVisible()) { |
||||
|
this.hideTooltip(); |
||||
|
} |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
private observeMouseEvents(): void { |
||||
|
fromEvent(this.elementRef.nativeElement, 'mouseenter') |
||||
|
.pipe( |
||||
|
filter(() => this.tooltipEnabled), |
||||
|
filter(() => this.isOverflown(this.elementRef.nativeElement)), |
||||
|
tap(() => this.showTooltip()), |
||||
|
takeUntil(this.destroy$), |
||||
|
) |
||||
|
.subscribe(); |
||||
|
fromEvent(this.elementRef.nativeElement, 'mouseleave') |
||||
|
.pipe( |
||||
|
filter(() => this.tooltipEnabled), |
||||
|
filter(() => this.tooltip._isTooltipVisible()), |
||||
|
tap(() => this.hideTooltip()), |
||||
|
takeUntil(this.destroy$), |
||||
|
) |
||||
|
.subscribe(); |
||||
|
} |
||||
|
|
||||
|
private applyTruncationStyles(): void { |
||||
|
this.renderer.setStyle(this.elementRef.nativeElement, 'white-space', 'nowrap'); |
||||
|
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'hidden'); |
||||
|
this.renderer.setStyle(this.elementRef.nativeElement, 'text-overflow', 'ellipsis'); |
||||
|
} |
||||
|
|
||||
|
private isOverflown(element: HTMLElement): boolean { |
||||
|
return element.clientWidth < element.scrollWidth; |
||||
|
} |
||||
|
|
||||
|
private showTooltip(): void { |
||||
|
this.tooltip.message = this.text; |
||||
|
|
||||
|
this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text); |
||||
|
this.tooltip.show(); |
||||
|
} |
||||
|
|
||||
|
private hideTooltip(): void { |
||||
|
this.tooltip.hide(); |
||||
|
} |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
Fields templatization feature allows you to process the incoming messages with dynamic configuration |
Fields templatization feature allows you to process the incoming messages with dynamic configuration |
||||
by substitution of templates specified in the configuration fields with values from message or message metadata. |
by substitution of templates specified in the configuration fields with values from message or message metadata. |
||||
For more detailed information, please refer to the ThingsBoard [documentation](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/templatization/) |
For more detailed information, please refer to the ThingsBoard [documentation{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/templatization/) |
||||
|
|
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue