Browse Source

UI: Implement rpc control widgets settings forms

pull/6545/head
Igor Kulikov 5 years ago
parent
commit
9d8504de4b
  1. 18
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  2. 41
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html
  3. 216
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts
  4. 81
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html
  5. 95
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts
  6. 104
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html
  7. 133
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts
  8. 40
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html
  9. 98
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts
  10. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html
  11. 53
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts
  12. 49
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html
  13. 75
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts
  14. 79
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html
  15. 99
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts
  16. 33
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html
  17. 97
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts
  18. 51
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html
  19. 68
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts
  20. 50
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  21. 2
      ui-ngx/src/app/shared/components/json-content.component.html
  22. 57
      ui-ngx/src/app/shared/components/json-content.component.scss
  23. 22
      ui-ngx/src/app/shared/components/json-content.component.ts
  24. 31
      ui-ngx/src/assets/locale/locale.constant-en_US.json

18
application/src/main/data/json/system/widget_bundles/control_widgets.json

File diff suppressed because one or more lines are too long

41
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html

@ -0,0 +1,41 @@
<!--
Copyright © 2016-2022 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-form-field fxFlex class="mat-block" [formGroup]="deviceKeyFormGroup">
<mat-label>{{ (keyType === dataKeyType.attribute
? attributeLabel
: timeseriesLabel) | translate }}</mat-label>
<input [required]="required" matInput
type="text" placeholder="{{ 'entity.key-name' | translate }}"
#keyInput
(focusin)="onFocus()"
formControlName="key"
[matAutocomplete]="keyAutocomplete">
<button *ngIf="deviceKeyFormGroup.get('key').value && !deviceKeyFormGroup.get('key').disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearKey()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
#keyAutocomplete="matAutocomplete">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span [innerHTML]="key | highlight:keySearchText"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

216
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts

@ -0,0 +1,216 @@
///
/// Copyright © 2016-2022 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, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable, of } from 'rxjs';
import { IAliasController } from '@core/api/widget-api.models';
import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators';
import { DataKey } from '@shared/models/widget.models';
import { EntityService } from '@core/http/entity.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@Component({
selector: 'tb-device-key-autocomplete',
templateUrl: './device-key-autocomplete.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceKeyAutocompleteComponent),
multi: true
}
]
})
export class DeviceKeyAutocompleteComponent extends PageComponent implements OnInit, ControlValueAccessor, OnChanges {
@ViewChild('keyInput') keyInput: ElementRef;
@Input()
disabled: boolean;
@Input()
aliasController: IAliasController;
@Input()
targetDeviceAliasId: string;
@Input()
keyType: DataKeyType;
@Input()
attributeLabel = 'widgets.rpc.attribute-value-key';
@Input()
timeseriesLabel = 'widgets.rpc.timeseries-value-key';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
dataKeyType = DataKeyType;
private modelValue: string;
private propagateChange = null;
public deviceKeyFormGroup: FormGroup;
filteredKeys: Observable<Array<string>>;
keySearchText = '';
private latestKeySearchResult: Array<string> = null;
private keysFetchObservable$: Observable<Array<string>> = null;
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private entityService: EntityService,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.deviceKeyFormGroup = this.fb.group({
key: [null, this.required ? [Validators.required] : []]
});
this.deviceKeyFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.filteredKeys = this.deviceKeyFormGroup.get('key').valueChanges
.pipe(
map(value => value ? value : ''),
mergeMap(name => this.fetchKeys(name) )
);
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'targetDeviceAliasId' || propName === 'keyType') {
this.clearKeysCache();
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.deviceKeyFormGroup.disable({emitEvent: false});
} else {
this.deviceKeyFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string): void {
this.modelValue = value;
this.deviceKeyFormGroup.patchValue(
{key: value}, {emitEvent: false}
);
}
clearKey() {
this.deviceKeyFormGroup.get('key').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.keyInput.nativeElement.blur();
this.keyInput.nativeElement.focus();
}, 0);
}
onFocus() {
this.deviceKeyFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true});
}
private updateModel() {
const value: string = this.deviceKeyFormGroup.get('key').value;
this.modelValue = value;
this.propagateChange(this.modelValue);
}
private clearKeysCache(): void {
this.latestKeySearchResult = null;
this.keysFetchObservable$ = null;
}
private fetchKeys(searchText?: string): Observable<Array<string>> {
if (this.keySearchText !== searchText || this.latestKeySearchResult === null) {
this.keySearchText = searchText;
const dataKeyFilter = this.createKeyFilter(this.keySearchText);
return this.getKeys().pipe(
map(name => name.filter(dataKeyFilter)),
tap(res => this.latestKeySearchResult = res)
);
}
return of(this.latestKeySearchResult);
}
private getKeys() {
if (this.keysFetchObservable$ === null) {
let fetchObservable: Observable<Array<DataKey>>;
if (this.targetDeviceAliasId) {
const dataKeyTypes = [this.keyType];
fetchObservable = this.fetchEntityKeys(this.targetDeviceAliasId, dataKeyTypes);
} else {
fetchObservable = of([]);
}
this.keysFetchObservable$ = fetchObservable.pipe(
map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)),
publishReplay(1),
refCount()
);
}
return this.keysFetchObservable$;
}
private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {
return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => {
return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
dataKeyTypes,
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
);
}),
catchError(() => of([] as Array<DataKey>))
);
}
private createKeyFilter(query: string): (key: string) => boolean {
const lowercaseQuery = query.toLowerCase();
return key => key.toLowerCase().startsWith(lowercaseQuery);
}
}

