UI: Greenhouse + ROUTING constants

This commit is contained in:
Patrick Haßel 2025-03-03 15:12:53 +01:00
parent 0561940861
commit 63548dc3ff
14 changed files with 294 additions and 95 deletions

View File

@ -1 +1,5 @@
<div class="menubar">
<div class="menuitem menuitemLeft" *ngFor="let route of menubar()" [routerLink]="[route.routerLink]" routerLinkActive="menuitemActive">{{ route.title }}</div>
</div>
<router-outlet/> <router-outlet/>

View File

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

View File

@ -1,12 +1,17 @@
import {Component} from '@angular/core'; 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({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet], imports: [RouterOutlet, RouterLink, NgForOf, RouterLinkActive],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.less' styleUrl: './app.component.less'
}) })
export class AppComponent { export class AppComponent {
protected readonly ROUTING = ROUTING;
protected readonly menubar = menubar;
} }

View File

@ -1,7 +1,34 @@
import {Routes} from '@angular/router'; import {Routes} from '@angular/router';
import {DashboardComponent} from './dashboard/dashboard.component'; 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 = [ export const routes: Routes = [
{path: 'Dashboard', component: DashboardComponent}, {path: ROUTING.ENERGY.path, component: DashboardComponent},
{path: '**', redirectTo: 'Dashboard'}, {path: ROUTING.GREENHOUSE.path, component: GreenhouseComponent},
{path: '**', redirectTo: ROUTING.ENERGY.path},
]; ];

View File

@ -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<Series>[] = [];
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<Series[]>) {
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<Series>): Subscription {
const wrapper: Next<Series> = 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);
}
}

View File

@ -4,7 +4,7 @@ import {map, Subscription} from 'rxjs';
import {StompService} from '@stomp/ng2-stompjs'; import {StompService} from '@stomp/ng2-stompjs';
import {FromJson, Next} from './types'; import {FromJson, Next} from './types';
const DEV_TO_PROD = false; const DEV_TO_PROD = true;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

