Browse Source

UI: Scada widget test

pull/11391/head
Igor Kulikov 3 years ago
parent
commit
eb50048c50
  1. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  2. 26
      ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.html
  3. 64
      ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts
  4. 19
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html
  5. 22
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss
  6. 102
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts
  7. 21
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts
  8. 398
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts
  9. 48
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html
  10. 168
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts
  11. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  12. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  13. 149
      ui-ngx/src/assets/widget/scada/drawing.svg

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -129,6 +129,9 @@ import {
import {
RadarChartBasicConfigComponent
} 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({
declarations: [
@ -171,7 +174,8 @@ import {
PieChartBasicConfigComponent,
BarChartBasicConfigComponent,
PolarAreaChartBasicConfigComponent,
RadarChartBasicConfigComponent
RadarChartBasicConfigComponent,
ScadaTestBasicConfigComponent
],
imports: [
CommonModule,
@ -216,7 +220,8 @@ import {
PieChartBasicConfigComponent,
BarChartBasicConfigComponent,
PolarAreaChartBasicConfigComponent,
RadarChartBasicConfigComponent
RadarChartBasicConfigComponent,
ScadaTestBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -255,5 +260,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-pie-chart-basic-config': PieChartBasicConfigComponent,
'tb-bar-chart-basic-config': BarChartBasicConfigComponent,
'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
};

26
ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.html

@ -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>

64
ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts

@ -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;
}
}

19
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html

@ -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>

22
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss

@ -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;
}

102
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts

@ -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);
}
}

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

@ -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;
}

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

@ -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;
}
}
}
}

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

@ -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>

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

@ -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;
}

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

@ -149,6 +149,9 @@ import {
StatusWidgetStateSettingsComponent
} 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 {
ScadaObjectSettingsComponent
} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.component';
@NgModule({
declarations: [
@ -204,6 +207,7 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
ScadaObjectSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],
@ -265,6 +269,7 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
ScadaObjectSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -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 { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-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({
declarations:
@ -150,7 +151,8 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
PieChartWidgetComponent,
BarChartWidgetComponent,
PolarAreaWidgetComponent,
RadarChartWidgetComponent
RadarChartWidgetComponent,
ScadaTestWidgetComponent
],
imports: [
CommonModule,
@ -212,7 +214,8 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
PieChartWidgetComponent,
BarChartWidgetComponent,
PolarAreaWidgetComponent,
RadarChartWidgetComponent
RadarChartWidgetComponent,
ScadaTestWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

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

@ -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

Loading…
Cancel
Save