81
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html

@ -0,0 +1,81 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="knobControlWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.common-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.knob-title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.value-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.initial-value</mat-label>
<input matInput type="number" formControlName="initialValue">
</mat-form-field>
<section fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.min-value</mat-label>
<input required matInput type="number" formControlName="minValue">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.max-value</mat-label>
<input required matInput type="number" formControlName="maxValue">
</mat-form-field>
</section>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.get-value-method</mat-label>
<input required matInput formControlName="getValueMethod">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.set-value-method</mat-label>
<input required matInput formControlName="setValueMethod">
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.rpc-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.request-timeout</mat-label>
<input required matInput type="number" min="0" formControlName="requestTimeout">
</mat-form-field>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.rpc.persistent-rpc-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="knobControlWidgetSettingsForm.get('requestPersistent').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="requestPersistent" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.rpc.request-persistent' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.persistent-polling-interval</mat-label>
<input matInput type="number" min="1000" formControlName="persistentPollingInterval">
</mat-form-field>
</ng-template>
</mat-expansion-panel>
</fieldset>
</fieldset>
</section>

95
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts

@ -0,0 +1,95 @@
///
/// Copyright © 2016-2022 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-knob-control-widget-settings',
templateUrl: './knob-control-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class KnobControlWidgetSettingsComponent extends WidgetSettingsComponent {
knobControlWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.knobControlWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
title: '',
minValue: 0,
maxValue: 100,
initialValue: 50,
getValueMethod: 'getValue',
setValueMethod: 'setValue',
requestTimeout: 500,
requestPersistent: false,
persistentPollingInterval: 5000
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.knobControlWidgetSettingsForm = this.fb.group({
// Common settings
title: [settings.title, []],
// Value settings
initialValue: [settings.initialValue, []],
minValue: [settings.minValue, [Validators.required]],
maxValue: [settings.maxValue, [Validators.required]],
getValueMethod: [settings.getValueMethod, [Validators.required]],
setValueMethod: [settings.setValueMethod, [Validators.required]],
// RPC settings
requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]],
// --> Persistent RPC settings
requestPersistent: [settings.requestPersistent, []],
persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]]
});
}
protected validatorTriggers(): string[] {
return ['requestPersistent'];
}
protected updateValidators(emitEvent: boolean): void {
const requestPersistent: boolean = this.knobControlWidgetSettingsForm.get('requestPersistent').value;
if (requestPersistent) {
this.knobControlWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent});
} else {
this.knobControlWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent});
}
this.knobControlWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false});
}
}

104
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html

