Browse Source

UI: SCADA symbol: Implement behavior settings. Improve iot svg widget initialization.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
2e4e580036
  1. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html
  2. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts
  3. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts
  4. 96
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts
  5. 160
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts
  6. 138
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html
  7. 67
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss
  8. 148
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts
  9. 3
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html
  10. 106
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts
  11. 10
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts
  12. 6
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts
  13. 2
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html
  14. 37
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts
  15. 26
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  16. 589
      ui-ngx/src/assets/widget/svg/drawing.svg

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html

@ -60,35 +60,35 @@
<div fxLayout="row" fxFlex fxLayoutAlign="end center" fxLayoutGap="8px"> <div fxLayout="row" fxFlex fxLayoutAlign="end center" fxLayoutGap="8px">
<ng-container *ngFor="let property of propertyRow.properties"> <ng-container *ngFor="let property of propertyRow.properties">
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div> <div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div>
<mat-form-field *ngIf="property.type === 'string'" [class]="property.fieldClass" appearance="outline" subscriptSizing="dynamic"> <mat-form-field *ngIf="property.type === IotSvgPropertyType.text" [class]="property.fieldClass" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="{{ property.id }}" [required]="property.required" placeholder="{{ 'widget-config.set' | translate }}">
<div fxHide.lt-md matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div> <div fxHide.lt-md matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field> </mat-form-field>
<tb-color-input *ngIf="property.type === 'color'" <tb-color-input *ngIf="property.type === IotSvgPropertyType.color"
[required]="property.required" [required]="property.required"
[class]="property.fieldClass" [class]="property.fieldClass"
asBoxInput asBoxInput
colorClearButton colorClearButton
formControlName="{{ property.id }}"> formControlName="{{ property.id }}">
</tb-color-input> </tb-color-input>
<tb-color-settings *ngIf="property.type === 'color-settings'" <tb-color-settings *ngIf="property.type === IotSvgPropertyType.color_settings"
[class]="property.fieldClass" [class]="property.fieldClass"
formControlName="{{ property.id }}" formControlName="{{ property.id }}"
settingsKey="{{ property.name | customTranslate }}"> settingsKey="{{ property.name | customTranslate }}">
</tb-color-settings> </tb-color-settings>
<mat-form-field *ngIf="property.type === 'number'" [class]="property.fieldClass" appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field *ngIf="property.type === IotSvgPropertyType.number" [class]="property.fieldClass" appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required" <input matInput formControlName="{{ property.id }}" [required]="property.required"
[min]="property.min" [max]="property.max" [step]="property.step" [min]="property.min" [max]="property.max" [step]="property.step"
type="number" placeholder="{{ 'widget-config.set' | translate }}"> type="number" placeholder="{{ 'widget-config.set' | translate }}">
<div fxHide.lt-md matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div> <div fxHide.lt-md matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field> </mat-form-field>
<tb-font-settings *ngIf="property.type === 'font'" <tb-font-settings *ngIf="property.type === IotSvgPropertyType.font"
[class]="property.fieldClass" [class]="property.fieldClass"
formControlName="{{ property.id }}" formControlName="{{ property.id }}"
clearButton clearButton
disabledLineHeight> disabledLineHeight>
</tb-font-settings> </tb-font-settings>
<tb-unit-input *ngIf="property.type === 'units'" <tb-unit-input *ngIf="property.type === IotSvgPropertyType.units"
[class]="property.fieldClass" [class]="property.fieldClass"
[required]="property.required" [required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input> formControlName="{{ property.id }}"></tb-unit-input>

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts

@ -30,13 +30,13 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
defaultIotSvgObjectSettings, defaultIotSvgObjectSettings,
parseIotSvgMetadataFromContent,
IotSvgBehaviorType, IotSvgBehaviorType,
IotSvgMetadata, IotSvgMetadata,
IotSvgObjectSettings IotSvgObjectSettings,
IotSvgPropertyType,
parseIotSvgMetadataFromContent
} from '@home/components/widget/lib/svg/iot-svg.models'; } from '@home/components/widget/lib/svg/iot-svg.models';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models'; import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
@ -69,6 +69,8 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
IotSvgBehaviorType = IotSvgBehaviorType; IotSvgBehaviorType = IotSvgBehaviorType;
IotSvgPropertyType = IotSvgPropertyType;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -205,7 +207,7 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control
if (property.required) { if (property.required) {
validators.push(Validators.required); validators.push(Validators.required);
} }
if (property.type === 'number') { if (property.type === IotSvgPropertyType.number) {
if (isDefinedAndNotNull(property.min)) { if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min)); validators.push(Validators.min(property.min));
} }

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { IotSvgProperty } from '@home/components/widget/lib/svg/iot-svg.models'; import { IotSvgProperty, IotSvgPropertyType } from '@home/components/widget/lib/svg/iot-svg.models';
export interface IotSvgPropertyRow { export interface IotSvgPropertyRow {
label: string; label: string;
@ -35,7 +35,7 @@ export const toPropertyRows = (properties: IotSvgProperty[]): IotSvgPropertyRow[
}; };
result.push(propertyRow); result.push(propertyRow);
} }
if (property.type === 'switch') { if (property.type === IotSvgPropertyType.switch) {
propertyRow.switch = property; propertyRow.switch = property;
} else { } else {
propertyRow.properties.push(property); propertyRow.properties.push(property);

96
ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts

@ -19,18 +19,18 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Renderer2, Renderer2,
TemplateRef,
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { IotSvgObject } from '@home/components/widget/lib/svg/iot-svg.models'; import { IotSvgObject, IotSvgObjectCallbacks } from '@home/components/widget/lib/svg/iot-svg.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { import {
iotSvgWidgetDefaultSettings, iotSvgWidgetDefaultSettings,
IotSvgWidgetSettings IotSvgWidgetSettings
@ -40,6 +40,7 @@ import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/wi
import { ImageService } from '@core/http/image.service'; import { ImageService } from '@core/http/image.service';
import { WidgetComponent } from '@home/components/widget/widget.component'; import { WidgetComponent } from '@home/components/widget/widget.component';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import { WidgetContext } from '@home/models/widget-component.models';
@Component({ @Component({
selector: 'tb-iot-svg-widget', selector: 'tb-iot-svg-widget',
@ -47,110 +48,79 @@ import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
styleUrls: ['../action/action-widget.scss', './iot-svg-widget.component.scss'], styleUrls: ['../action/action-widget.scss', './iot-svg-widget.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class IotSvgWidgetComponent extends export class IotSvgWidgetComponent implements OnInit, AfterViewInit, OnDestroy, IotSvgObjectCallbacks {
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('iotSvgShape', {static: false}) @ViewChild('iotSvgShape', {static: false})
iotSvgShape: ElementRef<HTMLElement>; iotSvgShape: ElementRef<HTMLElement>;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
private settings: IotSvgWidgetSettings; private settings: IotSvgWidgetSettings;
private svgContent$: Observable<string>;
backgroundStyle$: Observable<ComponentStyle>; backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {}; overlayStyle: ComponentStyle = {};
iotSvgObject: IotSvgObject; iotSvgObject: IotSvgObject;
private autoScale = true;
private shapeResize$: ResizeObserver;
constructor(public widgetComponent: WidgetComponent, constructor(public widgetComponent: WidgetComponent,
protected imagePipe: ImagePipe, protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer, protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private imageService: ImageService, private imageService: ImageService,
protected cd: ChangeDetectorRef, protected cd: ChangeDetectorRef,
private http: HttpClient) { private http: HttpClient) {
super(cd);
} }
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); this.ctx.$scope.actionWidget = this;
this.settings = mergeDeep({} as IotSvgWidgetSettings, iotSvgWidgetDefaultSettings, this.ctx.settings || {}); this.settings = mergeDeep({} as IotSvgWidgetSettings, iotSvgWidgetDefaultSettings, this.ctx.settings || {});
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay); this.overlayStyle = overlayStyle(this.settings.background.overlay);
let svgContent$: Observable<string>;
if (this.settings.iotSvgContent) { if (this.settings.iotSvgContent) {
svgContent$ = of(this.settings.iotSvgContent); this.svgContent$ = of(this.settings.iotSvgContent);
} else if (this.settings.iotSvgUrl) { } else if (this.settings.iotSvgUrl) {
svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl); this.svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl);
} else { } else {
svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'}); this.svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'});
}
svgContent$.subscribe(
(content) => {
this.initObject(content);
}
);
}
private initObject(svgContent: string) {
const simulated = this.ctx.utilsService.widgetEditMode ||
this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false);
this.iotSvgObject = new IotSvgObject(this.ctx, svgContent, this.settings.iotSvgObject, simulated);
this.iotSvgObject.onError((error) => {
this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true);
});
this.iotSvgObject.onMessage((message) => {
this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true);
});
this.iotSvgObject.init();
if (this.iotSvgShape) {
this.iotSvgObject.addTo(this.iotSvgShape.nativeElement);
if (this.autoScale) {
this.onResize();
}
} }
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
if (this.iotSvgObject) { this.svgContent$.subscribe((content) => {
this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); this.initObject(this.iotSvgShape.nativeElement, content);
} });
if (this.autoScale) {
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.iotSvgShape.nativeElement);
this.onResize();
}
super.ngAfterViewInit();
} }
ngOnDestroy() { ngOnDestroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
if (this.iotSvgObject) { if (this.iotSvgObject) {
this.iotSvgObject.destroy(); this.iotSvgObject.destroy();
} }
super.ngOnDestroy();
} }
public onInit() { public onInit() {
super.onInit();
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges(); this.cd.detectChanges();
} }
private onResize() { onSvgObjectError(error: string) {
const shapeWidth = this.iotSvgShape.nativeElement.getBoundingClientRect().width; this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true);
const shapeHeight = this.iotSvgShape.nativeElement.getBoundingClientRect().height;
if (this.iotSvgObject) {
this.iotSvgObject.setSize(shapeWidth, shapeHeight);
}
} }
onSvgObjectMessage(message: string) {
this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true);
}
private initObject(rootElement: HTMLElement,
svgContent: string) {
const simulated = this.ctx.utilsService.widgetEditMode ||
this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false);
this.iotSvgObject = new IotSvgObject(rootElement, this.ctx, svgContent, this.settings.iotSvgObject, this, simulated);
}
} }

160
ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts

@ -15,7 +15,7 @@
/// ///
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, SVG, Svg, Text } from '@svgdotjs/svg.js'; import { Box, Element, Runner, SVG, Svg, Text, Timeline } from '@svgdotjs/svg.js';
import '@svgdotjs/svg.panzoom.js'; import '@svgdotjs/svg.panzoom.js';
import { import {
DataToValueType, DataToValueType,
@ -33,7 +33,7 @@ import {
mergeDeep, mergeDeep,
parseFunction parseFunction
} from '@core/utils'; } from '@core/utils';
import { BehaviorSubject, forkJoin, Observable, Observer, of } from 'rxjs'; import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs';
import { share } from 'rxjs/operators'; import { share } from 'rxjs/operators';
import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
@ -41,6 +41,7 @@ import { ColorProcessor, constantColor, Font } from '@shared/models/widget-setti
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { WidgetAction, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; import { WidgetAction, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models';
import { ResizeObserver } from '@juggle/resize-observer';
export interface IotSvgApi { export interface IotSvgApi {
formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined;
@ -112,14 +113,37 @@ export interface IotSvgBehaviorValue extends IotSvgBehaviorBase {
} }
export interface IotSvgBehaviorAction extends IotSvgBehaviorBase { export interface IotSvgBehaviorAction extends IotSvgBehaviorBase {
valueType: ValueType;
valueToDataType: ValueToDataType; valueToDataType: ValueToDataType;
constantValue: any; constantValue: any;
valueToDataFunction: string; valueToDataFunction: string;
} }
export type IotSvgBehavior = IotSvgBehaviorValue | IotSvgBehaviorAction; export type IotSvgBehavior = IotSvgBehaviorValue & IotSvgBehaviorAction;
export enum IotSvgPropertyType {
text = 'text',
number = 'number',
switch = 'switch',
color = 'color',
color_settings = 'color_settings',
font = 'font',
units = 'units'
}
export const iotSvgPropertyTypes = Object.keys(IotSvgPropertyType) as IotSvgPropertyType[];
export type IotSvgPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch'; export const iotSvgPropertyTypeTranslations = new Map<IotSvgPropertyType, string>(
[
[IotSvgPropertyType.text, 'scada.property.type-text'],
[IotSvgPropertyType.number, 'scada.property.type-number'],
[IotSvgPropertyType.switch, 'scada.property.type-switch'],
[IotSvgPropertyType.color, 'scada.property.type-color'],
[IotSvgPropertyType.color_settings, 'scada.property.type-color-settings'],
[IotSvgPropertyType.font, 'scada.property.type-font'],
[IotSvgPropertyType.units, 'scada.property.type-units']
]
);
export interface IotSvgPropertyBase { export interface IotSvgPropertyBase {
id: string; id: string;
@ -320,17 +344,19 @@ export type IotSvgObjectSettings = {
const parseError = (ctx: WidgetContext, err: any): string => const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
export interface IotSvgObjectCallbacks {
onSvgObjectError: (error: string) => void;
onSvgObjectMessage: (message: string) => void;
}
export class IotSvgObject { export class IotSvgObject {
private metadata: IotSvgMetadata; private metadata: IotSvgMetadata;
private settings: IotSvgObjectSettings; private settings: IotSvgObjectSettings;
private context: IotSvgContext; private context: IotSvgContext;
private rootElement: HTMLElement;
private svgShape: Svg; private svgShape: Svg;
private box: Box; private box: Box;
private targetWidth: number;
private targetHeight: number;
private loadingSubject = new BehaviorSubject(false); private loadingSubject = new BehaviorSubject(false);
private valueGetters: ValueGetter<any>[] = []; private valueGetters: ValueGetter<any>[] = [];
@ -339,19 +365,22 @@ export class IotSvgObject {
private stateValueSubjects: {[id: string]: BehaviorSubject<any>} = {}; private stateValueSubjects: {[id: string]: BehaviorSubject<any>} = {};
loading$ = this.loadingSubject.asObservable().pipe(share()); private readonly shapeResize$: ResizeObserver;
private scale = 1;
private _onError: (error: string) => void = () => {}; private performInit = true;
private _onMessage: (message: string) => void = () => {}; loading$ = this.loadingSubject.asObservable().pipe(share());
constructor(private ctx: WidgetContext, constructor(private rootElement: HTMLElement,
private ctx: WidgetContext,
private svgContent: string, private svgContent: string,
private inputSettings: IotSvgObjectSettings, private inputSettings: IotSvgObjectSettings,
private callbacks: IotSvgObjectCallbacks,
private simulated: boolean) { private simulated: boolean) {
} this.shapeResize$ = new ResizeObserver(() => {
this.resize();
public init() { });
const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml'); const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml');
this.metadata = parseIotSvgMetadataFromDom(doc); this.metadata = parseIotSvgMetadataFromDom(doc);
const defaults = defaultIotSvgObjectSettings(this.metadata); const defaults = defaultIotSvgObjectSettings(this.metadata);
@ -359,25 +388,13 @@ export class IotSvgObject {
defaults, this.inputSettings || {} as IotSvgObjectSettings); defaults, this.inputSettings || {} as IotSvgObjectSettings);
this.prepareMetadata(); this.prepareMetadata();
this.prepareSvgShape(doc); this.prepareSvgShape(doc);
this.initialize(); this.shapeResize$.observe(this.rootElement);
}
public onError(onError: (error: string) => void) {
this._onError = onError;
}
public onMessage(onMessage: (message: string) => void) {
this._onMessage = onMessage;
}
public addTo(element: HTMLElement) {
this.rootElement = element;
if (this.svgShape) {
this.svgShape.addTo(element);
}
} }
public destroy() { public destroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
for (const stateValueId of Object.keys(this.stateValueSubjects)) { for (const stateValueId of Object.keys(this.stateValueSubjects)) {
this.stateValueSubjects[stateValueId].complete(); this.stateValueSubjects[stateValueId].complete();
this.stateValueSubjects[stateValueId].unsubscribe(); this.stateValueSubjects[stateValueId].unsubscribe();
@ -385,16 +402,15 @@ export class IotSvgObject {
this.valueActions.forEach(v => v.destroy()); this.valueActions.forEach(v => v.destroy());
this.loadingSubject.complete(); this.loadingSubject.complete();
this.loadingSubject.unsubscribe(); this.loadingSubject.unsubscribe();
if (this.svgShape) { for (const tag of this.metadata.tags) {
this.svgShape.remove(); const elements = this.context.tags[tag.tag];
elements.forEach(element => {
element.timeline().finish();
element.timeline(null);
});
} }
}
public setSize(targetWidth: number, targetHeight: number) {
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
if (this.svgShape) { if (this.svgShape) {
this.resize(); this.svgShape.remove();
} }
} }
@ -421,15 +437,10 @@ export class IotSvgObject {
this.svgShape.node.style['user-select'] = 'none'; this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox(); this.box = this.svgShape.bbox();
this.svgShape.size(this.box.width, this.box.height); this.svgShape.size(this.box.width, this.box.height);
if (this.rootElement) { this.svgShape.addTo(this.rootElement);
this.svgShape.addTo(this.rootElement);
}
if (this.targetWidth && this.targetHeight) {
this.resize();
}
} }
private initialize() { private init() {
this.context = { this.context = {
api: { api: {
formatValue, formatValue,
@ -447,7 +458,7 @@ export class IotSvgObject {
}; };
const taggedElements = this.svgShape.find(`[tb\\:tag]`); const taggedElements = this.svgShape.find(`[tb\\:tag]`);
for (const element of taggedElements) { for (const element of taggedElements) {
const tag = element.attr('tb:tag'); const tag: string = element.attr('tb:tag');
let elements = this.context.tags[tag]; let elements = this.context.tags[tag];
if (!elements) { if (!elements) {
elements = []; elements = [];
@ -489,7 +500,7 @@ export class IotSvgObject {
next: (val) => {this.onValue(getBehavior.id, val);}, next: (val) => {this.onValue(getBehavior.id, val);},
error: (err) => { error: (err) => {
const message = parseError(this.ctx, err); const message = parseError(this.ctx, err);
this._onError(message); this.onError(message);
} }
}, this.simulated); }, this.simulated);
this.valueGetters.push(valueGetter); this.valueGetters.push(valueGetter);
@ -526,6 +537,14 @@ export class IotSvgObject {
} }
} }
private onError(error: string) {
this.callbacks.onSvgObjectError(error);
}
private onMessage(message: string) {
this.callbacks.onSvgObjectMessage(message);
}
private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) { private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) {
const behavior = this.metadata.behavior.find(b => b.id === behaviorId); const behavior = this.metadata.behavior.find(b => b.id === behaviorId);
if (behavior) { if (behavior) {
@ -547,7 +566,7 @@ export class IotSvgObject {
observer.error(err); observer.error(err);
} }
const message = parseError(this.ctx, err); const message = parseError(this.ctx, err);
this._onError(message); this.onError(message);
} }
} }
); );
@ -557,22 +576,38 @@ export class IotSvgObject {
if (this.simulated) { if (this.simulated) {
const translatedType = this.ctx.translate.instant(widgetActionTypeTranslationMap.get(widgetAction.type)); const translatedType = this.ctx.translate.instant(widgetActionTypeTranslationMap.get(widgetAction.type));
const message = this.ctx.translate.instant('scada.preview-widget-action-text', {type: translatedType}); const message = this.ctx.translate.instant('scada.preview-widget-action-text', {type: translatedType});
this._onMessage(message); this.onMessage(message);
} else { } else {
this.ctx.actionsApi.onWidgetAction(event, widgetAction); this.ctx.actionsApi.onWidgetAction(event, widgetAction);
} }
if (observer?.next) {
observer.next();
}
} }
} }
} }
private resize() { private resize() {
let scale: number; if (this.svgShape) {
if (this.targetWidth < this.targetHeight) { const targetWidth = this.rootElement.getBoundingClientRect().width;
scale = this.targetWidth / this.box.width; const targetHeight = this.rootElement.getBoundingClientRect().height;
} else { if (targetWidth && targetHeight) {
scale = this.targetHeight / this.box.height; let scale: number;
if (targetWidth < targetHeight) {
scale = targetWidth / this.box.width;
} else {
scale = targetHeight / this.box.height;
}
if (this.scale !== scale) {
this.scale = scale;
this.svgShape.node.style.transform = `scale(${this.scale})`;
}
if (this.performInit) {
this.performInit = false;
this.init();
}
}
} }
this.svgShape.node.style.transform = `scale(${scale})`;
} }
private onValue(id: string, value: any) { private onValue(id: string, value: any) {
@ -598,7 +633,7 @@ export class IotSvgObject {
private renderState(): void { private renderState(): void {
this.metadata.stateRender(this.svgShape, this.context); this.metadata.stateRender(this.svgShape, this.context);
for (const tag of this.metadata.tags) { for (const tag of this.metadata.tags) {
const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); const elements = this.context.tags[tag.tag];// this.svgShape.find(`[tb\\:tag="${tag.tag}"]`);
elements.forEach(element => { elements.forEach(element => {
tag.stateRender(element, this.context); tag.stateRender(element, this.context);
}); });
@ -664,6 +699,7 @@ export class IotSvgObject {
private animate(element: Element, duration: number): Runner { private animate(element: Element, duration: number): Runner {
element.timeline().finish(); element.timeline().finish();
element.timeline(new Timeline());
return element.animate(duration, 0, 'now'); return element.animate(duration, 0, 'now');
} }
@ -692,9 +728,9 @@ export class IotSvgObject {
if (property) { if (property) {
const value = this.settings.properties[id]; const value = this.settings.properties[id];
if (isDefinedAndNotNull(value)) { if (isDefinedAndNotNull(value)) {
if (property.type === 'color-settings') { if (property.type === IotSvgPropertyType.color_settings) {
return ColorProcessor.fromSettings(value); return ColorProcessor.fromSettings(value);
} else if (property.type === 'string') { } else if (property.type === IotSvgPropertyType.text) {
const result = this.ctx.utilsService.customTranslation(value, value); const result = this.ctx.utilsService.customTranslation(value, value);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo(); const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
return createLabelFromSubscriptionEntityInfo(entityInfo, result); return createLabelFromSubscriptionEntityInfo(entityInfo, result);
@ -702,13 +738,13 @@ export class IotSvgObject {
return value; return value;
} else { } else {
switch (property.type) { switch (property.type) {
case 'string': case IotSvgPropertyType.text:
return ''; return '';
case 'number': case IotSvgPropertyType.number:
return 0; return 0;
case 'color': case IotSvgPropertyType.color:
return '#000'; return '#000';
case 'color-settings': case IotSvgPropertyType.color_settings:
return ColorProcessor.fromSettings(constantColor('#000')); return ColorProcessor.fromSettings(constantColor('#000'));
} }
} }

138
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html

@ -0,0 +1,138 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-scada-symbol-behavior-settings-panel" [formGroup]="behaviorFormGroup">
<div class="tb-scada-symbol-behavior-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-scada-symbol-behavior-settings-panel-content">
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.id</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.name</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.hint</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="hint" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.type</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of iotSvgBehaviorTypes" [value]="type">
{{ iotSvgBehaviorTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="[IotSvgBehaviorType.value, IotSvgBehaviorType.action].includes(behaviorFormGroup.get('type').value)" class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.value-type</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex tb-value-type">
<mat-select formControlName="valueType">
<mat-select-trigger>
<div class="tb-value-type-row">
<mat-icon class="tb-mat-18" svgIcon="{{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).icon }}"></mat-icon>
<span>{{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }}</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let valueType of valueTypes" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypesMap.get(valueType).icon }}"></mat-icon>
<span>{{ valueTypesMap.get(valueType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container *ngIf="behaviorFormGroup.get('type').value === IotSvgBehaviorType.value">
<div class="tb-form-row ">
<div class="fixed-title-width" translate>scada.behavior.default-value</div>
<tb-value-input fxFlex [valueType]="behaviorFormGroup.get('valueType').value"
formControlName="defaultValue"></tb-value-input>
</div>
<ng-container *ngIf="behaviorFormGroup.get('valueType').value === ValueType.BOOLEAN">
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.true-label</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="trueLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.false-label</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="falseLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.behavior.state-label</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="stateLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</ng-container>
</ng-container>
<ng-container *ngIf="behaviorFormGroup.get('type').value === IotSvgBehaviorType.action">
<div class="tb-form-panel stroked">
<div class="tb-form-row no-padding no-border column-xs">
<div class="fixed-title-width">{{ 'scada.behavior.default-payload' | translate }}</div>
<tb-toggle-select fxFlex formControlName="valueToDataType">
<tb-toggle-option *ngIf="behaviorFormGroup.get('valueType').value !== ValueType.BOOLEAN" [value]="ValueToDataType.VALUE">{{ 'widgets.value-action.converter-value' | translate }}</tb-toggle-option>
<tb-toggle-option *ngIf="behaviorFormGroup.get('valueType').value === ValueType.BOOLEAN" [value]="ValueToDataType.CONSTANT">{{ 'widgets.value-action.converter-constant' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ValueToDataType.FUNCTION">{{ 'widgets.value-action.converter-function' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ValueToDataType.NONE">{{ 'widgets.value-action.converter-none' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-value-input *ngIf="behaviorFormGroup.get('valueToDataType').value === ValueToDataType.CONSTANT"
fxFlex
[layout]="{
layout: 'row',
breakpoints: {xs: 'column'}
}"
formControlName="constantValue">
</tb-value-input>
<tb-js-func *ngIf="behaviorFormGroup.get('valueToDataType').value === ValueToDataType.FUNCTION"
formControlName="valueToDataFunction"
required
[globalVariables]="functionScopeVariables"
[functionArgs]="['value']"
functionTitle="{{ 'widgets.value-action.convert-value-function' | translate }}"
helpId="widget/lib/rpc/convert_value_fn">
</tb-js-func>
</div>
</ng-container>
</div>
<div class="tb-scada-symbol-behavior-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyBehaviorSettings()"
[disabled]="behaviorFormGroup.invalid || !behaviorFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.apply') | translate }}
</button>
</div>
</div>

67
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss

@ -0,0 +1,67 @@
/**
* 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-scada-symbol-behavior-settings-panel {
width: 540px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-scada-symbol-behavior-settings-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
margin: -10px;
padding: 10px;
.mat-mdc-form-field.tb-value-type {
.mat-mdc-form-field-infix {
max-height: 40px;
}
mat-select-trigger {
.tb-value-type-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
vertical-align: bottom;
}
}
}
}
}
.tb-scada-symbol-behavior-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-scada-symbol-behavior-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

148
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts

@ -0,0 +1,148 @@
///
/// 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 { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { merge } from 'rxjs';
import {
IotSvgBehavior,
IotSvgBehaviorType,
iotSvgBehaviorTypes,
iotSvgBehaviorTypeTranslations
} from '@app/modules/home/components/widget/lib/svg/iot-svg.models';
import { ValueType, valueTypesMap } from '@shared/models/constants';
import { ValueToDataType } from '@shared/models/action-widget-settings.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-scada-symbol-behavior-panel',
templateUrl: './scada-symbol-behavior-panel.component.html',
styleUrls: ['./scada-symbol-behavior-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolBehaviorPanelComponent implements OnInit {
IotSvgBehaviorType = IotSvgBehaviorType;
ValueType = ValueType;
ValueToDataType = ValueToDataType;
iotSvgBehaviorTypes = iotSvgBehaviorTypes;
iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations;
valueTypes = Object.keys(ValueType) as ValueType[];
valueTypesMap = valueTypesMap;
@Input()
isAdd = false;
@Input()
behavior: IotSvgBehavior;
@Input()
popover: TbPopoverComponent<ScadaSymbolBehaviorPanelComponent>;
@Output()
behaviorSettingsApplied = new EventEmitter<IotSvgBehavior>();
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
panelTitle: string;
behaviorFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService) {
}
ngOnInit(): void {
this.panelTitle = this.isAdd ? 'scada.behavior.add-behavior' : 'scada.behavior.behavior-settings';
this.behaviorFormGroup = this.fb.group(
{
id: [this.behavior.id, [Validators.required]],
name: [this.behavior.name, [Validators.required]],
hint: [this.behavior.hint, []],
type: [this.behavior.type, [Validators.required]],
valueType: [this.behavior.valueType, [Validators.required]],
defaultValue: [this.behavior.defaultValue, [Validators.required]],
trueLabel: [this.behavior.trueLabel, []],
falseLabel: [this.behavior.falseLabel, []],
stateLabel: [this.behavior.stateLabel, []],
valueToDataType: [this.behavior.valueToDataType, [Validators.required]],
constantValue: [this.behavior.constantValue, [Validators.required]],
valueToDataFunction: [this.behavior.valueToDataFunction, [Validators.required]]
}
);
merge(this.behaviorFormGroup.get('type').valueChanges,
this.behaviorFormGroup.get('valueType').valueChanges,
this.behaviorFormGroup.get('valueToDataType').valueChanges).subscribe(() => {
this.updateValidators();
});
this.updateValidators();
}
cancel() {
this.popover?.hide();
}
applyBehaviorSettings() {
const behavior = this.behaviorFormGroup.getRawValue();
this.behaviorSettingsApplied.emit(behavior);
}
private updateValidators() {
const type: IotSvgBehaviorType = this.behaviorFormGroup.get('type').value;
const valueType: ValueType = this.behaviorFormGroup.get('valueType').value;
let valueToDataType: ValueToDataType = this.behaviorFormGroup.get('valueToDataType').value;
this.behaviorFormGroup.disable({emitEvent: false});
this.behaviorFormGroup.get('id').enable({emitEvent: false});
this.behaviorFormGroup.get('name').enable({emitEvent: false});
this.behaviorFormGroup.get('type').enable({emitEvent: false});
this.behaviorFormGroup.get('hint').enable({emitEvent: false});
switch (type) {
case IotSvgBehaviorType.value:
this.behaviorFormGroup.get('valueType').enable({emitEvent: false});
this.behaviorFormGroup.get('defaultValue').enable({emitEvent: false});
if (valueType === ValueType.BOOLEAN) {
this.behaviorFormGroup.get('trueLabel').enable({emitEvent: false});
this.behaviorFormGroup.get('falseLabel').enable({emitEvent: false});
this.behaviorFormGroup.get('stateLabel').enable({emitEvent: false});
}
break;
case IotSvgBehaviorType.action:
if (valueType === ValueType.BOOLEAN && valueToDataType === ValueToDataType.VALUE) {
this.behaviorFormGroup.patchValue({valueToDataType: ValueToDataType.CONSTANT}, {emitEvent: false});
valueToDataType = ValueToDataType.CONSTANT;
} else if (valueType !== ValueType.BOOLEAN && valueToDataType === ValueToDataType.CONSTANT) {
this.behaviorFormGroup.patchValue({valueToDataType: ValueToDataType.VALUE}, {emitEvent: false});
valueToDataType = ValueToDataType.VALUE;
}
this.behaviorFormGroup.get('valueType').enable({emitEvent: false});
this.behaviorFormGroup.get('valueToDataType').enable({emitEvent: false});
if (valueToDataType === ValueToDataType.CONSTANT) {
this.behaviorFormGroup.get('constantValue').enable({emitEvent: false});
} else if (valueToDataType === ValueToDataType.FUNCTION) {
this.behaviorFormGroup.get('valueToDataFunction').enable({emitEvent: false});
}
break;
case IotSvgBehaviorType.widgetAction:
break;
}
}
}

3
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html

@ -32,7 +32,8 @@
<div class="tb-form-table-row-cell-buttons"> <div class="tb-form-table-row-cell-buttons">
<button type="button" <button type="button"
mat-icon-button mat-icon-button
(click)="editBehavior()" #editButton
(click)="editBehavior($event, editButton)"
matTooltip="{{ 'scada.behavior.behavior-settings' | translate }}" matTooltip="{{ 'scada.behavior.behavior-settings' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>

106
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts

@ -23,7 +23,9 @@ import {
Input, Input,
OnInit, OnInit,
Output, Output,
Renderer2,
ViewChild, ViewChild,
ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { import {
@ -32,17 +34,22 @@ import {
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormGroup, UntypedFormGroup,
ValidationErrors ValidationErrors,
Validators
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { import {
IotSvgBehavior, IotSvgBehavior,
IotSvgBehaviorAction,
IotSvgBehaviorType, IotSvgBehaviorType,
iotSvgBehaviorTypes, iotSvgBehaviorTypes,
iotSvgBehaviorTypeTranslations, iotSvgBehaviorTypeTranslations
IotSvgBehaviorValue
} from '@home/components/widget/lib/svg/iot-svg.models'; } from '@home/components/widget/lib/svg/iot-svg.models';
import { deepClone, isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import {
ScadaSymbolBehaviorPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component';
import { ValueToDataType } from '@shared/models/action-widget-settings.models';
export const behaviorValid = (behavior: IotSvgBehavior): boolean => { export const behaviorValid = (behavior: IotSvgBehavior): boolean => {
if (!behavior.id || !behavior.name || !behavior.type) { if (!behavior.id || !behavior.name || !behavior.type) {
@ -50,14 +57,20 @@ export const behaviorValid = (behavior: IotSvgBehavior): boolean => {
} }
switch (behavior.type) { switch (behavior.type) {
case IotSvgBehaviorType.value: case IotSvgBehaviorType.value:
const valueBehavior = behavior as IotSvgBehaviorValue; if (!behavior.valueType || isUndefinedOrNull(behavior.defaultValue)) {
if (!valueBehavior.valueType) {
return false; return false;
} }
break; break;
case IotSvgBehaviorType.action: case IotSvgBehaviorType.action:
const actionBehavior = behavior as IotSvgBehaviorAction; if (!behavior.valueToDataType) {
if (!actionBehavior.valueToDataType) { return false;
}
if (behavior.valueToDataType === ValueToDataType.CONSTANT
&& isUndefinedOrNull(behavior.constantValue)) {
return false;
}
if (behavior.valueToDataType === ValueToDataType.FUNCTION
&& isUndefinedOrNull(behavior.valueToDataFunction)) {
return false; return false;
} }
break; break;
@ -95,6 +108,9 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On
@ViewChild('idInput') @ViewChild('idInput')
idInput: ElementRef<HTMLInputElement>; idInput: ElementRef<HTMLInputElement>;
@ViewChild('editButton')
editButton: MatButton;
iotSvgBehaviorTypes = iotSvgBehaviorTypes; iotSvgBehaviorTypes = iotSvgBehaviorTypes;
iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations; iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations;
@ -111,19 +127,24 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog, private cd: ChangeDetectorRef,
private cd: ChangeDetectorRef) { private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {
} }
ngOnInit() { ngOnInit() {
this.behaviorRowFormGroup = this.fb.group({ this.behaviorRowFormGroup = this.fb.group({
id: [null, []], id: [null, [Validators.required]],
name: [null, []], name: [null, [Validators.required]],
type: [null, []] type: [null, [Validators.required]]
}); });
this.behaviorRowFormGroup.valueChanges.subscribe( this.behaviorRowFormGroup.valueChanges.subscribe(
() => this.updateModel() () => this.updateModel()
); );
this.behaviorRowFormGroup.get('type').valueChanges.subscribe((newType: IotSvgBehaviorType) => {
this.onTypeChanged(newType);
});
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -154,8 +175,42 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On
this.cd.markForCheck(); this.cd.markForCheck();
} }
editBehavior() { editBehavior($event: Event, matButton: MatButton, add = false, editCanceled = () => {}) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
isAdd: add,
behavior: deepClone(this.modelValue)
};
const scadaSymbolBehaviorPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ScadaSymbolBehaviorPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
scadaSymbolBehaviorPanelPopover.tbComponentRef.instance.popover = scadaSymbolBehaviorPanelPopover;
scadaSymbolBehaviorPanelPopover.tbComponentRef.instance.behaviorSettingsApplied.subscribe((behavior) => {
scadaSymbolBehaviorPanelPopover.hide();
this.behaviorRowFormGroup.patchValue(
{
id: behavior.id,
name: behavior.name,
type: behavior.type
}, {emitEvent: false}
);
this.modelValue = behavior;
this.propagateChange(this.modelValue);
});
scadaSymbolBehaviorPanelPopover.tbDestroy.subscribe(() => {
if (!behaviorValid(this.modelValue)) {
editCanceled();
}
});
}
} }
focus() { focus() {
@ -163,6 +218,25 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On
this.idInput.nativeElement.focus(); this.idInput.nativeElement.focus();
} }
onAdd(onCanceled: () => void) {
this.idInput.nativeElement.scrollIntoView();
this.editBehavior(null, this.editButton, true, onCanceled);
}
private onTypeChanged(newType: IotSvgBehaviorType) {
const prevType = this.modelValue.type;
this.modelValue = {...this.modelValue, ...{type: newType}};
if (!behaviorValid(this.modelValue)) {
this.editBehavior(null, this.editButton, false, () => {
this.behaviorRowFormGroup.patchValue(
{
type: prevType
}, {emitEvent: true}
);
});
}
}
private updateModel() { private updateModel() {
const value: IotSvgBehavior = this.behaviorRowFormGroup.value; const value: IotSvgBehavior = this.behaviorRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value}; this.modelValue = {...this.modelValue, ...value};

10
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts

@ -43,6 +43,7 @@ import {
behaviorValidator, behaviorValidator,
ScadaSymbolBehaviorRowComponent ScadaSymbolBehaviorRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component'; } from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component';
import { ValueToDataType } from '@shared/models/action-widget-settings.models';
@Component({ @Component({
selector: 'tb-scada-symbol-metadata-behaviors', selector: 'tb-scada-symbol-metadata-behaviors',
@ -154,14 +155,19 @@ export class ScadaSymbolBehaviorsComponent implements ControlValueAccessor, OnIn
name: '', name: '',
type: IotSvgBehaviorType.value, type: IotSvgBehaviorType.value,
valueType: ValueType.BOOLEAN, valueType: ValueType.BOOLEAN,
defaultValue: false defaultValue: false,
valueToDataType: ValueToDataType.CONSTANT,
constantValue: false,
valueToDataFunction: ''
}; };
const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray;
const behaviorControl = this.fb.control(behavior, [behaviorValidator]); const behaviorControl = this.fb.control(behavior, [behaviorValidator]);
behaviorsArray.push(behaviorControl); behaviorsArray.push(behaviorControl);
setTimeout(() => { setTimeout(() => {
const behaviorRow = this.behaviorRows.get(this.behaviorRows.length-1); const behaviorRow = this.behaviorRows.get(this.behaviorRows.length-1);
behaviorRow.focus(); behaviorRow.onAdd(() => {
this.removeBehavior(behaviorsArray.length-1);
});
}); });
} }

6
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts

@ -30,6 +30,9 @@ import {
import { import {
ScadaSymbolBehaviorRowComponent ScadaSymbolBehaviorRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component'; } from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component';
import {
ScadaSymbolBehaviorPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -38,7 +41,8 @@ import {
ScadaSymbolMetadataTagComponent, ScadaSymbolMetadataTagComponent,
ScadaSymbolMetadataTagsComponent, ScadaSymbolMetadataTagsComponent,
ScadaSymbolBehaviorsComponent, ScadaSymbolBehaviorsComponent,
ScadaSymbolBehaviorRowComponent ScadaSymbolBehaviorRowComponent,
ScadaSymbolBehaviorPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

2
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html

@ -26,7 +26,7 @@
</div> </div>
</div> </div>
<div> <div>
<button type="button" mat-stroked-button color="primary" (click)="addTag()"> <button *ngIf="availableTags?.length" type="button" mat-stroked-button color="primary" (click)="addTag()">
{{ 'scada.add-tag' | translate }} {{ 'scada.add-tag' | translate }}
</button> </button>
</div> </div>

37
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts

@ -74,13 +74,14 @@ export class ScadaSymbolEditObject {
this.scale = 1; this.scale = 1;
const contentData = iotSvgContentData(svgContent); const contentData = iotSvgContentData(svgContent);
this.svgRootNodePart = contentData.svgRootNode; this.svgRootNodePart = contentData.svgRootNode;
this.svgShape = SVG().addTo(this.rootElement).svg(contentData.innerSvg); this.svgShape = SVG().svg(contentData.innerSvg);
this.svgShape.node.style.overflow = 'visible'; this.svgShape.node.style.overflow = 'visible';
this.svgShape.node.style['user-select'] = 'none'; this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox(); this.box = this.svgShape.bbox();
this.svgShape.size(this.box.width, this.box.height); this.svgShape.size(this.box.width, this.box.height);
this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`); this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`);
this.svgShape.style().attr('tb:inner', true).rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'}); this.svgShape.style().attr('tb:inner', true).rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'});
this.svgShape.addTo(this.rootElement);
this.updateHoverFilterStyle(); this.updateHoverFilterStyle();
this.performSetup = true; this.performSetup = true;
this.shapeResize$.observe(this.rootElement); this.shapeResize$.observe(this.rootElement);
@ -245,22 +246,24 @@ export class ScadaSymbolEditObject {
if (this.svgShape) { if (this.svgShape) {
const targetWidth = this.rootElement.getBoundingClientRect().width; const targetWidth = this.rootElement.getBoundingClientRect().width;
const targetHeight = this.rootElement.getBoundingClientRect().height; const targetHeight = this.rootElement.getBoundingClientRect().height;
let scale: number; if (targetWidth && targetHeight) {
if (targetWidth < targetHeight) { let scale: number;
scale = targetWidth / this.box.width; if (targetWidth < targetHeight) {
} else { scale = targetWidth / this.box.width;
scale = targetHeight / this.box.height; } else {
} scale = targetHeight / this.box.height;
if (this.scale !== scale) { }
this.scale = scale; if (this.scale !== scale) {
this.svgShape.node.style.transform = `scale(${this.scale})`; this.scale = scale;
this.updateHoverFilterStyle(); this.svgShape.node.style.transform = `scale(${this.scale})`;
this.updateZoomOptions(); this.updateHoverFilterStyle();
this.updateTooltipPositions(); this.updateZoomOptions();
} this.updateTooltipPositions();
if (this.performSetup) { }
this.performSetup = false; if (this.performSetup) {
this.doSetup(); this.performSetup = false;
this.doSetup();
}
} }
} }
} }

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

@ -3432,7 +3432,31 @@
"type-value": "Value", "type-value": "Value",
"type-widget-action": "Widget action", "type-widget-action": "Widget action",
"behavior-settings": "Behavior settings", "behavior-settings": "Behavior settings",
"remove-behavior": "Remove behavior" "remove-behavior": "Remove behavior",
"hint": "Hint",
"value-type": "Value type",
"default-value": "Default value",
"true-label": "True label",
"false-label": "False label",
"state-label": "State label",
"default-payload": "Default payload"
},
"property": {
"property": "Property",
"id": "Id",
"name": "Name",
"type": "Type",
"type-text": "Text",
"type-number": "Number",
"type-switch": "Switch",
"type-color": "Color",
"type-color-settings": "Color settings",
"type-font": "Font",
"type-units": "Units",
"no-properties": "No properties configured",
"add-property": "Add property",
"property-settings": "Property settings",
"remove-property": "Remove property"
} }
}, },
"item": { "item": {

589
ui-ngx/src/assets/widget/svg/drawing.svg

@ -1,307 +1,310 @@
<svg width="100" height="100" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" version="1.1" viewBox="0 0 100 100">
<tb:metadata><![CDATA[{ <tb:metadata xmlns=""><![CDATA[{
"title": "My first IoT SVG Object", "title": "Level and Fan12",
"stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}", "stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}",
"tags": [ "tags": [
{ {
"tag": "level", "tag": "level",
"stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});" "stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});"
}, },
{ {
"tag": "levelTitle", "tag": "levelTitle",
"stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}" "stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}"
}, },
{ {
"tag": "levelValue", "tag": "levelValue",
"stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}", "stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'levelValueClick');" "actionFunction": "ctx.api.callAction(event, 'levelValueClick');"
}
}
},
{
"tag": "fan",
"stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n element.addClass('fan-rotate');\n} else {\n element.removeClass('fan-rotate');\n}\n"
},
{
"tag": "onButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});"
}
}
},
{
"tag": "offButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n"
}
}
},
{
"tag": "levelUpButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
},
{
"tag": "levelDownButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
} }
], }
"behavior": [ },
{ {
"id": "level", "tag": "fan",
"name": "Level", "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n var level = ctx.values.level; \n var minLevel = ctx.properties.minLevel; \n var maxLevel = ctx.properties.maxLevel;\n\n var speed = (level - minLevel) / (maxLevel - minLevel);\n speed = Math.max(0, Math.min(1, speed))*2; \n\n var t = element.timeline();\n if (!t.active()) {\n ctx.api.animate(element, 1000).ease('-').rotate(360).loop();\n t = element.timeline();\n }\n t.speed(speed);\n} else {\n element.timeline().stop();\n}\n",
"type": "value", "actions": null
"valueType": "DOUBLE", },
"defaultValue": 0 {
}, "tag": "onButton",
{ "actions": {
"id": "disabled", "click": {
"name": "{i18n:widgets.rpc-state.disabled-state}", "actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});\n\n"
"hint": "{i18n:widgets.rpc-state.disabled-state-hint}",
"stateLabel": "{i18n:widgets.rpc-state.disabled}",
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": false
},
{
"id": "on",
"name": "On/Off state",
"trueLabel": "{i18n:widgets.rpc-state.on}",
"falseLabel": "{i18n:widgets.rpc-state.off}",
"stateLabel": "{i18n:widgets.rpc-state.on}",
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": false
},
{
"id": "onUpdateState",
"name": "{i18n:widgets.rpc-state.turn-on}",
"hint": "{i18n:widgets.rpc-state.turn-on-hint}",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": true
},
{
"id": "offUpdateState",
"name": "{i18n:widgets.rpc-state.turn-off}",
"hint": "{i18n:widgets.rpc-state.turn-off-hint}",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": false
},
{
"id": "levelUpdateState",
"name": "Update level",
"type": "action",
"valueType": "DOUBLE",
"valueToDataType": "VALUE",
"constantValue": 0
},
{
"id": "levelValueClick",
"name": "On level value click",
"type": "widgetAction"
} }
], }
"properties": [ },
{ {
"id": "showLevelTitle", "tag": "offButton",
"name": "Level title", "actions": {
"type": "switch", "click": {
"default": true, "actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n"
"rowClass": "column-xs" }
}, }
{ },
"id": "levelTitle", {
"name": "Level title", "tag": "levelUpButton",
"type": "string", "actions": {
"default": "{i18n:widgets.battery-level.value}", "click": {
"disableOnProperty": "showLevelTitle", "actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
"fieldClass": "flex" }
}, }
{ },
"id": "levelTitleFont", {
"name": "Level title", "tag": "levelDownButton",
"type": "font", "actions": {
"default": { "click": {
"size": 6, "actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
"sizeUnit": "px", }
"family": "Roboto", }
"weight": "normal", },
"style": "normal" {
}, "tag": "ramka",
"disableOnProperty": "showLevelTitle" "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n if (!element.timeline().active()) {\n ctx.api.animate(element, 1000).scale(1.1).loop(0, true);\n }\n} else {\n element.timeline().stop();\n}",
}, "actions": null
{ }
"id": "levelTitleColor", ],
"name": "Level title", "behavior": [
"type": "color", {
"default": "#000000", "id": "level",
"disableOnProperty": "showLevelTitle" "name": "Level",
}, "hint": null,
{ "type": "value",
"id": "showValue", "valueType": "DOUBLE",
"name": "Value", "defaultValue": 10,
"type": "switch", "trueLabel": null,
"default": true, "falseLabel": null,
"rowClass": "column-xs" "stateLabel": null,
}, "valueToDataType": null,
{ "constantValue": null,
"id": "valueUnits", "valueToDataFunction": null
"name": "Value", },
"type": "units", {
"default": "", "id": "disabled",
"disableOnProperty": "showValue", "name": "{i18n:widgets.rpc-state.disabled-state}",
"fieldClass": "flex" "hint": "{i18n:widgets.rpc-state.disabled-state-hint}",
}, "type": "value",
{ "valueType": "BOOLEAN",
"id": "valueDecimals", "defaultValue": false,
"name": "Value", "trueLabel": "",
"type": "number", "falseLabel": "",
"default": 2, "stateLabel": "{i18n:widgets.rpc-state.disabled}",
"min": 0, "valueToDataType": null,
"max": 15, "constantValue": null,
"step": 1, "valueToDataFunction": null
"fieldSuffix": "decimals", },
"disableOnProperty": "showValue", {
"fieldClass": "flex" "id": "on",
}, "name": "On/Off state",
{ "hint": null,
"id": "valueFont", "type": "value",
"name": "Value", "valueType": "BOOLEAN",
"type": "font", "defaultValue": true,
"default": { "trueLabel": "{i18n:widgets.rpc-state.on}",
"size": 6, "falseLabel": "{i18n:widgets.rpc-state.off}",
"sizeUnit": "px", "stateLabel": "{i18n:widgets.rpc-state.on}",
"family": "Roboto", "valueToDataType": null,
"weight": "normal", "constantValue": null,
"style": "normal" "valueToDataFunction": null
}, },
"disableOnProperty": "showValue" {
}, "id": "onUpdateState",
{ "name": "{i18n:widgets.rpc-state.turn-on}",
"id": "valueColor", "hint": "{i18n:widgets.rpc-state.turn-on-hint}",
"name": "Value", "type": "action",
"type": "color", "valueType": "BOOLEAN",
"default": "#000000", "valueToDataType": "CONSTANT",
"disableOnProperty": "showValue" "constantValue": true
}, },
{ {
"id": "minLevel", "id": "offUpdateState",
"name": "Level range", "name": "{i18n:widgets.rpc-state.turn-off}",
"subLabel": "min", "hint": "{i18n:widgets.rpc-state.turn-off-hint}",
"type": "number", "type": "action",
"required": true, "valueType": "BOOLEAN",
"default": 0, "valueToDataType": "CONSTANT",
"min": 0, "constantValue": false
"rowClass": "column-xs", },
"fieldClass": "flex-xs" {
}, "id": "levelUpdateState",
{ "name": "Update level",
"id": "maxLevel", "type": "action",
"name": "Level range", "valueType": "DOUBLE",
"subLabel": "max", "valueToDataType": "VALUE",
"type": "number", "constantValue": 0
"required": true, },
"default": 100, {
"min": 0, "id": "levelValueClick",
"fieldClass": "flex-xs" "name": "On level value click",
}, "type": "widgetAction"
{ }
"id": "showMinMaxLevel", ],
"name": "Min/Max label", "properties": [
"type": "switch", {
"default": true "id": "showLevelTitle",
}, "name": "Level title",
{ "type": "switch",
"id": "minMaxLevelFont", "default": true,
"name": "Min/Max label", "rowClass": "column-xs"
"type": "font", },
"default": { {
"size": 6, "id": "levelTitle",
"sizeUnit": "px", "name": "Level title",
"family": "Roboto", "type": "text",
"weight": "normal", "default": "{i18n:widgets.battery-level.value}",
"style": "normal" "disableOnProperty": "showLevelTitle",
}, "fieldClass": "flex"
"disableOnProperty": "showMinMaxLevel" },
}, {
{ "id": "levelTitleFont",
"id": "minMaxLevelColor", "name": "Level title",
"name": "Min/Max label", "type": "font",
"type": "color", "default": {
"default": "#666", "size": 6,
"disableOnProperty": "showMinMaxLevel" "sizeUnit": "px",
}, "family": "Roboto",
{ "weight": "normal",
"id": "levelBackground", "style": "normal"
"name": "Level background", },
"subLabel": "Enabled", "disableOnProperty": "showLevelTitle"
"type": "color-settings", },
"default": { {
"type": "constant", "id": "levelTitleColor",
"color": "#1abb48", "name": "Level title",
"colorFunction": "return value &gt; 70 ? '#d5280d' : '#1abb48';" "type": "color",
}, "default": "#000000",
"divider": true, "disableOnProperty": "showLevelTitle"
"rowClass": "column-xs" },
}, {
{ "id": "showValue",
"id": "disabledLevelBackground", "name": "Value",
"name": "Level background", "type": "switch",
"subLabel": "Disabled", "default": true,
"type": "color", "rowClass": "column-xs"
"default": "#ccc" },
} {
] "id": "valueUnits",
}]]></tb:metadata> "name": "Value",
<style>.fan-rotate { "type": "units",
animation: rotate 2s linear infinite; "default": "",
transform-origin: 71.894px 39.787px; "disableOnProperty": "showValue",
} "fieldClass": "flex"
@keyframes rotate { },
0% { transform: rotate(0deg); } {
100% { transform: rotate(360deg); } "id": "valueDecimals",
}</style> "name": "Value",
<rect width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/> "type": "number",
<rect x="8" y="15" width="20" height="81" rx="2" fill="#ececec" stroke="#000" stroke-width="1.0359"/> "default": 2,
<rect transform="scale(1 -1)" x="8.5" y="-95.5" width="19" height="40" rx="1.5" fill="#1abb48" tb:tag="level"/> "min": 0,
<text x="32" y="95" fill="#666" font-family="Roboto" font-size="6px" tb:tag="minLevel" xml:space="preserve"><tspan x="31.354307" y="96.626251">min</tspan></text> "max": 15,
<rect x="10.941" y="49.544" width="14.155" height="12" rx="2.8668" fill="#fff" fill-opacity=".45148" tb:tag="levelValueBackground"/> "step": 1,
<g font-family="Roboto" font-size="6px"> "fieldSuffix": "decimals",
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueFont",
"name": "Value",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showValue"
},
{
"id": "valueColor",
"name": "Value",
"type": "color",
"default": "#000000",
"disableOnProperty": "showValue"
},
{
"id": "minLevel",
"name": "Level range",
"subLabel": "min",
"type": "number",
"required": true,
"default": 0,
"min": 0,
"rowClass": "column-xs",
"fieldClass": "flex-xs"
},
{
"id": "maxLevel",
"name": "Level range",
"subLabel": "max",
"type": "number",
"required": true,
"default": 100,
"min": 0,
"fieldClass": "flex-xs"
},
{
"id": "showMinMaxLevel",
"name": "Min/Max label",
"type": "switch",
"default": true
},
{
"id": "minMaxLevelFont",
"name": "Min/Max label",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "minMaxLevelColor",
"name": "Min/Max label",
"type": "color",
"default": "#666",
"disableOnProperty": "showMinMaxLevel"
},
{
"id": "levelBackground",
"name": "Level background",
"subLabel": "Enabled",
"type": "color_settings",
"default": {
"type": "constant",
"color": "#1abb48",
"colorFunction": "return value > 70 ? '#d5280d' : '#1abb48';"
},
"divider": true,
"rowClass": "column-xs"
},
{
"id": "disabledLevelBackground",
"name": "Level background",
"subLabel": "Disabled",
"type": "color",
"default": "#ccc"
}
]
}]]></tb:metadata>
<rect width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/><rect x="8" y="15" width="20" height="81" rx="2" fill="#ececec" stroke="#000" stroke-width="1.0359"/><rect transform="scale(1 -1)" x="8.5" y="-95.5" width="19" height="40" rx="1.5" fill="#1abb48" tb:tag="level"/><text x="32" y="95" fill="#666" font-family="Roboto" font-size="6px" tb:tag="minLevel" xml:space="preserve"><tspan x="31.354307" y="96.626251">min</tspan></text><rect x="10.941" y="49.544" width="14.155" height="12" rx="2.8668" fill="#fff" fill-opacity=".45148" tb:tag="levelValueBackground"/><g font-family="Roboto" font-size="6px">
<text x="18" y="56" dominant-baseline="middle" fill="#000000" text-anchor="middle" tb:tag="levelValue" xml:space="preserve"><tspan>N/A</tspan></text> <text x="18" y="56" dominant-baseline="middle" fill="#000000" text-anchor="middle" tb:tag="levelValue" xml:space="preserve"><tspan>N/A</tspan></text>
<text x="17.636805" y="10.347933" dominant-baseline="middle" fill="#000000" text-anchor="middle" tb:tag="levelTitle" xml:space="preserve"><tspan>Level</tspan></text> <text x="17.636805" y="10.347933" dominant-baseline="middle" fill="#000000" text-anchor="middle" tb:tag="levelTitle" xml:space="preserve"><tspan>Level</tspan></text>
<text x="32" y="20" fill="#666666" tb:tag="maxLevel" xml:space="preserve"><tspan x="29.865496" y="18.716127">max</tspan></text> <text x="32" y="20" fill="#666666" tb:tag="maxLevel" xml:space="preserve"><tspan x="29.865496" y="18.716127">max</tspan></text>
</g> </g><path d="m82.143 27.538h-20.499c-1.1045 0-1.9999 0.8954-1.9999 1.9999v20.499c0 1.1045 0.8954 1.9999 1.9999 1.9999h20.499c1.1045 0 1.9999-0.8954 1.9999-1.9999v-20.499c0-1.1045-0.8954-1.9999-1.9999-1.9999zm-20.499 1.4999c0.27613 0 0.49998 0.22385 0.49998 0.49998s-0.22385 0.49998-0.49998 0.49998-0.49998-0.22385-0.49998-0.49998 0.22385-0.49998 0.49998-0.49998zm0 21.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm10.249-0.79997c-5.495 0-9.9495-4.4545-9.9495-9.9495s4.4545-9.9495 9.9495-9.9495 9.9495 4.4545 9.9495 9.9495-4.4545 9.9495-9.9495 9.9495zm10.249 0.79997c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm0-20.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998z" fill="#080d7d" stroke-width=".040031" tb:tag="ramka"/><path d="m80.66 37.983c-0.04956-0.24078-0.15288-0.4126-0.34242-0.56916-1.5148-1.2518-3.6154-1.1677-6.3021 0.25239-0.17533-0.17533-0.36512-0.32353-0.5778-0.45114 1.8517-1.6612 3.5396-2.3586 5.0637-2.0924 0.44934 0.0785 0.7605-0.43637 0.48209-0.79765-0.61795-0.80201-1.3212-1.455-2.1668-2.0119-0.20536-0.13522-0.39987-0.18358-0.64461-0.16032-1.9563 0.18594-3.3822 1.7308-4.2778 4.6347-0.24799 0-0.48697 0.02938-0.72756 0.08955 0.1347-2.484 0.83504-4.1707 2.101-5.0601 0.37324-0.2622 0.22918-0.84629-0.22313-0.90497-1.0041-0.13014-1.9631-0.09459-2.9548 0.10956-0.24078 0.04956-0.4126 0.15288-0.56916 0.34242-1.2518 1.5148-1.1677 3.6154 0.25239 6.3021-0.17533 0.17533-0.32353 0.36512-0.45114 0.5778-1.6612-1.8517-2.3586-3.5396-2.0924-5.0637 0.0785-0.44934-0.43637-0.7605-0.79765-0.48213-0.80201 0.61795-1.455 1.3212-2.0119 2.1668-0.13522 0.20536-0.18358 0.39986-0.16032 0.64461 0.18594 1.9563 1.7308 3.3822 4.6347 4.2778 0 0.24799 0.02938 0.48697 0.08955 0.72756-2.484-0.1347-4.1707-0.83504-5.0601-2.101-0.2622-0.37324-0.84629-0.22918-0.90497 0.22313-0.13014 1.0041-0.09459 1.9631 0.10956 2.9548 0.04956 0.24078 0.15288 0.4126 0.34242 0.56916 1.5148 1.2518 3.6154 1.1677 6.3021-0.25239 0.17533 0.17533 0.36512 0.32353 0.5778 0.45114-1.8517 1.6612-3.5396 2.3586-5.0637 2.0924-0.44934-0.0785-0.7605 0.43637-0.48209 0.79765 0.61795 0.80201 1.3212 1.455 2.1668 2.0119 0.20536 0.13522 0.39986 0.18358 0.64461 0.16032 1.9563-0.18594 3.3822-1.7308 4.2778-4.6347 0.24799 0 0.48697-0.02938 0.72756-0.08959-0.1347 2.484-0.83504 4.1707-2.101 5.0601-0.37324 0.2622-0.22918 0.84629 0.22313 0.90497 1.0041 0.13014 1.9631 0.09459 2.9548-0.10956 0.24078-0.04956 0.4126-0.15288 0.56916-0.34242 1.2518-1.5148 1.1677-3.6154-0.25239-6.3021 0.17533-0.17533 0.32353-0.36512 0.45114-0.5778 1.6612 1.8517 2.3586 3.5396 2.0924 5.0637-0.0785 0.44934 0.43637 0.7605 0.79765 0.48209 0.80201-0.61795 1.455-1.3212 2.0119-2.1668 0.13522-0.20536 0.18358-0.39987 0.16032-0.64461-0.18594-1.9563-1.7308-3.3822-4.6347-4.2778 0-0.24799-0.02938-0.48697-0.08955-0.72756 2.484 0.1347 4.1707 0.83504 5.0601 2.101 0.2622 0.37324 0.84629 0.22918 0.90497-0.22313 0.13014-1.004 0.09459-1.963-0.10956-2.9547zm-8.7657 4.3043c-1.3806 0-2.4999-1.1193-2.4999-2.4999s1.1193-2.4999 2.4999-2.4999 2.4999 1.1193 2.4999 2.4999-1.1193 2.4999-2.4999 2.4999z" fill="#7d081e" stroke-width=".040031" tb:tag="fan"/><g tb:tag="onButton">
<path d="m82.143 27.538h-20.499c-1.1045 0-1.9999 0.8954-1.9999 1.9999v20.499c0 1.1045 0.8954 1.9999 1.9999 1.9999h20.499c1.1045 0 1.9999-0.8954 1.9999-1.9999v-20.499c0-1.1045-0.8954-1.9999-1.9999-1.9999zm-20.499 1.4999c0.27613 0 0.49998 0.22385 0.49998 0.49998s-0.22385 0.49998-0.49998 0.49998-0.49998-0.22385-0.49998-0.49998 0.22385-0.49998 0.49998-0.49998zm0 21.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm10.249-0.79997c-5.495 0-9.9495-4.4545-9.9495-9.9495s4.4545-9.9495 9.9495-9.9495 9.9495 4.4545 9.9495 9.9495-4.4545 9.9495-9.9495 9.9495zm10.249 0.79997c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998zm0-20.499c-0.27613 0-0.49998-0.22385-0.49998-0.49998s0.22385-0.49998 0.49998-0.49998 0.49998 0.22385 0.49998 0.49998-0.22385 0.49998-0.49998 0.49998z" fill="#080d7d" stroke-width=".040031"/>
<path class="fan-rotate" d="m80.66 37.983c-0.04956-0.24078-0.15288-0.4126-0.34242-0.56916-1.5148-1.2518-3.6154-1.1677-6.3021 0.25239-0.17533-0.17533-0.36512-0.32353-0.5778-0.45114 1.8517-1.6612 3.5396-2.3586 5.0637-2.0924 0.44934 0.0785 0.7605-0.43637 0.48209-0.79765-0.61795-0.80201-1.3212-1.455-2.1668-2.0119-0.20536-0.13522-0.39987-0.18358-0.64461-0.16032-1.9563 0.18594-3.3822 1.7308-4.2778 4.6347-0.24799 0-0.48697 0.02938-0.72756 0.08955 0.1347-2.484 0.83504-4.1707 2.101-5.0601 0.37324-0.2622 0.22918-0.84629-0.22313-0.90497-1.0041-0.13014-1.9631-0.09459-2.9548 0.10956-0.24078 0.04956-0.4126 0.15288-0.56916 0.34242-1.2518 1.5148-1.1677 3.6154 0.25239 6.3021-0.17533 0.17533-0.32353 0.36512-0.45114 0.5778-1.6612-1.8517-2.3586-3.5396-2.0924-5.0637 0.0785-0.44934-0.43637-0.7605-0.79765-0.48213-0.80201 0.61795-1.455 1.3212-2.0119 2.1668-0.13522 0.20536-0.18358 0.39986-0.16032 0.64461 0.18594 1.9563 1.7308 3.3822 4.6347 4.2778 0 0.24799 0.02938 0.48697 0.08955 0.72756-2.484-0.1347-4.1707-0.83504-5.0601-2.101-0.2622-0.37324-0.84629-0.22918-0.90497 0.22313-0.13014 1.0041-0.09459 1.9631 0.10956 2.9548 0.04956 0.24078 0.15288 0.4126 0.34242 0.56916 1.5148 1.2518 3.6154 1.1677 6.3021-0.25239 0.17533 0.17533 0.36512 0.32353 0.5778 0.45114-1.8517 1.6612-3.5396 2.3586-5.0637 2.0924-0.44934-0.0785-0.7605 0.43637-0.48209 0.79765 0.61795 0.80201 1.3212 1.455 2.1668 2.0119 0.20536 0.13522 0.39986 0.18358 0.64461 0.16032 1.9563-0.18594 3.3822-1.7308 4.2778-4.6347 0.24799 0 0.48697-0.02938 0.72756-0.08959-0.1347 2.484-0.83504 4.1707-2.101 5.0601-0.37324 0.2622-0.22918 0.84629 0.22313 0.90497 1.0041 0.13014 1.9631 0.09459 2.9548-0.10956 0.24078-0.04956 0.4126-0.15288 0.56916-0.34242 1.2518-1.5148 1.1677-3.6154-0.25239-6.3021 0.17533-0.17533 0.32353-0.36512 0.45114-0.5778 1.6612 1.8517 2.3586 3.5396 2.0924 5.0637-0.0785 0.44934 0.43637 0.7605 0.79765 0.48209 0.80201-0.61795 1.455-1.3212 2.0119-2.1668 0.13522-0.20536 0.18358-0.39987 0.16032-0.64461-0.18594-1.9563-1.7308-3.3822-4.6347-4.2778 0-0.24799-0.02938-0.48697-0.08955-0.72756 2.484 0.1347 4.1707 0.83504 5.0601 2.101 0.2622 0.37324 0.84629 0.22918 0.90497-0.22313 0.13014-1.004 0.09459-1.963-0.10956-2.9547zm-8.7657 4.3043c-1.3806 0-2.4999-1.1193-2.4999-2.4999s1.1193-2.4999 2.4999-2.4999 2.4999 1.1193 2.4999 2.4999-1.1193 2.4999-2.4999 2.4999z" fill="#7d081e" stroke-width=".040031" tb:tag="fan"/>
<g tb:tag="onButton">
<rect x="54.702" y="60.372" width="14.263" height="7.4261" rx="1.5" fill="#12ed19" stroke="#000" tb:tag="onButtonBackground"/> <rect x="54.702" y="60.372" width="14.263" height="7.4261" rx="1.5" fill="#12ed19" stroke="#000" tb:tag="onButtonBackground"/>
<text x="61.855518" y="64.49128" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">On</tspan></text> <text x="61.855518" y="64.49128" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">On</tspan></text>
</g> </g><g tb:tag="offButton">
<g tb:tag="offButton">
<rect x="74.367" y="60.311" width="14.263" height="7.4261" rx="1.5" fill="#ed121f" stroke="#000" tb:tag="offButtonBackground"/> <rect x="74.367" y="60.311" width="14.263" height="7.4261" rx="1.5" fill="#ed121f" stroke="#000" tb:tag="offButtonBackground"/>
<text x="81.365944" y="64.518349" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">Off</tspan></text> <text x="81.365944" y="64.518349" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">Off</tspan></text>
</g> </g><g stroke="#000" stroke-miterlimit="1" tb:tag="levelUpButton">
<g stroke="#000" stroke-miterlimit="1" tb:tag="levelUpButton">
<rect x="34.97" y="39.377" width="9.5005" height="12.584" rx=".75" fill="none"/> <rect x="34.97" y="39.377" width="9.5005" height="12.584" rx=".75" fill="none"/>
<path transform="matrix(.4753 -.2938 .2938 .4753 4.8821 31.04)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/> <path transform="matrix(.4753 -.2938 .2938 .4753 4.8821 31.04)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/>
</g> </g><g stroke="#000" stroke-miterlimit="1" tb:tag="levelDownButton">
<g stroke="#000" stroke-miterlimit="1" tb:tag="levelDownButton">
<rect x="34.97" y="59.039" width="9.5005" height="12.584" rx=".75" fill="none"/> <rect x="34.97" y="59.039" width="9.5005" height="12.584" rx=".75" fill="none"/>
<path transform="matrix(-.47514 .29405 -.29405 -.47514 74.568 79.771)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/> <path transform="matrix(-.47514 .29405 -.29405 -.47514 74.568 79.771)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Loading…
Cancel
Save