diff --git a/src/main/angular/src/app/common.ts b/src/main/angular/src/app/common.ts index a6b5172..9242470 100644 --- a/src/main/angular/src/app/common.ts +++ b/src/main/angular/src/app/common.ts @@ -31,7 +31,8 @@ export function validateString(value: any): string { } export function validateDate(value: any): Date { - return new Date(Date.parse(validateString(value))); + const parsed = Date.parse(validateString(value)); + return new Date(parsed); } export function validateList(value: any, fromJson: FromJson): T[] { 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 941b3d1..3184c3b 100644 --- a/src/main/angular/src/app/location/detail/location-detail.html +++ b/src/main/angular/src/app/location/detail/location-detail.html @@ -2,9 +2,9 @@ - + - +
  @@ -15,7 +15,7 @@
{{ offsetDayTitle() }}
-
+
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 e58f14f..d8ba6ee 100644 --- a/src/main/angular/src/app/location/detail/location-detail.ts +++ b/src/main/angular/src/app/location/detail/location-detail.ts @@ -6,7 +6,7 @@ import {Text} from '../../shared/text/text'; import {Number} from '../../shared/number/number'; import {SeriesSelect} from '../../series/select/series-select'; import {Subscription} from 'rxjs'; -import {LocationEnergy} from '../electricity/location-energy'; +import {LocationEnergy} from '../energy/location-energy'; import {Interval} from '../../series/Interval'; import {MenuService} from '../../menu-service'; import {DatePipe} from '@angular/common'; diff --git a/src/main/angular/src/app/location/electricity/location-energy.html b/src/main/angular/src/app/location/energy/location-energy.html similarity index 75% rename from src/main/angular/src/app/location/electricity/location-energy.html rename to src/main/angular/src/app/location/energy/location-energy.html index 49e929e..4871577 100644 --- a/src/main/angular/src/app/location/electricity/location-energy.html +++ b/src/main/angular/src/app/location/energy/location-energy.html @@ -11,7 +11,7 @@
Bezug
-
+
{{ purchase.toValueString(interval ? null : now) }}
@@ -20,7 +20,7 @@
Solar
-
+
{{ produce.toValueString(interval ? null : now) }}
@@ -29,7 +29,7 @@
Verbrauch
-
+
{{ consume.toValueString(interval ? null : now) }}
@@ -38,15 +38,13 @@
Einspeisung
-
+
{{ deliver.toValueString(interval ? null : now) }}
- @if (interval) { - - } +
diff --git a/src/main/angular/src/app/location/electricity/location-energy.less b/src/main/angular/src/app/location/energy/location-energy.less similarity index 100% rename from src/main/angular/src/app/location/electricity/location-energy.less rename to src/main/angular/src/app/location/energy/location-energy.less diff --git a/src/main/angular/src/app/location/electricity/location-energy.ts b/src/main/angular/src/app/location/energy/location-energy.ts similarity index 92% rename from src/main/angular/src/app/location/electricity/location-energy.ts rename to src/main/angular/src/app/location/energy/location-energy.ts index 65d6e94..a20b626 100644 --- a/src/main/angular/src/app/location/electricity/location-energy.ts +++ b/src/main/angular/src/app/location/energy/location-energy.ts @@ -7,10 +7,13 @@ import {PointService} from '../../point/point-service'; import {SeriesService} from '../../series/series-service'; import {Subscription} from 'rxjs'; import {Value} from '../../series/Value'; +import {EnergyPlot} from './plot/energy-plot'; @Component({ - selector: 'app-series-history', - imports: [], + selector: 'app-location-energy', + imports: [ + EnergyPlot + ], templateUrl: './location-energy.html', styleUrl: './location-energy.less', }) @@ -98,7 +101,7 @@ export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy { return } if (this.interval) { - this.pointService.relative([series], this.interval, this.offset, 1, response => callNextAndUpdateConsume(Value.ofPoint(response, 0, 0, 1))); + this.pointService.relative([series], this.interval, this.offset, 1, this.interval, response => callNextAndUpdateConsume(Value.ofPoint(response, 0, 0, 1))); } else { callNextAndUpdateConsume(series.value); } diff --git a/src/main/angular/src/app/location/energy/plot/EnergyPoint.ts b/src/main/angular/src/app/location/energy/plot/EnergyPoint.ts new file mode 100644 index 0000000..1e4abd0 --- /dev/null +++ b/src/main/angular/src/app/location/energy/plot/EnergyPoint.ts @@ -0,0 +1,98 @@ +export class EnergyPoint { + + readonly epochSeconds: number; + + private _purchase: number | null = null; + + private _produce: number | null = null; + + private _deliver: number | null = null; + + private _self: number | null = null; + + private _consume: number | null = null; + + private _purchaseY: number | null = null; + + private _selfY: number | null = null; + + private _deliverY: number | null = null; + + getPurchaseY(yFactor: number): number { + return this.getPurchaseH(yFactor) + this.getSelfH(yFactor); + } + + getPurchaseH(yFactor: number): number { + if (this._purchaseY === null) { + this._purchaseY = (this.purchase || 0) * yFactor; + } + return this._purchaseY; + } + + getSelfY(yFactor: number): number { + return this.getSelfH(yFactor); + } + + getSelfH(yFactor: number): number { + if (this._selfY === null) { + this._selfY = (this.self || 0) * yFactor; + } + return this._selfY; + } + + getDeliverH(yFactor: number): number { + if (this._deliverY === null) { + this._deliverY = (this.deliver || 0) * yFactor; + } + return this._deliverY; + } + + constructor( + point: number[], + ) { + this.epochSeconds = point[0]; + } + + set deliver(value: number | null) { + this._deliver = value; + this.update(); + } + + set produce(value: number | null) { + this._produce = value; + this.update(); + } + + set purchase(value: number | null) { + this._purchase = value; + this.update(); + } + + private update() { + if (this._purchase !== null && this._produce !== null && this._deliver !== null) { + this._self = Math.max(0, this._produce - this._deliver); + this._consume = Math.max(0, this._purchase + this._self); + } + } + + get consume(): number | null { + return this._consume; + } + + get self(): number | null { + return this._self; + } + + get deliver(): number | null { + return this._deliver; + } + + get produce(): number | null { + return this._produce; + } + + get purchase(): number | null { + return this._purchase; + } + +} diff --git a/src/main/angular/src/app/location/energy/plot/energy-plot.html b/src/main/angular/src/app/location/energy/plot/energy-plot.html new file mode 100644 index 0000000..ff82c19 --- /dev/null +++ b/src/main/angular/src/app/location/energy/plot/energy-plot.html @@ -0,0 +1,33 @@ + + @for (point of points; track point.epochSeconds) { + + + + + } + diff --git a/src/main/angular/src/app/location/energy/plot/energy-plot.less b/src/main/angular/src/app/location/energy/plot/energy-plot.less new file mode 100644 index 0000000..de6aeea --- /dev/null +++ b/src/main/angular/src/app/location/energy/plot/energy-plot.less @@ -0,0 +1 @@ +@import "../../../../colors"; diff --git a/src/main/angular/src/app/location/energy/plot/energy-plot.ts b/src/main/angular/src/app/location/energy/plot/energy-plot.ts new file mode 100644 index 0000000..fef812f --- /dev/null +++ b/src/main/angular/src/app/location/energy/plot/energy-plot.ts @@ -0,0 +1,135 @@ +import {AfterViewInit, Component, Input} from '@angular/core'; +import {Location} from '../../Location'; +import {Interval} from '../../../series/Interval'; +import {PointService} from '../../../point/point-service'; +import {PointResponse} from '../../../point/PointResponse'; +import {PointSeries} from '../../../point/PointSeries'; +import {EnergyPoint} from './EnergyPoint'; + +@Component({ + selector: 'app-energy-plot', + imports: [], + templateUrl: './energy-plot.html', + styleUrl: './energy-plot.less', +}) +export class EnergyPlot implements AfterViewInit { + + readonly widthPx = 800; + + readonly heightPx = 100; + + private _location!: Location; + + private _interval: Interval = Interval.FIVE; + + private _offset: number = 0; + + private _count: number = 1; + + protected points: EnergyPoint[] = []; + + protected yMin: number = 0; + + protected yMinPx: number = 0; + + protected yMax: number = 0; + + protected yFactor: number = 0; + + protected xMin: number = 0; + + protected xMax: number = 0; + + protected xFactor: number = 0; + + protected xWidthPx: number = 0; + + constructor( + readonly pointService: PointService, + ) { + // + } + + ngAfterViewInit(): void { + if (!this._location.energyPurchase) { + return; + } + if (!this._location.energyProduce) { + return; + } + if (!this._location.energyDeliver) { + return; + } + const series = [this._location.energyPurchase, this._location.energyProduce, this._location.energyDeliver]; + this.pointService.relative(series, this._interval, this._offset, this._count, this._interval.inner, this.update); + } + + public readonly update = (response: PointResponse): void => { + this.points.length = 0; + this.add(response.series[0], (p, v) => p.purchase = v); + this.add(response.series[1], (p, v) => p.produce = v); + this.add(response.series[2], (p, v) => p.deliver = v); + this.yMax = -Infinity; + this.yMin = Infinity; + for (let point of this.points) { + this.yMax = Math.max(this.yMax, point.consume || 0); + this.yMin = Math.min(this.yMin, -(point.deliver || 0)); + } + this.yMinPx = this.yMin * this.yFactor; + this.yFactor = this.heightPx / (this.yMax - this.yMin); + this.xMin = response.begin.getTime() / 1000; + this.xMax = response.end.getTime() / 1000; + this.xFactor = this.widthPx / (this.xMax - this.xMin); + this.xWidthPx = this.widthPx / response.expectedCount; + }; + + private add(series: PointSeries, setter: (p: EnergyPoint, v: number) => void): void { + for (const point of series.points) { + const index = this.insert(point, setter); + if (index >= 0) { + const fresh = new EnergyPoint(point); + setter(fresh, point[1]) + this.points.splice(index, 0, fresh); + } + } + } + + private insert(point: number[], setter: (p: EnergyPoint, v: number) => any): number { + let index = 0; + for (let old of this.points) { + const age = old.epochSeconds - point[0]; + if (age === 0) { + setter(old, point[1]) + return -1; + } else if (age < 0) { + return index; + } + index++; + } + return index; + } + + @Input() + set location(value: Location) { + this._location = value; + this.ngAfterViewInit(); + } + + @Input() + set interval(value: Interval) { + this._interval = value; + this.ngAfterViewInit(); + } + + @Input() + set offset(value: number) { + this._offset = value; + this.ngAfterViewInit(); + } + + @Input() + set count(value: number) { + this._count = value; + this.ngAfterViewInit(); + } +} diff --git a/src/main/angular/src/app/location/graph/simple-plot.html b/src/main/angular/src/app/location/graph/simple-plot.html deleted file mode 100644 index 7d4ce7e..0000000 --- a/src/main/angular/src/app/location/graph/simple-plot.html +++ /dev/null @@ -1,9 +0,0 @@ -
- @for (segment of segments; track segment) { -
- - - -
- } -
diff --git a/src/main/angular/src/app/location/graph/simple-plot.less b/src/main/angular/src/app/location/graph/simple-plot.less deleted file mode 100644 index 9474b56..0000000 --- a/src/main/angular/src/app/location/graph/simple-plot.less +++ /dev/null @@ -1,9 +0,0 @@ -.segments { - display: flex; - height: 4em; - - .segment { - flex: 1; - height: 100%; - } -} diff --git a/src/main/angular/src/app/location/graph/simple-plot.ts b/src/main/angular/src/app/location/graph/simple-plot.ts deleted file mode 100644 index a32c7a2..0000000 --- a/src/main/angular/src/app/location/graph/simple-plot.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {AfterViewInit, Component, Input} from '@angular/core'; -import {Location} from '../Location'; -import {Interval} from '../../series/Interval'; -import {PointService} from '../../point/point-service'; - -@Component({ - selector: 'app-series-history-graph', - imports: [], - templateUrl: './simple-plot.html', - styleUrl: './simple-plot.less', -}) -export class SeriesHistoryGraph implements AfterViewInit { - - protected segments = Array.from(Array(288).keys()); - - protected totals: number[] = []; - - protected historyEnergyPurchase: number[] | null = null; - - protected historyEnergyDeliver: number[] | null = null; - - protected historyEnergyProduce: number[] | null = null; - - protected readonly Interval = Interval; - - @Input() - heading!: string; - - @Input() - date!: Date; - - @Input() - interval!: Interval; - - @Input() - location!: Location; - - constructor( - readonly pointService: PointService, - ) { - // - } - - 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); - } - - // public readonly updateSeries = (fresh: Series): void => { - // if (fresh.id === this.location?.energyPurchase?.id) { - // this.history(this.location?.energyPurchase, history => this.historyEnergyPurchase = 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); - // } - // }; - // - // private history(series: Series | null | undefined, next: Next) { - // if (!series || !this.interval) { - // next(null); - // return - // } - // this.pointService.points(series, this.date, this.interval, 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 index 1683e7a..7898456 100644 --- a/src/main/angular/src/app/location/power/location-power.html +++ b/src/main/angular/src/app/location/power/location-power.html @@ -10,7 +10,7 @@
Bezug
-
+
{{ location.powerPurchase?.value?.toValueString(dateService.now) }}
@@ -19,7 +19,7 @@
Solar
-
+
{{ location.powerProduce?.value?.toValueString(dateService.now) }}
@@ -28,7 +28,7 @@
Verbrauch
-
+
{{ location.powerConsume?.toValueString(dateService.now) }}
@@ -37,7 +37,7 @@
Einspeisung
-
+
{{ location.powerDeliver?.value?.toValueString(dateService.now) }}
diff --git a/src/main/angular/src/app/point/PointResponse.ts b/src/main/angular/src/app/point/PointResponse.ts index 4484c4e..15210ab 100644 --- a/src/main/angular/src/app/point/PointResponse.ts +++ b/src/main/angular/src/app/point/PointResponse.ts @@ -1,4 +1,4 @@ -import {validateDate, validateList} from "../common"; +import {validateDate, validateList, validateNumber} from "../common"; import {PointSeries} from './PointSeries'; @@ -8,6 +8,7 @@ export class PointResponse { readonly begin: Date, readonly end: Date, readonly series: PointSeries[], + readonly expectedCount: number, ) { // } @@ -17,6 +18,7 @@ export class PointResponse { validateDate(json.begin), validateDate(json.end), validateList(json.series, PointSeries.fromJson), + validateNumber(json.expectedCount), ); } diff --git a/src/main/angular/src/app/point/point-service.ts b/src/main/angular/src/app/point/point-service.ts index b92e36c..d48746f 100644 --- a/src/main/angular/src/app/point/point-service.ts +++ b/src/main/angular/src/app/point/point-service.ts @@ -16,12 +16,13 @@ export class PointService extends CrudService { super(api, ws, ['Point'], PointResponse.fromJson); } - relative(series: Series[], interval: Interval, offset: number, count: number, next: Next): void { + relative(series: Series[], outer: Interval, offset: number, count: number, interval: Interval, next: Next): void { const request = { ids: series.map(s => s.id), - interval: interval, + outerInterval: outer.name, offset: offset, count: count, + interval: interval.name, }; this.postSingle(['relative'], request, next); } diff --git a/src/main/angular/src/app/series/Interval.ts b/src/main/angular/src/app/series/Interval.ts index c76cd5b..c28ec48 100644 --- a/src/main/angular/src/app/series/Interval.ts +++ b/src/main/angular/src/app/series/Interval.ts @@ -1,8 +1,22 @@ -export enum Interval { - FIVE = 'FIVE', - HOUR = 'HOUR', - DAY = 'DAY', - WEEK = 'WEEK', - MONTH = 'MONTH', - YEAR = 'YEAR', +export class Interval { + + static readonly FIVE = new Interval('FIVE'); + + static readonly HOUR = new Interval('HOUR', this.FIVE); + + static readonly DAY = new Interval('DAY', this.FIVE); + + static readonly WEEK = new Interval('WEEK', this.HOUR); + + static readonly MONTH = new Interval('MONTH', this.HOUR); + + static readonly YEAR = new Interval('YEAR', this.DAY); + + constructor( + readonly name: string, + readonly inner: Interval = this, + ) { + // + } + } diff --git a/src/main/angular/src/app/series/Value.ts b/src/main/angular/src/app/series/Value.ts index 208d97e..9a97916 100644 --- a/src/main/angular/src/app/series/Value.ts +++ b/src/main/angular/src/app/series/Value.ts @@ -83,6 +83,13 @@ export class Value { return ageSeconds > this.seconds * 2.1; } + onlyPositive(): Value { + if (this.value < 0) { + return Value.ZERO; + } + return this; + } + } export class BiValue extends Value { diff --git a/src/main/angular/src/colors.less b/src/main/angular/src/colors.less index 350e7fb..dc9b9c7 100644 --- a/src/main/angular/src/colors.less +++ b/src/main/angular/src/colors.less @@ -1,23 +1,60 @@ @empty: gray; -@purchase: red; -@deliver: magenta; -@produce: #0095ff; -@consume: #ff8800; -.purchase { - color: @purchase; +@COLOR_FONT_PURCHASE: red; +@COLOR_FONT_DELIVER: magenta; +@COLOR_FONT_PRODUCE: #0095ff; +@COLOR_FONT_SELF: #0095ff; +@COLOR_FONT_CONSUME: #ff8800; + +@COLOR_BACK_PURCHASE: #ffa7a7; +@COLOR_BACK_DELIVER: #ff00ff; +@COLOR_BACK_PRODUCE: #5cbcff; +@COLOR_BACK_SELF: #00ff69; +@COLOR_BACK_CONSUME: #ffc07a; + +.COLOR_FONT_PURCHASE { + color: @COLOR_FONT_PURCHASE; } -.deliver { - color: @deliver; +.COLOR_FONT_DELIVER { + color: @COLOR_FONT_DELIVER; } -.produce { - color: @produce; +.COLOR_FONT_PRODUCE { + color: @COLOR_FONT_PRODUCE; } -.consume { - color: @consume; +.COLOR_FONT_SELF { + color: @COLOR_FONT_SELF; +} + +.COLOR_FONT_CONSUME { + color: @COLOR_FONT_CONSUME; +} + +.COLOR_BACK_PURCHASE { + color: @COLOR_BACK_PURCHASE; + fill: @COLOR_BACK_PURCHASE; +} + +.COLOR_BACK_DELIVER { + color: @COLOR_BACK_DELIVER; + fill: @COLOR_BACK_DELIVER; +} + +.COLOR_BACK_PRODUCE { + color: @COLOR_BACK_PRODUCE; + fill: @COLOR_BACK_PRODUCE; +} + +.COLOR_BACK_SELF { + color: @COLOR_BACK_SELF; + fill: @COLOR_BACK_SELF; +} + +.COLOR_BACK_CONSUME { + color: @COLOR_BACK_CONSUME; + fill: @COLOR_BACK_CONSUME; } .empty { diff --git a/src/main/java/de/ph87/data/plot/Plot.java b/src/main/java/de/ph87/data/plot/Plot.java index 2a0c3d6..1d9d05a 100644 --- a/src/main/java/de/ph87/data/plot/Plot.java +++ b/src/main/java/de/ph87/data/plot/Plot.java @@ -51,7 +51,7 @@ public class Plot { @Setter @Column(nullable = false) - private long duration = 288; + private long duration = 1; @Setter @Column(nullable = false) diff --git a/src/main/java/de/ph87/data/point/IPointRequest.java b/src/main/java/de/ph87/data/point/IPointRequest.java index 4aa19eb..949eabc 100644 --- a/src/main/java/de/ph87/data/point/IPointRequest.java +++ b/src/main/java/de/ph87/data/point/IPointRequest.java @@ -15,13 +15,15 @@ public interface IPointRequest { @NonNull List getIds(); - @NonNull - Interval getInterval(); - @NonNull ZonedDateTime getBegin(); @NonNull ZonedDateTime getEnd(); + @NonNull + Interval getInterval(); + + long getExpectedCount(); + } diff --git a/src/main/java/de/ph87/data/point/PointRequestRelative.java b/src/main/java/de/ph87/data/point/PointRequestRelative.java index a3c2def..084cd0e 100644 --- a/src/main/java/de/ph87/data/point/PointRequestRelative.java +++ b/src/main/java/de/ph87/data/point/PointRequestRelative.java @@ -5,34 +5,40 @@ import lombok.Data; import java.time.ZonedDateTime; import java.util.List; +import java.util.stream.Stream; @Data public class PointRequestRelative implements IPointRequest { public final List ids; - public final Interval interval; + public final Interval outerInterval; public final long offset; public final long count; + public final Interval interval; + public final ZonedDateTime begin; public final ZonedDateTime end; - public PointRequestRelative( - final List ids, - final Interval interval, - final long offset, - final long count - ) { + public final long expectedCount; + + public PointRequestRelative(final List ids, final Interval outerInterval, final long offset, final long count, final Interval interval) { this.ids = ids; - this.interval = interval; + this.outerInterval = outerInterval; this.offset = offset; this.count = count; - this.end = interval.align.apply(ZonedDateTime.now()).minus(interval.amount * (offset - 1), interval.unit); - this.begin = this.end.minus(interval.amount * count, interval.unit); + this.interval = interval; + this.end = outerInterval.align.apply(ZonedDateTime.now()).minus(outerInterval.amount * (offset - 1), outerInterval.unit); + this.begin = this.end.minus(outerInterval.amount * count, outerInterval.unit); + this.expectedCount = calculateExpectedCount(); + } + + private long calculateExpectedCount() { + return Stream.iterate(begin, d -> d.isBefore(end), d -> d.plus(interval.amount, interval.unit)).count(); } } diff --git a/src/main/java/de/ph87/data/point/PointResponse.java b/src/main/java/de/ph87/data/point/PointResponse.java index 91f6c47..5f588ae 100644 --- a/src/main/java/de/ph87/data/point/PointResponse.java +++ b/src/main/java/de/ph87/data/point/PointResponse.java @@ -14,4 +14,6 @@ public class PointResponse { public final List series; + public final long expectedCount; + } diff --git a/src/main/java/de/ph87/data/point/PointService.java b/src/main/java/de/ph87/data/point/PointService.java index 1092c70..85385b5 100644 --- a/src/main/java/de/ph87/data/point/PointService.java +++ b/src/main/java/de/ph87/data/point/PointService.java @@ -30,7 +30,7 @@ public class PointService { @NonNull public PointResponse points(@NonNull final IPointRequest request) { final List series = request.getIds().stream().map(s -> points(s, request)).toList(); - return new PointResponse(request.getBegin(), request.getEnd(), series); + return new PointResponse(request.getBegin(), request.getEnd(), series, request.getExpectedCount()); } @NonNull