From 63548dc3ffb7ade52286cd4760a7e7a5d881740e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Mon, 3 Mar 2025 15:12:53 +0100 Subject: [PATCH] UI: Greenhouse + ROUTING constants --- src/main/angular/src/app/app.component.html | 4 + src/main/angular/src/app/app.component.less | 24 ++++ src/main/angular/src/app/app.component.ts | 9 +- src/main/angular/src/app/app.routes.ts | 31 ++++- .../src/app/core/AbstractRepositoryService.ts | 91 +++++++++++++++ src/main/angular/src/app/core/api.service.ts | 2 +- .../electro/power/electro-power.component.ts | 15 ++- .../greenhouse/greenhouse.component.html | 22 ++++ .../greenhouse/greenhouse.component.less | 0 .../greenhouse/greenhouse.component.ts | 28 +++++ .../angular/src/app/series/series.service.ts | 108 ++++-------------- src/main/angular/src/app/value/Unit.ts | 8 ++ src/main/angular/src/app/value/Value.ts | 14 ++- src/main/angular/src/styles.less | 33 ++++++ 14 files changed, 294 insertions(+), 95 deletions(-) create mode 100644 src/main/angular/src/app/core/AbstractRepositoryService.ts create mode 100644 src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.html create mode 100644 src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.less create mode 100644 src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.ts diff --git a/src/main/angular/src/app/app.component.html b/src/main/angular/src/app/app.component.html index 7dd570e..1855c39 100644 --- a/src/main/angular/src/app/app.component.html +++ b/src/main/angular/src/app/app.component.html @@ -1 +1,5 @@ + + diff --git a/src/main/angular/src/app/app.component.less b/src/main/angular/src/app/app.component.less index e69de29..69a3009 100644 --- a/src/main/angular/src/app/app.component.less +++ b/src/main/angular/src/app/app.component.less @@ -0,0 +1,24 @@ +.menubar { + border-bottom: 1px solid black; + background-color: #303d47; + + .menuitem { + padding: 0.1em 0.25em; + } + + .menuitemLeft { + float: left; + border-right: 1px solid black; + } + + .menuitemRight { + float: right; + border-left: 1px solid black; + } + + .menuitemActive { + color: white; + background-color: #006ebc; + } + +} diff --git a/src/main/angular/src/app/app.component.ts b/src/main/angular/src/app/app.component.ts index ec110d1..42e6800 100644 --- a/src/main/angular/src/app/app.component.ts +++ b/src/main/angular/src/app/app.component.ts @@ -1,12 +1,17 @@ import {Component} from '@angular/core'; -import {RouterOutlet} from '@angular/router'; +import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router'; +import {menubar, ROUTING} from './app.routes'; +import {NgForOf} from '@angular/common'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, RouterLink, NgForOf, RouterLinkActive], templateUrl: './app.component.html', styleUrl: './app.component.less' }) export class AppComponent { + protected readonly ROUTING = ROUTING; + + protected readonly menubar = menubar; } diff --git a/src/main/angular/src/app/app.routes.ts b/src/main/angular/src/app/app.routes.ts index c85a6e2..b8039df 100644 --- a/src/main/angular/src/app/app.routes.ts +++ b/src/main/angular/src/app/app.routes.ts @@ -1,7 +1,34 @@ import {Routes} from '@angular/router'; import {DashboardComponent} from './dashboard/dashboard.component'; +import {GreenhouseComponent} from './greenhouse/greenhouse/greenhouse.component'; + +export class Path { + + constructor( + readonly path: string, + readonly title: string, + readonly menu: boolean, + ) { + // + } + + get routerLink(): string { + return `/${this.path}`; + } + +} + +export const ROUTING = { + ENERGY: new Path('Energie', 'Energie', true), + GREENHOUSE: new Path('Greenhouse', 'Gewächshaus', true), +} + +export function menubar(): Path[] { + return Object.values(ROUTING).filter(v => v.menu); +} export const routes: Routes = [ - {path: 'Dashboard', component: DashboardComponent}, - {path: '**', redirectTo: 'Dashboard'}, + {path: ROUTING.ENERGY.path, component: DashboardComponent}, + {path: ROUTING.GREENHOUSE.path, component: GreenhouseComponent}, + {path: '**', redirectTo: ROUTING.ENERGY.path}, ]; diff --git a/src/main/angular/src/app/core/AbstractRepositoryService.ts b/src/main/angular/src/app/core/AbstractRepositoryService.ts new file mode 100644 index 0000000..1b4ca05 --- /dev/null +++ b/src/main/angular/src/app/core/AbstractRepositoryService.ts @@ -0,0 +1,91 @@ +import {Subscription} from "rxjs"; +import {Next} from "./types"; +import {Series} from "../series/Series"; +import {SeriesWrapper} from "../series/SeriesWrapper"; +import {Inject, LOCALE_ID} from "@angular/core"; +import {ApiService} from "./api.service"; + +export abstract class AbstractRepositoryService { + + private readonly clientSubscriptions: Subscription[] = []; + + private readonly subs: Subscription[] = []; + + private readonly clientCallbacks: Next[] = []; + + protected abstract get liveValues(): SeriesWrapper[]; + + constructor( + @Inject(LOCALE_ID) readonly locale: string, + protected readonly api: ApiService, + ) { + // + } + + protected onSubscribe(subscription: Subscription): Subscription { + this.clientSubscriptions.push(subscription); + this.ensureApiSubscribed(); + return subscription; + } + + protected onUnsubscribe(subscription: Subscription): Subscription { + this.clientSubscriptions.splice(this.clientSubscriptions.indexOf(subscription), 1); + if (this.clientSubscriptions.length === 0) { + this.ensureApiUnsubscribed(); + } + return subscription; + } + + private ensureApiSubscribed() { + if (this.subs.length !== 0) { + return; + } + this.subs.push(this.api.subscribe(['Series'], j => Series.fromJson(j, this.locale), series => this.update(series))); + this.subs.push(this.api.subscribeConnection(connected => { + if (connected) { + this.all(); + } else { + this.liveValues.forEach(liveValue => liveValue.series = null); + } + })); + } + + private ensureApiUnsubscribed() { + if (this.subs.length <= 0) { + return; + } + this.subs.forEach(sub => sub.unsubscribe()); + this.subs.length = 0; + } + + private update(series: Series) { + this.liveValues + .filter(liveValue => liveValue.name === series.name) + .forEach(liveValue => liveValue.series = series); + this.clientCallbacks.forEach(next => next(series)); + } + + all(next?: Next) { + this.api.getList(['Series', 'all'], j => Series.fromJson(j, this.locale), list => { + list.forEach(item => this.update(item)); + if (next) { + next(list); + } + }); + } + + subscribeAny(next?: Next): Subscription { + const wrapper: Next = series => { // to let clientCallbacks only contain unique instances + if (next) { + next(series); + } + }; + this.clientCallbacks.push(wrapper); + const subscription = new Subscription(() => { + this.onUnsubscribe(subscription); + this.clientCallbacks.splice(this.clientCallbacks.indexOf(wrapper), 1); + }); + return this.onSubscribe(subscription); + } + +} diff --git a/src/main/angular/src/app/core/api.service.ts b/src/main/angular/src/app/core/api.service.ts index b6d864a..49ff931 100644 --- a/src/main/angular/src/app/core/api.service.ts +++ b/src/main/angular/src/app/core/api.service.ts @@ -4,7 +4,7 @@ import {map, Subscription} from 'rxjs'; import {StompService} from '@stomp/ng2-stompjs'; import {FromJson, Next} from './types'; -const DEV_TO_PROD = false; +const DEV_TO_PROD = true; @Injectable({ providedIn: 'root' diff --git a/src/main/angular/src/app/electro/power/electro-power.component.ts b/src/main/angular/src/app/electro/power/electro-power.component.ts index cf3fbc9..4849690 100644 --- a/src/main/angular/src/app/electro/power/electro-power.component.ts +++ b/src/main/angular/src/app/electro/power/electro-power.component.ts @@ -1,7 +1,8 @@ -import {Component} from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import {PercentBarComponent} from "../../shared/percent-bar/percent-bar.component"; import {Value} from '../../value/Value'; import {SeriesService} from '../../series/series.service'; +import {Subscription} from 'rxjs'; @Component({ selector: 'app-electro-power', @@ -11,13 +12,23 @@ import {SeriesService} from '../../series/series.service'; templateUrl: './electro-power.component.html', styleUrl: './electro-power.component.less' }) -export class ElectroPowerComponent { +export class ElectroPowerComponent implements OnInit, OnDestroy { + + private subs: Subscription[] = []; constructor( readonly seriesService: SeriesService, ) { } + ngOnInit(): void { + this.subs.push(this.seriesService.subscribeAny()); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + get powerProduction(): Value | undefined { return this.seriesService.powerProduced.series?.lastValue; } diff --git a/src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.html b/src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.html new file mode 100644 index 0000000..d109cd4 --- /dev/null +++ b/src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + +
Temperatur{{seriesService.greenhouseTemperature.series?.lastValue?.localeString}}{{seriesService.greenhouseTemperature.series?.lastValue?.unit?.unit}}
Relative Luftfeuchte{{seriesService.greenhouseHumidityRelative.series?.lastValue?.localeString}}{{seriesService.greenhouseHumidityRelative.series?.lastValue?.unit?.unit}}
Absolute Luftfeuchte{{seriesService.greenhouseHumidityAbsolute.series?.lastValue?.localeString}}{{seriesService.greenhouseHumidityAbsolute.series?.lastValue?.unit?.unit}}
Beleuchtungsstärke{{seriesService.greenhouseIlluminance.series?.lastValue?.localeString}}{{seriesService.greenhouseIlluminance.series?.lastValue?.unit?.unit}}
diff --git a/src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.less b/src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.ts b/src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.ts new file mode 100644 index 0000000..4d534fc --- /dev/null +++ b/src/main/angular/src/app/greenhouse/greenhouse/greenhouse.component.ts @@ -0,0 +1,28 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {SeriesService} from '../../series/series.service'; +import {Subscription} from 'rxjs'; + +@Component({ + selector: 'app-greenhouse', + imports: [], + templateUrl: './greenhouse.component.html', + styleUrl: './greenhouse.component.less' +}) +export class GreenhouseComponent implements OnInit, OnDestroy { + + private subs: Subscription[] = []; + + constructor( + readonly seriesService: SeriesService, + ) { + } + + ngOnInit(): void { + this.subs.push(this.seriesService.subscribeAny()); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + +} diff --git a/src/main/angular/src/app/series/series.service.ts b/src/main/angular/src/app/series/series.service.ts index b6bb422..07b6301 100644 --- a/src/main/angular/src/app/series/series.service.ts +++ b/src/main/angular/src/app/series/series.service.ts @@ -2,21 +2,15 @@ import {Inject, Injectable, LOCALE_ID} from '@angular/core'; import {ApiService} from '../core/api.service'; import {Alignment} from './Alignment'; import {AggregationWrapperDto} from './AggregationWrapperDto'; -import {Subscription} from 'rxjs'; import {Series} from './Series'; import {SeriesWrapper} from './SeriesWrapper'; import {Next} from '../core/types'; +import {AbstractRepositoryService} from '../core/AbstractRepositoryService'; @Injectable({ providedIn: 'root' }) -export class SeriesService { - - private readonly clientSubscriptions: Subscription[] = []; - - private readonly subs: Subscription[] = []; - - private readonly clientCallbacks: Next[] = []; +export class SeriesService extends AbstractRepositoryService { readonly powerConsumed: SeriesWrapper = new SeriesWrapper("power/consumed", this.onSubscribe, this.onUnsubscribe); @@ -26,84 +20,32 @@ export class SeriesService { readonly powerBalance: SeriesWrapper = new SeriesWrapper("power/balance", this.onSubscribe, this.onUnsubscribe); - readonly liveValues: SeriesWrapper[] = [ - this.powerConsumed, - this.powerProduced, - this.powerSelf, - this.powerBalance, - ] + readonly greenhouseTemperature: SeriesWrapper = new SeriesWrapper("greenhouse/temperature", this.onSubscribe, this.onUnsubscribe); + + readonly greenhouseHumidityRelative: SeriesWrapper = new SeriesWrapper("greenhouse/humidity/relative", this.onSubscribe, this.onUnsubscribe); + + readonly greenhouseHumidityAbsolute: SeriesWrapper = new SeriesWrapper("greenhouse/humidity/absolute", this.onSubscribe, this.onUnsubscribe); + + readonly greenhouseIlluminance: SeriesWrapper = new SeriesWrapper("greenhouse/illuminance", this.onSubscribe, this.onUnsubscribe); + + protected get liveValues(): SeriesWrapper[] { + return [ + this.powerConsumed, + this.powerProduced, + this.powerSelf, + this.powerBalance, + this.greenhouseTemperature, + this.greenhouseHumidityRelative, + this.greenhouseHumidityAbsolute, + this.greenhouseIlluminance, + ]; + } constructor( - @Inject(LOCALE_ID) readonly locale: string, - protected readonly api: ApiService, + @Inject(LOCALE_ID) locale: string, + api: ApiService, ) { - // - } - - private onSubscribe(subscription: Subscription): Subscription { - this.clientSubscriptions.push(subscription); - this.ensureApiSubscribed(); - return subscription; - } - - private onUnsubscribe(subscription: Subscription): Subscription { - this.clientSubscriptions.splice(this.clientSubscriptions.indexOf(subscription), 1); - if (this.clientSubscriptions.length === 0) { - this.ensureApiUnsubscribed(); - } - return subscription; - } - - private ensureApiSubscribed() { - if (this.subs.length !== 0) { - return; - } - this.subs.push(this.api.subscribe(['Series'], j => Series.fromJson(j, this.locale), series => this.update(series))); - this.subs.push(this.api.subscribeConnection(connected => { - if (connected) { - this.all(); - } else { - this.liveValues.forEach(liveValue => liveValue.series = null); - } - })); - } - - private ensureApiUnsubscribed() { - if (this.subs.length <= 0) { - return; - } - this.subs.forEach(sub => sub.unsubscribe()); - this.subs.length = 0; - } - - private update(series: Series) { - this.liveValues - .filter(liveValue => liveValue.name === series.name) - .forEach(liveValue => liveValue.series = series); - this.clientCallbacks.forEach(next => next(series)); - } - - subscribeAny(next?: Next): Subscription { - const wrapper: Next = series => { // to let clientCallbacks only contain unique instances - if (next) { - next(series); - } - }; - this.clientCallbacks.push(wrapper); - const subscription = new Subscription(() => { - this.onUnsubscribe(subscription); - this.clientCallbacks.splice(this.clientCallbacks.indexOf(wrapper), 1); - }); - return this.onSubscribe(subscription); - } - - all(next?: Next) { - this.api.getList(['Series', 'all'], j => Series.fromJson(j, this.locale), list => { - list.forEach(item => this.update(item)); - if (next) { - next(list); - } - }); + super(locale, api); } aggregations(alignment: Alignment, offset: number, next: Next) { diff --git a/src/main/angular/src/app/value/Unit.ts b/src/main/angular/src/app/value/Unit.ts index 90a15f4..cd53798 100644 --- a/src/main/angular/src/app/value/Unit.ts +++ b/src/main/angular/src/app/value/Unit.ts @@ -20,6 +20,14 @@ export class Unit { static readonly PRECIPITATION_MM = new Unit('PRECIPITATION_MM', 'mm'); + static readonly TEMPERATURE_C = new Unit('TEMPERATURE_C', '°C'); + + static readonly HUMIDITY_RELATIVE_PERCENT = new Unit('HUMIDITY_RELATIVE_PERCENT', '%'); + + static readonly HUMIDITY_ABSOLUTE_GM3 = new Unit('HUMIDITY_ABSOLUTE_GM3', 'g/m³'); + + static readonly ILLUMINANCE_LUX = new Unit('ILLUMINANCE_LUX', 'lux'); + private constructor( readonly name: string, readonly unit: string, diff --git a/src/main/angular/src/app/value/Value.ts b/src/main/angular/src/app/value/Value.ts index c6e1705..c3f1bc6 100644 --- a/src/main/angular/src/app/value/Value.ts +++ b/src/main/angular/src/app/value/Value.ts @@ -3,12 +3,20 @@ import {validateNumber} from "../core/validators"; export class Value { + readonly localeString: string; + + readonly valueInteger: string; + + readonly valueFraction: string; + constructor( readonly value: number, readonly unit: Unit, readonly decimals: number, readonly locale: string) { - // + this.localeString = this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals}); + this.valueInteger = this.localeString.split(/[,.]/)[0]; + this.valueFraction = this.localeString.split(/[,.]/)[1]; } static fromJson2(json: any, locale: string): Value { @@ -41,10 +49,6 @@ export class Value { return `${(this.localeString)}${this.unit.unit}`; } - get localeString(): string { - return this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals}); - } - negate() { return new Value(-this.value, this.unit, this.decimals, this.locale); } diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index 2bd6789..74633b3 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -23,3 +23,36 @@ button { div { overflow: hidden; } + +table.vertical { + width: 100%; + + th { + text-align: left; + } + + td.valueInteger { + width: 0; + white-space: nowrap; + text-align: right; + } + + td.valueDelimiter { + width: 0; + white-space: nowrap; + text-align: center; + } + + td.valueFraction { + width: 0; + white-space: nowrap; + text-align: left; + } + + td.unit { + width: 0; + white-space: nowrap; + text-align: left; + } + +}