@ -0,0 +1,104 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="ledIndicatorWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.common-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.led-title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<tb-color-input fxFlex
formControlName="ledColor"
icon="format_color_fill"
label="{{ 'widgets.rpc.led-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.value-settings</legend>
<mat-checkbox formControlName="initialValue" style="margin-bottom: 16px;">
{{ 'widgets.rpc.initial-value' | translate }}
</mat-checkbox>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.check-status-settings</legend>
<mat-slide-toggle formControlName="performCheckStatus" class="slide-block">
{{ 'widgets.rpc.perform-rpc-status-check' | translate }}
</mat-slide-toggle>
<mat-form-field [fxShow]="!ledIndicatorWidgetSettingsForm.get('performCheckStatus').value" fxFlex class="mat-block">
<mat-label translate>widgets.rpc.retrieve-led-status-value-method</mat-label>
<mat-select formControlName="retrieveValueMethod">
<mat-option [value]="'attribute'">
{{ 'widgets.rpc.retrieve-value-method-attribute' | translate }}
</mat-option>
<mat-option [value]="'timeseries'">
{{ 'widgets.rpc.retrieve-value-method-timeseries' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-device-key-autocomplete fxFlex
[fxShow]="!ledIndicatorWidgetSettingsForm.get('performCheckStatus').value"
[aliasController]="aliasController"
[targetDeviceAliasId]="targetDeviceAliasId"
[keyType]="ledIndicatorWidgetSettingsForm.get('retrieveValueMethod').value === 'attribute' ? dataKeyType.attribute : dataKeyType.timeseries"
attributeLabel="widgets.rpc.led-status-value-attribute"
timeseriesLabel="widgets.rpc.led-status-value-timeseries"
required
formControlName="valueAttribute">
</tb-device-key-autocomplete>
<mat-form-field [fxShow]="ledIndicatorWidgetSettingsForm.get('performCheckStatus').value" fxFlex class="mat-block">
<mat-label translate>widgets.rpc.check-status-method</mat-label>
<input required matInput formControlName="checkStatusMethod">
</mat-form-field>
<tb-js-func formControlName="parseValueFunction"
[globalVariables]="functionScopeVariables"
[functionArgs]="['data']"
functionTitle="{{ 'widgets.rpc.parse-led-status-value-function' | translate }}"
helpId="widget/lib/rpc/parse_value_fn">
</tb-js-func>
</fieldset>
</fieldset>
<fieldset [fxShow]="ledIndicatorWidgetSettingsForm.get('performCheckStatus').value" class="fields-group">
<legend class="group-title" translate>widgets.rpc.rpc-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.request-timeout</mat-label>
<input matInput type="number" min="0" formControlName="requestTimeout">
</mat-form-field>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.rpc.persistent-rpc-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="ledIndicatorWidgetSettingsForm.get('requestPersistent').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="requestPersistent" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.rpc.request-persistent' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.persistent-polling-interval</mat-label>
<input matInput type="number" min="1000" formControlName="persistentPollingInterval">
</mat-form-field>
</ng-template>
</mat-expansion-panel>
</fieldset>
</fieldset>
</section>

133
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts

@ -0,0 +1,133 @@
///
/// Copyright © 2016-2022 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@Component({
selector: 'tb-led-indicator-widget-settings',
templateUrl: './led-indicator-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class LedIndicatorWidgetSettingsComponent extends WidgetSettingsComponent {
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
get targetDeviceAliasId(): string {
const aliasIds = this.widget.config.targetDeviceAliasIds;
if (aliasIds && aliasIds.length) {
return aliasIds[0];
}
return null;
}
dataKeyType = DataKeyType;
ledIndicatorWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>,
private widgetService: WidgetService,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.ledIndicatorWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
title: '',
ledColor: 'green',
initialValue: false,
performCheckStatus: true,
checkStatusMethod: 'checkStatus',
retrieveValueMethod: 'attribute',
valueAttribute: 'value',
parseValueFunction: 'return data ? true : false;',
requestTimeout: 500,
requestPersistent: false,
persistentPollingInterval: 5000
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.ledIndicatorWidgetSettingsForm = this.fb.group({
// Common settings
title: [settings.title, []],
ledColor: [settings.ledColor, []],
// Value settings
initialValue: [settings.initialValue, []],
// --> Check status settings
performCheckStatus: [settings.performCheckStatus, []],
retrieveValueMethod: [settings.retrieveValueMethod, []],
checkStatusMethod: [settings.checkStatusMethod, [Validators.required]],
valueAttribute: [settings.valueAttribute, [Validators.required]],
parseValueFunction: [settings.parseValueFunction, []],
// RPC settings
requestTimeout: [settings.requestTimeout, [Validators.min(0)]],
// --> Persistent RPC settings
requestPersistent: [settings.requestPersistent, []],
persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]]
});
}
protected validatorTriggers(): string[] {
return ['performCheckStatus', 'requestPersistent'];
}
protected updateValidators(emitEvent: boolean): void {
const performCheckStatus: boolean = this.ledIndicatorWidgetSettingsForm.get('performCheckStatus').value;
const requestPersistent: boolean = this.ledIndicatorWidgetSettingsForm.get('requestPersistent').value;
if (performCheckStatus) {
this.ledIndicatorWidgetSettingsForm.get('valueAttribute').disable({emitEvent});
this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').enable({emitEvent});
this.ledIndicatorWidgetSettingsForm.get('requestTimeout').enable({emitEvent});
this.ledIndicatorWidgetSettingsForm.get('requestPersistent').enable({emitEvent: false});
if (requestPersistent) {
this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent});
} else {
this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent});
}
} else {
this.ledIndicatorWidgetSettingsForm.get('valueAttribute').enable({emitEvent});
this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').disable({emitEvent});
this.ledIndicatorWidgetSettingsForm.get('requestTimeout').disable({emitEvent});
this.ledIndicatorWidgetSettingsForm.get('requestPersistent').disable({emitEvent: false});
this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent});
}
this.ledIndicatorWidgetSettingsForm.get('valueAttribute').updateValueAndValidity({emitEvent: false});
this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').updateValueAndValidity({emitEvent: false});
this.ledIndicatorWidgetSettingsForm.get('requestTimeout').updateValueAndValidity({emitEvent: false});
this.ledIndicatorWidgetSettingsForm.get('requestPersistent').updateValueAndValidity({emitEvent: false});
this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false});
}
}

40
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="rpcButtonStyleFormGroup" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.button-style</legend>
<mat-slide-toggle formControlName="isRaised" class="slide-block">
{{ 'widgets.rpc.button-raised' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="isPrimary" class="slide-block">
{{ 'widgets.rpc.button-primary' | translate }}
</mat-slide-toggle>
<section fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap="8px">
<tb-color-input fxFlex
formControlName="textColor"
icon="format_color_fill"
label="{{ 'widgets.rpc.button-text-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
<tb-color-input fxFlex
formControlName="bgColor"
icon="format_color_fill"
label="{{ 'widgets.rpc.button-background-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
</fieldset>
</section>

98
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts

@ -0,0 +1,98 @@
///
/// Copyright © 2016-2022 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
export interface RpcButtonStyle {
isRaised: boolean;
isPrimary: boolean;
bgColor: string;
textColor: string;
}
@Component({
selector: 'tb-rpc-button-style',
templateUrl: './rpc-button-style.component.html',
styleUrls: ['./../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RpcButtonStyleComponent),
multi: true
}
]
})
export class RpcButtonStyleComponent extends PageComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
private modelValue: RpcButtonStyle;
private propagateChange = null;
public rpcButtonStyleFormGroup: FormGroup;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.rpcButtonStyleFormGroup = this.fb.group({
isRaised: [true, []],
isPrimary: [false, []],
bgColor: [null, []],
textColor: [null, []]
});
this.rpcButtonStyleFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.rpcButtonStyleFormGroup.disable({emitEvent: false});
} else {
this.rpcButtonStyleFormGroup.enable({emitEvent: false});
}
}
writeValue(value: RpcButtonStyle): void {
this.modelValue = value;
this.rpcButtonStyleFormGroup.patchValue(
value, {emitEvent: false}
);
}
private updateModel() {
this.modelValue = this.rpcButtonStyleFormGroup.value;
this.propagateChange(this.modelValue);
}
}

27
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html

@ -0,0 +1,27 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="rpcShellWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.rpc-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.request-timeout</mat-label>
<input required matInput type="number" min="0" formControlName="requestTimeout">
</mat-form-field>
</fieldset>
</section>

53
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts

@ -0,0 +1,53 @@
///
/// Copyright © 2016-2022 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-rpc-shell-widget-settings',
templateUrl: './rpc-shell-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class RpcShellWidgetSettingsComponent extends WidgetSettingsComponent {
rpcShellWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.rpcShellWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
requestTimeout: 500
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.rpcShellWidgetSettingsForm = this.fb.group({
// RPC settings
requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]]
});
}
}

49
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html

@ -0,0 +1,49 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="rpcTerminalWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.rpc-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.request-timeout</mat-label>
<input required matInput type="number" min="0" formControlName="requestTimeout">
</mat-form-field>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.rpc.persistent-rpc-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="rpcTerminalWidgetSettingsForm.get('requestPersistent').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="requestPersistent" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.rpc.request-persistent' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.persistent-polling-interval</mat-label>
<input matInput type="number" min="1000" formControlName="persistentPollingInterval">
</mat-form-field>
</ng-template>
</mat-expansion-panel>
</fieldset>
</fieldset>
</section>

75
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts

@ -0,0 +1,75 @@
///
/// Copyright © 2016-2022 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-rpc-terminal-widget-settings',
templateUrl: './rpc-terminal-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class RpcTerminalWidgetSettingsComponent extends WidgetSettingsComponent {
rpcTerminalWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.rpcTerminalWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
requestTimeout: 500,
requestPersistent: false,
persistentPollingInterval: 5000
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.rpcTerminalWidgetSettingsForm = this.fb.group({
// RPC settings
requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]],
// --> Persistent RPC settings
requestPersistent: [settings.requestPersistent, []],
persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]]
});
}
protected validatorTriggers(): string[] {
return ['requestPersistent'];
}
protected updateValidators(emitEvent: boolean): void {
const requestPersistent: boolean = this.rpcTerminalWidgetSettingsForm.get('requestPersistent').value;
if (requestPersistent) {
this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent});
} else {
this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent});
}
this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false});
}
}

79
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html

@ -0,0 +1,79 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="sendRpcWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.common-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.widget-title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.button-label</mat-label>
<input matInput formControlName="buttonText">
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.rpc-settings</legend>
<mat-checkbox formControlName="oneWayElseTwoWay" style="margin-bottom: 16px;">
{{ 'widgets.rpc.is-one-way-command' | translate }}
</mat-checkbox>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.rpc-method</mat-label>
<input matInput formControlName="methodName">
</mat-form-field>
<tb-json-content
label="{{ 'widgets.rpc.rpc-method-params' | translate }}"
[contentType]="contentTypes.JSON"
validateOnChange
formControlName="methodParams">
</tb-json-content>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.request-timeout</mat-label>
<input matInput type="number" min="0" formControlName="requestTimeout">
</mat-form-field>
<mat-checkbox formControlName="showError" style="margin-bottom: 16px;">
{{ 'widgets.rpc.show-rpc-error' | translate }}
</mat-checkbox>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.rpc.persistent-rpc-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="sendRpcWidgetSettingsForm.get('requestPersistent').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="requestPersistent" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.rpc.request-persistent' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.persistent-polling-interval</mat-label>
<input matInput type="number" min="1000" formControlName="persistentPollingInterval">
</mat-form-field>
</ng-template>
</mat-expansion-panel>
</fieldset>
</fieldset>
<tb-rpc-button-style
formControlName="styleButton">
</tb-rpc-button-style>
</section>

99
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts

@ -0,0 +1,99 @@
///
/// Copyright © 2016-2022 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ContentType } from '@shared/models/constants';
@Component({
selector: 'tb-send-rpc-widget-settings',
templateUrl: './send-rpc-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class SendRpcWidgetSettingsComponent extends WidgetSettingsComponent {
sendRpcWidgetSettingsForm: FormGroup;
contentTypes = ContentType;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.sendRpcWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
title: '',
buttonText: 'Send RPC',
oneWayElseTwoWay: true,
showError: false,
methodName: 'rpcCommand',
methodParams: '{}',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 5000,
styleButton: {
isRaised: true,
isPrimary: false,
bgColor: null,
textColor: null
}
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.sendRpcWidgetSettingsForm = this.fb.group({
title: [settings.title, []],
buttonText: [settings.buttonText, []],
// RPC settings
oneWayElseTwoWay: [settings.oneWayElseTwoWay, []],
methodName: [settings.methodName, []],
methodParams: [settings.methodParams, []],
requestTimeout: [settings.requestTimeout, [Validators.min(0)]],
showError: [settings.showError, []],
// Persistent RPC settings
requestPersistent: [settings.requestPersistent, []],
persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]],
styleButton: [settings.styleButton, []]
});
}
protected validatorTriggers(): string[] {
return ['requestPersistent'];
}
protected updateValidators(emitEvent: boolean): void {
const requestPersistent: boolean = this.sendRpcWidgetSettingsForm.get('requestPersistent').value;
if (requestPersistent) {
this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent});
} else {
this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent});
}
this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false});
}
}

33
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html

@ -40,30 +40,15 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field [fxShow]="switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' || <tb-device-key-autocomplete fxFlex
switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'timeseries'" fxFlex class="mat-block"> [fxShow]="switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' ||
<mat-label>{{ (switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'timeseries'"
? 'widgets.rpc.attribute-value-key' [aliasController]="aliasController"
: 'widgets.rpc.timeseries-value-key') | translate }}</mat-label> [targetDeviceAliasId]="targetDeviceAliasId"
<input required matInput [keyType]="switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' ? dataKeyType.attribute : dataKeyType.timeseries"
type="text" placeholder="{{ 'entity.key-name' | translate }}" required
#keyInput formControlName="valueKey">
formControlName="valueKey" </tb-device-key-autocomplete>
[matAutocomplete]="keyAutocomplete">
<button *ngIf="switchRpcSettingsFormGroup.get('valueKey').value && !switchRpcSettingsFormGroup.get('valueKey').disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearKey()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
#keyAutocomplete="matAutocomplete">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span [innerHTML]="key | highlight:keySearchText"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field [fxShow]="switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'rpc'" fxFlex class="mat-block"> <mat-form-field [fxShow]="switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'rpc'" fxFlex class="mat-block">
<mat-label translate>widgets.rpc.get-value-method</mat-label> <mat-label translate>widgets.rpc.get-value-method</mat-label>
<input required matInput formControlName="getValueMethod"> <input required matInput formControlName="getValueMethod">

