Browse Source

UI: Scada test improvements, add actions.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
937b48d85b
  1. 316
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts
  2. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html
  3. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts
  4. 272
      ui-ngx/src/assets/widget/scada/drawing.svg

316
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts

@ -16,7 +16,12 @@
import { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, Svg, SVG, Timeline, Text } from '@svgdotjs/svg.js';
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
import {
DataToValueType,
GetValueAction,
GetValueSettings, SetValueAction,
SetValueSettings, ValueToDataType
} from '@shared/models/action-widget-settings.models';
import {
formatValue,
insertVariable,
@ -29,34 +34,10 @@ import {
} from '@core/utils';
import { BehaviorSubject, forkJoin, Observable } from 'rxjs';
import { map, share } from 'rxjs/operators';
import { ValueAction, ValueGetter } 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 { Font } from '@shared/models/widget-settings.models';
export type ValueMatcherType = 'any' | 'constant' | 'range';
export interface ValueMatcher {
type: ValueMatcherType;
value?: any;
range?: {from: number; to: number};
}
const valueMatches = (matcher: ValueMatcher, val: any): boolean => {
switch (matcher.type) {
case 'any':
return true;
case 'constant':
return matcher.value === val;
case 'range':
if (isDefinedAndNotNull(val) && isNumeric(val)) {
const num = Number(val);
const range = matcher.range;
return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to));
} else {
return false;
}
}
};
import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
export type ScadaObjectValueType = 'input' | 'constant' | 'property' | 'function' | 'valueFormat';
@ -100,26 +81,28 @@ export interface ScadaObjectText {
export interface ScadaObjectElementState {
tag: string;
inputValue?: string;
show?: ScadaObjectValue;
text?: ScadaObjectText;
attributes?: ScadaObjectAttribute[];
animate?: number;
addClass?: string;
removeClass?: string;
animationTimeline?: Timeline;
}
export interface ScadaObjectState {
initial?: boolean;
value?: any;
triggerValues?: string[];
conditionFunction?: string;
condition?: (values: {[key: string]: any}) => boolean;
state: ScadaObjectElementState[];
}
export interface ScadaObjectUpdateState {
matcher: ValueMatcher;
stateId: string;
updateValue: string;
}
const filterUpdateStates = (states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] =>
states.filter(s => valueMatches(s.matcher, val));
export enum ScadaObjectBehaviorType {
setValue = 'setValue',
getValue = 'getValue'
@ -138,12 +121,14 @@ export interface ScadaObjectBehaviorGet extends ScadaObjectBehaviorBase {
}
export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase {
todo: any;
valueToDataType: ValueToDataType;
constantValue: any;
valueToDataFunction: string;
}
export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet;
export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'font' | 'units' | 'switch';
export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch';
export interface ScadaObjectPropertyBase {
id: string;
@ -155,6 +140,8 @@ export interface ScadaObjectPropertyBase {
divider?: boolean;
fieldSuffix?: string;
disableOnProperty?: string;
rowClass?: string;
fieldClass?: string;
}
export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase {
@ -165,8 +152,38 @@ export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase {
export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty;
export interface ScadaObjectCallBehavior {
behaviorId: string;
conditionFunction?: string;
condition?: (value: any) => boolean;
}
export interface ScadaObjectStateValue {
initialValue: any;
callBehavior?: ScadaObjectCallBehavior[];
}
export type ScadaObjectElementActionTrigger = 'click';
export type ScadaObjectActionType = 'updateValue';
export type ScadaObjectActionUpdateValueType = 'toggle' | 'increment' | 'constant';
export interface ScadaObjectElementAction {
trigger: ScadaObjectElementActionTrigger;
enabledTriggerValues?: string[];
enabledConditionFunction?: string;
enabledCondition?: (values: {[key: string]: any}) => boolean;
actionType: ScadaObjectActionType;
updateValueId?: string;
updateValueType?: ScadaObjectActionUpdateValueType;
updateValueConstant?: any;
updateValueInc?: number;
}
export interface ScadaObjectMetadata {
title: string;
stateValues: {[id: string]: ScadaObjectStateValue};
actions: {[tag: string]: ScadaObjectElementAction};
states: {[id: string]: ScadaObjectState};
behavior: ScadaObjectBehavior[];
properties: ScadaObjectProperty[];
@ -174,6 +191,8 @@ export interface ScadaObjectMetadata {
export const emptyMetadata: ScadaObjectMetadata = {
title: '',
stateValues: {},
actions: {},
states: {},
behavior: [],
properties: []
@ -211,7 +230,7 @@ const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings<
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
},
getAttribute: {
key: 'state',
scope: null
@ -221,19 +240,41 @@ const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings<
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
});
const defaultSetValueSettings = (set: ScadaObjectBehaviorSet): SetValueSettings => ({
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SERVER_SCOPE
},
putTimeSeries: {
key: 'state'
},
valueToData: {
type: set.valueToDataType,
constantValue: set.constantValue,
valueToDataFunction: set.valueToDataFunction ? set.valueToDataFunction :
'/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;'
}
});
export const defaultScadaObjectSettings = (metadata: ScadaObjectMetadata): ScadaObjectSettings => {
const settings: ScadaObjectSettings = {};
for (const behaviour of metadata.behavior) {
//behaviour.id
if (behaviour.type === ScadaObjectBehaviorType.getValue) {
settings[behaviour.id] = defaultGetValueSettings(behaviour as ScadaObjectBehaviorGet);
} else if (behaviour.type === ScadaObjectBehaviorType.setValue) {
// TODO:
for (const behavior of metadata.behavior) {
if (behavior.type === ScadaObjectBehaviorType.getValue) {
settings[behavior.id] = defaultGetValueSettings(behavior as ScadaObjectBehaviorGet);
} else if (behavior.type === ScadaObjectBehaviorType.setValue) {
settings[behavior.id] = defaultSetValueSettings(behavior as ScadaObjectBehaviorSet);
}
}
for (const property of metadata.properties) {
@ -258,8 +299,10 @@ export class ScadaObject {
private loadingSubject = new BehaviorSubject(false);
private valueGetters: ValueGetter<any>[] = [];
private valueActions: ValueAction[] = [];
private valueSetters: {[behaviorId: string]: ValueSetter<any>} = {};
private animationTimeline: Timeline;
private stateValueSubjects: {[id: string]: BehaviorSubject<any>} = {};
private stateValues: {[id: string]: any} = {};
loading$ = this.loadingSubject.asObservable().pipe(share());
@ -289,6 +332,10 @@ export class ScadaObject {
}
public destroy() {
for (const stateValueId of Object.keys(this.stateValueSubjects)) {
this.stateValueSubjects[stateValueId].complete();
this.stateValueSubjects[stateValueId].unsubscribe();
}
this.valueActions.forEach(v => v.destroy());
this.loadingSubject.complete();
this.loadingSubject.unsubscribe();
@ -303,8 +350,33 @@ export class ScadaObject {
}
private prepareMetadata() {
for (const stateValueId of Object.keys(this.metadata.stateValues)) {
const stateValue = this.metadata.stateValues[stateValueId];
if (stateValue.callBehavior) {
stateValue.callBehavior.forEach(callBehavior => {
let condition = () => true;
if (callBehavior.conditionFunction) {
try {
condition = parseFunction(this.insertVariables(callBehavior.conditionFunction), ['value']);
} catch (e) {
condition = () => false;
}
}
callBehavior.condition = condition;
});
}
}
for (const stateId of Object.keys(this.metadata.states)) {
const state = this.metadata.states[stateId];
let condition = () => true;
if (state.conditionFunction) {
try {
condition = parseFunction(this.insertVariables(state.conditionFunction), ['values']);
} catch (e) {
condition = () => false;
}
}
state.condition = condition;
for (const elementState of state.state) {
this.prepareValue(elementState.show);
this.prepareValue(elementState.text?.content);
@ -367,6 +439,35 @@ export class ScadaObject {
}
private initStates() {
for (const stateValueId of Object.keys(this.metadata.stateValues)) {
const stateValue = this.metadata.stateValues[stateValueId];
this.stateValueSubjects[stateValueId] = new BehaviorSubject<any>(stateValue.initialValue);
this.stateValues[stateValueId] = stateValue.initialValue;
this.stateValueSubjects[stateValueId].subscribe(val => {
this.stateValues[stateValueId] = val;
const states = Object.values(this.metadata.states);
const triggerStates = states.filter(s => s.triggerValues && s.triggerValues.includes(stateValueId));
for (const state of triggerStates) {
if (state.condition(this.stateValues)) {
this.updateState(state);
}
}
});
}
for (const tag of Object.keys(this.metadata.actions)) {
const action = this.metadata.actions[tag];
const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`);
switch (action.trigger) {
case 'click':
elements.forEach(e => {
e.attr('cursor', 'pointer');
e.on('click', () => {
this.triggerAction(action);
});
});
break;
}
}
for (const behavior of this.metadata.behavior) {
if (behavior.type === ScadaObjectBehaviorType.getValue) {
const getBehavior = behavior as ScadaObjectBehaviorGet;
@ -379,6 +480,13 @@ export class ScadaObject {
});
this.valueGetters.push(valueGetter);
this.valueActions.push(valueGetter);
} else if (behavior.type === ScadaObjectBehaviorType.setValue) {
const setBehavior = behavior as ScadaObjectBehaviorSet;
let setValueSettings: SetValueSettings = this.settings[setBehavior.id];
setValueSettings = {...setValueSettings, actionLabel: setBehavior.name};
const valueSetter = ValueSetter.fromSettings<any>(this.ctx, setValueSettings);
this.valueSetters[setBehavior.id] = valueSetter;
this.valueActions.push(valueSetter);
}
}
const initialState = Object.values(this.metadata.states).find(s => s.initial);
@ -404,6 +512,59 @@ export class ScadaObject {
}
}
private triggerAction(action: ScadaObjectElementAction) {
switch (action.actionType) {
case 'updateValue':
const targetValue = action.updateValueId;
const valueSubject = this.stateValueSubjects[targetValue];
if (valueSubject) {
const currentVal = valueSubject.value;
let newValue: any;
switch (action.updateValueType) {
case 'toggle':
newValue = !currentVal;
break;
case 'increment':
if (isNumber(currentVal)) {
newValue = currentVal + action.updateValueInc;
}
break;
case 'constant':
newValue = action.updateValueConstant;
break;
}
if (isDefinedAndNotNull(newValue)) {
valueSubject.next(newValue);
const stateValue = this.metadata.stateValues[targetValue];
if (stateValue.callBehavior) {
const observables: Observable<any>[] = [];
for (const behavior of stateValue.callBehavior) {
if (behavior.condition(newValue)) {
const valueSetter = this.valueSetters[behavior.behaviorId];
observables.push(valueSetter.setValue(newValue));
}
}
if (observables.length) {
this.loadingSubject.next(true);
forkJoin(observables).subscribe(
{
next: () => {
this.loadingSubject.next(false);
},
error: (err) => {
this.loadingSubject.next(false);
valueSubject.next(currentVal);
}
}
);
}
}
}
}
break;
}
}
private resize() {
let scale: number;
if (this.targetWidth < this.targetHeight) {
@ -417,20 +578,23 @@ export class ScadaObject {
private onValue(id: string, value: any) {
const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet;
value = this.normalizeValue(value, getBehavior.valueType);
const updateStates = filterUpdateStates(getBehavior.onUpdate, value);
if (this.animationTimeline) {
this.animationTimeline.finish();
}
for (const updateState of updateStates) {
const state = this.metadata.states[updateState.stateId];
this.updateState(state, value);
for (const onUpdate of getBehavior.onUpdate) {
const targetStateValueId = onUpdate.updateValue;
this.stateValueSubjects[targetStateValueId].next(value);
}
}
private updateState(state: ScadaObjectState, value?: any) {
private updateState(state: ScadaObjectState) {
if (state) {
for (const elementState of state.state) {
const tag = elementState.tag;
let value;
if (elementState.inputValue) {
value = this.stateValues[elementState.inputValue];
}
if (elementState.animationTimeline) {
elementState.animationTimeline.finish();
}
const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`);
if (elements.length) {
if (elementState.show) {
@ -443,10 +607,20 @@ export class ScadaObject {
}
});
}
if (elementState.addClass) {
elements.forEach(e => {
e.addClass(elementState.addClass);
});
}
if (elementState.removeClass) {
elements.forEach(e => {
e.removeClass(elementState.removeClass);
});
}
if (elementState.attributes) {
const attrs = this.computeAttributes(elementState.attributes, value);
elements.forEach(e => {
this.setElementAttributes(e, attrs, elementState.animate);
this.setElementAttributes(elementState, e, attrs, elementState.animate);
});
}
if (elementState.text) {
@ -502,9 +676,9 @@ export class ScadaObject {
return res;
}
private setElementAttributes(element: Element, attrs: {[attr: string]: any}, animate?: number) {
private setElementAttributes(elementState: ScadaObjectElementState, element: Element, attrs: {[attr: string]: any}, animate?: number) {
if (isDefinedAndNotNull(animate)) {
this.animation(element, animate).attr(attrs);
this.animation(elementState, element, animate).attr(attrs);
} else {
element.attr(attrs);
}
@ -545,11 +719,11 @@ export class ScadaObject {
}
}
private animation(element: Element, duration: number): Runner {
if (!this.animationTimeline) {
this.animationTimeline = new Timeline();
private animation(elementState: ScadaObjectElementState, element: Element, duration: number): Runner {
if (!elementState.animationTimeline) {
elementState.animationTimeline = new Timeline();
}
element.timeline(this.animationTimeline);
element.timeline(elementState.animationTimeline);
return element.animate(duration, 0, 'now');
}
@ -561,7 +735,14 @@ export class ScadaObject {
case 'constant':
return objectValue.constantValue;
case 'property':
return objectValue.computedPropertyValue;
const property = this.getProperty(objectValue.propertyId);
if (property.type === 'color-settings') {
const colorProcessor: ColorProcessor = objectValue.computedPropertyValue;
colorProcessor.update(value);
return colorProcessor.color;
} else {
return objectValue.computedPropertyValue;
}
case 'function':
try {
return objectValue.valueConverter(value);
@ -584,11 +765,18 @@ export class ScadaObject {
}
}
private getProperty(id: string): ScadaObjectProperty {
return this.metadata.properties.find(p => p.id === id);
}
private getPropertyValue(id: string): any {
const property = this.metadata.properties.find(p => p.id === id);
const property = this.getProperty(id);
if (property) {
const value = this.settings[id];
if (isDefinedAndNotNull(value)) {
if (property.type === 'color-settings') {
return ColorProcessor.fromSettings(value);
}
return value;
} else {
switch (property.type) {
@ -598,6 +786,8 @@ export class ScadaObject {
return 0;
case 'color':
return '#000';
case 'color-settings':
return constantColor('#000');
}
}
} else {

27
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html

@ -30,39 +30,54 @@
[widgetType]="widgetType"
formControlName="{{ behaviour.id }}">
</tb-get-value-action-settings>
<tb-set-value-action-settings *ngIf="behaviour.type === ScadaObjectBehaviorType.setValue"
fxFlex
panelTitle="{{ behaviour.name }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="{{ behaviour.id }}"></tb-set-value-action-settings>
</div>
</div>
<div *ngIf="propertyRows?.length" class="tb-form-panel">
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between">
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label }}
</mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width">{{ propertyRow.label }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div fxLayout="row" fxFlex fxLayoutAlign="end center" fxLayoutGap="8px">
<ng-container *ngFor="let property of propertyRow.properties">
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel }}</div>
<mat-form-field *ngIf="property.type === 'string'" appearance="outline" subscriptSizing="dynamic">
<mat-form-field *ngIf="property.type === 'string'" [class]="property.fieldClass" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix }}</div>
<div fxHide.lt-md matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix }}</div>
</mat-form-field>
<tb-color-input *ngIf="property.type === 'color'"
[required]="property.required"
[class]="property.fieldClass"
asBoxInput
colorClearButton
formControlName="{{ property.id }}">
</tb-color-input>
<mat-form-field *ngIf="property.type === 'number'" appearance="outline" class="number" subscriptSizing="dynamic">
<tb-color-settings *ngIf="property.type === 'color-settings'"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
settingsKey="{{ property.name }}">
</tb-color-settings>
<mat-form-field *ngIf="property.type === 'number'" [class]="property.fieldClass" appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required"
[min]="property.min" [max]="property.max" [step]="property.step"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix }}</div>
<div fxHide.lt-md matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix }}</div>
</mat-form-field>
<tb-font-settings *ngIf="property.type === 'font'"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
clearButton
disabledLineHeight>
</tb-font-settings>
<tb-unit-input *ngIf="property.type === 'units'"
[class]="property.fieldClass"
[required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input>
<mat-divider *ngIf="property.divider" vertical></mat-divider>

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

@ -20,6 +20,7 @@ export interface ScadaPropertyRow {
label: string;
properties: ScadaObjectProperty[];
switch?: ScadaObjectProperty;
rowClass?: string;
}
export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaPropertyRow[] => {
@ -29,7 +30,8 @@ export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaProperty
if (!propertyRow) {
propertyRow = {
label: property.name,
properties: []
properties: [],
rowClass: property.rowClass
};
result.push(propertyRow);
}

272
ui-ngx/src/assets/widget/scada/drawing.svg

@ -1,12 +1,71 @@
<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">
<tb:metadata>{
"title": "My first SCADA Object",
"stateValues": {
"level": {
"initialValue": 0
},
"disabled": {
"initialValue": false
},
"on": {
"initialValue": false,
"callBehavior": [
{
"behaviorId": "onUpdateState",
"conditionFunction": "return value === true;"
},
{
"behaviorId": "offUpdateState",
"conditionFunction": "return value === false;"
}
]
}
},
"actions": {
"onButton": {
"trigger": "click",
"actionType": "updateValue",
"updateValueId": "on",
"updateValueType": "constant",
"updateValueConstant": true
},
"offButton": {
"trigger": "click",
"actionType": "updateValue",
"updateValueId": "on",
"updateValueType": "constant",
"updateValueConstant": false
}
},
"states": {
"initialState": {
"initial": true,
"state": [
{
"tag": "levelTitle",
"show": {
"type": "property",
"propertyId": "showLevelTitle"
},
"text": {
"content": {
"type": "property",
"propertyId": "levelTitle"
},
"font": {
"type": "property",
"propertyId": "levelTitleFont"
},
"color": {
"type": "property",
"propertyId": "levelTitleColor"
}
}
},
{
"tag": "level",
"inputValue": "level",
"attributes": [
{
"name": "fill",
@ -86,9 +145,11 @@
]
},
"updateLevelState": {
"triggerValues": ["level"],
"state": [
{
"tag": "level",
"inputValue": "level",
"animate": 200,
"attributes": [
{
@ -102,6 +163,7 @@
},
{
"tag": "levelValue",
"inputValue": "level",
"text": {
"content": {
"type": "valueFormat",
@ -118,7 +180,28 @@
}
]
},
"updateLevelBackgroundState": {
"triggerValues": ["disabled", "level"],
"conditionFunction": "return values.disabled === false;",
"state": [
{
"tag": "level",
"inputValue": "level",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "levelBackground"
}
}
]
}
]
},
"disabledState": {
"triggerValues": ["disabled"],
"conditionFunction": "return values.disabled === true;",
"state": [
{
"tag": "level",
@ -134,16 +217,68 @@
}
]
},
"enabledState": {
"onState": {
"triggerValues": ["on"],
"conditionFunction": "return values.on === true;",
"state": [
{
"tag": "level",
"tag": "fan",
"addClass": "fan-rotate"
},
{
"tag": "onButtonBackground",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "levelBackground"
"type": "constant",
"constantValue": "#777"
}
}
]
},
{
"tag": "offButtonBackground",
"attributes": [
{
"name": "fill",
"value": {
"type": "constant",
"constantValue": "#ed121f"
}
}
]
}
]
},
"offState": {
"triggerValues": ["on"],
"conditionFunction": "return values.on === false;",
"state": [
{
"tag": "fan",
"removeClass": "fan-rotate"
},
{
"tag": "onButtonBackground",
"attributes": [
{
"name": "fill",
"value": {
"type": "constant",
"constantValue": "#12ed19"
}
}
]
},
{
"tag": "offButtonBackground",
"attributes": [
{
"name": "fill",
"value": {
"type": "constant",
"constantValue": "#777"
}
}
]
@ -160,10 +295,7 @@
"defaultValue": 0,
"onUpdate": [
{
"matcher": {
"type": "any"
},
"stateId": "updateLevelState"
"updateValue": "level"
}
]
},
@ -175,35 +307,87 @@
"defaultValue": false,
"onUpdate": [
{
"matcher": {
"type": "constant",
"value": true
},
"stateId": "disabledState"
},
"updateValue": "disabled"
}
]
},
{
"id": "onState",
"name": "On/Off state",
"type": "getValue",
"valueType": "BOOLEAN",
"defaultValue": false,
"onUpdate": [
{
"matcher": {
"type": "constant",
"value": false
},
"stateId": "enabledState"
"updateValue": "on"
}
]
},
{
"id": "onUpdateState",
"name": "Power On",
"type": "setValue",
"valueToDataType": "CONSTANT",
"constantValue": true
},
{
"id": "offUpdateState",
"name": "Power Off",
"type": "setValue",
"valueToDataType": "CONSTANT",
"constantValue": false
}
],
"properties": [
{
"id": "showLevelTitle",
"name": "Level title",
"type": "switch",
"default": true,
"rowClass": "column-xs"
},
{
"id": "levelTitle",
"name": "Level title",
"type": "string",
"default": "Level",
"disableOnProperty": "showLevelTitle",
"fieldClass": "flex"
},
{
"id": "levelTitleFont",
"name": "Level title",
"type": "font",
"default": {
"size": 6,
"sizeUnit": "px",
"family": "Roboto",
"weight": "normal",
"style": "normal"
},
"disableOnProperty": "showLevelTitle"
},
{
"id": "levelTitleColor",
"name": "Level title",
"type": "color",
"default": "#000000",
"disableOnProperty": "showLevelTitle"
},
{
"id": "showValue",
"name": "Value",
"type": "switch",
"default": true
"default": true,
"rowClass": "column-xs"
},
{
"id": "valueUnits",
"name": "Value",
"type": "units",
"default": "",
"disableOnProperty": "showValue"
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueDecimals",
@ -214,7 +398,8 @@
"max": 15,
"step": 1,
"fieldSuffix": "decimals",
"disableOnProperty": "showValue"
"disableOnProperty": "showValue",
"fieldClass": "flex"
},
{
"id": "valueFont",
@ -243,7 +428,9 @@
"type": "number",
"required": true,
"default": 0,
"min": 0
"min": 0,
"rowClass": "column-xs",
"fieldClass": "flex-xs"
},
{
"id": "maxLevel",
@ -252,7 +439,8 @@
"type": "number",
"required": true,
"default": 100,
"min": 0
"min": 0,
"fieldClass": "flex-xs"
},
{
"id": "showMinMaxLevel",
@ -284,9 +472,14 @@
"id": "levelBackground",
"name": "Level background",
"subLabel": "Enabled",
"type": "color",
"default": "#1abb48",
"divider": true
"type": "color-settings",
"default": {
"type": "constant",
"color": "#1abb48",
"colorFunction": "return value &gt; 70 ? '#d5280d' : '#1abb48';"
},
"divider": true,
"rowClass": "column-xs"
},
{
"id": "disabledLevelBackground",
@ -297,11 +490,32 @@
}
]
}</tb:metadata>
<style>.fan-rotate {
animation: rotate 2s linear infinite;
transform-origin: 71.894px 39.787px;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}</style>
<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"/>
<text x="18" y="56" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="6px" text-anchor="middle" tb:tag="levelValue" xml:space="preserve"><tspan>N/A</tspan></text>
<text x="32" y="20" fill="#666" font-family="Roboto" font-size="6px" tb:tag="maxLevel" xml:space="preserve"><tspan x="29.865496" y="18.716127">max</tspan></text>
<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="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>
</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"/>
<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"/>
<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 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"/>
<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>
</svg>

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Loading…
Cancel
Save