@ -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 {PercentBarComponent} from "../../shared/percent-bar/percent-bar.component";
import {Value} from '../../value/Value'; import {Value} from '../../value/Value';
import {SeriesService} from '../../series/series.service'; import {SeriesService} from '../../series/series.service';
import {Subscription} from 'rxjs';
@Component({ @Component({
selector: 'app-electro-power', selector: 'app-electro-power',
@ -11,13 +12,23 @@ import {SeriesService} from '../../series/series.service';
templateUrl: './electro-power.component.html', templateUrl: './electro-power.component.html',
styleUrl: './electro-power.component.less' styleUrl: './electro-power.component.less'
}) })
export class ElectroPowerComponent { export class ElectroPowerComponent implements OnInit, OnDestroy {
private subs: Subscription[] = [];
constructor( constructor(
readonly seriesService: SeriesService, readonly seriesService: SeriesService,
) { ) {
} }
ngOnInit(): void {
this.subs.push(this.seriesService.subscribeAny());
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
get powerProduction(): Value | undefined { get powerProduction(): Value | undefined {
return this.seriesService.powerProduced.series?.lastValue; return this.seriesService.powerProduced.series?.lastValue;
} }

View File

@ -0,0 +1,22 @@
<table class="vertical">
<tr>
<th>Temperatur</th>
<td class="valueInteger">{{seriesService.greenhouseTemperature.series?.lastValue?.localeString}}</td>
<td class="unit">{{seriesService.greenhouseTemperature.series?.lastValue?.unit?.unit}}</td>
</tr>
<tr>
<th>Relative Luftfeuchte</th>
<td class="valueInteger">{{seriesService.greenhouseHumidityRelative.series?.lastValue?.localeString}}</td>
<td class="unit">{{seriesService.greenhouseHumidityRelative.series?.lastValue?.unit?.unit}}</td>
</tr>
<tr>
<th>Absolute Luftfeuchte</th>
<td class="valueInteger">{{seriesService.greenhouseHumidityAbsolute.series?.lastValue?.localeString}}</td>
<td class="unit">{{seriesService.greenhouseHumidityAbsolute.series?.lastValue?.unit?.unit}}</td>
</tr>
<tr>
<th>Beleuchtungsstärke</th>
<td class="valueInteger">{{seriesService.greenhouseIlluminance.series?.lastValue?.localeString}}</td>
<td class="unit">{{seriesService.greenhouseIlluminance.series?.lastValue?.unit?.unit}}</td>
</tr>
</table>

View File

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

View File

@ -2,21 +2,15 @@ import {Inject, Injectable, LOCALE_ID} from '@angular/core';
import {ApiService} from '../core/api.service'; import {ApiService} from '../core/api.service';
import {Alignment} from './Alignment'; import {Alignment} from './Alignment';
import {AggregationWrapperDto} from './AggregationWrapperDto'; import {AggregationWrapperDto} from './AggregationWrapperDto';
import {Subscription} from 'rxjs';
import {Series} from './Series'; import {Series} from './Series';
import {SeriesWrapper} from './SeriesWrapper'; import {SeriesWrapper} from './SeriesWrapper';
import {Next} from '../core/types'; import {Next} from '../core/types';
import {AbstractRepositoryService} from '../core/AbstractRepositoryService';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class SeriesService { export class SeriesService extends AbstractRepositoryService {
private readonly clientSubscriptions: Subscription[] = [];
private readonly subs: Subscription[] = [];
private readonly clientCallbacks: Next<Series>[] = [];
readonly powerConsumed: SeriesWrapper = new SeriesWrapper("power/consumed", this.onSubscribe, this.onUnsubscribe); 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 powerBalance: SeriesWrapper = new SeriesWrapper("power/balance", this.onSubscribe, this.onUnsubscribe);
readonly liveValues: SeriesWrapper[] = [ 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.powerConsumed,
this.powerProduced, this.powerProduced,
this.powerSelf, this.powerSelf,
this.powerBalance, this.powerBalance,
] this.greenhouseTemperature,
this.greenhouseHumidityRelative,
this.greenhouseHumidityAbsolute,
this.greenhouseIlluminance,
];
}
constructor( constructor(
@Inject(LOCALE_ID) readonly locale: string, @Inject(LOCALE_ID) locale: string,
protected readonly api: ApiService, api: ApiService,
) { ) {
// super(locale, api);
}
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<Series>): Subscription {
const wrapper: Next<Series> = 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<Series[]>) {
this.api.getList(['Series', 'all'], j => Series.fromJson(j, this.locale), list => {
list.forEach(item => this.update(item));
if (next) {
next(list);
}
});
} }
aggregations(alignment: Alignment, offset: number, next: Next<AggregationWrapperDto>) { aggregations(alignment: Alignment, offset: number, next: Next<AggregationWrapperDto>) {

View File

@ -20,6 +20,14 @@ export class Unit {
static readonly PRECIPITATION_MM = new Unit('PRECIPITATION_MM', 'mm'); 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( private constructor(
readonly name: string, readonly name: string,
readonly unit: string, readonly unit: string,

View File

@ -3,12 +3,20 @@ import {validateNumber} from "../core/validators";
export class Value { export class Value {
readonly localeString: string;
readonly valueInteger: string;
readonly valueFraction: string;
constructor( constructor(
readonly value: number, readonly value: number,
readonly unit: Unit, readonly unit: Unit,
readonly decimals: number, readonly decimals: number,
readonly locale: string) { 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 { static fromJson2(json: any, locale: string): Value {
@ -41,10 +49,6 @@ export class Value {
return `${(this.localeString)}${this.unit.unit}`; return `${(this.localeString)}${this.unit.unit}`;
} }
get localeString(): string {
return this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals});
}
negate() { negate() {
return new Value(-this.value, this.unit, this.decimals, this.locale); return new Value(-this.value, this.unit, this.decimals, this.locale);
} }

View File

@ -23,3 +23,36 @@ button {
div { div {
overflow: hidden; 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;
}
}