97
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -31,10 +31,7 @@ import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { Observable, of } from 'rxjs';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators';
import { DataKey } from '@shared/models/widget.models';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
export declare type RpcRetrieveValueMethod = 'none' | 'rpc' | 'attribute' | 'timeseries'; export declare type RpcRetrieveValueMethod = 'none' | 'rpc' | 'attribute' | 'timeseries';
@ -84,7 +81,7 @@ export function switchRpcDefaultSettings(): SwitchRpcSettings {
} }
] ]
}) })
export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
@ViewChild('keyInput') keyInput: ElementRef; @ViewChild('keyInput') keyInput: ElementRef;
@ -97,6 +94,8 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit,
@Input() @Input()
targetDeviceAliasId: string; targetDeviceAliasId: string;
dataKeyType = DataKeyType;
functionScopeVariables = this.widgetService.getWidgetScopeVariables(); functionScopeVariables = this.widgetService.getWidgetScopeVariables();
private modelValue: SwitchRpcSettings; private modelValue: SwitchRpcSettings;
@ -105,12 +104,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit,
public switchRpcSettingsFormGroup: FormGroup; public switchRpcSettingsFormGroup: FormGroup;
filteredKeys: Observable<Array<string>>;
keySearchText = '';
private latestKeySearchResult: Array<string> = null;
private keysFetchObservable$: Observable<Array<string>> = null;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private translate: TranslateService, private translate: TranslateService,
private widgetService: WidgetService, private widgetService: WidgetService,
@ -148,7 +141,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit,
persistentPollingInterval: [5000, [Validators.min(1000)]], persistentPollingInterval: [5000, [Validators.min(1000)]],
}); });
this.switchRpcSettingsFormGroup.get('retrieveValueMethod').valueChanges.subscribe(() => { this.switchRpcSettingsFormGroup.get('retrieveValueMethod').valueChanges.subscribe(() => {
this.clearKeysCache();
this.updateValidators(true); this.updateValidators(true);
}); });
this.switchRpcSettingsFormGroup.get('requestPersistent').valueChanges.subscribe(() => { this.switchRpcSettingsFormGroup.get('requestPersistent').valueChanges.subscribe(() => {
@ -158,23 +150,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit,
this.updateModel(); this.updateModel();
}); });
this.updateValidators(false); this.updateValidators(false);
this.filteredKeys = this.switchRpcSettingsFormGroup.get('valueKey').valueChanges
.pipe(
map(value => value ? value : ''),
mergeMap(name => this.fetchKeys(name) )
);
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'targetDeviceAliasId') {
this.clearKeysCache();
}
}
}
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -201,14 +176,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit,
this.updateValidators(false); this.updateValidators(false);
} }
clearKey() {
this.switchRpcSettingsFormGroup.get('valueKey').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.keyInput.nativeElement.blur();
this.keyInput.nativeElement.focus();
}, 0);
}
public validate(c: FormControl) { public validate(c: FormControl) {
return this.switchRpcSettingsFormGroup.valid ? null : { return this.switchRpcSettingsFormGroup.valid ? null : {
switchRpcSettings: { switchRpcSettings: {
@ -249,60 +216,4 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit,
this.switchRpcSettingsFormGroup.get('parseValueFunction').updateValueAndValidity({emitEvent: false}); this.switchRpcSettingsFormGroup.get('parseValueFunction').updateValueAndValidity({emitEvent: false});
this.switchRpcSettingsFormGroup.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); this.switchRpcSettingsFormGroup.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false});
} }
private clearKeysCache(): void {
this.latestKeySearchResult = null;
this.keysFetchObservable$ = null;
}
private fetchKeys(searchText?: string): Observable<Array<string>> {
if (this.keySearchText !== searchText || this.latestKeySearchResult === null) {
this.keySearchText = searchText;
const dataKeyFilter = this.createKeyFilter(this.keySearchText);
return this.getKeys().pipe(
map(name => name.filter(dataKeyFilter)),
tap(res => this.latestKeySearchResult = res)
);
}
return of(this.latestKeySearchResult);
}
private getKeys() {
if (this.keysFetchObservable$ === null) {
let fetchObservable: Observable<Array<DataKey>>;
if (this.targetDeviceAliasId) {
const retrieveValueMethod: RpcRetrieveValueMethod = this.switchRpcSettingsFormGroup.get('retrieveValueMethod').value;
const dataKeyTypes = retrieveValueMethod === 'attribute' ? [DataKeyType.attribute] : [DataKeyType.timeseries];
fetchObservable = this.fetchEntityKeys(this.targetDeviceAliasId, dataKeyTypes);
} else {
fetchObservable = of([]);
}
this.keysFetchObservable$ = fetchObservable.pipe(
map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)),
publishReplay(1),
refCount()
);
}
return this.keysFetchObservable$;
}
private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {
return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => {
return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
dataKeyTypes,
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
);
}),
catchError(() => of([] as Array<DataKey>))
);
}
private createKeyFilter(query: string): (key: string) => boolean {
const lowercaseQuery = query.toLowerCase();
return key => key.toLowerCase().startsWith(lowercaseQuery);
}
} }

51
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html

