From 1e676d8e3bc61fe480f180ea60d1bd645cb0dcba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Fri, 31 Oct 2025 11:30:54 +0100 Subject: [PATCH] CENTRALIZED: locationService.location, seriesService.all, dateService.now --- src/main/angular/src/app/date.service.ts | 19 +++ src/main/angular/src/app/location/Location.ts | 72 +++++++++-- .../app/location/detail/location-detail.html | 26 ++-- .../app/location/detail/location-detail.ts | 90 ++++--------- .../electricity/location-electricity.ts | 119 ------------------ ...-electricity.html => location-energy.html} | 8 +- ...-electricity.less => location-energy.less} | 0 .../location/electricity/location-energy.ts | 107 ++++++++++++++++ .../{electricity => }/graph/simple-plot.html | 0 .../{electricity => }/graph/simple-plot.less | 0 .../{electricity => }/graph/simple-plot.ts | 6 +- .../src/app/location/location-service.ts | 32 ++++- .../app/location/power/location-power.html | 47 +++++++ .../app/location/power/location-power.less | 1 + .../src/app/location/power/location-power.ts | 22 ++++ src/main/angular/src/app/series/Series.ts | 4 + src/main/angular/src/app/series/Value.ts | 83 +++++++----- .../src/app/series/select/series-select.html | 2 +- .../src/app/series/select/series-select.ts | 50 +++++--- .../angular/src/app/series/series-service.ts | 22 +++- 20 files changed, 443 insertions(+), 267 deletions(-) create mode 100644 src/main/angular/src/app/date.service.ts delete mode 100644 src/main/angular/src/app/location/electricity/location-electricity.ts rename src/main/angular/src/app/location/electricity/{location-electricity.html => location-energy.html} (78%) rename src/main/angular/src/app/location/electricity/{location-electricity.less => location-energy.less} (100%) create mode 100644 src/main/angular/src/app/location/electricity/location-energy.ts rename src/main/angular/src/app/location/{electricity => }/graph/simple-plot.html (100%) rename src/main/angular/src/app/location/{electricity => }/graph/simple-plot.less (100%) rename src/main/angular/src/app/location/{electricity => }/graph/simple-plot.ts (92%) create mode 100644 src/main/angular/src/app/location/power/location-power.html create mode 100644 src/main/angular/src/app/location/power/location-power.less create mode 100644 src/main/angular/src/app/location/power/location-power.ts diff --git a/src/main/angular/src/app/date.service.ts b/src/main/angular/src/app/date.service.ts new file mode 100644 index 0000000..49d0806 --- /dev/null +++ b/src/main/angular/src/app/date.service.ts @@ -0,0 +1,19 @@ +import {Injectable} from '@angular/core'; +import {timer} from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class DateService { + + private _now: Date = new Date(); + + get now(): Date { + return this._now; + } + + constructor() { + timer(1000, 1000).subscribe(() => this._now = new Date()); + } + +} diff --git a/src/main/angular/src/app/location/Location.ts b/src/main/angular/src/app/location/Location.ts index 5070ef9..a26a11d 100644 --- a/src/main/angular/src/app/location/Location.ts +++ b/src/main/angular/src/app/location/Location.ts @@ -1,5 +1,6 @@ import {or, validateNumber, validateString} from '../common'; import {Series} from '../series/Series'; +import {Value} from '../series/Value'; export class Location { @@ -8,14 +9,43 @@ export class Location { readonly name: string, readonly latitude: number, readonly longitude: number, - readonly energyPurchase: Series | null, - readonly energyDeliver: Series | null, - readonly energyProduce: Series | null, - readonly powerPurchase: Series | null, - readonly powerDeliver: Series | null, - readonly powerProduce: Series | null, + private _energyPurchase: Series | null, + private _energyDeliver: Series | null, + private _energyProduce: Series | null, + private _powerPurchase: Series | null, + private _powerDeliver: Series | null, + private _powerProduce: Series | null, + private _powerConsume: Value = Value.NULL, ) { - // + this.updateConsume(); + } + + readonly updateSeries = (series: Series) => { + if (series.equals(this._energyPurchase)) { + this._energyPurchase = series; + } + if (series.equals(this._energyDeliver)) { + this._energyDeliver = series; + } + if (series.equals(this._energyProduce)) { + this._energyProduce = series; + } + if (series.equals(this._powerProduce)) { + this._powerProduce = series; + this.updateConsume(); + } + if (series.equals(this._powerPurchase)) { + this._powerPurchase = series; + this.updateConsume(); + } + if (series.equals(this._powerDeliver)) { + this._powerDeliver = series; + this.updateConsume(); + } + }; + + private updateConsume() { + this._powerConsume = Value.ZERO.plus(this._powerPurchase?.value, true).plus(this._powerProduce?.value, true).minus(this._powerDeliver?.value, true); } static fromJson(json: any): Location { @@ -33,4 +63,32 @@ export class Location { ); } + get energyPurchase(): Series | null { + return this._energyPurchase; + } + + get energyDeliver(): Series | null { + return this._energyDeliver; + } + + get energyProduce(): Series | null { + return this._energyProduce; + } + + get powerPurchase(): Series | null { + return this._powerPurchase; + } + + get powerDeliver(): Series | null { + return this._powerDeliver; + } + + get powerProduce(): Series | null { + return this._powerProduce; + } + + get powerConsume(): Value | null { + return this._powerConsume; + } + } diff --git a/src/main/angular/src/app/location/detail/location-detail.html b/src/main/angular/src/app/location/detail/location-detail.html index 604d471..941b3d1 100644 --- a/src/main/angular/src/app/location/detail/location-detail.html +++ b/src/main/angular/src/app/location/detail/location-detail.html @@ -1,10 +1,10 @@ @if (location) { - + - + - +
  @@ -31,17 +31,17 @@
- +
- Breitegrad + Breitengrad
- +
@@ -51,7 +51,7 @@
- +
@@ -71,7 +71,7 @@
- +
@@ -81,7 +81,7 @@
- +
@@ -91,7 +91,7 @@
- +
@@ -111,7 +111,7 @@
- +
@@ -121,7 +121,7 @@
- +
@@ -131,7 +131,7 @@
- +
diff --git a/src/main/angular/src/app/location/detail/location-detail.ts b/src/main/angular/src/app/location/detail/location-detail.ts index e6913f6..e58f14f 100644 --- a/src/main/angular/src/app/location/detail/location-detail.ts +++ b/src/main/angular/src/app/location/detail/location-detail.ts @@ -5,21 +5,15 @@ import {Location} from '../Location'; import {Text} from '../../shared/text/text'; import {Number} from '../../shared/number/number'; import {SeriesSelect} from '../../series/select/series-select'; -import {Series} from '../../series/Series'; -import {SeriesService} from '../../series/series-service'; -import {SeriesType} from '../../series/SeriesType'; -import {Subscription, timer} from 'rxjs'; -import {LocationElectricity} from '../electricity/location-electricity'; +import {Subscription} from 'rxjs'; +import {LocationEnergy} from '../electricity/location-energy'; import {Interval} from '../../series/Interval'; import {MenuService} from '../../menu-service'; import {DatePipe} from '@angular/common'; -import {WebsocketService} from '../../common'; - -function yesterday(now: any) { - const yesterday = new Date(now.getTime()); - yesterday.setDate(yesterday.getDate() - 1); - return yesterday; -} +import {Series} from '../../series/Series'; +import {SeriesType} from '../../series/SeriesType'; +import {DateService} from '../../date.service'; +import {LocationPower} from '../power/location-power'; @Component({ selector: 'app-location-detail', @@ -27,13 +21,18 @@ function yesterday(now: any) { Text, Number, SeriesSelect, - LocationElectricity + LocationEnergy, + LocationPower ], templateUrl: './location-detail.html', styleUrl: './location-detail.less', }) export class LocationDetail implements OnInit, OnDestroy { + protected readonly filterEnergy = (series: Series) => series.type === SeriesType.DELTA && series.unit === 'kWh'; + + protected readonly filterPower = (series: Series) => series.type === SeriesType.VARYING && series.unit === 'W'; + protected readonly Interval = Interval; protected readonly Math = Math; @@ -42,85 +41,46 @@ export class LocationDetail implements OnInit, OnDestroy { private readonly subs: Subscription [] = []; - private series: Series[] = []; - - protected now: Date = new Date(); - - protected yesterday: Date = yesterday(this.now); - protected offset: number = 1; private readonly datePipe: DatePipe; constructor( readonly locationService: LocationService, - readonly seriesService: SeriesService, readonly activatedRoute: ActivatedRoute, readonly menuService: MenuService, - readonly websocketService: WebsocketService, + readonly dateService: DateService, @Inject(LOCALE_ID) readonly locale: string, ) { this.datePipe = new DatePipe(locale); } ngOnInit(): void { - this.subs.push(this.websocketService.onChange((connected) => { - if (connected) { - this.seriesService.findAll(list => this.series = list); - } else { - this.location = null; - } - })); - this.subs.push(this.activatedRoute.params.subscribe(params => { - this.locationService.getById(params['id'], location => { - this.location = location; - this.menuService.title = this.location.name; - }); - })); - this.subs.push(this.seriesService.subscribe(this.updateSeries)); - this.subs.push(timer(1000, 1000).subscribe(() => { - this.now = new Date(); - this.yesterday = yesterday(this.now); - })); + this.locationService.id = null; + this.subs.push(this.activatedRoute.params.subscribe(params => this.locationService.id = params['id'] || null)); + this.subs.push(this.locationService.location$.subscribe(this.onLocationChange)); } + private readonly onLocationChange = (location: Location | null): void => { + this.location = location; + if (this.location) { + this.menuService.title = this.location.name; + } + }; + ngOnDestroy(): void { this.location = null; this.menuService.title = ""; this.subs.forEach(sub => sub.unsubscribe()); + this.subs.length = 0; } - protected readonly updateLocation = (location: Location): void => { - if (this.location?.id === location.id) { - this.location = location; - } - }; - - protected readonly updateSeries = (fresh: Series): void => { - const index = this.series.findIndex(series => series.id === fresh.id); - if (index >= 0) { - this.series.splice(index, 1, fresh); - } else { - this.series.push(fresh); - } - }; - - protected readonly filterEnergy = (): Series[] => { - return this.series.filter(series => series.type === SeriesType.DELTA && series.unit === 'kWh'); - }; - - protected readonly filterPower = (): Series[] => { - return this.series.filter(series => series.type === SeriesType.VARYING && series.unit === 'W'); - }; - protected offsetDayTitle(): string { if (this.offset === 1) { return 'Gestern'; - } else if (this.offset === 2) { - return 'Vorgestern'; } else { if (this.offset < 7) { - const d = new Date(this.now); + const d = new Date(this.dateService.now); d.setDate(d.getDate() - this.offset); return this.datePipe.transform(d, 'EEEE') || ''; } diff --git a/src/main/angular/src/app/location/electricity/location-electricity.ts b/src/main/angular/src/app/location/electricity/location-electricity.ts deleted file mode 100644 index 907af62..0000000 --- a/src/main/angular/src/app/location/electricity/location-electricity.ts +++ /dev/null @@ -1,119 +0,0 @@ -import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core'; -import {Location} from '../Location'; -import {Series} from '../../series/Series'; -import {Next} from '../../common'; -import {Interval} from '../../series/Interval'; -import {PointService} from '../../point/point-service'; -import {SeriesService} from '../../series/series-service'; -import {Subscription} from 'rxjs'; -import {Value} from '../../series/Value'; - -@Component({ - selector: 'app-series-history', - imports: [], - templateUrl: './location-electricity.html', - styleUrl: './location-electricity.less', -}) -export class LocationElectricity implements OnInit, AfterViewInit, OnDestroy { - - protected readonly Interval = Interval; - - private readonly subs: Subscription[] = []; - - protected purchase: Value = Value.NONE; - - protected deliver: Value = Value.NONE; - - protected produce: Value = Value.NONE; - - protected consume: Value = Value.NONE; - - @Input() - heading: string = ""; - - private _o_: number = 0; - - @Input() - set offset(value: number) { - this._o_ = value; - this.ngAfterViewInit(); - } - - get offset(): number { - return this._o_; - } - - @Input() - interval: Interval | null = null; - - @Input() - location!: Location; - - @Input() - now: Date = new Date(); - - constructor( - readonly pointService: PointService, - readonly serieService: SeriesService, - ) { - // - } - - ngOnInit(): void { - this.subs.push(this.serieService.subscribe(this.update)); - } - - ngAfterViewInit(): void { - if (this.interval) { - this.history(null, this.location?.energyPurchase, history => this.purchase = history); - this.history(null, this.location?.energyDeliver, history => this.deliver = history); - this.history(null, this.location?.energyProduce, history => this.produce = history); - } else { - this.history(null, this.location?.powerPurchase, history => this.purchase = history); - this.history(null, this.location?.powerDeliver, history => this.deliver = history); - this.history(null, this.location?.powerProduce, history => this.produce = history); - } - } - - ngOnDestroy(): void { - this.subs.forEach(sub => sub.unsubscribe()); - } - - protected readonly update = (fresh: Series): void => { - if (this.interval) { - if (this.offset > 0) { - return; - } - this.history(fresh, this.location?.energyPurchase, value => this.purchase = value); - this.history(fresh, this.location?.energyDeliver, value => this.deliver = value); - this.history(fresh, this.location?.energyProduce, value => this.produce = value); - } else { - this.history(fresh, this.location?.powerPurchase, value => this.purchase = value); - this.history(fresh, this.location?.powerDeliver, value => this.deliver = value); - this.history(fresh, this.location?.powerProduce, value => this.produce = value); - } - }; - - private history(fresh: Series | null | undefined, series: Series | null | undefined, next: Next): void { - const callNextAndUpdateConsume = (value: Value) => { - next(value); - this.consume = this.purchase.plus(this.produce).minus(this.deliver); - }; - if (fresh !== null && fresh !== undefined) { - if (fresh.id !== series?.id) { - return; - } - series = fresh; - } - if (!series) { - callNextAndUpdateConsume(Value.NONE); - return - } - if (this.interval) { - this.pointService.relative([series], this.interval, this.offset, 1, response => callNextAndUpdateConsume(Value.ofPoint(response, 0, 0, 1))); - } else { - callNextAndUpdateConsume(series.value); - } - } - -} diff --git a/src/main/angular/src/app/location/electricity/location-electricity.html b/src/main/angular/src/app/location/electricity/location-energy.html similarity index 78% rename from src/main/angular/src/app/location/electricity/location-electricity.html rename to src/main/angular/src/app/location/electricity/location-energy.html index 3159e9a..49e929e 100644 --- a/src/main/angular/src/app/location/electricity/location-electricity.html +++ b/src/main/angular/src/app/location/electricity/location-energy.html @@ -12,7 +12,7 @@ Bezug
- {{ purchase.toValueString(true, interval ? null : now) }} + {{ purchase.toValueString(interval ? null : now) }}
@@ -21,7 +21,7 @@ Solar
- {{ produce.toValueString(true, interval ? null : now) }} + {{ produce.toValueString(interval ? null : now) }}
@@ -30,7 +30,7 @@ Verbrauch
- {{ consume.toValueString(true, interval ? null : now) }} + {{ consume.toValueString(interval ? null : now) }}
@@ -39,7 +39,7 @@ Einspeisung
- {{ deliver.toValueString(true, interval ? null : now) }} + {{ deliver.toValueString(interval ? null : now) }}
diff --git a/src/main/angular/src/app/location/electricity/location-electricity.less b/src/main/angular/src/app/location/electricity/location-energy.less similarity index 100% rename from src/main/angular/src/app/location/electricity/location-electricity.less rename to src/main/angular/src/app/location/electricity/location-energy.less diff --git a/src/main/angular/src/app/location/electricity/location-energy.ts b/src/main/angular/src/app/location/electricity/location-energy.ts new file mode 100644 index 0000000..65d6e94 --- /dev/null +++ b/src/main/angular/src/app/location/electricity/location-energy.ts @@ -0,0 +1,107 @@ +import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Location} from '../Location'; +import {Series} from '../../series/Series'; +import {Next} from '../../common'; +import {Interval} from '../../series/Interval'; +import {PointService} from '../../point/point-service'; +import {SeriesService} from '../../series/series-service'; +import {Subscription} from 'rxjs'; +import {Value} from '../../series/Value'; + +@Component({ + selector: 'app-series-history', + imports: [], + templateUrl: './location-energy.html', + styleUrl: './location-energy.less', +}) +export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy { + + protected readonly Interval = Interval; + + private readonly subs: Subscription[] = []; + + protected purchase: Value = Value.NULL; + + protected deliver: Value = Value.NULL; + + protected produce: Value = Value.NULL; + + protected consume: Value = Value.NULL; + + @Input() + heading!: string; + + private _o_: number = 0; + + @Input() + set offset(value: number) { + this._o_ = value; + this.ngAfterViewInit(); + } + + get offset(): number { + return this._o_; + } + + @Input() + interval!: Interval; + + @Input() + location!: Location; + + @Input() + now: Date = new Date(); + + constructor( + readonly pointService: PointService, + readonly serieService: SeriesService, + ) { + // + } + + ngOnInit(): void { + this.subs.push(this.serieService.subscribe(this.update)); + } + + ngAfterViewInit(): void { + this.fetch(null, this.location?.energyPurchase, history => this.purchase = history); + this.fetch(null, this.location?.energyDeliver, history => this.deliver = history); + this.fetch(null, this.location?.energyProduce, history => this.produce = history); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + + protected readonly update = (fresh: Series): void => { + if (this.offset > 0) { + return; + } + this.fetch(fresh, this.location?.energyPurchase, value => this.purchase = value); + this.fetch(fresh, this.location?.energyDeliver, value => this.deliver = value); + this.fetch(fresh, this.location?.energyProduce, value => this.produce = value); + }; + + private fetch(fresh: Series | null | undefined, series: Series | null | undefined, next: Next): void { + const callNextAndUpdateConsume = (value: Value) => { + next(value); + this.consume = this.purchase.plus(this.produce, true).minus(this.deliver, true); + }; + if (fresh !== null && fresh !== undefined) { + if (fresh.id !== series?.id) { + return; + } + series = fresh; + } + if (!series) { + callNextAndUpdateConsume(Value.NULL); + return + } + if (this.interval) { + this.pointService.relative([series], this.interval, this.offset, 1, response => callNextAndUpdateConsume(Value.ofPoint(response, 0, 0, 1))); + } else { + callNextAndUpdateConsume(series.value); + } + } + +} diff --git a/src/main/angular/src/app/location/electricity/graph/simple-plot.html b/src/main/angular/src/app/location/graph/simple-plot.html similarity index 100% rename from src/main/angular/src/app/location/electricity/graph/simple-plot.html rename to src/main/angular/src/app/location/graph/simple-plot.html diff --git a/src/main/angular/src/app/location/electricity/graph/simple-plot.less b/src/main/angular/src/app/location/graph/simple-plot.less similarity index 100% rename from src/main/angular/src/app/location/electricity/graph/simple-plot.less rename to src/main/angular/src/app/location/graph/simple-plot.less diff --git a/src/main/angular/src/app/location/electricity/graph/simple-plot.ts b/src/main/angular/src/app/location/graph/simple-plot.ts similarity index 92% rename from src/main/angular/src/app/location/electricity/graph/simple-plot.ts rename to src/main/angular/src/app/location/graph/simple-plot.ts index e31a8f2..a32c7a2 100644 --- a/src/main/angular/src/app/location/electricity/graph/simple-plot.ts +++ b/src/main/angular/src/app/location/graph/simple-plot.ts @@ -1,7 +1,7 @@ import {AfterViewInit, Component, Input} from '@angular/core'; -import {Location} from '../../Location'; -import {Interval} from '../../../series/Interval'; -import {PointService} from '../../../point/point-service'; +import {Location} from '../Location'; +import {Interval} from '../../series/Interval'; +import {PointService} from '../../point/point-service'; @Component({ selector: 'app-series-history-graph', diff --git a/src/main/angular/src/app/location/location-service.ts b/src/main/angular/src/app/location/location-service.ts index 9269e55..ce0944a 100644 --- a/src/main/angular/src/app/location/location-service.ts +++ b/src/main/angular/src/app/location/location-service.ts @@ -1,14 +1,44 @@ import {Injectable} from '@angular/core'; import {ApiService, CrudService, Next, WebsocketService} from '../common'; import {Location} from './Location' +import {SeriesService} from '../series/series-service'; +import {BehaviorSubject, Observable} from 'rxjs'; @Injectable({ providedIn: 'root' }) export class LocationService extends CrudService { - constructor(api: ApiService, ws: WebsocketService) { + private readonly _location = new BehaviorSubject(null); + + private _id: number | null = null; + + constructor( + api: ApiService, + ws: WebsocketService, + readonly seriesService: SeriesService, + ) { super(api, ws, ['Location'], Location.fromJson); + this.seriesService.subscribe(series => this._location.value?.updateSeries(series)); + this.ws.onConnect(this.fetch); + this.ws.onDisconnect(() => this._location.next(null)); + } + + set id(id: number | null) { + this._id = id; + this.fetch(); + } + + private readonly fetch = () => { + if (this._id === null) { + this._location.next(null); + } else { + this.getById(this._id, location => this._location.next(location)); + } + }; + + get location$(): Observable { + return this._location.asObservable(); } findAll(next: Next) { diff --git a/src/main/angular/src/app/location/power/location-power.html b/src/main/angular/src/app/location/power/location-power.html new file mode 100644 index 0000000..1683e7a --- /dev/null +++ b/src/main/angular/src/app/location/power/location-power.html @@ -0,0 +1,47 @@ +
+
+
+ Aktuelle Leistung +
+
+
+ +
+
+ Bezug +
+
+ {{ location.powerPurchase?.value?.toValueString(dateService.now) }} +
+
+ +
+
+ Solar +
+
+ {{ location.powerProduce?.value?.toValueString(dateService.now) }} +
+
+ +
+
+ Verbrauch +
+
+ {{ location.powerConsume?.toValueString(dateService.now) }} +
+
+ +
+
+ Einspeisung +
+
+ {{ location.powerDeliver?.value?.toValueString(dateService.now) }} +
+
+ +
+ +
diff --git a/src/main/angular/src/app/location/power/location-power.less b/src/main/angular/src/app/location/power/location-power.less new file mode 100644 index 0000000..876e2a5 --- /dev/null +++ b/src/main/angular/src/app/location/power/location-power.less @@ -0,0 +1 @@ +@import "../../../colors"; diff --git a/src/main/angular/src/app/location/power/location-power.ts b/src/main/angular/src/app/location/power/location-power.ts new file mode 100644 index 0000000..d067e77 --- /dev/null +++ b/src/main/angular/src/app/location/power/location-power.ts @@ -0,0 +1,22 @@ +import {Component, Input} from '@angular/core'; +import {Location} from '../Location'; +import {DateService} from '../../date.service'; + +@Component({ + selector: 'app-location-power', + imports: [], + templateUrl: './location-power.html', + styleUrl: './location-power.less', +}) +export class LocationPower { + + @Input() + location!: Location; + + constructor( + readonly dateService: DateService, + ) { + // + } + +} diff --git a/src/main/angular/src/app/series/Series.ts b/src/main/angular/src/app/series/Series.ts index 8d455a8..a77a00a 100644 --- a/src/main/angular/src/app/series/Series.ts +++ b/src/main/angular/src/app/series/Series.ts @@ -33,4 +33,8 @@ export class Series { ); } + equals(other: Series | null | undefined): boolean { + return this.id === other?.id; + } + } diff --git a/src/main/angular/src/app/series/Value.ts b/src/main/angular/src/app/series/Value.ts index 1baee61..208d97e 100644 --- a/src/main/angular/src/app/series/Value.ts +++ b/src/main/angular/src/app/series/Value.ts @@ -4,10 +4,12 @@ import {Series} from './Series'; export class Value { - static readonly NONE: Value = new Value(null, 0, 0, "", new Date()); + static readonly NULL: Value = new Value(NaN, 0, 0, "", new Date()); - private constructor( - readonly value: number | null, + static readonly ZERO: Value = new Value(0, 0, Infinity, "", new Date()); + + protected constructor( + readonly value: number, readonly precision: number, readonly seconds: number, readonly unit: string, @@ -16,57 +18,43 @@ export class Value { // } - toValueString(zeroToDash: boolean, now_ageCheckToDash: Date | null): string { - if (this.value === null) { + toValueString(now: Date | null): string { + if (isNaN(this.value)) { return "-"; } if (this.value === 0) { - return zeroToDash ? "-" : `0 ${this.unit}`; + return `0 ${this.unit}`; } - if (now_ageCheckToDash !== null) { - const ageSeconds = (now_ageCheckToDash.getTime() - this.date.getTime()) / 1000; - if (ageSeconds > this.seconds * 2.1) { - return `--- ${this.unit}` - } + if (now !== null && this.isOld(now)) { + return `--- ${this.unit}` } - const scale = Math.floor(Math.log10(this.value)); const rest = scale - this.precision + 1; - if (isNaN(rest)) { - console.log(this); - } if (rest >= 0) { return `${Math.round(this.value)} ${this.unit}`; } return formatNumber(this.value, "de-DE", `0.${-rest}-${-rest}`) + ' ' + this.unit; } - plus(other: Value): Value { - return this.operateSameUnit("plus", other, (a, b) => a + b); + plus(other: Value | null | undefined, nullToZero: boolean): Value { + if (!nullToZero && (other === null || other === undefined)) { + return Value.NULL; + } + return new BiValue(this, other || Value.ZERO, (a, b) => a + b); } - minus(other: Value): Value { - return this.operateSameUnit("minus", other, (a, b) => a - b); - } - - operateSameUnit(name: string, other: Value, operation: (a: number, b: number) => number): Value { - if (this.value === null || other.value === null) { - return Value.NONE; + minus(other: Value | null | undefined, nullToZero: boolean): Value { + if (!nullToZero && (other === null || other === undefined)) { + return Value.NULL; } - if (this.unit !== other.unit) { - throw new Error(`Operation '${name} needs units to be the same: this=${this}, other=${other}`); - } - const decimals = Math.max(this.precision, other.precision); - const seconds = Math.max(this.seconds, other.seconds); - const date = this.date.getTime() < other.date.getTime() ? this.date : other.date; - return new Value(operation(this.value, other.value), decimals, seconds, this.unit, date); + return new BiValue(this, other || Value.ZERO, (a, b) => a - b); } static of(series: Series, value: number | null | undefined, date: Date | null | undefined): Value { value = value === undefined ? null : value; date = date === undefined ? null : date; if (value === null) { - return this.NONE; + return this.NULL; } if (date === null) { throw new Error("When 'value' is set, 'last' must be set too, but isn't!") @@ -77,12 +65,12 @@ export class Value { static ofPoint(response: PointResponse, seriesIndex: number, pointIndex: number, valueIndex: number): Value { const series = response.series[seriesIndex]; if (!series) { - return this.NONE; + return this.NULL; } const point = series.points[pointIndex]; if (!point) { - return this.NONE; + return this.NULL; } const date = new Date(point[0] * 1000); @@ -90,4 +78,31 @@ export class Value { return Value.of(series.series, value, date); } + isOld(now: Date) { + const ageSeconds = (now.getTime() - this.date.getTime()) / 1000; + return ageSeconds > this.seconds * 2.1; + } + +} + +export class BiValue extends Value { + + constructor( + readonly a: Value, + readonly b: Value, + readonly operation: (a: number, b: number) => number, + ) { + if (a.unit !== "" && b.unit !== "" && a.unit !== b.unit) { + throw new Error(`Operation needs units to be equal or empty: this=${a}, other=${b}`); + } + const precision = Math.max(a.precision, b.precision); + const unit = a.unit || b.unit; + const date = a.date.getTime() < b.date.getTime() ? a.date : b.date; + super(operation(a.value, b.value), precision, 0, unit, date); + } + + override isOld(now: Date): boolean { + return this.a.isOld(now) || this.b.isOld(now); + } + } diff --git a/src/main/angular/src/app/series/select/series-select.html b/src/main/angular/src/app/series/select/series-select.html index c7844e1..2bfed28 100644 --- a/src/main/angular/src/app/series/select/series-select.html +++ b/src/main/angular/src/app/series/select/series-select.html @@ -8,7 +8,7 @@ @for (series of series; track series.id) { } diff --git a/src/main/angular/src/app/series/select/series-select.ts b/src/main/angular/src/app/series/select/series-select.ts index b6cfb2c..be95bb9 100644 --- a/src/main/angular/src/app/series/select/series-select.ts +++ b/src/main/angular/src/app/series/select/series-select.ts @@ -1,10 +1,13 @@ -import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; import {FaIconComponent} from '@fortawesome/angular-fontawesome'; import {FormsModule} from '@angular/forms'; import {NgClass} from '@angular/common'; import {faPen} from '@fortawesome/free-solid-svg-icons'; import {Series} from '../Series'; import {or} from '../../common'; +import {SeriesService} from '../series-service'; +import {map, Subscription} from 'rxjs'; +import {DateService} from '../../date.service'; @Component({ selector: 'app-series-select', @@ -16,21 +19,21 @@ import {or} from '../../common'; templateUrl: './series-select.html', styleUrl: './series-select.less', }) -export class SeriesSelect { +export class SeriesSelect implements OnInit, OnDestroy { protected readonly faPen = faPen; private _initial: Series | null = null; - @Input() - now!: Date; - @Input() series!: Series[]; @Input() allowEmpty: boolean = true; + @Input() + filter: (series: Series) => boolean = () => true; + @Output() readonly onChange = new EventEmitter(); @@ -40,24 +43,43 @@ export class SeriesSelect { protected readonly Series = Series; + private readonly subs: Subscription[] = []; + + constructor( + readonly seriesService: SeriesService, + readonly dateService: DateService, + ) { + // + } + + ngOnInit(): void { + this.subs.push( + this.seriesService + .all$ + .pipe(map(series => series.filter(this.filter))) + .subscribe(list => this.series = list) + ); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + this.subs.length = 0; + } + @Input() set initial(value: Series | null) { this._initial = value; this.reset(); } - private reset() { - this.model = or(this.initial, i => i.id, null); - } - - get initial(): Series | null { - return this._initial; - } + private readonly reset = (): void => { + this.model = or(this._initial, i => i.id, null); + }; protected classes(): {} { return { - "unchanged": this.model === this.initial, - "changed": this.model !== or(this.initial, i => i.id, null), + "unchanged": this.model === this._initial, + "changed": this.model !== or(this._initial, i => i.id, null), "invalid": !this.allowEmpty && this.model === null, }; } diff --git a/src/main/angular/src/app/series/series-service.ts b/src/main/angular/src/app/series/series-service.ts index 33a8f66..1ba2aa7 100644 --- a/src/main/angular/src/app/series/series-service.ts +++ b/src/main/angular/src/app/series/series-service.ts @@ -1,14 +1,14 @@ import {Inject, Injectable, LOCALE_ID} from '@angular/core'; -import {ApiService, CrudService, Next, WebsocketService} from '../common'; +import {ApiService, CrudService, WebsocketService} from '../common'; import {Series} from './Series'; -import {DatePipe} from '@angular/common'; +import {BehaviorSubject, Observable} from 'rxjs'; @Injectable({ providedIn: 'root' }) export class SeriesService extends CrudService { - private readonly datePipe: DatePipe; + private readonly allSubject: BehaviorSubject = new BehaviorSubject([]); constructor( api: ApiService, @@ -16,11 +16,21 @@ export class SeriesService extends CrudService { @Inject(LOCALE_ID) readonly locale: string, ) { super(api, ws, ['Series'], Series.fromJson); - this.datePipe = new DatePipe(locale); + this.subscribe(fresh => { + const index = this.allSubject.value.findIndex(series => series.id === fresh.id); + if (index >= 0) { + const list = [...this.allSubject.value]; + list.splice(index, 1, fresh); + this.allSubject.next(list); + } else { + this.allSubject.next([...this.allSubject.value, fresh]); + } + }); + this.getList(['findAll'], list => this.allSubject.next(list)); } - findAll(next: Next) { - this.getList(['findAll'], next); + get all$(): Observable { + return this.allSubject.asObservable(); } }