committed by
GitHub
62 changed files with 3894 additions and 437 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(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue