UI: Scada widget test

This commit is contained in:
Igor Kulikov 2024-05-02 19:44:47 +03:00
parent e48bd45ab8
commit eb50048c50
13 changed files with 1036 additions and 5 deletions

View File

@ -129,6 +129,9 @@ import {
import { import {
RadarChartBasicConfigComponent RadarChartBasicConfigComponent
} from '@home/components/widget/config/basic/chart/radar-chart-basic-config.component'; } from '@home/components/widget/config/basic/chart/radar-chart-basic-config.component';
import {
ScadaTestBasicConfigComponent
} from '@home/components/widget/config/basic/scada/scada-test-basic-config.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -171,7 +174,8 @@ import {
PieChartBasicConfigComponent, PieChartBasicConfigComponent,
BarChartBasicConfigComponent, BarChartBasicConfigComponent,
PolarAreaChartBasicConfigComponent, PolarAreaChartBasicConfigComponent,
RadarChartBasicConfigComponent RadarChartBasicConfigComponent,
ScadaTestBasicConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -216,7 +220,8 @@ import {
PieChartBasicConfigComponent, PieChartBasicConfigComponent,
BarChartBasicConfigComponent, BarChartBasicConfigComponent,
PolarAreaChartBasicConfigComponent, PolarAreaChartBasicConfigComponent,
RadarChartBasicConfigComponent RadarChartBasicConfigComponent,
ScadaTestBasicConfigComponent
] ]
}) })
export class BasicWidgetConfigModule { export class BasicWidgetConfigModule {
@ -255,5 +260,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-pie-chart-basic-config': PieChartBasicConfigComponent, 'tb-pie-chart-basic-config': PieChartBasicConfigComponent,
'tb-bar-chart-basic-config': BarChartBasicConfigComponent, 'tb-bar-chart-basic-config': BarChartBasicConfigComponent,
'tb-polar-area-chart-basic-config': PolarAreaChartBasicConfigComponent, 'tb-polar-area-chart-basic-config': PolarAreaChartBasicConfigComponent,
'tb-radar-chart-basic-config': RadarChartBasicConfigComponent 'tb-radar-chart-basic-config': RadarChartBasicConfigComponent,
'tb-scada-test-basic-config': ScadaTestBasicConfigComponent
}; };

View File

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="scadaTestWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<tb-scada-object-settings
formControlName="scadaObject"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType">
</tb-scada-object-settings>
</ng-container>

View File

@ -0,0 +1,64 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TargetDevice, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models';
@Component({
selector: 'tb-scada-test-basic-config',
templateUrl: './scada-test-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class ScadaTestBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.scadaTestWidgetConfigForm.get('targetDevice').value;
}
scadaTestWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.scadaTestWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: ScadaTestWidgetSettings = {...(configData.config.settings as ScadaTestWidgetSettings || { scadaObject: {}})};
this.scadaTestWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
scadaObject: [settings.scadaObject, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.targetDevice = config.targetDevice;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.scadaObject = config.scadaObject;
return this.widgetConfig;
}
}

View File

@ -0,0 +1,19 @@
<!--
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 #scadaShape class="tb-scada-shape">
</div>

View File

@ -0,0 +1,22 @@
/**
* 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.
*/
.tb-scada-shape {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,102 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
Renderer2,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { SVG, Svg } from '@svgdotjs/svg.js';
import { HttpClient } from '@angular/common/http';
import { ScadaObject, ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models';
@Component({
selector: 'tb-scada-test-widget',
templateUrl: './scada-test-widget.component.html',
styleUrls: ['../action/action-widget.scss', './scada-test-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ScadaTestWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('scadaShape', {static: false})
scadaShape: ElementRef<HTMLElement>;
private settings: ScadaTestWidgetSettings;
private autoScale = true;
private scadaObject: ScadaObject;
private shapeResize$: ResizeObserver;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
protected cd: ChangeDetectorRef,
private http: HttpClient) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...this.ctx.settings};
this.scadaObject = new ScadaObject(this.ctx, '/assets/widget/scada/drawing.svg', this.settings.scadaObject);
this.scadaObject.init().subscribe();
}
ngAfterViewInit(): void {
this.scadaObject.addTo(this.scadaShape.nativeElement);
if (this.autoScale) {
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.scadaShape.nativeElement);
this.onResize();
}
super.ngAfterViewInit();
}
ngOnDestroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
if (this.scadaObject) {
this.scadaObject.destroy();
}
super.ngOnDestroy();
}
public onInit() {
super.onInit();
}
private onResize() {
const shapeWidth = this.scadaShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.scadaShape.nativeElement.getBoundingClientRect().height;
this.scadaObject.setSize(shapeWidth, shapeHeight);
}
}

View File

@ -0,0 +1,21 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models';
export interface ScadaTestWidgetSettings {
scadaObject: ScadaObjectSettings;
}

View File

@ -0,0 +1,398 @@
///
/// 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 { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, Svg, SVG, Timeline } from '@svgdotjs/svg.js';
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
import { insertVariable, isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, mergeDeep } 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 { WidgetContext } from '@home/models/widget-component.models';
export type ValueMatcherType = 'any' | 'constant' | 'range';
export interface ValueMatcher {
type: ValueMatcherType;
value?: any;
range?: {from: number; to: number};
}
export type ScadaObjectAttributeValueType = 'input' | 'property';
export interface ScadaObjectAttributeValue {
type: ScadaObjectAttributeValueType;
propertyId?: string;
}
export interface ScadaObjectAttributeState {
name: string;
value: ScadaObjectAttributeValue;
}
export interface ScadaObjectState {
tag: string;
attributes: ScadaObjectAttributeState[];
animate?: number;
}
export interface ScadaObjectUpdateState {
matcher: ValueMatcher;
state: ScadaObjectState[];
}
export enum ScadaObjectBehaviorType {
setValue = 'setValue',
getValue = 'getValue'
}
export interface ScadaObjectBehaviorBase {
id: string;
name: string;
type: ScadaObjectBehaviorType;
}
export interface ScadaObjectBehaviorGet extends ScadaObjectBehaviorBase {
valueType: ValueType;
defaultValue: any;
onUpdate: ScadaObjectUpdateState[];
}
export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase {
todo: any;
}
export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet;
export type ScadaObjectPropertyType = 'string' | 'number' | 'color';
export interface ScadaObjectPropertyBase {
id: string;
name: string;
type: ScadaObjectPropertyType;
default: any;
}
export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase {
min?: number;
max?: number;
}
export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty;
export interface ScadaObjectMetadata {
title: string;
initial: ScadaObjectState[];
behavior: ScadaObjectBehavior[];
properties: ScadaObjectProperty[];
}
export const emptyMetadata: ScadaObjectMetadata = {
title: '',
initial: [],
behavior: [],
properties: []
};
export const parseScadaObjectMetadataFromContent = (svgContent: string): ScadaObjectMetadata => {
try {
const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml');
return parseScadaObjectMetadataFromDom(svgDoc);
} catch (_e) {
return emptyMetadata;
}
};
const parseScadaObjectMetadataFromDom = (svgDoc: Document): ScadaObjectMetadata => {
try {
const elements = svgDoc.getElementsByTagName('tb:metadata');
if (elements.length) {
return JSON.parse(elements[0].innerHTML);
} else {
return emptyMetadata;
}
} catch (_e) {
return emptyMetadata;
}
};
const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings<any> => ({
action: GetValueAction.DO_NOTHING,
defaultValue: get.defaultValue,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
});
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 property of metadata.properties) {
settings[property.id] = property.default;
}
return settings;
};
export type ScadaObjectSettings = {[id: string]: any};
export class ScadaObject {
private metadata: ScadaObjectMetadata;
private settings: ScadaObjectSettings;
private rootElement: HTMLElement;
private svgShape: Svg;
private box: Box;
private targetWidth: number;
private targetHeight: number;
private loadingSubject = new BehaviorSubject(false);
private valueGetters: ValueGetter<any>[] = [];
private valueActions: ValueAction[] = [];
private animationTimeline: Timeline;
loading$ = this.loadingSubject.asObservable().pipe(share());
constructor(private ctx: WidgetContext,
private svgPath: string,
private inputSettings: ScadaObjectSettings) {}
public init(): Observable<any> {
return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe(
map((inputSvgContent) => {
const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml');
this.metadata = parseScadaObjectMetadataFromDom(doc);
const defaults = defaultScadaObjectSettings(this.metadata);
this.settings = mergeDeep<ScadaObjectSettings>({}, defaults, this.inputSettings || {});
this.prepareSvgShape(doc);
this.prepareStates();
})
);
}
public addTo(element: HTMLElement) {
this.rootElement = element;
if (this.svgShape) {
this.svgShape.addTo(element);
}
}
public destroy() {
this.valueActions.forEach(v => v.destroy());
this.loadingSubject.complete();
this.loadingSubject.unsubscribe();
}
public setSize(targetWidth: number, targetHeight: number) {
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
if (this.svgShape) {
this.resize();
}
}
private prepareSvgShape(doc: XMLDocument) {
const elements = doc.getElementsByTagName('tb:metadata');
for (let i=0;i<elements.length;i++) {
elements.item(i).remove();
}
let svgContent = doc.documentElement.innerHTML;
for (const property of this.metadata.properties) {
const value = this.settings[property.id] || '';
svgContent = insertVariable(svgContent, property.id, value);
}
this.svgShape = SVG().svg(svgContent);
this.svgShape.node.style.overflow = 'visible';
this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox();
this.svgShape.size(this.box.width, this.box.height);
if (this.rootElement) {
this.svgShape.addTo(this.rootElement);
}
if (this.targetWidth && this.targetHeight) {
this.resize();
}
}
private prepareStates() {
for (const behavior of this.metadata.behavior) {
if (behavior.type === ScadaObjectBehaviorType.getValue) {
const getBehavior = behavior as ScadaObjectBehaviorGet;
let getValueSettings: GetValueSettings<any> = this.settings[getBehavior.id];
getValueSettings = {...getValueSettings, actionLabel: getBehavior.name};
const valueGetter =
ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, {
next: (val) => {this.onValue(getBehavior.id, val);},
error: (e) => {}
});
this.valueGetters.push(valueGetter);
this.valueActions.push(valueGetter);
}
}
if (this.metadata.initial) {
this.updateState(this.metadata.initial);
}
if (this.valueGetters.length) {
const getValueObservables: Array<Observable<any>> = [];
this.valueGetters.forEach(valueGetter => {
getValueObservables.push(valueGetter.getValue());
});
this.loadingSubject.next(true);
forkJoin(getValueObservables).subscribe(
{
next: () => {
this.loadingSubject.next(false);
},
error: () => {
this.loadingSubject.next(false);
}
}
);
}
}
private resize() {
let scale: number;
if (this.targetWidth < this.targetHeight) {
scale = this.targetWidth / this.box.width;
} else {
scale = this.targetHeight / this.box.height;
}
this.svgShape.node.style.transform = `scale(${scale})`;
}
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 = this.filterUpdateStates(getBehavior.onUpdate, value);
if (this.animationTimeline) {
this.animationTimeline.finish();
}
for (const updateState of updateStates) {
this.updateState(updateState.state, value);
}
}
private updateState(state: ScadaObjectState[], value?: any) {
for (const stateEntry of state) {
const tag = stateEntry.tag;
const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`);
const attrs = this.computeAttributes(stateEntry.attributes, value);
elements.forEach(e => {
this.setElementAttributes(e, attrs, stateEntry.animate);
});
}
}
private normalizeValue(value: any, type: ValueType): any {
if (isUndefinedOrNull(value)) {
switch (type) {
case ValueType.STRING:
return '';
case ValueType.INTEGER:
case ValueType.DOUBLE:
return 0;
case ValueType.BOOLEAN:
return false;
case ValueType.JSON:
return {};
}
} else {
return value;
}
}
private computeAttributes(attributes: ScadaObjectAttributeState[], value: any): {[attr: string]: any} {
const res: {[attr: string]: any} = {};
for (const attribute of attributes) {
const attr = attribute.name;
res[attr] = this.getAttributeValue(attribute, value);
}
return res;
}
private setElementAttributes(element: Element, attrs: {[attr: string]: any}, animate?: number) {
if (isDefinedAndNotNull(animate)) {
this.animation(element, animate).attr(attrs);
} else {
element.attr(attrs);
}
}
private animation(element: Element, duration: number): Runner {
if (!this.animationTimeline) {
this.animationTimeline = new Timeline();
}
element.timeline(this.animationTimeline);
return element.animate(duration, 0, 'now');
}
private getAttributeValue(attribute: ScadaObjectAttributeState, value: any): any {
if (attribute.value.type === 'input') {
return value;
} else if (attribute.value.type === 'property') {
const id = attribute.value.propertyId;
return this.settings[id] || '';
} else {
return '';
}
}
private filterUpdateStates(states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] {
return states.filter(s => this.valueMatches(s.matcher, val));
}
private 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;
}
}
}
}

View File

@ -0,0 +1,48 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="scadaObjectSettingsFormGroup">
<div>{{ metadata?.title }}</div>
<div *ngIf="metadata?.behavior?.length" class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.slider.behavior</div>
<div *ngFor="let behaviour of metadata.behavior" class="tb-form-row">
<div class="fixed-title-width">{{ behaviour.name }}</div>
<tb-get-value-action-settings *ngIf="behaviour.type === ScadaObjectBehaviorType.getValue"
fxFlex
panelTitle="{{ behaviour.name }}"
[valueType]="behaviour.valueType"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="{{ behaviour.id }}">
</tb-get-value-action-settings>
</div>
</div>
<div *ngIf="metadata?.properties?.length" class="tb-form-panel">
<div *ngFor="let property of metadata.properties" class="tb-form-row space-between">
<div class="fixed-title-width">{{ property.name }}</div>
<tb-color-input *ngIf="property.type === 'color'"
asBoxInput
colorClearButton
formControlName="{{ property.id }}">
</tb-color-input>
<mat-form-field *ngIf="property.type === 'number'" appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [min]="property.min" [max]="property.max" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</ng-container>

View File

@ -0,0 +1,168 @@
///
/// 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, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
ValidatorFn,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
defaultScadaObjectSettings,
parseScadaObjectMetadataFromContent,
ScadaObjectBehaviorType,
ScadaObjectMetadata,
ScadaObjectSettings
} from '@home/components/widget/lib/scada/scada.models';
import { HttpClient } from '@angular/common/http';
import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-scada-object-settings',
templateUrl: './scada-object-settings.component.html',
styleUrls: ['./../../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaObjectSettingsComponent),
multi: true
}
]
})
export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor {
ScadaObjectBehaviorType = ScadaObjectBehaviorType;
@Input()
disabled: boolean;
@Input()
svgPath = '/assets/widget/scada/drawing.svg';
@Input()
aliasController: IAliasController;
@Input()
targetDevice: TargetDevice;
@Input()
widgetType: widgetType;
private modelValue: ScadaObjectSettings;
private propagateChange = null;
public scadaObjectSettingsFormGroup: UntypedFormGroup;
metadata: ScadaObjectMetadata;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,
private http: HttpClient) {
}
ngOnInit(): void {
this.scadaObjectSettingsFormGroup = this.fb.group({});
this.scadaObjectSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.loadMetadata();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['svgPath'].includes(propName)) {
this.loadMetadata();
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.scadaObjectSettingsFormGroup.disable({emitEvent: false});
} else {
this.scadaObjectSettingsFormGroup.enable({emitEvent: false});
}
}
writeValue(value: ScadaObjectSettings): void {
this.modelValue = value || {};
this.setupValue();
}
private loadMetadata() {
this.http.get(this.svgPath, {responseType: 'text'}).subscribe(
(svgContent) => {
this.metadata = parseScadaObjectMetadataFromContent(svgContent);
for (const control of Object.keys(this.scadaObjectSettingsFormGroup.controls)) {
this.scadaObjectSettingsFormGroup.removeControl(control, {emitEvent: false});
}
for (const behaviour of this.metadata.behavior) {
this.scadaObjectSettingsFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false});
}
for (const property of this.metadata.properties) {
const validators: ValidatorFn[] = [];
if (property.type === 'number') {
if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min));
}
if (isDefinedAndNotNull(property.max)) {
validators.push(Validators.max(property.max));
}
}
this.scadaObjectSettingsFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
}
this.setupValue();
}
);
}
private setupValue() {
if (this.metadata) {
const defaults = defaultScadaObjectSettings(this.metadata);
this.modelValue = {...defaults, ...this.modelValue};
this.scadaObjectSettingsFormGroup.patchValue(
this.modelValue, {emitEvent: false}
);
}
}
private updateModel() {
this.modelValue = this.scadaObjectSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
protected readonly ValueType = ValueType;
}

View File

@ -149,6 +149,9 @@ import {
StatusWidgetStateSettingsComponent StatusWidgetStateSettingsComponent
} from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component'; } from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component';
import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/common/chart/chart-bar-settings.component'; import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/common/chart/chart-bar-settings.component';
import {
ScadaObjectSettingsComponent
} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -204,6 +207,7 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/
TimeSeriesChartStateRowComponent, TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent, TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent, StatusWidgetStateSettingsComponent,
ScadaObjectSettingsComponent,
DataKeyInputComponent, DataKeyInputComponent,
EntityAliasInputComponent EntityAliasInputComponent
], ],
@ -265,6 +269,7 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/
TimeSeriesChartStateRowComponent, TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent, TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent, StatusWidgetStateSettingsComponent,
ScadaObjectSettingsComponent,
DataKeyInputComponent, DataKeyInputComponent,
EntityAliasInputComponent EntityAliasInputComponent
], ],

View File

@ -92,6 +92,7 @@ import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-c
import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component'; import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component';
import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component'; import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component';
import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component'; import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component';
import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scada-test-widget.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -150,7 +151,8 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
PieChartWidgetComponent, PieChartWidgetComponent,
BarChartWidgetComponent, BarChartWidgetComponent,
PolarAreaWidgetComponent, PolarAreaWidgetComponent,
RadarChartWidgetComponent RadarChartWidgetComponent,
ScadaTestWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -212,7 +214,8 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
PieChartWidgetComponent, PieChartWidgetComponent,
BarChartWidgetComponent, BarChartWidgetComponent,
PolarAreaWidgetComponent, PolarAreaWidgetComponent,
RadarChartWidgetComponent RadarChartWidgetComponent,
ScadaTestWidgetComponent
], ],
providers: [ providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

View File

@ -0,0 +1,149 @@
<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",
"initial": [
{
"tag": "RECT",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "background"
}
},
{
"name": "stroke",
"value": {
"type": "property",
"propertyId": "strokeColor"
}
},
{
"name": "stroke-width",
"value": {
"type": "property",
"propertyId": "strokeWidth"
}
}
]
}
],
"behavior": [
{
"id": "initialState",
"name": "Initial state",
"type": "getValue",
"valueType": "DOUBLE",
"defaultValue": 0,
"onUpdate": [
{
"matcher": {
"type": "any"
},
"state": [
{
"tag": "RECT",
"animate": 200,
"attributes": [
{
"name": "height",
"value": {
"type": "input"
}
},
{
"name": "width",
"value": {
"type": "input"
}
}
]
}
]
}
]
},
{
"id": "disabledState",
"name": "Disabled state",
"type": "getValue",
"valueType": "BOOLEAN",
"defaultValue": false,
"onUpdate": [
{
"matcher": {
"type": "constant",
"value": true
},
"state": [
{
"tag": "RECT",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "disabledBackground"
}
}
]
}
]
},
{
"matcher": {
"type": "constant",
"value": false
},
"state": [
{
"tag": "RECT",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "background"
}
}
]
}
]
}
]
}
],
"properties": [
{
"id": "strokeColor",
"name": "Stroke color",
"type": "color",
"default": "#aaa"
},
{
"id": "strokeWidth",
"name": "Stroke width",
"type": "number",
"default": 5,
"min": 0,
"max": 20
},
{
"id": "background",
"name": "Background",
"type": "color",
"default": "green"
},
{
"id": "disabledBackground",
"name": "Disabled background",
"type": "color",
"default": "#ccc"
}
]
}
</tb:metadata>
<rect tb:tag="RECT" width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB