diff --git a/src/main/angular/src/app/app.html b/src/main/angular/src/app/app.html index a969382..a385b55 100644 --- a/src/main/angular/src/app/app.html +++ b/src/main/angular/src/app/app.html @@ -1,13 +1,18 @@
+
- Bezogen + Bezug
-
- {{ historyEnergyPurchase?.valueString }} +
+ {{ purchase?.toValueString(true, interval ? null : now) }}
+
- Eingespeist + Solar
-
- {{ historyEnergyDeliver?.valueString }} +
+ {{ produce?.toValueString(true, interval ? null : now) }}
+
- Erzeugt + Verbrauch
-
- {{ historyEnergyProduce?.valueString }} +
+ {{ consume?.toValueString(true, interval ? null : now) }}
+ +
+
+ Einspeisung +
+
+ {{ deliver?.toValueString(true, interval ? null : now) }} +
+
+
- + + @if (interval) { + + } +
diff --git a/src/main/angular/src/app/location/detail/history/series-history.less b/src/main/angular/src/app/location/detail/history/series-history.less index e69de29..de6aeea 100644 --- a/src/main/angular/src/app/location/detail/history/series-history.less +++ b/src/main/angular/src/app/location/detail/history/series-history.less @@ -0,0 +1 @@ +@import "../../../../colors"; diff --git a/src/main/angular/src/app/location/detail/history/series-history.ts b/src/main/angular/src/app/location/detail/history/series-history.ts index 16d4cf0..4f5ab18 100644 --- a/src/main/angular/src/app/location/detail/history/series-history.ts +++ b/src/main/angular/src/app/location/detail/history/series-history.ts @@ -1,30 +1,33 @@ -import {AfterViewInit, Component, Input} from '@angular/core'; +import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core'; import {Location} from '../../Location'; import {Series} from '../../../series/Series'; import {Next} from '../../../common'; -import {SeriesHistoryGraph} from './graph/simple-plot.component'; import {Interval} from '../../../series/Interval'; import {PointService} from '../../../point/point-service'; -import {PointSeries} from '../../../point/PointSeries'; +import {SeriesService} from '../../../series/series-service'; +import {Subscription} from 'rxjs'; +import {Value} from '../../../series/Value'; @Component({ selector: 'app-series-history', - imports: [ - SeriesHistoryGraph - ], + imports: [], templateUrl: './series-history.html', styleUrl: './series-history.less', }) -export class SeriesHistory implements AfterViewInit { - - protected historyEnergyPurchase: PointSeries | null = null; - - protected historyEnergyDeliver: PointSeries | null = null; - - protected historyEnergyProduce: PointSeries | null = null; +export class SeriesHistory implements OnInit, AfterViewInit, OnDestroy { protected readonly Interval = Interval; + private readonly subs: Subscription[] = []; + + protected purchase: Value | null = null; + + protected deliver: Value | null = null; + + protected produce: Value | null = null; + + protected consume: Value | null = null; + @Input() heading!: string; @@ -32,41 +35,80 @@ export class SeriesHistory implements AfterViewInit { offset: number = 0; @Input() - interval!: Interval; + interval: Interval | null = null; @Input() location!: Location; + @Input() + now: Date = new Date(); + constructor( readonly pointService: PointService, + readonly serieService: SeriesService, ) { // } - ngAfterViewInit(): void { - this.history(this.location?.energyPurchase, history => this.historyEnergyPurchase = history); - this.history(this.location?.energyDeliver, history => this.historyEnergyDeliver = history); - this.history(this.location?.energyProduce, history => this.historyEnergyProduce = history); + ngOnInit(): void { + this.subs.push(this.serieService.subscribe(this.update)); } - public readonly updateSeries = (fresh: Series): void => { - if (fresh.id === this.location?.energyPurchase?.id) { - this.history(this.location?.energyPurchase, history => this.historyEnergyPurchase = history); + 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); } - if (fresh.id === this.location?.energyDeliver?.id) { - this.history(this.location?.energyDeliver, history => this.historyEnergyDeliver = history); - } - if (fresh.id === this.location?.energyProduce?.id) { - this.history(this.location?.energyProduce, history => this.historyEnergyProduce = 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(series: Series | null | undefined, next: Next) { - if (!series || !this.interval) { - next(null); + private history(fresh: Series | null | undefined, series: Series | null | undefined, next: Next) { + const n = (value: Value | null) => { + next(value); + this.consume = this.purchase?.plus(this.produce)?.minus(this.deliver) || null; + } + if (fresh !== null && fresh !== undefined) { + if (fresh.id !== series?.id) { + return; + } + series = fresh; + } + if (!series) { + n(null); return } - this.pointService.relative([series], this.interval, this.offset, 1, response => next(response.series[0])); + if (this.interval) { + this.pointService.relative([series], this.interval, this.offset, 1, response => n(Value.ofPoint(response, 0, 0, 1))); + } else { + n(series.value); + } + } + + protected nullOrZero(value: Value | null | undefined): boolean { + return value === null || value === undefined || value.value === 0; } } 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 e572f18..e5561f7 100644 --- a/src/main/angular/src/app/location/detail/location-detail.html +++ b/src/main/angular/src/app/location/detail/location-detail.html @@ -1,8 +1,10 @@ @if (location) { - + - + + +
@@ -14,7 +16,7 @@
- Name: + Name
@@ -24,7 +26,7 @@
- Breitegrad: + Breitegrad
@@ -34,7 +36,7 @@
- Längengrad: + Längengrad
@@ -54,7 +56,7 @@
- Bezug: + Bezug
@@ -64,7 +66,7 @@
- Einspeisung: + Einspeisung
@@ -74,7 +76,7 @@
- Erzeugung: + Erzeugung
@@ -94,7 +96,7 @@
- Bezug: + Bezug
@@ -104,7 +106,7 @@
- Einspeisung: + Einspeisung
@@ -114,7 +116,7 @@
- Erzeugung: + Erzeugung
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 7e7d492..085f0eb 100644 --- a/src/main/angular/src/app/location/detail/location-detail.ts +++ b/src/main/angular/src/app/location/detail/location-detail.ts @@ -1,4 +1,4 @@ -import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import {LocationService} from '../location-service'; import {ActivatedRoute} from '@angular/router'; import {Location} from '../Location'; @@ -11,6 +11,7 @@ import {SeriesType} from '../../series/SeriesType'; import {Subscription, timer} from 'rxjs'; import {SeriesHistory} from './history/series-history'; import {Interval} from '../../series/Interval'; +import {MenuService} from '../../menu-service'; function yesterday(now: any) { const yesterday = new Date(now.getTime()); @@ -31,9 +32,6 @@ function yesterday(now: any) { }) export class LocationDetail implements OnInit, OnDestroy { - @ViewChild("today") - protected today!: SeriesHistory; - protected readonly Interval = Interval; protected location: Location | null = null; @@ -50,13 +48,17 @@ export class LocationDetail implements OnInit, OnDestroy { readonly locationService: LocationService, readonly seriesService: SeriesService, readonly activatedRoute: ActivatedRoute, + readonly menuService: MenuService, ) { // } ngOnInit(): void { this.activatedRoute.params.subscribe(params => { - this.locationService.getById(params['id'], location => this.location = location); + this.locationService.getById(params['id'], location => { + this.location = location; + this.menuService.title = this.location.name; + }); }); this.seriesService.findAll(list => this.series = list); this.subs.push(this.seriesService.subscribe(this.updateSeries)); @@ -67,6 +69,7 @@ export class LocationDetail implements OnInit, OnDestroy { } ngOnDestroy(): void { + this.menuService.title = ""; this.subs.forEach(sub => sub.unsubscribe()); } @@ -83,7 +86,6 @@ export class LocationDetail implements OnInit, OnDestroy { } else { this.series.push(fresh); } - this.today.updateSeries(fresh); }; protected readonly filterEnergy = (): Series[] => { diff --git a/src/main/angular/src/app/location/list/location-list.ts b/src/main/angular/src/app/location/list/location-list.ts index c1dfcea..c84b987 100644 --- a/src/main/angular/src/app/location/list/location-list.ts +++ b/src/main/angular/src/app/location/list/location-list.ts @@ -1,7 +1,8 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import {LocationService} from '../location-service'; import {Location} from '../Location'; import {RouterLink} from '@angular/router'; +import {MenuService} from '../../menu-service'; @Component({ selector: 'app-location-list', @@ -11,18 +12,24 @@ import {RouterLink} from '@angular/router'; templateUrl: './location-list.html', styleUrl: './location-list.less', }) -export class LocationList implements OnInit { +export class LocationList implements OnInit, OnDestroy { protected list: Location[] = []; constructor( readonly locationService: LocationService, + readonly menuService: MenuService, ) { // } ngOnInit(): void { this.locationService.findAll(list => this.list = list); + this.menuService.title = "Orte"; + } + + ngOnDestroy(): void { + this.menuService.title = ""; } } diff --git a/src/main/angular/src/app/menu-service.ts b/src/main/angular/src/app/menu-service.ts new file mode 100644 index 0000000..7b59bda --- /dev/null +++ b/src/main/angular/src/app/menu-service.ts @@ -0,0 +1,10 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class MenuService { + + title: string = ""; + +} diff --git a/src/main/angular/src/app/point/PointSeries.ts b/src/main/angular/src/app/point/PointSeries.ts index 9d56fbe..6438121 100644 --- a/src/main/angular/src/app/point/PointSeries.ts +++ b/src/main/angular/src/app/point/PointSeries.ts @@ -3,13 +3,11 @@ import {validateList, validateNumber} from "../common"; export class PointSeries { - readonly valueString: string; - constructor( readonly series: Series, readonly points: number[][], ) { - this.valueString = series.getValueString(points.length === 0 ? null : points[0][1]); + // } static fromJson(json: any): PointSeries { diff --git a/src/main/angular/src/app/series/Series.ts b/src/main/angular/src/app/series/Series.ts index d96443f..f4e0bc1 100644 --- a/src/main/angular/src/app/series/Series.ts +++ b/src/main/angular/src/app/series/Series.ts @@ -1,48 +1,36 @@ import {or, validateDate, validateEnum, validateNumber, validateString} from "../common"; import {SeriesType} from './SeriesType'; -import {formatNumber} from '@angular/common'; +import {Value} from './Value'; export class Series { - readonly valueString: string; + readonly value: Value | null = null; constructor( readonly id: number, readonly name: string, - readonly decimals: number, + readonly precision: number, readonly seconds: number, - readonly value: number | null, - readonly last: Date | null, - readonly unit: string, readonly type: SeriesType, + value: number | null, + readonly unit: string, + readonly last: Date | null, ) { - this.valueString = this.getValueString(value); - } - - getValueString(value: number | null | undefined): string { - return (value === null || value === undefined ? '-' : formatNumber(value, "de-DE", `0.${this.decimals}-${this.decimals}`)) + ' ' + this.unit; + this.value = Value.of(this, value, last); } static fromJson(json: any): Series { return new Series( validateNumber(json.id), validateString(json.name), - validateNumber(json.decimals), + validateNumber(json.precision), validateNumber(json.seconds), - or(json.value, validateNumber, null), - or(json.last, validateDate, null), - validateString(json.unit), validateEnum(json.type, SeriesType), + or(json.value, validateNumber, null), + validateString(json.unit), + or(json.last, validateDate, null), ); } - isOld(now: Date): boolean { - if (this.last === null) { - return true; - } - const ageSeconds = (now.getTime() - this.last.getTime()) / 1000; - return ageSeconds > this.seconds * 2.1; - } - } diff --git a/src/main/angular/src/app/series/Value.ts b/src/main/angular/src/app/series/Value.ts new file mode 100644 index 0000000..c0bd367 --- /dev/null +++ b/src/main/angular/src/app/series/Value.ts @@ -0,0 +1,89 @@ +import {formatNumber} from '@angular/common'; +import {PointResponse} from '../point/PointResponse'; +import {Series} from './Series'; + +export class Value { + + constructor( + readonly value: number, + readonly precision: number, + readonly seconds: number, + readonly unit: string, + readonly date: Date, + ) { + // + } + + toValueString(zeroToDash: boolean, now_ageCheckToDash: Date | null): string { + if (this.value === null || this.value === undefined) { + return "[???]"; + } + if (this.value === 0) { + return zeroToDash ? "-" : `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}` + } + } + + 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 | null | undefined): Value | null { + return this.operateSameUnit("plus", other, (a, b) => a + b); + } + + minus(other: Value | null | undefined): Value | null { + return this.operateSameUnit("minus", other, (a, b) => a - b); + } + + operateSameUnit(operationName: string, other: Value | null | undefined, operation: (a: number, b: number) => number): Value | null { + if (!other) { + return null; + } + if (this.unit !== other.unit) { + throw new Error(`Operation '${operationName} 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); + } + + static of(series: Series, value: number | null | undefined, date: Date | null | undefined): Value | null { + if (value === null || value === undefined) { + return null; + } + if (date === null || date === undefined) { + throw new Error("When 'value' is set, 'last' must be set too, but isn't!") + } + return new Value(value, series.precision, series.seconds, series.unit, date); + } + + static ofPoint(response: PointResponse, seriesIndex: number, pointIndex: number, valueIndex: number): Value | null { + const series = response.series[seriesIndex]; + if (!series) { + return null; + } + + const point = series.points[pointIndex]; + if (!point) { + return null; + } + + const date = new Date(point[0] * 1000); + const value = point[valueIndex]; + return Value.of(series.series, value, date); + } + +} 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 f5e896d..8ed6faa 100644 --- a/src/main/angular/src/app/series/select/series-select.html +++ b/src/main/angular/src/app/series/select/series-select.html @@ -8,12 +8,7 @@ @for (series of series; track series.id) { } diff --git a/src/main/angular/src/colors.less b/src/main/angular/src/colors.less new file mode 100644 index 0000000..350e7fb --- /dev/null +++ b/src/main/angular/src/colors.less @@ -0,0 +1,25 @@ +@empty: gray; +@purchase: red; +@deliver: magenta; +@produce: #0095ff; +@consume: #ff8800; + +.purchase { + color: @purchase; +} + +.deliver { + color: @deliver; +} + +.produce { + color: @produce; +} + +.consume { + color: @consume; +} + +.empty { + color: @empty !important; +} diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index 313a070..e8cf5cc 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -32,10 +32,10 @@ div { > .SectionHeading { display: flex; + color: dimgray; margin-top: -1.25em; > .SectionHeadingText { - font-weight: bold; background-color: white; } } @@ -45,7 +45,9 @@ div { overflow: visible; > .SectionHeading { + color: dimgray; > .SectionHeadingText { + font-size: 70%; font-style: italic; } } @@ -65,10 +67,10 @@ div { > .SectionHeading { display: flex; + color: dimgray; margin-top: -1.25em; > .SectionHeadingText { - font-weight: bold; background-color: white; } } @@ -81,14 +83,17 @@ div { .Section4 { flex: 1; - text-align: center; > .SectionHeadingText { - font-weight: bold; + text-align: right; + font-size: 70%; + color: dimgray; + font-style: italic; background-color: white; } > .SectionBody { + text-align: right; } } diff --git a/src/main/java/de/ph87/data/series/Series.java b/src/main/java/de/ph87/data/series/Series.java index d44ff4a..f30cd00 100644 --- a/src/main/java/de/ph87/data/series/Series.java +++ b/src/main/java/de/ph87/data/series/Series.java @@ -42,7 +42,7 @@ public class Series { @Setter @Column(nullable = false) - private int decimals = 1; + private int precision = 2; @Column @Nullable @@ -66,10 +66,10 @@ public class Series { @Enumerated(EnumType.STRING) private SeriesType type = SeriesType.VARYING; - public Series(@NonNull final String name, @NonNull final String unit, final int decimals, final int seconds, @NonNull final SeriesType type) { + public Series(@NonNull final String name, @NonNull final String unit, final int precision, final int seconds, @NonNull final SeriesType type) { this.name = name; this.unit = unit; - this.decimals = decimals; + this.precision = precision; this.seconds = seconds; this.type = type; } diff --git a/src/main/java/de/ph87/data/series/SeriesController.java b/src/main/java/de/ph87/data/series/SeriesController.java index 6a49ee9..bcaf290 100644 --- a/src/main/java/de/ph87/data/series/SeriesController.java +++ b/src/main/java/de/ph87/data/series/SeriesController.java @@ -42,7 +42,7 @@ public class SeriesController { @NonNull @PostMapping("{id}/decimals") public SeriesDto decimals(@PathVariable final long id, @RequestBody final int decimals) { - return seriesService.modify(id, series -> series.setDecimals(decimals)); + return seriesService.modify(id, series -> series.setPrecision(decimals)); } @NonNull diff --git a/src/main/java/de/ph87/data/series/SeriesDto.java b/src/main/java/de/ph87/data/series/SeriesDto.java index c38de70..494e9cc 100644 --- a/src/main/java/de/ph87/data/series/SeriesDto.java +++ b/src/main/java/de/ph87/data/series/SeriesDto.java @@ -19,7 +19,7 @@ public class SeriesDto implements IWebsocketMessage { @NonNull public final String unit; - public final int decimals; + public final int precision; @Nullable public final ZonedDateTime first; @@ -40,7 +40,7 @@ public class SeriesDto implements IWebsocketMessage { this.version = series.getVersion(); this.name = series.getName(); this.unit = series.getUnit(); - this.decimals = series.getDecimals(); + this.precision = series.getPrecision(); this.first = series.getFirst(); this.last = series.getLast(); this.value = series.getValue();