@ -0,0 +1,51 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="updateDeviceAttributeWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.common-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.widget-title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.button-label</mat-label>
<input matInput formControlName="buttonText">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.device-attribute-scope</mat-label>
<mat-select formControlName="entityAttributeType">
<mat-option [value]="'SERVER_SCOPE'">
{{ 'widgets.rpc.server-attribute' | translate }}
</mat-option>
<mat-option [value]="'SHARED_SCOPE'">
{{ 'widgets.rpc.shared-attribute' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-json-content
label="{{ 'widgets.rpc.device-attribute-parameters' | translate }}"
[contentType]="contentTypes.JSON"
validateOnChange
formControlName="entityParameters">
</tb-json-content>
</fieldset>
<tb-rpc-button-style
formControlName="styleButton">
</tb-rpc-button-style>
</section>

68
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts

@ -0,0 +1,68 @@
///
/// Copyright © 2016-2022 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ContentType } from '@shared/models/constants';
@Component({
selector: 'tb-update-device-attribute-widget-settings',
templateUrl: './update-device-attribute-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class UpdateDeviceAttributeWidgetSettingsComponent extends WidgetSettingsComponent {
updateDeviceAttributeWidgetSettingsForm: FormGroup;
contentTypes = ContentType;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.updateDeviceAttributeWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
title: '',
buttonText: 'Update device attribute',
entityAttributeType: 'SERVER_SCOPE',
entityParameters: '{}',
styleButton: {
isRaised: true,
isPrimary: false,
bgColor: null,
textColor: null
}
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.updateDeviceAttributeWidgetSettingsForm = this.fb.group({
title: [settings.title, []],
buttonText: [settings.buttonText, []],
entityAttributeType: [settings.entityAttributeType, []],
entityParameters: [settings.entityParameters, []],
styleButton: [settings.styleButton, []]
});
}
}

50
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -119,6 +119,28 @@ import {
import { import {
PersistentTableWidgetSettingsComponent PersistentTableWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/persistent-table-widget-settings.component'; } from '@home/components/widget/lib/settings/control/persistent-table-widget-settings.component';
import { RpcButtonStyleComponent } from '@home/components/widget/lib/settings/control/rpc-button-style.component';
import {
UpdateDeviceAttributeWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component';
import {
SendRpcWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/send-rpc-widget-settings.component';
import {
DeviceKeyAutocompleteComponent
} from '@home/components/widget/lib/settings/control/device-key-autocomplete.component';
import {
LedIndicatorWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/led-indicator-widget-settings.component';
import {
KnobControlWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/knob-control-widget-settings.component';
import {
RpcTerminalWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component';
import {
RpcShellWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/rpc-shell-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -159,11 +181,19 @@ import {
FlotPieKeySettingsComponent, FlotPieKeySettingsComponent,
ChartWidgetSettingsComponent, ChartWidgetSettingsComponent,
DoughnutChartWidgetSettingsComponent, DoughnutChartWidgetSettingsComponent,
DeviceKeyAutocompleteComponent,
SwitchRpcSettingsComponent, SwitchRpcSettingsComponent,
RoundSwitchWidgetSettingsComponent, RoundSwitchWidgetSettingsComponent,
SwitchControlWidgetSettingsComponent, SwitchControlWidgetSettingsComponent,
SlideToggleWidgetSettingsComponent, SlideToggleWidgetSettingsComponent,
PersistentTableWidgetSettingsComponent PersistentTableWidgetSettingsComponent,
RpcButtonStyleComponent,
UpdateDeviceAttributeWidgetSettingsComponent,
SendRpcWidgetSettingsComponent,
LedIndicatorWidgetSettingsComponent,
KnobControlWidgetSettingsComponent,
RpcTerminalWidgetSettingsComponent,
RpcShellWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -208,11 +238,19 @@ import {
FlotPieKeySettingsComponent, FlotPieKeySettingsComponent,
ChartWidgetSettingsComponent, ChartWidgetSettingsComponent,
DoughnutChartWidgetSettingsComponent, DoughnutChartWidgetSettingsComponent,
DeviceKeyAutocompleteComponent,
SwitchRpcSettingsComponent, SwitchRpcSettingsComponent,
RoundSwitchWidgetSettingsComponent, RoundSwitchWidgetSettingsComponent,
SwitchControlWidgetSettingsComponent, SwitchControlWidgetSettingsComponent,
SlideToggleWidgetSettingsComponent, SlideToggleWidgetSettingsComponent,
PersistentTableWidgetSettingsComponent PersistentTableWidgetSettingsComponent,
RpcButtonStyleComponent,
UpdateDeviceAttributeWidgetSettingsComponent,
SendRpcWidgetSettingsComponent,
LedIndicatorWidgetSettingsComponent,
KnobControlWidgetSettingsComponent,
RpcTerminalWidgetSettingsComponent,
RpcShellWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -249,5 +287,11 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-round-switch-widget-settings': RoundSwitchWidgetSettingsComponent, 'tb-round-switch-widget-settings': RoundSwitchWidgetSettingsComponent,
'tb-switch-control-widget-settings': SwitchControlWidgetSettingsComponent, 'tb-switch-control-widget-settings': SwitchControlWidgetSettingsComponent,
'tb-slide-toggle-widget-settings': SlideToggleWidgetSettingsComponent, 'tb-slide-toggle-widget-settings': SlideToggleWidgetSettingsComponent,
'tb-persistent-table-widget-settings': PersistentTableWidgetSettingsComponent 'tb-persistent-table-widget-settings': PersistentTableWidgetSettingsComponent,
'tb-update-device-attribute-widget-settings': UpdateDeviceAttributeWidgetSettingsComponent,
'tb-send-rpc-widget-settings': SendRpcWidgetSettingsComponent,
'tb-led-indicator-widget-settings': LedIndicatorWidgetSettingsComponent,
'tb-knob-control-widget-settings': KnobControlWidgetSettingsComponent,
'tb-rpc-terminal-widget-settings': RpcTerminalWidgetSettingsComponent,
'tb-rpc-shell-widget-settings': RpcShellWidgetSettingsComponent
}; };

2
ui-ngx/src/app/shared/components/json-content.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}" <div class="tb-json-content" style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar"> <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar">

57
ui-ngx/src/app/shared/components/json-content.component.scss

@ -13,44 +13,47 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { .tb-json-content {
position: relative; position: relative;
.fill-height { .fill-height {
height: 100%; height: 100%;
} }
}
.tb-json-content-toolbar { &:not(.tb-fullscreen) {
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { padding-bottom: 15px;
align-items: center;
vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
&:not(:last-child) {
margin-right: 4px;
}
} }
}
.tb-json-content-panel { .tb-json-content-toolbar {
height: 100%; button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
margin-left: 15px; align-items: center;
border: 1px solid #c0c0c0; vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
&:not(:last-child) {
margin-right: 4px;
}
}
}
#tb-json-input { .tb-json-content-panel {
width: 100%;
min-width: 200px;
height: 100%; height: 100%;
border: 1px solid #c0c0c0;
#tb-json-input {
width: 100%;
min-width: 200px;
height: 100%;
&:not(.fill-height) { &:not(.fill-height) {
min-height: 200px; min-height: 200px;
}
} }
} }
} }

22
ui-ngx/src/app/shared/components/json-content.component.ts

@ -15,6 +15,7 @@
/// ///
import { import {
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
forwardRef, forwardRef,
@ -23,7 +24,7 @@ import {
OnDestroy, OnDestroy,
OnInit, OnInit,
SimpleChanges, SimpleChanges,
ViewChild ViewChild, ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { Ace } from 'ace-builds'; import { Ace } from 'ace-builds';
@ -53,7 +54,8 @@ import { beautifyJs } from '@shared/models/beautify.models';
useExisting: forwardRef(() => JsonContentComponent), useExisting: forwardRef(() => JsonContentComponent),
multi: true, multi: true,
} }
] ],
encapsulation: ViewEncapsulation.None
}) })
export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy { export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy {
@ -116,7 +118,8 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
constructor(public elementRef: ElementRef, constructor(public elementRef: ElementRef,
protected store: Store<AppState>, protected store: Store<AppState>,
private raf: RafService) { private raf: RafService,
private cd: ChangeDetectorRef) {
} }
ngOnInit(): void { ngOnInit(): void {
@ -151,9 +154,12 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
this.updateView(); this.updateView();
} }
}); });
this.jsonEditor.on('blur', () => { if (this.validateContent) {
this.contentValid = !this.validateContent || this.doValidate(true); this.jsonEditor.on('blur', () => {
}); this.contentValid = this.doValidate(true);
this.cd.markForCheck();
});
}
this.editorResize$ = new ResizeObserver(() => { this.editorResize$ = new ResizeObserver(() => {
this.onAceEditorResize(); this.onAceEditorResize();
}); });
@ -225,12 +231,13 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
this.propagateChange(this.contentBody); this.propagateChange(this.contentBody);
this.contentValid = this.doValidate(true); this.contentValid = this.doValidate(true);
this.propagateChange(this.contentBody); this.propagateChange(this.contentBody);
this.cd.markForCheck();
} }
} }
private doValidate(showErrorToast = false): boolean { private doValidate(showErrorToast = false): boolean {
try { try {
if (this.validateContent && this.contentType === ContentType.JSON) { if (this.contentType === ContentType.JSON) {
JSON.parse(this.contentBody); JSON.parse(this.contentBody);
} }
return true; return true;
@ -283,6 +290,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
this.contentBody = editorValue; this.contentBody = editorValue;
this.contentValid = !this.validateOnChange || this.doValidate(); this.contentValid = !this.validateOnChange || this.doValidate();
this.propagateChange(this.contentBody); this.propagateChange(this.contentBody);
this.cd.markForCheck();
} }
} }

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

