diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts
index 762e56f5d6..c4bbdca1db 100644
--- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts
@@ -39,6 +39,7 @@ import { DialogService } from '@core/services/dialog.service';
import { deepClone, isUndefined } from '@core/utils';
import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
+import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
export interface FiltersDialogData {
filters: Filters;
@@ -77,6 +78,7 @@ export class FiltersDialogComponent extends DialogComponent
,
private fb: UntypedFormBuilder,
private utils: UtilsService,
+ private dashboardUtils: DashboardUtilsService,
private translate: TranslateService,
private dialogs: DialogService,
private dialog: MatDialog) {
@@ -94,7 +96,7 @@ export class FiltersDialogComponent extends DialogComponent {
- const datasources = this.utils.validateDatasources(widget.config.datasources);
+ const datasources = this.dashboardUtils.validateAndUpdateDatasources(widget.config.datasources);
datasources.forEach((datasource) => {
if (datasource.type === DatasourceType.entity && datasource.filterId) {
widgetsTitleList = this.filterToWidgetsMap[datasource.filterId];
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
index 5d7439feae..9f8586b12e 100644
--- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
@@ -91,6 +91,9 @@ import {
import {
BarChartWithLabelsBasicConfigComponent
} from '@home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component';
+import {
+ SingSwitchBasicConfigComponent
+} from '@home/components/widget/config/basic/rpc/single-switch-basic-config.component';
@NgModule({
declarations: [
@@ -119,7 +122,8 @@ import {
LiquidLevelCardBasicConfigComponent,
DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent,
- BarChartWithLabelsBasicConfigComponent
+ BarChartWithLabelsBasicConfigComponent,
+ SingSwitchBasicConfigComponent
],
imports: [
CommonModule,
@@ -152,7 +156,8 @@ import {
LiquidLevelCardBasicConfigComponent,
DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent,
- BarChartWithLabelsBasicConfigComponent
+ BarChartWithLabelsBasicConfigComponent,
+ SingSwitchBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@@ -179,5 +184,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts
new file mode 100644
index 0000000000..ed95847654
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts
@@ -0,0 +1,231 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component } from '@angular/core';
+import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
+import { WidgetConfigComponentData } from '@home/models/widget-component.models';
+import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models';
+import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
+import { isUndefined } from '@core/utils';
+import {
+ singleSwitchDefaultSettings,
+ singleSwitchLayoutImages,
+ singleSwitchLayouts,
+ singleSwitchLayoutTranslations,
+ SingleSwitchWidgetSettings
+} from '@home/components/widget/lib/rpc/single-switch-widget.models';
+import { ValueType } from '@shared/models/constants';
+
+@Component({
+ selector: 'tb-single-switch-basic-config',
+ templateUrl: './single-switch-basic-config.component.html',
+ styleUrls: ['../basic-config.scss']
+})
+export class SingSwitchBasicConfigComponent extends BasicWidgetConfigComponent {
+
+ get targetDevice(): TargetDevice {
+ return this.singleSwitchWidgetConfigForm.get('targetDevice').value;
+ }
+
+ singleSwitchLayouts = singleSwitchLayouts;
+
+ singleSwitchLayoutTranslationMap = singleSwitchLayoutTranslations;
+ singleSwitchLayoutImageMap = singleSwitchLayoutImages;
+
+ valueType = ValueType;
+
+ singleSwitchWidgetConfigForm: UntypedFormGroup;
+
+ constructor(protected store: Store,
+ protected widgetConfigComponent: WidgetConfigComponent,
+ private fb: UntypedFormBuilder) {
+ super(store, widgetConfigComponent);
+ }
+
+ protected configForm(): UntypedFormGroup {
+ return this.singleSwitchWidgetConfigForm;
+ }
+
+ protected onConfigSet(configData: WidgetConfigComponentData) {
+ const settings: SingleSwitchWidgetSettings = {...singleSwitchDefaultSettings, ...(configData.config.settings || {})};
+ this.singleSwitchWidgetConfigForm = this.fb.group({
+ targetDevice: [configData.config.targetDevice, []],
+
+ initialState: [settings.initialState, []],
+ onUpdateState: [settings.onUpdateState, []],
+ offUpdateState: [settings.offUpdateState, []],
+
+ layout: [settings.layout, []],
+ autoScale: [settings.autoScale, []],
+
+ showLabel: [settings.showLabel, []],
+ label: [settings.label, []],
+ labelFont: [settings.labelFont, []],
+ labelColor: [settings.labelColor, []],
+
+ showIcon: [settings.showIcon, []],
+ iconSize: [settings.iconSize, [Validators.min(0)]],
+ iconSizeUnit: [settings.iconSizeUnit, []],
+ icon: [settings.icon, []],
+ iconColor: [settings.iconColor, []],
+
+ switchColorOn: [settings.switchColorOn, []],
+ switchColorOff: [settings.switchColorOff, []],
+ switchColorDisabled: [settings.switchColorDisabled, []],
+
+ tumblerColorOn: [settings.tumblerColorOn, []],
+ tumblerColorOff: [settings.tumblerColorOff, []],
+ tumblerColorDisabled: [settings.tumblerColorDisabled, []],
+
+ showOnLabel: [settings.showOnLabel, []],
+ onLabel: [settings.onLabel, []],
+ onLabelFont: [settings.onLabelFont, []],
+ onLabelColor: [settings.onLabelColor, []],
+
+ showOffLabel: [settings.showOffLabel, []],
+ offLabel: [settings.offLabel, []],
+ offLabelFont: [settings.offLabelFont, []],
+ offLabelColor: [settings.offLabelColor, []],
+
+ background: [settings.background, []],
+
+ cardButtons: [this.getCardButtons(configData.config), []],
+ borderRadius: [configData.config.borderRadius, []],
+
+ actions: [configData.config.actions || {}, []]
+ });
+ }
+
+ protected prepareOutputConfig(config: any): WidgetConfigComponentData {
+ this.widgetConfig.config.targetDevice = config.targetDevice;
+
+ this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
+
+ this.widgetConfig.config.settings.initialState = config.initialState;
+ this.widgetConfig.config.settings.onUpdateState = config.onUpdateState;
+ this.widgetConfig.config.settings.offUpdateState = config.offUpdateState;
+
+ this.widgetConfig.config.settings.layout = config.layout;
+ this.widgetConfig.config.settings.autoScale = config.autoScale;
+
+ this.widgetConfig.config.settings.showLabel = config.showLabel;
+ this.widgetConfig.config.settings.label = config.label;
+ this.widgetConfig.config.settings.labelFont = config.labelFont;
+ this.widgetConfig.config.settings.labelColor = config.labelColor;
+
+ this.widgetConfig.config.settings.showIcon = config.showIcon;
+ this.widgetConfig.config.settings.iconSize = config.iconSize;
+ this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit;
+ this.widgetConfig.config.settings.icon = config.icon;
+ this.widgetConfig.config.settings.iconColor = config.iconColor;
+
+ this.widgetConfig.config.settings.switchColorOn = config.switchColorOn;
+ this.widgetConfig.config.settings.switchColorOff = config.switchColorOff;
+ this.widgetConfig.config.settings.switchColorDisabled = config.switchColorDisabled;
+
+ this.widgetConfig.config.settings.tumblerColorOn = config.tumblerColorOn;
+ this.widgetConfig.config.settings.tumblerColorOff = config.tumblerColorOff;
+ this.widgetConfig.config.settings.tumblerColorDisabled = config.tumblerColorDisabled;
+
+ this.widgetConfig.config.settings.showOnLabel = config.showOnLabel;
+ this.widgetConfig.config.settings.onLabel = config.onLabel;
+ this.widgetConfig.config.settings.onLabelFont = config.onLabelFont;
+ this.widgetConfig.config.settings.onLabelColor = config.onLabelColor;
+
+ this.widgetConfig.config.settings.showOffLabel = config.showOffLabel;
+ this.widgetConfig.config.settings.offLabel = config.offLabel;
+ this.widgetConfig.config.settings.offLabelFont = config.offLabelFont;
+ this.widgetConfig.config.settings.offLabelColor = config.offLabelColor;
+
+ this.widgetConfig.config.settings.background = config.background;
+
+ this.setCardButtons(config.cardButtons, this.widgetConfig.config);
+ this.widgetConfig.config.borderRadius = config.borderRadius;
+
+ this.widgetConfig.config.actions = config.actions;
+ return this.widgetConfig;
+ }
+
+ protected validatorTriggers(): string[] {
+ return ['showLabel', 'showIcon', 'showOnLabel', 'showOffLabel'];
+ }
+
+ protected updateValidators(emitEvent: boolean, trigger?: string) {
+ const showLabel: boolean = this.singleSwitchWidgetConfigForm.get('showLabel').value;
+ const showIcon: boolean = this.singleSwitchWidgetConfigForm.get('showIcon').value;
+ const showOnLabel: boolean = this.singleSwitchWidgetConfigForm.get('showOnLabel').value;
+ const showOffLabel: boolean = this.singleSwitchWidgetConfigForm.get('showOffLabel').value;
+
+ if (showLabel) {
+ this.singleSwitchWidgetConfigForm.get('label').enable();
+ this.singleSwitchWidgetConfigForm.get('labelFont').enable();
+ this.singleSwitchWidgetConfigForm.get('labelColor').enable();
+ } else {
+ this.singleSwitchWidgetConfigForm.get('label').disable();
+ this.singleSwitchWidgetConfigForm.get('labelFont').disable();
+ this.singleSwitchWidgetConfigForm.get('labelColor').disable();
+ }
+
+ if (showIcon) {
+ this.singleSwitchWidgetConfigForm.get('iconSize').enable();
+ this.singleSwitchWidgetConfigForm.get('iconSizeUnit').enable();
+ this.singleSwitchWidgetConfigForm.get('icon').enable();
+ this.singleSwitchWidgetConfigForm.get('iconColor').enable();
+ } else {
+ this.singleSwitchWidgetConfigForm.get('iconSize').disable();
+ this.singleSwitchWidgetConfigForm.get('iconSizeUnit').disable();
+ this.singleSwitchWidgetConfigForm.get('icon').disable();
+ this.singleSwitchWidgetConfigForm.get('iconColor').disable();
+ }
+
+ if (showOnLabel) {
+ this.singleSwitchWidgetConfigForm.get('onLabel').enable();
+ this.singleSwitchWidgetConfigForm.get('onLabelFont').enable();
+ this.singleSwitchWidgetConfigForm.get('onLabelColor').enable();
+ } else {
+ this.singleSwitchWidgetConfigForm.get('onLabel').disable();
+ this.singleSwitchWidgetConfigForm.get('onLabelFont').disable();
+ this.singleSwitchWidgetConfigForm.get('onLabelColor').disable();
+ }
+
+ if (showOffLabel) {
+ this.singleSwitchWidgetConfigForm.get('offLabel').enable();
+ this.singleSwitchWidgetConfigForm.get('offLabelFont').enable();
+ this.singleSwitchWidgetConfigForm.get('offLabelColor').enable();
+ } else {
+ this.singleSwitchWidgetConfigForm.get('offLabel').disable();
+ this.singleSwitchWidgetConfigForm.get('offLabelFont').disable();
+ this.singleSwitchWidgetConfigForm.get('offLabelColor').disable();
+ }
+
+ }
+
+ private getCardButtons(config: WidgetConfig): string[] {
+ const buttons: string[] = [];
+ if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
+ buttons.push('fullscreen');
+ }
+ return buttons;
+ }
+
+ private setCardButtons(buttons: string[], config: WidgetConfig) {
+ config.enableFullscreen = buttons.includes('fullscreen');
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html
new file mode 100644
index 0000000000..ad2728484c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html
@@ -0,0 +1,38 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts
new file mode 100644
index 0000000000..c1d09f50b6
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts
@@ -0,0 +1,166 @@
+///
+/// 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 {
+ ControlValueAccessor,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ UntypedFormBuilder,
+ UntypedFormControl,
+ UntypedFormGroup,
+ Validator,
+ Validators
+} from '@angular/forms';
+import { Component, forwardRef, Input, OnInit } from '@angular/core';
+import { UtilsService } from '@core/services/utils.service';
+import { TranslateService } from '@ngx-translate/core';
+import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
+import { TargetDevice, TargetDeviceType } from '@shared/models/widget.models';
+import { EntityType } from '@shared/models/entity-type.models';
+import { IAliasController } from '@core/api/widget-api.models';
+import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models';
+
+@Component({
+ selector: 'tb-target-device',
+ templateUrl: './target-device.component.html',
+ styleUrls: [],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => TargetDeviceComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => TargetDeviceComponent),
+ multi: true,
+ }
+ ]
+})
+export class TargetDeviceComponent implements ControlValueAccessor, OnInit, Validator {
+
+ public get aliasController(): IAliasController {
+ return this.widgetConfigComponent.aliasController;
+ }
+
+ public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks {
+ return this.widgetConfigComponent.widgetConfigCallbacks;
+ }
+
+ targetDeviceType = TargetDeviceType;
+
+ entityType = EntityType;
+
+ @Input()
+ disabled: boolean;
+
+ widgetEditMode = this.utils.widgetEditMode;
+
+ targetDeviceFormGroup: UntypedFormGroup;
+
+ private propagateChange = (_val: any) => {};
+
+ constructor(private fb: UntypedFormBuilder,
+ private utils: UtilsService,
+ public translate: TranslateService,
+ private widgetConfigComponent: WidgetConfigComponent) {
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ if (!this.targetDeviceFormGroup.valid) {
+ setTimeout(() => {
+ this.targetDeviceUpdated(this.targetDeviceFormGroup.value);
+ }, 0);
+ }
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.targetDeviceFormGroup.disable({emitEvent: false});
+ } else {
+ this.targetDeviceFormGroup.enable({emitEvent: false});
+ this.updateValidators();
+ }
+ }
+
+ ngOnInit() {
+ this.targetDeviceFormGroup = this.fb.group({
+ type: [null, !this.widgetEditMode ? [Validators.required] : []],
+ deviceId: [null, !this.widgetEditMode ? [Validators.required] : []],
+ entityAliasId: [null, !this.widgetEditMode ? [Validators.required] : []]
+ });
+ this.targetDeviceFormGroup.get('type').valueChanges.subscribe(() => {
+ this.updateValidators();
+ });
+ this.targetDeviceFormGroup.valueChanges.subscribe(
+ () => {
+ this.targetDeviceUpdated(this.targetDeviceFormGroup.value);
+ }
+ );
+ }
+
+ writeValue(targetDevice?: TargetDevice): void {
+ this.targetDeviceFormGroup.patchValue({
+ type: targetDevice?.type || TargetDeviceType.device,
+ deviceId: targetDevice?.deviceId,
+ entityAliasId: targetDevice?.entityAliasId
+ }, {emitEvent: false});
+ this.updateValidators();
+ if (targetDevice?.type === TargetDeviceType.entity && targetDevice?.entityAliasId) {
+ const entityAliases = this.aliasController.getEntityAliases();
+ if (!entityAliases[targetDevice.entityAliasId]) {
+ this.targetDeviceFormGroup.get('entityAliasId').patchValue(null, {emitEvent: false});
+ setTimeout(() => {
+ this.targetDeviceUpdated(this.targetDeviceFormGroup.value);
+ }, 0);
+ }
+ }
+ }
+
+ validate(c: UntypedFormControl) {
+ return (this.targetDeviceFormGroup.valid) ? null : {
+ targetDevice: {
+ valid: false,
+ },
+ };
+ }
+
+ private targetDeviceUpdated(targetDevice: TargetDevice) {
+ this.propagateChange(targetDevice);
+ }
+
+ private updateValidators() {
+ const type: TargetDeviceType = this.targetDeviceFormGroup.get('type').value;
+ if (!this.widgetEditMode && type) {
+ if (type === TargetDeviceType.device) {
+ this.targetDeviceFormGroup.get('deviceId').enable({emitEvent: false});
+ this.targetDeviceFormGroup.get('entityAliasId').disable({emitEvent: false});
+ } else {
+ this.targetDeviceFormGroup.get('deviceId').disable({emitEvent: false});
+ this.targetDeviceFormGroup.get('entityAliasId').enable({emitEvent: false});
+ }
+ } else {
+ this.targetDeviceFormGroup.get('deviceId').disable({emitEvent: false});
+ this.targetDeviceFormGroup.get('entityAliasId').disable({emitEvent: false});
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
index fa19f2b2b5..4470fc1327 100644
--- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
@@ -32,6 +32,7 @@ import { TimewindowConfigPanelComponent } from '@home/components/widget/config/t
import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module';
import { TimewindowStyleComponent } from '@home/components/widget/config/timewindow-style.component';
import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component';
+import { TargetDeviceComponent } from '@home/components/widget/config/target-device.component';
@NgModule({
declarations:
@@ -43,6 +44,7 @@ import { TimewindowStylePanelComponent } from '@home/components/widget/config/ti
DataKeyConfigComponent,
DatasourceComponent,
DatasourcesComponent,
+ TargetDeviceComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowStyleComponent,
@@ -64,6 +66,7 @@ import { TimewindowStylePanelComponent } from '@home/components/widget/config/ti
DataKeyConfigComponent,
DatasourceComponent,
DatasourcesComponent,
+ TargetDeviceComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowStyleComponent,
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
index 6373770e38..1132e8bf91 100644
--- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
@@ -27,6 +27,7 @@ import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode } from '@shared/mode
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils';
+import { IAliasController } from '@core/api/widget-api.models';
export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks;
@@ -58,6 +59,10 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
return this.widgetConfigValue;
}
+ get aliasController(): IAliasController {
+ return this.widgetConfigComponent.aliasController;
+ }
+
widgetConfigChangedEmitter = new EventEmitter();
widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable();
diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
index 206adcc588..6cef868e66 100644
--- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
@@ -57,7 +57,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcErrorText: string;
- rpcRejection: HttpErrorResponse;
+ rpcRejection: HttpErrorResponse | Error;
[key: string]: any;
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widget.models.ts
new file mode 100644
index 0000000000..f47e654fdb
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widget.models.ts
@@ -0,0 +1,626 @@
+///
+/// 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 {
+ AttributeData,
+ AttributeScope,
+ LatestTelemetry,
+ telemetryTypeTranslationsShort
+} from '@shared/models/telemetry/telemetry.models';
+import { WidgetContext } from '@home/models/widget-component.models';
+import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
+import { catchError, delay, map, share } from 'rxjs/operators';
+import { UtilsService } from '@core/services/utils.service';
+import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnInit, TemplateRef } from '@angular/core';
+import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models';
+import { ImagePipe } from '@shared/pipe/image.pipe';
+import { DomSanitizer } from '@angular/platform-browser';
+import {
+ RpcActionSettings,
+ RpcGetAttributeSettings,
+ RpcSetAttributeSettings,
+ RpcSettings,
+ RpcStateToParamsSettings,
+ RpcStateToParamsType,
+ RpcStateWidgetSettings,
+ RpcTelemetrySettings,
+ RpcUpdateStateAction
+} from '@shared/models/rpc-widget-settings.models';
+import {
+ RpcDataToStateSettings,
+ RpcDataToStateType,
+ RpcInitialStateAction,
+ RpcInitialStateSettings,
+ RpcStateBehaviourSettings,
+ RpcUpdateStateSettings
+} from '@app/shared/models/rpc-widget-settings.models';
+import { ValueType } from '@shared/models/constants';
+import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
+
+@Directive()
+// eslint-disable-next-line @angular-eslint/directive-class-suffix
+export abstract class BasicRpcStateWidgetComponent> implements OnInit, AfterViewInit {
+
+ @Input()
+ ctx: WidgetContext;
+
+ @Input()
+ widgetTitlePanel: TemplateRef;
+
+ settings: S;
+
+ behaviorApi: RpcStateBehaviorApi;
+ loading$: Observable;
+
+ backgroundStyle$: Observable;
+ overlayStyle: ComponentStyle = {};
+
+ value: V;
+
+ error = '';
+
+ protected constructor(protected imagePipe: ImagePipe,
+ protected sanitizer: DomSanitizer,
+ protected cd: ChangeDetectorRef) {
+ }
+
+ ngOnInit(): void {
+ this.ctx.$scope.rpcWidget = this;
+ this.settings = {...this.defaultSettings(), ...this.ctx.settings};
+ this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
+ this.overlayStyle = overlayStyle(this.settings.background.overlay);
+
+ const behaviourSettings: RpcStateBehaviourSettings = {
+ initialState: this.initialState(),
+ updateStateByValue: val => this.getUpdateStateSettingsForValue(val)
+ };
+
+ const callbacks: RpcStateCallbacks = {
+ onStateValue: val => this.setValue(val),
+ onError: err => this.onError(err),
+ validateStateValue: val => this.validateValue(val)
+ };
+
+ this.behaviorApi = new RpcStateBehaviorApi(this.defaultValue(), this.ctx,
+ behaviourSettings, callbacks, this.stateValueType());
+ this.loading$ = this.behaviorApi.loading$.pipe(share());
+ }
+
+ ngAfterViewInit(): void {
+ this.behaviorApi.initState();
+ }
+
+ public onInit() {
+ const borderRadius = this.ctx.$widgetElement.css('borderRadius');
+ this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
+ this.cd.detectChanges();
+ }
+
+ public clearError() {
+ this.error = '';
+ }
+
+ public updateValue() {
+ this.behaviorApi.updateState(this.value);
+ }
+
+ private onError(error: string) {
+ this.error = error;
+ this.ctx.detectChanges();
+ }
+
+ protected abstract defaultSettings(): S;
+
+ protected abstract initialState(): RpcInitialStateSettings;
+
+ protected abstract getUpdateStateSettingsForValue(value: V): RpcUpdateStateSettings;
+
+ protected stateValueType(): ValueType {
+ return ValueType.BOOLEAN;
+ }
+
+ protected defaultValue(): V {
+ return null;
+ }
+
+ protected setValue(value: V) {
+ this.value = value;
+ }
+
+ protected validateValue(value: any): V {
+ return value;
+ }
+
+}
+
+export abstract class RpcHasLoading {
+
+ public get loading$() {
+ return this.loadingSubject.asObservable();
+ }
+
+ protected loadingSubject = new BehaviorSubject(false);
+}
+
+export interface RpcStateCallbacks {
+ onStateValue: (value: V) => void;
+ validateStateValue: (value: any) => V;
+ onError: (error: string) => void;
+}
+
+export class RpcStateBehaviorApi extends RpcHasLoading {
+
+ private readonly initialStateGetter: RpcInitialStateGetter;
+ private readonly stateUpdatersMap: Map>;
+
+ constructor(private state: V,
+ private ctx: WidgetContext,
+ private settings: RpcStateBehaviourSettings,
+ private callbacks: RpcStateCallbacks,
+ stateValueType: ValueType) {
+ super();
+ this.initialStateGetter = RpcInitialStateGetter.fromSettings(ctx, settings.initialState, stateValueType, callbacks);
+ this.stateUpdatersMap = new Map>();
+ }
+
+ initState() {
+ if (this.ctx.defaultSubscription.targetEntityId || this.ctx.defaultSubscription.rpcEnabled) {
+ this.loadingSubject.next(true);
+ this.initialStateGetter.initState().subscribe(
+ {
+ next: (value) => {
+ this.state = value;
+ this.loadingSubject.next(false);
+ this.callbacks.onStateValue(value);
+ this.ctx.detectChanges();
+ },
+ error: (err: any) => {
+ this.loadingSubject.next(false);
+ const message = parseError(this.ctx, err);
+ this.callbacks.onError(message);
+ }
+ }
+ );
+ } else {
+ this.callbacks.onError(this.ctx.translate.instant('widgets.rpc-state.error.target-entity-is-not-set'));
+ }
+ }
+
+ updateState(value: V) {
+ this.callbacks.onError(null);
+ let updater: RpcStateUpdater;
+ const updateStateSettings = this.settings.updateStateByValue(value);
+ if (updateStateSettings) {
+ updater = this.stateUpdatersMap.get(updateStateSettings);
+ if (!updater) {
+ updater = RpcStateUpdater.fromSettings(this.ctx, updateStateSettings);
+ this.stateUpdatersMap.set(updateStateSettings, updater);
+ }
+ }
+ if (updater) {
+ this.loadingSubject.next(true);
+ updater.updateState(value).subscribe(
+ {
+ next: () => {
+ this.state = value;
+ this.loadingSubject.next(false);
+ this.ctx.detectChanges();
+ },
+ error: (err: any) => {
+ this.loadingSubject.next(false);
+ const message = parseError(this.ctx, err);
+ this.callbacks.onStateValue(this.state);
+ this.callbacks.onError(message);
+ this.ctx.detectChanges();
+ }
+ });
+ }
+ }
+
+}
+
+type RpcDataToStateFunction = (data: any) => V;
+
+export class RpcDataToStateConverter {
+
+ private readonly dataToStateFunction: RpcDataToStateFunction;
+ private readonly compareToValue: any;
+
+ constructor(private settings: RpcDataToStateSettings,
+ private stateValueType: ValueType,
+ private callbacks: RpcStateCallbacks) {
+ this.compareToValue = settings.compareToValue;
+ switch (settings.type) {
+ case RpcDataToStateType.FUNCTION:
+ try {
+ this.dataToStateFunction = new Function('data', settings.dataToStateFunction) as RpcDataToStateFunction;
+ } catch (e) {
+ this.dataToStateFunction = (data) => data;
+ }
+ break;
+ case RpcDataToStateType.NONE:
+ break;
+ }
+ }
+
+ dataToStateValue(data: any): V {
+ let result: V;
+ switch (this.settings.type) {
+ case RpcDataToStateType.FUNCTION:
+ result = data;
+ try {
+ result = this.dataToStateFunction(!!data ? JSON.parse(data) : data);
+ } catch (e) {}
+ break;
+ case RpcDataToStateType.NONE:
+ result = data;
+ break;
+ }
+ if (this.stateValueType === ValueType.BOOLEAN) {
+ result = (result === this.compareToValue) as any;
+ }
+ result = this.callbacks.validateStateValue(result);
+ return result;
+ }
+}
+
+export abstract class RpcAction {
+
+ protected constructor(protected ctx: WidgetContext,
+ protected settings: RpcActionSettings) {}
+
+ handleError(err: any): Error {
+ const reason = parseError(this.ctx, err);
+ let errorMessage = this.ctx.translate.instant('widgets.rpc-state.error.failed-to-perform-action',
+ {actionLabel: this.settings.actionLabel});
+ if (reason) {
+ errorMessage += '
' + reason;
+ }
+ return new Error(errorMessage);
+ }
+}
+
+export abstract class RpcInitialStateGetter extends RpcAction {
+
+ static fromSettings(ctx: WidgetContext,
+ settings: RpcInitialStateSettings,
+ stateValueType: ValueType,
+ callbacks: RpcStateCallbacks): RpcInitialStateGetter {
+ switch (settings.action) {
+ case RpcInitialStateAction.DO_NOTHING:
+ return new RpcDefaultStateGetter(ctx, settings, stateValueType, callbacks);
+ case RpcInitialStateAction.EXECUTE_RPC:
+ return new ExecuteRpcStateGetter(ctx, settings, stateValueType, callbacks);
+ case RpcInitialStateAction.GET_ATTRIBUTE:
+ return new RpcAttributeStateGetter(ctx, settings, stateValueType, callbacks);
+ case RpcInitialStateAction.GET_TIME_SERIES:
+ return new RpcTimeSeriesStateGetter(ctx, settings, stateValueType, callbacks);
+ }
+ }
+
+ private readonly isSimulated: boolean;
+ private readonly dataConverter: RpcDataToStateConverter;
+
+ protected constructor(protected ctx: WidgetContext,
+ protected settings: RpcInitialStateSettings,
+ protected stateValueType: ValueType,
+ protected callbacks: RpcStateCallbacks) {
+ super(ctx, settings);
+ this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
+ if (this.settings.action !== RpcInitialStateAction.DO_NOTHING) {
+ this.dataConverter = new RpcDataToStateConverter(settings.dataToState, stateValueType, this.callbacks);
+ }
+ }
+
+ initState(): Observable {
+ const stateObservable: Observable = this.isSimulated ? of(null).pipe(delay(500)) : this.doGetState();
+ return stateObservable.pipe(
+ map((data) => {
+ if (this.dataConverter) {
+ return this.dataConverter.dataToStateValue(data);
+ } else {
+ return data;
+ }
+ }),
+ catchError(err => {
+ throw this.handleError(err);
+ })
+ );
+ }
+
+ protected abstract doGetState(): Observable;
+}
+
+type RpcStateToParamsFunction = (state: V) => any;
+
+export class RpcStateToParamsConverter {
+
+ private readonly constantValue: any;
+ private readonly stateToParamsFunction: RpcStateToParamsFunction;
+
+ constructor(protected settings: RpcStateToParamsSettings) {
+ switch (settings.type) {
+ case RpcStateToParamsType.CONSTANT:
+ this.constantValue = this.settings.constantValue;
+ break;
+ case RpcStateToParamsType.FUNCTION:
+ try {
+ this.stateToParamsFunction = new Function('value', settings.stateToParamsFunction) as RpcStateToParamsFunction;
+ } catch (e) {
+ this.stateToParamsFunction = (data) => data;
+ }
+ break;
+ case RpcStateToParamsType.NONE:
+ break;
+ }
+ }
+
+ stateToParams(state: V): any {
+ switch (this.settings.type) {
+ case RpcStateToParamsType.CONSTANT:
+ return this.constantValue;
+ case RpcStateToParamsType.FUNCTION:
+ let result = state;
+ try {
+ result = this.stateToParamsFunction(state);
+ } catch (e) {}
+ return result;
+ case RpcStateToParamsType.NONE:
+ return null;
+ }
+ }
+}
+
+export abstract class RpcStateUpdater extends RpcAction {
+
+ static fromSettings(ctx: WidgetContext,
+ settings: RpcUpdateStateSettings): RpcStateUpdater {
+ switch (settings.action) {
+ case RpcUpdateStateAction.EXECUTE_RPC:
+ return new ExecuteRpcStateUpdater(ctx, settings);
+ case RpcUpdateStateAction.SET_ATTRIBUTE:
+ return new RpcAttributeStateUpdater(ctx, settings);
+ case RpcUpdateStateAction.ADD_TIME_SERIES:
+ return new RpcTimeSeriesStateUpdater(ctx, settings);
+ }
+ }
+
+ private readonly isSimulated: boolean;
+ private readonly paramsConverter: RpcStateToParamsConverter;
+
+ protected constructor(protected ctx: WidgetContext,
+ protected settings: RpcUpdateStateSettings) {
+ super(ctx, settings);
+ this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
+ this.paramsConverter = new RpcStateToParamsConverter(settings.stateToParams);
+ }
+
+ updateState(state: V): Observable {
+ if (this.isSimulated) {
+ return of(null).pipe(delay(500));
+ } else {
+ return this.doUpdateState(this.paramsConverter.stateToParams(state)).pipe(
+ catchError(err => {
+ throw this.handleError(err);
+ })
+ );
+ }
+ }
+
+ protected abstract doUpdateState(params: any): Observable;
+}
+
+export class RpcDefaultStateGetter extends RpcInitialStateGetter {
+
+ private readonly defaultValue: V;
+
+ constructor(protected ctx: WidgetContext,
+ protected settings: RpcInitialStateSettings,
+ protected stateValueType: ValueType,
+ protected callbacks: RpcStateCallbacks) {
+ super(ctx, settings, stateValueType, callbacks);
+ this.defaultValue = settings.defaultValue;
+ }
+
+ protected doGetState(): Observable {
+ return of(this.defaultValue);
+ }
+}
+
+export class ExecuteRpcStateGetter extends RpcInitialStateGetter {
+
+ private readonly executeRpcSettings: RpcSettings;
+
+ constructor(protected ctx: WidgetContext,
+ protected settings: RpcInitialStateSettings,
+ protected stateValueType: ValueType,
+ protected callbacks: RpcStateCallbacks) {
+ super(ctx, settings, stateValueType, callbacks);
+ this.executeRpcSettings = settings.executeRpc;
+ }
+
+ protected doGetState(): Observable {
+ return this.ctx.controlApi.sendTwoWayCommand(this.executeRpcSettings.method, null,
+ this.executeRpcSettings.requestTimeout,
+ this.executeRpcSettings.requestPersistent,
+ this.executeRpcSettings.persistentPollingInterval).pipe(
+ catchError((err) => {
+ throw handleRpcError(this.ctx, err);
+ })
+ );
+ }
+}
+
+export class RpcAttributeStateGetter extends RpcInitialStateGetter {
+
+ private readonly getAttributeSettings: RpcGetAttributeSettings;
+
+ constructor(protected ctx: WidgetContext,
+ protected settings: RpcInitialStateSettings,
+ protected stateValueType: ValueType,
+ protected callbacks: RpcStateCallbacks) {
+ super(ctx, settings, stateValueType, callbacks);
+ this.getAttributeSettings = settings.getAttribute;
+ }
+
+ protected doGetState(): Observable {
+ if (this.ctx.defaultSubscription.targetEntityId) {
+ const err = validateAttributeScope(this.ctx, this.getAttributeSettings.scope);
+ if (err) {
+ return throwError(() => err);
+ }
+ return this.ctx.attributeService.getEntityAttributes(this.ctx.defaultSubscription.targetEntityId,
+ this.getAttributeSettings.scope, [this.getAttributeSettings.key], {ignoreLoading: true, ignoreErrors: true})
+ .pipe(
+ map((data) => data.find(attr => attr.key === this.getAttributeSettings.key)?.value)
+ );
+ } else {
+ return of(null);
+ }
+ }
+
+}
+
+export class RpcTimeSeriesStateGetter extends RpcInitialStateGetter {
+
+ private readonly getTimeSeriesSettings: RpcTelemetrySettings;
+
+ constructor(protected ctx: WidgetContext,
+ protected settings: RpcInitialStateSettings,
+ protected stateValueType: ValueType,
+ protected callbacks: RpcStateCallbacks) {
+ super(ctx, settings, stateValueType, callbacks);
+ this.getTimeSeriesSettings = settings.getTimeSeries;
+ }
+
+ protected doGetState(): Observable {
+ if (this.ctx.defaultSubscription.targetEntityId) {
+ return this.ctx.attributeService.getEntityTimeseriesLatest(this.ctx.defaultSubscription.targetEntityId,
+ [this.getTimeSeriesSettings.key], true, {ignoreLoading: true, ignoreErrors: true})
+ .pipe(
+ map((data) => {
+ let value: any = null;
+ if (data[this.getTimeSeriesSettings.key]) {
+ const dataSet = data[this.getTimeSeriesSettings.key];
+ if (dataSet.length) {
+ value = dataSet[0].value;
+ }
+ }
+ return value;
+ })
+ );
+ } else {
+ return of(null);
+ }
+ }
+
+}
+
+export class ExecuteRpcStateUpdater extends RpcStateUpdater {
+
+ private readonly executeRpcSettings: RpcSettings;
+
+ constructor(protected ctx: WidgetContext,
+ protected settings: RpcUpdateStateSettings) {
+ super(ctx, settings);
+ this.executeRpcSettings = settings.executeRpc;
+ }
+
+ protected doUpdateState(params: any): Observable {
+ return this.ctx.controlApi.sendOneWayCommand(this.executeRpcSettings.method, params,
+ this.executeRpcSettings.requestTimeout,
+ this.executeRpcSettings.requestPersistent,
+ this.executeRpcSettings.persistentPollingInterval).pipe(
+ catchError((err) => {
+ throw handleRpcError(this.ctx, err);
+ })
+ );
+ }
+}
+
+export class RpcAttributeStateUpdater extends RpcStateUpdater {
+
+ private readonly setAttributeSettings: RpcSetAttributeSettings;
+
+ constructor(protected ctx: WidgetContext,
+ protected settings: RpcUpdateStateSettings) {
+ super(ctx, settings);
+ this.setAttributeSettings = settings.setAttribute;
+ }
+
+ protected doUpdateState(params: any): Observable {
+ if (this.ctx.defaultSubscription.targetEntityId) {
+ const err = validateAttributeScope(this.ctx, this.setAttributeSettings.scope);
+ if (err) {
+ return throwError(() => err);
+ }
+ const attributes: Array = [{key: this.setAttributeSettings.key, value: params}];
+ return this.ctx.attributeService.saveEntityAttributes(this.ctx.defaultSubscription.targetEntityId,
+ this.setAttributeSettings.scope, attributes, {ignoreLoading: true, ignoreErrors: true});
+ } else {
+ return of(null);
+ }
+ }
+
+}
+
+export class RpcTimeSeriesStateUpdater extends RpcStateUpdater {
+
+ private readonly putTimeSeriesSettings: RpcTelemetrySettings;
+
+ constructor(protected ctx: WidgetContext,
+ protected settings: RpcUpdateStateSettings) {
+ super(ctx, settings);
+ this.putTimeSeriesSettings = settings.putTimeSeries;
+ }
+
+ protected doUpdateState(params: any): Observable {
+ if (this.ctx.defaultSubscription.targetEntityId) {
+ const timeSeries: Array = [{key: this.putTimeSeriesSettings.key, value: params}];
+ return this.ctx.attributeService.saveEntityTimeseries(this.ctx.defaultSubscription.targetEntityId,
+ LatestTelemetry.LATEST_TELEMETRY, timeSeries, {ignoreLoading: true, ignoreErrors: true});
+ } else {
+ return of(null);
+ }
+ }
+
+}
+
+const parseError = (ctx: WidgetContext, err: any): string =>
+ ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
+
+const handleRpcError = (ctx: WidgetContext, err: any): Error => {
+ let reason: string;
+ if (ctx.defaultSubscription.rpcErrorText) {
+ reason = ctx.defaultSubscription.rpcErrorText;
+ } else {
+ reason = parseError(ctx, err);
+ }
+ return new Error(reason);
+};
+
+const validateAttributeScope = (ctx: WidgetContext, scope?: AttributeScope): Error | null => {
+ if (ctx.defaultSubscription.targetEntityId.entityType !== EntityType.DEVICE && scope && scope !== AttributeScope.SERVER_SCOPE) {
+ const scopeStr = ctx.translate.instant(telemetryTypeTranslationsShort.get(scope));
+ const entityType =
+ ctx.translate.instant(entityTypeTranslations.get(ctx.defaultSubscription.targetEntityId.entityType).type);
+ const errorMessage =
+ ctx.translate.instant('widgets.rpc-state.error.invalid-attribute-scope', {scope: scopeStr, entityType});
+ return new Error(errorMessage);
+ } else {
+ return null;
+ }
+};
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html
new file mode 100644
index 0000000000..ee27f83458
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
{{ icon }}
+
{{ label$ | async }}
+
+
+
{{ offLabel }}
+
+
+
{{ onLabel }}
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss
new file mode 100644
index 0000000000..884bea0e43
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss
@@ -0,0 +1,204 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+$tumblerColorOn: var(--tb-single-switch-tumbler-color-on, #fff);
+$tumblerColorOff: var(--tb-single-switch-tumbler-color-off, #fff);
+$tumblerColorDisabled: var(--tb-single-switch-tumbler-color-disabled, #fff);
+
+$switchColorOn: var(--tb-single-switch-color-on, #5469FF);
+$switchColorOff: var(--tb-single-switch-color-off, rgba(84, 105, 255, 0.30));
+$switchColorDisabled: var(--tb-single-switch-color-disabled, #D5D7E5);
+
+.tb-single-switch-panel {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 18px 24px;
+ > div:not(.tb-single-switch-overlay), > tb-icon {
+ z-index: 1;
+ }
+ .tb-single-switch-overlay {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ bottom: 12px;
+ right: 12px;
+ }
+ .tb-single-switch-progress {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+ .tb-single-switch-error-container {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ .tb-single-switch-error-panel {
+ display: flex;
+ padding: 4px 4px 4px 12px;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+ border-radius: 4px;
+ background-color: #fff2f3;
+ box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2);
+ .tb-single-switch-error-text {
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 16px;
+ color: rgba(209, 39, 48, 1);
+ }
+ .tb-single-switch-error-clear {
+ color: rgba(209, 39, 48, 1);
+ }
+ }
+ }
+ > div.tb-single-switch-title-panel {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ right: 12px;
+ z-index: 2;
+ }
+ .tb-single-switch-content {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ &.right {
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ &.no-label.right {
+ justify-content: flex-end;
+ }
+ &.left {
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ }
+ &.centered {
+ flex-direction: row;
+ justify-content: center;
+ }
+ .tb-single-switch-label-row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ .tb-single-switch-label {
+ white-space: nowrap;
+ }
+ }
+ &:not(.auto-scale) {
+ .tb-single-switch-label-row {
+ overflow: hidden;
+ .tb-single-switch-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+ .tb-single-switch-slide-toggle-row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ }
+ .mat-mdc-slide-toggle {
+ &.tb-single-switch-toggle {
+ .mdc-switch {
+ width: 42px;
+ height: 22px;
+ .mdc-switch__track {
+ height: 22px;
+ border-radius: 11px;
+ &::after, &::before {
+ border: none;
+ }
+ }
+ .mdc-switch__handle-track {
+ width: calc(100% - 22px);
+ left: 2px;
+ }
+ .mdc-switch__handle {
+ border-radius: 9px;
+ width: 18px;
+ height: 18px;
+ }
+ .mdc-switch__ripple {
+ width: 36px;
+ height: 36px;
+ }
+ .mdc-switch__icons {
+ display: none;
+ }
+ &:disabled {
+ .mdc-switch__track {
+ opacity: 1;
+ &::after, &::before {
+ background: $switchColorDisabled;
+ }
+ }
+ .mdc-switch__handle::after {
+ opacity: 1;
+ background: $tumblerColorDisabled;
+ }
+ }
+ &:enabled, &:enabled:hover {
+ .mdc-switch__track {
+ &::after {
+ background: $switchColorOn;
+ }
+ &::before {
+ background: $switchColorOff;
+ }
+ }
+ &.mdc-switch--selected {
+ .mdc-switch__handle::after {
+ background: $tumblerColorOn;
+ }
+ .mdc-switch__ripple::after {
+ background-color: $switchColorOn;
+ }
+ }
+ &.mdc-switch--unselected {
+ .mdc-switch__handle::after {
+ background: $tumblerColorOff;
+ }
+ .mdc-switch__ripple::after {
+ background-color: $switchColorOff;
+ }
+ }
+ }
+ }
+ .mdc-form-field > label {
+ display: none;
+ }
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts
new file mode 100644
index 0000000000..7794e51ebc
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts
@@ -0,0 +1,202 @@
+///
+/// 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,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ OnDestroy,
+ OnInit,
+ Renderer2,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import { BasicRpcStateWidgetComponent } from '@home/components/widget/lib/rpc/rpc-widget.models';
+import {
+ singleSwitchDefaultSettings,
+ SingleSwitchLayout,
+ SingleSwitchWidgetSettings
+} from '@home/components/widget/lib/rpc/single-switch-widget.models';
+import { ComponentStyle, iconStyle, textStyle } from '@shared/models/widget-settings.models';
+import { Observable } from 'rxjs';
+import { ResizeObserver } from '@juggle/resize-observer';
+import { ImagePipe } from '@shared/pipe/image.pipe';
+import { DomSanitizer } from '@angular/platform-browser';
+import cssjs from '@core/css/css';
+import { hashCode } from '@core/utils';
+import { RpcInitialStateSettings, RpcUpdateStateSettings } from '@shared/models/rpc-widget-settings.models';
+import { ValueType } from '@shared/models/constants';
+
+const horizontalLayoutPadding = 48;
+const verticalLayoutPadding = 36;
+
+@Component({
+ selector: 'tb-single-switch-widget',
+ templateUrl: './single-switch-widget.component.html',
+ styleUrls: ['./single-switch-widget.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class SingleSwitchWidgetComponent extends
+ BasicRpcStateWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
+
+ @ViewChild('singleSwitchPanel', {static: false})
+ singleSwitchPanel: ElementRef;
+
+ @ViewChild('singleSwitchContent', {static: false})
+ singleSwitchContent: ElementRef;
+
+ @ViewChild('singleSwitchLabelRow', {static: false})
+ singleSwitchLabelRow: ElementRef;
+
+ @ViewChild('singleSwitchToggleRow', {static: false})
+ singleSwitchToggleRow: ElementRef;
+
+ layout: SingleSwitchLayout;
+
+ showIcon = false;
+ icon = '';
+ iconStyle: ComponentStyle = {};
+
+ showLabel = true;
+ label$: Observable;
+ labelStyle: ComponentStyle = {};
+
+ showOnLabel = false;
+ onLabel = '';
+ onLabelStyle: ComponentStyle = {};
+
+ showOffLabel = false;
+ offLabel = '';
+ offLabelStyle: ComponentStyle = {};
+
+ autoScale = false;
+
+ private panelResize$: ResizeObserver;
+
+ constructor(protected imagePipe: ImagePipe,
+ protected sanitizer: DomSanitizer,
+ private renderer: Renderer2,
+ protected cd: ChangeDetectorRef,
+ private elementRef: ElementRef) {
+ super(imagePipe, sanitizer, cd);
+ }
+
+ ngOnInit(): void {
+ super.ngOnInit();
+ this.layout = this.settings.layout;
+
+ this.autoScale = this.settings.autoScale;
+
+ this.showLabel = this.settings.showLabel;
+ this.label$ = this.ctx.registerLabelPattern(this.settings.label, this.label$);
+ this.labelStyle = textStyle(this.settings.labelFont);
+ this.labelStyle.color = this.settings.labelColor;
+
+ this.showIcon = this.settings.showIcon;
+ this.icon = this.settings.icon;
+ this.iconStyle = iconStyle(this.settings.iconSize, this.settings.iconSizeUnit );
+ this.iconStyle.color = this.settings.iconColor;
+
+ this.showOnLabel = this.settings.showOnLabel;
+ this.onLabel = this.settings.onLabel;
+ this.onLabelStyle = textStyle(this.settings.onLabelFont);
+ this.onLabelStyle.color = this.settings.onLabelColor;
+
+ this.showOffLabel = this.settings.showOffLabel;
+ this.offLabel = this.settings.offLabel;
+ this.offLabelStyle = textStyle(this.settings.offLabelFont);
+ this.offLabelStyle.color = this.settings.offLabelColor;
+ const switchVariablesCss = `.tb-single-switch-panel {\n`+
+ `--tb-single-switch-tumbler-color-on: ${this.settings.tumblerColorOn};\n`+
+ `--tb-single-switch-tumbler-color-off: ${this.settings.tumblerColorOff};\n`+
+ `--tb-single-switch-tumbler-color-disabled: ${this.settings.tumblerColorDisabled};\n`+
+ `--tb-single-switch-color-on: ${this.settings.switchColorOn};\n`+
+ `--tb-single-switch-color-off: ${this.settings.switchColorOff};\n`+
+ `--tb-single-switch-color-disabled: ${this.settings.switchColorDisabled};\n`+
+ `}`;
+ const cssParser = new cssjs();
+ cssParser.testMode = false;
+ const namespace = 'single-switch-' + hashCode(switchVariablesCss);
+ cssParser.cssPreviewNamespace = namespace;
+ cssParser.createStyleElement(namespace, switchVariablesCss);
+ this.renderer.addClass(this.elementRef.nativeElement, namespace);
+ }
+
+ ngAfterViewInit(): void {
+ if (this.autoScale) {
+ this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'overflow', 'visible');
+ this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'position', 'absolute');
+ this.panelResize$ = new ResizeObserver(() => {
+ this.onResize();
+ });
+ this.panelResize$.observe(this.singleSwitchPanel.nativeElement);
+ if (this.showLabel) {
+ this.panelResize$.observe(this.singleSwitchLabelRow.nativeElement);
+ }
+ this.onResize();
+ }
+ super.ngAfterViewInit();
+ }
+
+ ngOnDestroy() {
+ if (this.panelResize$) {
+ this.panelResize$.disconnect();
+ }
+ }
+
+ protected stateValueType(): ValueType {
+ return ValueType.BOOLEAN;
+ }
+
+ protected defaultValue(): boolean {
+ return false;
+ }
+
+ protected defaultSettings(): SingleSwitchWidgetSettings {
+ return {...singleSwitchDefaultSettings};
+ }
+
+ protected initialState(): RpcInitialStateSettings {
+ return {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
+ }
+
+ protected getUpdateStateSettingsForValue(value: boolean): RpcUpdateStateSettings {
+ const targetSettings = value ? this.settings.onUpdateState : this.settings.offUpdateState;
+ return {...targetSettings, actionLabel: this.ctx.translate.instant(value ? 'widgets.rpc-state.turn-on' : 'widgets.rpc-state.turn-off')};
+ }
+
+ protected validateValue(value: any): boolean {
+ return !!value;
+ }
+
+ private onResize() {
+ const panelWidth = this.singleSwitchPanel.nativeElement.getBoundingClientRect().width - horizontalLayoutPadding;
+ const panelHeight = this.singleSwitchPanel.nativeElement.getBoundingClientRect().height - verticalLayoutPadding;
+ this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(1)`);
+ let contentWidth = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().width;
+ let contentHeight = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().height;
+ if (this.showIcon || this.showLabel) {
+ contentWidth += (8 + this.singleSwitchLabelRow.nativeElement.getBoundingClientRect().width);
+ contentHeight = Math.max(contentHeight, this.singleSwitchLabelRow.nativeElement.getBoundingClientRect().height);
+ }
+ const scale = Math.min(panelWidth / contentWidth, panelHeight / contentHeight);
+ const width = panelWidth / scale;
+ this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'width', width + 'px');
+ this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(${scale})`);
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.models.ts
new file mode 100644
index 0000000000..57c2af3b5e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.models.ts
@@ -0,0 +1,202 @@
+///
+/// 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 { BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
+import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
+import {
+ RpcDataToStateType,
+ RpcInitialStateAction,
+ RpcStateToParamsType,
+ RpcStateWidgetSettings,
+ RpcUpdateStateAction,
+ RpcUpdateStateSettings
+} from '@shared/models/rpc-widget-settings.models';
+
+export enum SingleSwitchLayout {
+ right = 'right',
+ left = 'left',
+ centered = 'centered'
+}
+
+export const singleSwitchLayouts = Object.keys(SingleSwitchLayout) as SingleSwitchLayout[];
+
+export const singleSwitchLayoutTranslations = new Map(
+ [
+ [SingleSwitchLayout.right, 'widgets.single-switch.layout-right'],
+ [SingleSwitchLayout.left, 'widgets.single-switch.layout-left'],
+ [SingleSwitchLayout.centered, 'widgets.single-switch.layout-centered']
+ ]
+);
+
+export const singleSwitchLayoutImages = new Map(
+ [
+ [SingleSwitchLayout.right, 'assets/widget/single-switch/right-layout.svg'],
+ [SingleSwitchLayout.left, 'assets/widget/single-switch/left-layout.svg'],
+ [SingleSwitchLayout.centered, 'assets/widget/single-switch/centered-layout.svg']
+ ]
+);
+
+export interface SingleSwitchWidgetSettings extends RpcStateWidgetSettings {
+ onUpdateState: RpcUpdateStateSettings;
+ offUpdateState: RpcUpdateStateSettings;
+ layout: SingleSwitchLayout;
+ autoScale: boolean;
+ showLabel: boolean;
+ label: string;
+ labelFont: Font;
+ labelColor: string;
+ showIcon: boolean;
+ icon: string;
+ iconSize: number;
+ iconSizeUnit: cssUnit;
+ iconColor: string;
+ switchColorOn: string;
+ switchColorOff: string;
+ switchColorDisabled: string;
+ tumblerColorOn: string;
+ tumblerColorOff: string;
+ tumblerColorDisabled: string;
+ showOnLabel: boolean;
+ onLabel: string;
+ onLabelFont: Font;
+ onLabelColor: string;
+ showOffLabel: boolean;
+ offLabel: string;
+ offLabelFont: Font;
+ offLabelColor: string;
+}
+
+export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
+ initialState: {
+ action: RpcInitialStateAction.EXECUTE_RPC,
+ defaultValue: false,
+ executeRpc: {
+ method: 'getState',
+ requestTimeout: 5000,
+ requestPersistent: false,
+ persistentPollingInterval: 1000
+ },
+ getAttribute: {
+ key: 'state',
+ scope: null
+ },
+ getTimeSeries: {
+ key: 'state'
+ },
+ dataToState: {
+ type: RpcDataToStateType.NONE,
+ compareToValue: true,
+ dataToStateFunction: '/* Should return boolean value */\nreturn data;'
+ }
+ },
+ onUpdateState: {
+ action: RpcUpdateStateAction.EXECUTE_RPC,
+ executeRpc: {
+ method: 'setState',
+ requestTimeout: 5000,
+ requestPersistent: false,
+ persistentPollingInterval: 1000
+ },
+ setAttribute: {
+ key: 'state',
+ scope: AttributeScope.SHARED_SCOPE
+ },
+ putTimeSeries: {
+ key: 'state'
+ },
+ stateToParams: {
+ type: RpcStateToParamsType.CONSTANT,
+ constantValue: true,
+ stateToParamsFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;'
+ }
+ },
+ offUpdateState: {
+ action: RpcUpdateStateAction.EXECUTE_RPC,
+ executeRpc: {
+ method: 'setState',
+ requestTimeout: 5000,
+ requestPersistent: false,
+ persistentPollingInterval: 1000
+ },
+ setAttribute: {
+ key: 'state',
+ scope: AttributeScope.SHARED_SCOPE
+ },
+ putTimeSeries: {
+ key: 'state'
+ },
+ stateToParams: {
+ type: RpcStateToParamsType.CONSTANT,
+ constantValue: false,
+ stateToParamsFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */ \n return value;'
+ }
+ },
+ layout: SingleSwitchLayout.right,
+ autoScale: true,
+ showLabel: true,
+ label: 'Switch',
+ labelFont: {
+ family: 'Roboto',
+ size: 16,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '24px'
+ },
+ labelColor: 'rgba(0, 0, 0, 0.76)',
+ showIcon: false,
+ icon: 'mdi:lightbulb-outline',
+ iconSize: 24,
+ iconSizeUnit: 'px',
+ iconColor: 'rgba(0, 0, 0, 0.76)',
+ switchColorOn: '#5469FF',
+ switchColorOff: 'rgba(84, 105, 255, 0.30)',
+ switchColorDisabled: '#D5D7E5',
+ tumblerColorOn: '#fff',
+ tumblerColorOff: '#fff',
+ tumblerColorDisabled: '#fff',
+ showOnLabel: false,
+ onLabel: 'On',
+ onLabelFont: {
+ family: 'Roboto',
+ size: 16,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '400',
+ lineHeight: '24px'
+ },
+ onLabelColor: 'rgba(0, 0, 0, 0.38)',
+ showOffLabel: false,
+ offLabel: 'Off',
+ offLabelFont: {
+ family: 'Roboto',
+ size: 16,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '400',
+ lineHeight: '24px'
+ },
+ offLabelColor: 'rgba(0, 0, 0, 0.38)',
+ background: {
+ type: BackgroundType.color,
+ color: '#fff',
+ overlay: {
+ enabled: false,
+ color: 'rgba(255,255,255,0.72)',
+ blur: 3
+ }
+ }
+};
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.html
new file mode 100644
index 0000000000..e290225f08
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.html
@@ -0,0 +1,211 @@
+
+
+
widgets.rpc-state.initial-state
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.ts
new file mode 100644
index 0000000000..b9f5f9b602
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.ts
@@ -0,0 +1,176 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { TbPopoverComponent } from '@shared/components/popover.component';
+import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { merge } from 'rxjs';
+import {
+ RpcDataToStateType,
+ RpcInitialStateAction,
+ rpcInitialStateActions,
+ RpcInitialStateSettings,
+ rpcInitialStateTranslations
+} from '@shared/models/rpc-widget-settings.models';
+import { ValueType } from '@shared/models/constants';
+import { TargetDevice } from '@shared/models/widget.models';
+import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
+import { IAliasController } from '@core/api/widget-api.models';
+import { WidgetService } from '@core/http/widget.service';
+
+@Component({
+ selector: 'tb-rpc-initial-state-settings-panel',
+ templateUrl: './rpc-initial-state-settings-panel.component.html',
+ providers: [],
+ styleUrls: ['./rpc-state-settings-panel.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class RpcInitialStateSettingsPanelComponent extends PageComponent implements OnInit {
+
+ @Input()
+ initialState: RpcInitialStateSettings;
+
+ @Input()
+ stateValueType: ValueType;
+
+ @Input()
+ aliasController: IAliasController;
+
+ @Input()
+ targetDevice: TargetDevice;
+
+ @Input()
+ popover: TbPopoverComponent;
+
+ @Output()
+ initialStateSettingsApplied = new EventEmitter>();
+
+ rpcInitialStateAction = RpcInitialStateAction;
+
+ rpcInitialStateActions = rpcInitialStateActions;
+
+ rpcInitialStateTranslationsMap = rpcInitialStateTranslations;
+
+ telemetryTypeTranslationsMap = telemetryTypeTranslationsShort;
+
+ attributeScopes = Object.keys(AttributeScope) as AttributeScope[];
+
+ dataKeyType = DataKeyType;
+
+ dataToStateType = RpcDataToStateType;
+
+ functionScopeVariables = this.widgetService.getWidgetScopeVariables();
+
+ valueType = ValueType;
+
+ initialStateSettingsFormGroup: UntypedFormGroup;
+
+ constructor(private fb: UntypedFormBuilder,
+ private widgetService: WidgetService,
+ protected store: Store) {
+ super(store);
+ }
+
+ ngOnInit(): void {
+ this.initialStateSettingsFormGroup = this.fb.group(
+ {
+ action: [this.initialState?.action, []],
+ defaultValue: [this.initialState?.defaultValue, [Validators.required]],
+ executeRpc: this.fb.group({
+ method: [this.initialState?.executeRpc?.method, [Validators.required]],
+ requestTimeout: [this.initialState?.executeRpc?.requestTimeout, [Validators.required, Validators.min(5000)]],
+ requestPersistent: [this.initialState?.executeRpc?.requestPersistent, []],
+ persistentPollingInterval: [this.initialState?.executeRpc?.persistentPollingInterval, [Validators.required, Validators.min(1000)]]
+ }),
+ getAttribute: this.fb.group({
+ scope: [this.initialState?.getAttribute?.scope, []],
+ key: [this.initialState?.getAttribute?.key, [Validators.required]],
+ }),
+ getTimeSeries: this.fb.group({
+ key: [this.initialState?.getTimeSeries?.key, [Validators.required]],
+ }),
+ dataToState: this.fb.group({
+ type: [this.initialState?.dataToState?.type, [Validators.required]],
+ dataToStateFunction: [this.initialState?.dataToState?.dataToStateFunction, [Validators.required]],
+ }),
+ }
+ );
+ if (this.stateValueType === ValueType.BOOLEAN) {
+ (this.initialStateSettingsFormGroup.get('dataToState') as UntypedFormGroup).addControl(
+ 'compareToValue', this.fb.control(this.initialState?.dataToState?.compareToValue, [Validators.required])
+ );
+ }
+
+ merge(this.initialStateSettingsFormGroup.get('action').valueChanges,
+ this.initialStateSettingsFormGroup.get('dataToState').get('type').valueChanges,
+ this.initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').valueChanges).subscribe(() => {
+ this.updateValidators();
+ });
+ this.updateValidators();
+ }
+
+ cancel() {
+ this.popover?.hide();
+ }
+
+ applyInitialStateSettings() {
+ const initialStateSettings: RpcInitialStateSettings = this.initialStateSettingsFormGroup.getRawValue();
+ this.initialStateSettingsApplied.emit(initialStateSettings);
+ }
+
+ private updateValidators() {
+ const action: RpcInitialStateAction = this.initialStateSettingsFormGroup.get('action').value;
+ const dataToStateType: RpcDataToStateType = this.initialStateSettingsFormGroup.get('dataToState').get('type').value;
+
+ this.initialStateSettingsFormGroup.get('defaultValue').disable({emitEvent: false});
+ this.initialStateSettingsFormGroup.get('executeRpc').disable({emitEvent: false});
+ this.initialStateSettingsFormGroup.get('getAttribute').disable({emitEvent: false});
+ this.initialStateSettingsFormGroup.get('getTimeSeries').disable({emitEvent: false});
+ switch (action) {
+ case RpcInitialStateAction.DO_NOTHING:
+ this.initialStateSettingsFormGroup.get('defaultValue').enable({emitEvent: false});
+ break;
+ case RpcInitialStateAction.EXECUTE_RPC:
+ this.initialStateSettingsFormGroup.get('executeRpc').enable({emitEvent: false});
+ const requestPersistent: boolean = this.initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value;
+ if (requestPersistent) {
+ this.initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').enable({emitEvent: false});
+ } else {
+ this.initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').disable({emitEvent: false});
+ }
+ break;
+ case RpcInitialStateAction.GET_ATTRIBUTE:
+ this.initialStateSettingsFormGroup.get('getAttribute').enable({emitEvent: false});
+ break;
+ case RpcInitialStateAction.GET_TIME_SERIES:
+ this.initialStateSettingsFormGroup.get('getTimeSeries').enable({emitEvent: false});
+ break;
+ }
+ if (action === RpcInitialStateAction.DO_NOTHING) {
+ this.initialStateSettingsFormGroup.get('dataToState').disable({emitEvent: false});
+ } else {
+ this.initialStateSettingsFormGroup.get('dataToState').enable({emitEvent: false});
+ if (dataToStateType === RpcDataToStateType.FUNCTION) {
+ this.initialStateSettingsFormGroup.get('dataToState').get('dataToStateFunction').enable({emitEvent: false});
+ } else {
+ this.initialStateSettingsFormGroup.get('dataToState').get('dataToStateFunction').disable({emitEvent: false});
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings.component.ts
new file mode 100644
index 0000000000..61c053d43c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings.component.ts
@@ -0,0 +1,156 @@
+///
+/// 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 {
+ ChangeDetectorRef,
+ Component,
+ forwardRef,
+ HostBinding,
+ Input,
+ OnInit,
+ Renderer2,
+ ViewContainerRef,
+ ViewEncapsulation
+} from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { MatButton } from '@angular/material/button';
+import { TbPopoverService } from '@shared/components/popover.service';
+import { RpcInitialStateAction, RpcInitialStateSettings } from '@shared/models/rpc-widget-settings.models';
+import { TranslateService } from '@ngx-translate/core';
+import { ValueType } from '@shared/models/constants';
+import {
+ RpcInitialStateSettingsPanelComponent
+} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component';
+import { IAliasController } from '@core/api/widget-api.models';
+import { TargetDevice } from '@shared/models/widget.models';
+
+@Component({
+ selector: 'tb-rpc-initial-state-settings',
+ templateUrl: './rpc-state-settings-button.component.html',
+ styleUrls: ['./rpc-state-settings-button.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => RpcInitialStateSettingsComponent),
+ multi: true
+ }
+ ],
+ encapsulation: ViewEncapsulation.None
+})
+export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAccessor {
+
+ @HostBinding('style.overflow')
+ overflow = 'hidden';
+
+ @Input()
+ stateValueType: ValueType;
+
+ @Input()
+ aliasController: IAliasController;
+
+ @Input()
+ targetDevice: TargetDevice;
+
+ @Input()
+ disabled = false;
+
+ modelValue: RpcInitialStateSettings;
+
+ displayValue: string;
+
+ private propagateChange = null;
+
+ constructor(private translate: TranslateService,
+ private popoverService: TbPopoverService,
+ private renderer: Renderer2,
+ private viewContainerRef: ViewContainerRef,
+ private cd: ChangeDetectorRef) {}
+
+ ngOnInit(): void {
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(_fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ if (this.disabled !== isDisabled) {
+ this.disabled = isDisabled;
+ }
+ }
+
+ writeValue(value: RpcInitialStateSettings): void {
+ this.modelValue = value;
+ this.updateDisplayValue();
+ }
+
+ openRpcStateSettingsPopup($event: Event, matButton: MatButton) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ const trigger = matButton._elementRef.nativeElement;
+ if (this.popoverService.hasPopover(trigger)) {
+ this.popoverService.hidePopover(trigger);
+ } else {
+ const ctx: any = {
+ initialState: this.modelValue,
+ stateValueType: this.stateValueType,
+ aliasController: this.aliasController,
+ targetDevice: this.targetDevice
+ };
+ const initialStateSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
+ this.viewContainerRef, RpcInitialStateSettingsPanelComponent,
+ ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
+ ctx,
+ {},
+ {}, {}, true);
+ initialStateSettingsPanelPopover.tbComponentRef.instance.popover = initialStateSettingsPanelPopover;
+ initialStateSettingsPanelPopover.tbComponentRef.instance.initialStateSettingsApplied.subscribe((initialState) => {
+ initialStateSettingsPanelPopover.hide();
+ this.modelValue = initialState;
+ this.updateDisplayValue();
+ this.propagateChange(this.modelValue);
+ });
+ }
+ }
+
+ private updateDisplayValue() {
+ switch (this.modelValue.action) {
+ case RpcInitialStateAction.DO_NOTHING:
+ if (this.stateValueType === ValueType.BOOLEAN) {
+ this.displayValue = this.translate.instant(!!this.modelValue.defaultValue ? 'widgets.rpc-state.on' : 'widgets.rpc-state.off');
+ } else {
+ this.displayValue = this.modelValue.defaultValue + '';
+ }
+ break;
+ case RpcInitialStateAction.EXECUTE_RPC:
+ const methodName = this.modelValue.executeRpc.method;
+ this.displayValue = this.translate.instant('widgets.rpc-state.execute-rpc-text', {methodName});
+ break;
+ case RpcInitialStateAction.GET_ATTRIBUTE:
+ this.displayValue = this.translate.instant('widgets.rpc-state.get-attribute-text', {key: this.modelValue.getAttribute.key});
+ break;
+ case RpcInitialStateAction.GET_TIME_SERIES:
+ this.displayValue = this.translate.instant('widgets.rpc-state.get-time-series-text', {key: this.modelValue.getTimeSeries.key});
+ break;
+ }
+ this.cd.markForCheck();
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-button.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-button.component.html
new file mode 100644
index 0000000000..da24002e31
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-button.component.html
@@ -0,0 +1,26 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-button.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-button.scss
new file mode 100644
index 0000000000..1a6d454152
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-button.scss
@@ -0,0 +1,34 @@
+/**
+ * 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.
+ */
+.mdc-button.tb-rpc-state-settings {
+ width: 100%;
+ .mdc-button__label {
+ width: 100%;
+ & > div {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+ justify-content: space-between;
+ align-items: center;
+ & > span {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-panel.component.scss
new file mode 100644
index 0000000000..e2ffae2c84
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-panel.component.scss
@@ -0,0 +1,47 @@
+/**
+ * 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';
+
+.tb-rpc-state-settings-panel {
+ width: 530px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ @media #{$mat-lt-md} {
+ width: 90vw;
+ }
+ .tb-rpc-state-settings-panel-content {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ overflow: auto;
+ }
+ .tb-rpc-state-settings-title {
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: 0.25px;
+ color: rgba(0, 0, 0, 0.87);
+ }
+ .tb-rpc-state-settings-panel-buttons {
+ height: 40px;
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ justify-content: flex-end;
+ align-items: flex-end;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.html
new file mode 100644
index 0000000000..ad70579d1c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.html
@@ -0,0 +1,201 @@
+
+
+
{{ panelTitle | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.ts
new file mode 100644
index 0000000000..78c5c6f44b
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.ts
@@ -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.
+///
+
+import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { TbPopoverComponent } from '@shared/components/popover.component';
+import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { merge } from 'rxjs';
+import {
+ RpcStateToParamsType,
+ RpcUpdateStateAction,
+ rpcUpdateStateActions,
+ RpcUpdateStateSettings,
+ rpcUpdateStateTranslations
+} from '@shared/models/rpc-widget-settings.models';
+import { ValueType } from '@shared/models/constants';
+import { TargetDevice } from '@shared/models/widget.models';
+import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
+import { IAliasController } from '@core/api/widget-api.models';
+import { WidgetService } from '@core/http/widget.service';
+
+@Component({
+ selector: 'tb-rpc-update-state-settings-panel',
+ templateUrl: './rpc-update-state-settings-panel.component.html',
+ providers: [],
+ styleUrls: ['./rpc-state-settings-panel.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class RpcUpdateStateSettingsPanelComponent extends PageComponent implements OnInit {
+
+ @Input()
+ panelTitle: string;
+
+ @Input()
+ updateState: RpcUpdateStateSettings;
+
+ @Input()
+ stateValueType: ValueType;
+
+ @Input()
+ aliasController: IAliasController;
+
+ @Input()
+ targetDevice: TargetDevice;
+
+ @Input()
+ popover: TbPopoverComponent;
+
+ @Output()
+ updateStateSettingsApplied = new EventEmitter();
+
+ rpcUpdateStateAction = RpcUpdateStateAction;
+
+ rpcUpdateStateActions = rpcUpdateStateActions;
+
+ rpcUpdateStateTranslationsMap = rpcUpdateStateTranslations;
+
+ telemetryTypeTranslationsMap = telemetryTypeTranslationsShort;
+
+ attributeScopes = [AttributeScope.SERVER_SCOPE, AttributeScope.SHARED_SCOPE];
+
+ dataKeyType = DataKeyType;
+
+ stateToParamsType = RpcStateToParamsType;
+
+ functionScopeVariables = this.widgetService.getWidgetScopeVariables();
+
+ valueType = ValueType;
+
+ updateStateSettingsFormGroup: UntypedFormGroup;
+
+ constructor(private fb: UntypedFormBuilder,
+ private widgetService: WidgetService,
+ protected store: Store) {
+ super(store);
+ }
+
+ ngOnInit(): void {
+ this.updateStateSettingsFormGroup = this.fb.group(
+ {
+ action: [this.updateState?.action, []],
+ executeRpc: this.fb.group({
+ method: [this.updateState?.executeRpc?.method, [Validators.required]],
+ requestTimeout: [this.updateState?.executeRpc?.requestTimeout, [Validators.required, Validators.min(5000)]],
+ requestPersistent: [this.updateState?.executeRpc?.requestPersistent, []],
+ persistentPollingInterval: [this.updateState?.executeRpc?.persistentPollingInterval, [Validators.required, Validators.min(1000)]]
+ }),
+ setAttribute: this.fb.group({
+ scope: [this.updateState?.setAttribute?.scope, []],
+ key: [this.updateState?.setAttribute?.key, [Validators.required]],
+ }),
+ putTimeSeries: this.fb.group({
+ key: [this.updateState?.putTimeSeries?.key, [Validators.required]],
+ }),
+ stateToParams: this.fb.group({
+ type: [this.updateState?.stateToParams?.type, [Validators.required]],
+ constantValue: [this.updateState?.stateToParams?.constantValue, [Validators.required]],
+ stateToParamsFunction: [this.updateState?.stateToParams?.stateToParamsFunction, [Validators.required]],
+ }),
+ }
+ );
+
+ merge(this.updateStateSettingsFormGroup.get('action').valueChanges,
+ this.updateStateSettingsFormGroup.get('stateToParams').get('type').valueChanges,
+ this.updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').valueChanges).subscribe(() => {
+ this.updateValidators();
+ });
+ this.updateValidators();
+ }
+
+ cancel() {
+ this.popover?.hide();
+ }
+
+ applyUpdateStateSettings() {
+ const updateStateSettings: RpcUpdateStateSettings = this.updateStateSettingsFormGroup.getRawValue();
+ this.updateStateSettingsApplied.emit(updateStateSettings);
+ }
+
+ private updateValidators() {
+ const action: RpcUpdateStateAction = this.updateStateSettingsFormGroup.get('action').value;
+ let stateToParamsType: RpcStateToParamsType = this.updateStateSettingsFormGroup.get('stateToParams').get('type').value;
+
+ this.updateStateSettingsFormGroup.get('executeRpc').disable({emitEvent: false});
+ this.updateStateSettingsFormGroup.get('setAttribute').disable({emitEvent: false});
+ this.updateStateSettingsFormGroup.get('putTimeSeries').disable({emitEvent: false});
+ switch (action) {
+ case RpcUpdateStateAction.EXECUTE_RPC:
+ this.updateStateSettingsFormGroup.get('executeRpc').enable({emitEvent: false});
+ const requestPersistent: boolean = this.updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value;
+ if (requestPersistent) {
+ this.updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').enable({emitEvent: false});
+ } else {
+ this.updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').disable({emitEvent: false});
+ }
+ break;
+ case RpcUpdateStateAction.SET_ATTRIBUTE:
+ case RpcUpdateStateAction.ADD_TIME_SERIES:
+ if (stateToParamsType === RpcStateToParamsType.NONE) {
+ stateToParamsType = RpcStateToParamsType.CONSTANT;
+ this.updateStateSettingsFormGroup.get('stateToParams').get('type').patchValue(stateToParamsType, {emitEvent: false});
+ }
+ if (action === RpcUpdateStateAction.SET_ATTRIBUTE) {
+ this.updateStateSettingsFormGroup.get('setAttribute').enable({emitEvent: false});
+ } else {
+ this.updateStateSettingsFormGroup.get('putTimeSeries').enable({emitEvent: false});
+ }
+ break;
+ }
+ switch (stateToParamsType) {
+ case RpcStateToParamsType.CONSTANT:
+ this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').enable({emitEvent: false});
+ this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').disable({emitEvent: false});
+ break;
+ case RpcStateToParamsType.FUNCTION:
+ this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').disable({emitEvent: false});
+ this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').enable({emitEvent: false});
+ break;
+ case RpcStateToParamsType.NONE:
+ this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').disable({emitEvent: false});
+ this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').disable({emitEvent: false});
+ break;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings.component.ts
new file mode 100644
index 0000000000..8a65861783
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings.component.ts
@@ -0,0 +1,174 @@
+///
+/// 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 {
+ ChangeDetectorRef,
+ Component,
+ forwardRef,
+ HostBinding,
+ Input,
+ OnInit,
+ Renderer2,
+ ViewContainerRef,
+ ViewEncapsulation
+} from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { MatButton } from '@angular/material/button';
+import { TbPopoverService } from '@shared/components/popover.service';
+import {
+ RpcStateToParamsType,
+ RpcUpdateStateAction,
+ RpcUpdateStateSettings
+} from '@shared/models/rpc-widget-settings.models';
+import { TranslateService } from '@ngx-translate/core';
+import { ValueType } from '@shared/models/constants';
+import { IAliasController } from '@core/api/widget-api.models';
+import { TargetDevice } from '@shared/models/widget.models';
+import { isDefinedAndNotNull } from '@core/utils';
+import {
+ RpcUpdateStateSettingsPanelComponent
+} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component';
+
+@Component({
+ selector: 'tb-rpc-update-state-settings',
+ templateUrl: './rpc-state-settings-button.component.html',
+ styleUrls: ['./rpc-state-settings-button.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => RpcUpdateStateSettingsComponent),
+ multi: true
+ }
+ ],
+ encapsulation: ViewEncapsulation.None
+})
+export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAccessor {
+
+ @HostBinding('style.overflow')
+ overflow = 'hidden';
+
+ @Input()
+ panelTitle: string;
+
+ @Input()
+ stateValueType: ValueType;
+
+ @Input()
+ aliasController: IAliasController;
+
+ @Input()
+ targetDevice: TargetDevice;
+
+ @Input()
+ disabled = false;
+
+ modelValue: RpcUpdateStateSettings;
+
+ displayValue: string;
+
+ private propagateChange = null;
+
+ constructor(private translate: TranslateService,
+ private popoverService: TbPopoverService,
+ private renderer: Renderer2,
+ private viewContainerRef: ViewContainerRef,
+ private cd: ChangeDetectorRef) {}
+
+ ngOnInit(): void {
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(_fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ if (this.disabled !== isDisabled) {
+ this.disabled = isDisabled;
+ }
+ }
+
+ writeValue(value: RpcUpdateStateSettings): void {
+ this.modelValue = value;
+ this.updateDisplayValue();
+ }
+
+ openRpcStateSettingsPopup($event: Event, matButton: MatButton) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ const trigger = matButton._elementRef.nativeElement;
+ if (this.popoverService.hasPopover(trigger)) {
+ this.popoverService.hidePopover(trigger);
+ } else {
+ const ctx: any = {
+ updateState: this.modelValue,
+ panelTitle: this.panelTitle,
+ stateValueType: this.stateValueType,
+ aliasController: this.aliasController,
+ targetDevice: this.targetDevice
+ };
+ const updateStateSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
+ this.viewContainerRef, RpcUpdateStateSettingsPanelComponent,
+ ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
+ ctx,
+ {},
+ {}, {}, true);
+ updateStateSettingsPanelPopover.tbComponentRef.instance.popover = updateStateSettingsPanelPopover;
+ updateStateSettingsPanelPopover.tbComponentRef.instance.updateStateSettingsApplied.subscribe((updateState) => {
+ updateStateSettingsPanelPopover.hide();
+ this.modelValue = updateState;
+ this.updateDisplayValue();
+ this.propagateChange(this.modelValue);
+ });
+ }
+ }
+
+ private updateDisplayValue() {
+ let value: any;
+ switch (this.modelValue.stateToParams.type) {
+ case RpcStateToParamsType.CONSTANT:
+ value = this.modelValue.stateToParams.constantValue;
+ break;
+ case RpcStateToParamsType.FUNCTION:
+ value = 'f(value)';
+ break;
+ case RpcStateToParamsType.NONE:
+ break;
+ }
+ switch (this.modelValue.action) {
+ case RpcUpdateStateAction.EXECUTE_RPC:
+ let methodName = this.modelValue.executeRpc.method;
+ if (isDefinedAndNotNull(value)) {
+ methodName = `${methodName}(${value})`;
+ }
+ this.displayValue = this.translate.instant('widgets.rpc-state.execute-rpc-text', {methodName});
+ break;
+ case RpcUpdateStateAction.SET_ATTRIBUTE:
+ this.displayValue = this.translate.instant('widgets.rpc-state.set-attribute-to-value-text',
+ {key: this.modelValue.setAttribute.key, value});
+ break;
+ case RpcUpdateStateAction.ADD_TIME_SERIES:
+ this.displayValue = this.translate.instant('widgets.rpc-state.add-time-series-value-text',
+ {key: this.modelValue.setAttribute.key, value});
+ break;
+ }
+ this.cd.markForCheck();
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
index 5eb9d598ff..ba08a39bb7 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
@@ -51,6 +51,21 @@ import { ColorRangePanelComponent } from '@home/components/widget/lib/settings/c
import {
ColorRangeSettingsComponent, ColorRangeSettingsComponentService
} from '@home/components/widget/lib/settings/common/color-range-settings.component';
+import {
+ RpcInitialStateSettingsComponent
+} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings.component';
+import {
+ RpcInitialStateSettingsPanelComponent
+} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component';
+import {
+ DeviceKeyAutocompleteComponent
+} from '@home/components/widget/lib/settings/control/device-key-autocomplete.component';
+import {
+ RpcUpdateStateSettingsComponent
+} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings.component';
+import {
+ RpcUpdateStateSettingsPanelComponent
+} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component';
@NgModule({
declarations: [
@@ -71,7 +86,12 @@ import {
CountWidgetSettingsComponent,
ColorRangeListComponent,
ColorRangePanelComponent,
- ColorRangeSettingsComponent
+ ColorRangeSettingsComponent,
+ RpcInitialStateSettingsComponent,
+ RpcInitialStateSettingsPanelComponent,
+ DeviceKeyAutocompleteComponent,
+ RpcUpdateStateSettingsComponent,
+ RpcUpdateStateSettingsPanelComponent
],
imports: [
CommonModule,
@@ -96,7 +116,12 @@ import {
CountWidgetSettingsComponent,
ColorRangeListComponent,
ColorRangePanelComponent,
- ColorRangeSettingsComponent
+ ColorRangeSettingsComponent,
+ RpcInitialStateSettingsComponent,
+ RpcInitialStateSettingsPanelComponent,
+ DeviceKeyAutocompleteComponent,
+ RpcUpdateStateSettingsComponent,
+ RpcUpdateStateSettingsPanelComponent
],
providers: [
ColorSettingsComponentService,
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html
index 5a2d11d3a5..977a493c5f 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html
@@ -15,8 +15,13 @@
limitations under the License.
-->
-
- {{ (keyType === dataKeyType.attribute
+
+ {{ (keyType === dataKeyType.attribute
? attributeLabel
: timeseriesLabel) | translate }}
close
+
+ warning
+
@@ -38,4 +52,7 @@
+
+ {{ requiredText | translate }}
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts
index 6255fa6e48..9c0691faed 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts
@@ -25,15 +25,17 @@ import {
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 { AttributeScope, 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 { DataKey, TargetDevice, TargetDeviceType, targetDeviceValid } from '@shared/models/widget.models';
import { EntityService } from '@core/http/entity.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { EntityType } from '@shared/models/entity-type.models';
+import { EntityFilter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models';
+import { AliasFilterType } from '@shared/models/alias.models';
+import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-device-key-autocomplete',
@@ -58,25 +60,30 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI
aliasController: IAliasController;
@Input()
- targetDeviceAliasId: string;
+ targetDevice: TargetDevice;
@Input()
keyType: DataKeyType;
+ @Input()
+ attributeScope: AttributeScope;
+
@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);
- }
+ requiredText: string;
+
+ @Input()
+ @coerceBoolean()
+ required: boolean;
+
+ @Input()
+ @coerceBoolean()
+ inlineField: boolean;
dataKeyType = DataKeyType;
@@ -93,7 +100,6 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI
private keysFetchObservable$: Observable> = null;
constructor(protected store: Store,
- private translate: TranslateService,
private entityService: EntityService,
private fb: UntypedFormBuilder) {
super(store);
@@ -118,7 +124,7 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
- if (propName === 'targetDeviceAliasId' || propName === 'keyType') {
+ if (propName === 'targetDevice' || propName === 'keyType' || propName === 'attributeScope') {
this.clearKeysCache();
}
}
@@ -186,9 +192,9 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI
private getKeys() {
if (this.keysFetchObservable$ === null) {
let fetchObservable: Observable>;
- if (this.targetDeviceAliasId) {
+ if (targetDeviceValid(this.targetDevice)) {
const dataKeyTypes = [this.keyType];
- fetchObservable = this.fetchEntityKeys(this.targetDeviceAliasId, dataKeyTypes);
+ fetchObservable = this.fetchEntityKeys(this.targetDevice, dataKeyTypes);
} else {
fetchObservable = of([]);
}
@@ -201,17 +207,25 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI
return this.keysFetchObservable$;
}
- private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> {
- return this.aliasController.getAliasInfo(entityAliasId).pipe(
- mergeMap((aliasInfo) => {
- return this.entityService.getEntityKeysByEntityFilter(
- aliasInfo.entityFilter,
+ private fetchEntityKeys(targetDevice: TargetDevice, dataKeyTypes: Array): Observable> {
+ let entityFilter$: Observable;
+ if (targetDevice.type === TargetDeviceType.device) {
+ entityFilter$ = of(singleEntityFilterFromDeviceId(targetDevice.deviceId));
+ } else {
+ entityFilter$ = this.aliasController.getAliasInfo(targetDevice.entityAliasId).pipe(
+ map(aliasInfo => aliasInfo.entityFilter)
+ );
+ }
+ return entityFilter$.pipe(
+ mergeMap((entityFilter) =>
+ this.entityService.getEntityKeysByEntityFilterAndScope(
+ entityFilter,
dataKeyTypes, [EntityType.DEVICE],
+ this.attributeScope,
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
- );
- }),
+ )),
catchError(() => of([] as Array))
);
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html
index 669c9a2fce..fa66dd6be1 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html
@@ -52,7 +52,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts
index f4c570bf4e..8bf58a9b22 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts
@@ -15,7 +15,7 @@
///
import { Component } from '@angular/core';
-import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
+import { TargetDevice, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@@ -36,12 +36,8 @@ export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent
super(store);
}
- get targetDeviceAliasId(): string {
- const aliasIds = this.widget?.config?.targetDeviceAliasIds;
- if (aliasIds && aliasIds.length) {
- return aliasIds[0];
- }
- return null;
+ get targetDevice(): TargetDevice {
+ return this.widget?.config?.targetDevice;
}
protected settingsForm(): UntypedFormGroup {
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html
new file mode 100644
index 0000000000..1006d65f55
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html
@@ -0,0 +1,198 @@
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts
new file mode 100644
index 0000000000..7e4f1d1d96
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts
@@ -0,0 +1,157 @@
+///
+/// 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 } from '@angular/core';
+import { TargetDevice, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
+import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import {
+ singleSwitchDefaultSettings, singleSwitchLayoutImages,
+ singleSwitchLayouts,
+ singleSwitchLayoutTranslations
+} from '@home/components/widget/lib/rpc/single-switch-widget.models';
+import { ValueType } from '@shared/models/constants';
+
+@Component({
+ selector: 'tb-single-switch-widget-settings',
+ templateUrl: './single-switch-widget-settings.component.html',
+ styleUrls: ['./../widget-settings.scss']
+})
+export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent {
+
+ get targetDevice(): TargetDevice {
+ return this.widget?.config?.targetDevice;
+ }
+
+ singleSwitchLayouts = singleSwitchLayouts;
+
+ singleSwitchLayoutTranslationMap = singleSwitchLayoutTranslations;
+ singleSwitchLayoutImageMap = singleSwitchLayoutImages;
+
+ valueType = ValueType;
+
+ singleSwitchWidgetSettingsForm: UntypedFormGroup;
+
+ constructor(protected store: Store,
+ private fb: UntypedFormBuilder) {
+ super(store);
+ }
+
+ protected settingsForm(): UntypedFormGroup {
+ return this.singleSwitchWidgetSettingsForm;
+ }
+
+ protected defaultSettings(): WidgetSettings {
+ return {...singleSwitchDefaultSettings};
+ }
+
+ protected onSettingsSet(settings: WidgetSettings) {
+ this.singleSwitchWidgetSettingsForm = this.fb.group({
+ initialState: [settings.initialState, []],
+ onUpdateState: [settings.onUpdateState, []],
+ offUpdateState: [settings.offUpdateState, []],
+ layout: [settings.layout, []],
+ autoScale: [settings.autoScale, []],
+
+ showLabel: [settings.showLabel, []],
+ label: [settings.label, []],
+ labelFont: [settings.labelFont, []],
+ labelColor: [settings.labelColor, []],
+
+ showIcon: [settings.showIcon, []],
+ iconSize: [settings.iconSize, [Validators.min(0)]],
+ iconSizeUnit: [settings.iconSizeUnit, []],
+ icon: [settings.icon, []],
+ iconColor: [settings.iconColor, []],
+
+ switchColorOn: [settings.switchColorOn, []],
+ switchColorOff: [settings.switchColorOff, []],
+ switchColorDisabled: [settings.switchColorDisabled, []],
+
+ tumblerColorOn: [settings.tumblerColorOn, []],
+ tumblerColorOff: [settings.tumblerColorOff, []],
+ tumblerColorDisabled: [settings.tumblerColorDisabled, []],
+
+ showOnLabel: [settings.showOnLabel, []],
+ onLabel: [settings.onLabel, []],
+ onLabelFont: [settings.onLabelFont, []],
+ onLabelColor: [settings.onLabelColor, []],
+
+ showOffLabel: [settings.showOffLabel, []],
+ offLabel: [settings.offLabel, []],
+ offLabelFont: [settings.offLabelFont, []],
+ offLabelColor: [settings.offLabelColor, []],
+
+ background: [settings.background, []]
+ });
+ }
+
+ protected validatorTriggers(): string[] {
+ return ['showLabel', 'showIcon', 'showOnLabel', 'showOffLabel'];
+ }
+
+ protected updateValidators(emitEvent: boolean): void {
+ const showLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showLabel').value;
+ const showIcon: boolean = this.singleSwitchWidgetSettingsForm.get('showIcon').value;
+ const showOnLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showOnLabel').value;
+ const showOffLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showOffLabel').value;
+
+ if (showLabel) {
+ this.singleSwitchWidgetSettingsForm.get('label').enable();
+ this.singleSwitchWidgetSettingsForm.get('labelFont').enable();
+ this.singleSwitchWidgetSettingsForm.get('labelColor').enable();
+ } else {
+ this.singleSwitchWidgetSettingsForm.get('label').disable();
+ this.singleSwitchWidgetSettingsForm.get('labelFont').disable();
+ this.singleSwitchWidgetSettingsForm.get('labelColor').disable();
+ }
+
+ if (showIcon) {
+ this.singleSwitchWidgetSettingsForm.get('iconSize').enable();
+ this.singleSwitchWidgetSettingsForm.get('iconSizeUnit').enable();
+ this.singleSwitchWidgetSettingsForm.get('icon').enable();
+ this.singleSwitchWidgetSettingsForm.get('iconColor').enable();
+ } else {
+ this.singleSwitchWidgetSettingsForm.get('iconSize').disable();
+ this.singleSwitchWidgetSettingsForm.get('iconSizeUnit').disable();
+ this.singleSwitchWidgetSettingsForm.get('icon').disable();
+ this.singleSwitchWidgetSettingsForm.get('iconColor').disable();
+ }
+
+ if (showOnLabel) {
+ this.singleSwitchWidgetSettingsForm.get('onLabel').enable();
+ this.singleSwitchWidgetSettingsForm.get('onLabelFont').enable();
+ this.singleSwitchWidgetSettingsForm.get('onLabelColor').enable();
+ } else {
+ this.singleSwitchWidgetSettingsForm.get('onLabel').disable();
+ this.singleSwitchWidgetSettingsForm.get('onLabelFont').disable();
+ this.singleSwitchWidgetSettingsForm.get('onLabelColor').disable();
+ }
+
+ if (showOffLabel) {
+ this.singleSwitchWidgetSettingsForm.get('offLabel').enable();
+ this.singleSwitchWidgetSettingsForm.get('offLabelFont').enable();
+ this.singleSwitchWidgetSettingsForm.get('offLabelColor').enable();
+ } else {
+ this.singleSwitchWidgetSettingsForm.get('offLabel').disable();
+ this.singleSwitchWidgetSettingsForm.get('offLabelFont').disable();
+ this.singleSwitchWidgetSettingsForm.get('offLabelColor').disable();
+ }
+ }
+
+ protected readonly ValueType = ValueType;
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html
index 31a9cc25be..1bf741eba7 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.html
@@ -51,7 +51,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts
index 95f8018db9..d14d0fd594 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts
@@ -15,7 +15,7 @@
///
import { Component } from '@angular/core';
-import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
+import { TargetDevice, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@@ -36,12 +36,8 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent
super(store);
}
- get targetDeviceAliasId(): string {
- const aliasIds = this.widget?.config?.targetDeviceAliasIds;
- if (aliasIds && aliasIds.length) {
- return aliasIds[0];
- }
- return null;
+ get targetDevice(): TargetDevice {
+ return this.widget?.config?.targetDevice;
}
protected settingsForm(): UntypedFormGroup {
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html
index 931f077015..f50ec5c903 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.html
@@ -27,7 +27,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts
index 9e9c46429b..197fc2d229 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts
@@ -15,7 +15,7 @@
///
import { Component } from '@angular/core';
-import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
+import { TargetDevice, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@@ -36,12 +36,8 @@ export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponen
super(store);
}
- get targetDeviceAliasId(): string {
- const aliasIds = this.widget?.config?.targetDeviceAliasIds;
- if (aliasIds && aliasIds.length) {
- return aliasIds[0];
- }
- return null;
+ get targetDevice(): TargetDevice {
+ return this.widget?.config?.targetDevice;
}
protected settingsForm(): UntypedFormGroup {
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html
index c1d6e4d2c5..12a722c77a 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html
@@ -44,7 +44,7 @@
[fxShow]="switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' ||
switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'timeseries'"
[aliasController]="aliasController"
- [targetDeviceAliasId]="targetDeviceAliasId"
+ [targetDevice]="targetDevice"
[keyType]="switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' ? dataKeyType.attribute : dataKeyType.timeseries"
required
formControlName="valueKey">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts
index c069774f8e..764bfcfce9 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts
@@ -17,11 +17,11 @@
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import {
ControlValueAccessor,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
- NG_VALIDATORS,
- NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
@@ -33,6 +33,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetService } from '@core/http/widget.service';
import { IAliasController } from '@core/api/widget-api.models';
import { EntityService } from '@core/http/entity.service';
+import { TargetDevice } from '@shared/models/widget.models';
export declare type RpcRetrieveValueMethod = 'none' | 'rpc' | 'attribute' | 'timeseries';
@@ -49,8 +50,7 @@ export interface SwitchRpcSettings {
persistentPollingInterval: number;
}
-export function switchRpcDefaultSettings(): SwitchRpcSettings {
- return {
+export const switchRpcDefaultSettings = (): SwitchRpcSettings => ({
initialValue: false,
retrieveValueMethod: 'rpc',
valueKey: 'value',
@@ -61,8 +61,7 @@ export function switchRpcDefaultSettings(): SwitchRpcSettings {
requestTimeout: 500,
requestPersistent: false,
persistentPollingInterval: 5000
- };
-}
+ });
@Component({
selector: 'tb-switch-rpc-settings',
@@ -92,7 +91,7 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit,
aliasController: IAliasController;
@Input()
- targetDeviceAliasId: string;
+ targetDevice: TargetDevice;
dataKeyType = DataKeyType;
@@ -185,8 +184,7 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit,
}
private updateModel() {
- const value: SwitchRpcSettings = this.switchRpcSettingsFormGroup.value;
- this.modelValue = value;
+ this.modelValue = this.switchRpcSettingsFormGroup.value;
this.propagateChange(this.modelValue);
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
index 7b32a1ca4a..30c1b748de 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
@@ -123,9 +123,6 @@ import {
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';
@@ -311,6 +308,9 @@ import {
import {
BarChartWithLabelsWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component';
+import {
+ SingleSwitchWidgetSettingsComponent
+} from '@home/components/widget/lib/settings/control/single-switch-widget-settings.component';
@NgModule({
declarations: [
@@ -348,7 +348,6 @@ import {
FlotPieKeySettingsComponent,
ChartWidgetSettingsComponent,
DoughnutChartWidgetSettingsComponent,
- DeviceKeyAutocompleteComponent,
SwitchRpcSettingsComponent,
RoundSwitchWidgetSettingsComponent,
SwitchControlWidgetSettingsComponent,
@@ -424,7 +423,8 @@ import {
LiquidLevelCardWidgetSettingsComponent,
DoughnutWidgetSettingsComponent,
RangeChartWidgetSettingsComponent,
- BarChartWithLabelsWidgetSettingsComponent
+ BarChartWithLabelsWidgetSettingsComponent,
+ SingleSwitchWidgetSettingsComponent
],
imports: [
CommonModule,
@@ -467,7 +467,6 @@ import {
FlotPieKeySettingsComponent,
ChartWidgetSettingsComponent,
DoughnutChartWidgetSettingsComponent,
- DeviceKeyAutocompleteComponent,
SwitchRpcSettingsComponent,
RoundSwitchWidgetSettingsComponent,
SwitchControlWidgetSettingsComponent,
@@ -543,7 +542,8 @@ import {
LiquidLevelCardWidgetSettingsComponent,
DoughnutWidgetSettingsComponent,
RangeChartWidgetSettingsComponent,
- BarChartWithLabelsWidgetSettingsComponent
+ BarChartWithLabelsWidgetSettingsComponent,
+ SingleSwitchWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@@ -628,5 +628,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type