@ -3628,7 +3628,7 @@
"set-value-method": "RPC set value method", "set-value-method": "RPC set value method",
"convert-value-function": "Convert value function", "convert-value-function": "Convert value function",
"rpc-settings": "RPC settings", "rpc-settings": "RPC settings",
"request-timeout": "RPC request timeout", "request-timeout": "RPC request timeout (ms)",
"persistent-rpc-settings": "Persistent RPC settings", "persistent-rpc-settings": "Persistent RPC settings",
"request-persistent": "RPC request persistent", "request-persistent": "RPC request persistent",
"persistent-polling-interval": "Polling interval (ms) to get persistent RPC command response", "persistent-polling-interval": "Polling interval (ms) to get persistent RPC command response",
@ -3642,7 +3642,34 @@
"slider-color": "Slider color", "slider-color": "Slider color",
"slider-color-primary": "Primary", "slider-color-primary": "Primary",
"slider-color-accent": "Accent", "slider-color-accent": "Accent",
"slider-color-warn": "Warn" "slider-color-warn": "Warn",
"button-style": "Button style",
"button-raised": "Raised button",
"button-primary": "Primary color",
"button-background-color": "Button background color",
"button-text-color": "Button text color",
"widget-title": "Widget title",
"button-label": "Button label",
"device-attribute-scope": "Device attribute scope",
"server-attribute": "Server attribute",
"shared-attribute": "Shared attribute",
"device-attribute-parameters": "Device attribute parameters",
"is-one-way-command": "Is one way command",
"rpc-method": "RPC method",
"rpc-method-params": "RPC method params",
"show-rpc-error": "Show RPC command execution error",
"led-title": "LED title",
"led-color": "LED color",
"check-status-settings": "Check status settings",
"perform-rpc-status-check": "Perform RPC device status check",
"retrieve-led-status-value-method": "Retrieve led status value using method",
"led-status-value-attribute": "Device attribute containing led status value",
"led-status-value-timeseries": "Device timeseries containing led status value",
"check-status-method": "RPC check device status method",
"parse-led-status-value-function": "Parse led status value function",
"knob-title": "Knob title",
"min-value": "Minimum value",
"max-value": "Maximum value"
}, },
"maps": { "maps": {
"select-entity": "Select entity", "select-entity": "Select entity",

Loading…
Cancel
Save