Slice, Unit->Interval, Today+Yesterday

This commit is contained in:
Patrick Haßel 2024-10-21 13:32:35 +02:00
parent f76b6cdec8
commit fe8afcad29
37 changed files with 477 additions and 429 deletions

View File

@ -14,9 +14,9 @@ Run `ng generate component component-name` to generate a new component. You can
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests ## Running interval tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). Run `ng test` to execute the interval tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests ## Running end-to-end tests

View File

@ -1,6 +1,6 @@
import {Period} from "./consumption/period/Period"; import {Period} from "./consumption/period/Period";
import {validateDate, validateNumber, validateString} from "../validators"; import {validateDate, validateNumber, validateString} from "../validators";
import {Value} from "../Value/Value"; import {Value} from "../value/Value";
export class Series extends Value { export class Series extends Value {

View File

@ -0,0 +1,27 @@
export const ELECTRICITY_GRID_PURCHASED_ENERGY = 'electricity.grid.purchase.energy';
export const ELECTRICITY_GRID_DELIVERED_ENERGY = 'electricity.grid.delivery.energy';
export const ELECTRICITY_GRID_POWER = 'electricity.grid.power';
export const ELECTRICITY_PHOTOVOLTAIC_PRODUCED = 'electricity.photovoltaic.energy';
export const ELECTRICITY_PHOTOVOLTAIC_POWER = 'electricity.photovoltaic.power';
export const SCHLAFZIMMER_TEMPERATURE = 'schlafzimmer.temperature';
export const SCHLAFZIMMER_HUMIDITY_RELATIVE = 'schlafzimmer.humidity_relative';
export const SCHLAFZIMMER_HUMIDITY_ABSOLUTE = 'schlafzimmer.humidity_absolute';
export const GARTEN_TEMPERATURE = 'garten.temperature';
export const GARTEN_HUMIDITY_RELATIVE = 'garten.humidity_relative';
export const GARTEN_HUMIDITY_ABSOLUTE = 'garten.humidity_absolute';
export const HEATING_ROOM_TEMPERATURE = 'heating.room.temperature';
export const HEATING_ROOM_HUMIDITY_RELATIVE = 'heating.room.humidity_relative';
export const HEATING_ROOM_HUMIDITY_ABSOLUTE = 'heating.room.humidity_absolute';
export const HEATING_EXHAUST_TEMPERATURE = 'heating.exhaust.temperature';
export const HEATING_BUFFER_SUPPLY_TEMPERATURE = 'heating.buffer.supply.temperature';
export const HEATING_BUFFER_RETURN_TEMPERATURE = 'heating.buffer.return.temperature';
export const HEATING_BUFFER_COLD_TEMPERATURE = 'heating.buffer.cold.temperature';
export const HEATING_BUFFER_INNER_TEMPERATURE = 'heating.buffer.inner.temperature';
export const HEATING_BUFFER_HOT_TEMPERATURE = 'heating.buffer.hot.temperature';
export const HEATING_BUFFER_CIRCULATION_TEMPERATURE = 'heating.buffer.circulation.temperature';
export const HEATING_LOOP_SUPPLY_TEMPERATURE = 'heating.loop.supply.temperature';
export const HEATING_LOOP_RETURN_TEMPERATURE = 'heating.loop.return.temperature';

View File

@ -0,0 +1,8 @@
export enum Interval {
Quarterhour = 'Quarterhour',
Hour = 'Hour',
Day = 'Day',
Week = 'Week',
Month = 'Month',
Year = 'Year',
}

View File

@ -0,0 +1,27 @@
import {validateDate, validateNumber, validateString} from "../../../validators";
import {Value} from "../../../value/Value";
export class Slice extends Value {
static readonly EMPTY: Slice = new Slice(null, null, '');
constructor(
date: Date | null,
amount: number | null,
unit: string,
) {
super(date, amount, unit);
}
static fromJson(json: any): Slice {
if (json === null) {
return new Slice(null, null, '');
}
return new Slice(
validateDate(json['date']),
validateNumber(json['amount']),
validateString(json['unit']),
);
}
}

View File

@ -0,0 +1,23 @@
import {Injectable} from '@angular/core';
import {ApiService} from "../../../api.service";
import {Next} from "../../../types";
import {Slice} from "./Slice";
import {Interval} from "../interval/Interval";
@Injectable({
providedIn: 'root'
})
export class SliceService {
constructor(
private readonly api: ApiService,
) {
// -
}
at(seriesName: string, interval: Interval, offset: number, next: Next<Slice>): void {
this.api.getSingle(['Slice', 'seriesName', seriesName, 'interval', interval, 'offset', offset], Slice.fromJson, next);
}
}

View File

@ -2,6 +2,7 @@ import {Injectable} from '@angular/core';
import {Series} from "./Series"; import {Series} from "./Series";
import {ApiService} from "../api.service"; import {ApiService} from "../api.service";
import {SeriesService} from "./series.service"; import {SeriesService} from "./series.service";
import {ELECTRICITY_GRID_DELIVERED_ENERGY, ELECTRICITY_GRID_POWER, ELECTRICITY_GRID_PURCHASED_ENERGY, ELECTRICITY_PHOTOVOLTAIC_POWER, ELECTRICITY_PHOTOVOLTAIC_PRODUCED, GARTEN_HUMIDITY_ABSOLUTE, GARTEN_HUMIDITY_RELATIVE, GARTEN_TEMPERATURE, HEATING_BUFFER_CIRCULATION_TEMPERATURE, HEATING_BUFFER_COLD_TEMPERATURE, HEATING_BUFFER_HOT_TEMPERATURE, HEATING_BUFFER_INNER_TEMPERATURE, HEATING_BUFFER_RETURN_TEMPERATURE, HEATING_BUFFER_SUPPLY_TEMPERATURE, HEATING_EXHAUST_TEMPERATURE, HEATING_LOOP_RETURN_TEMPERATURE, HEATING_LOOP_SUPPLY_TEMPERATURE, HEATING_ROOM_HUMIDITY_ABSOLUTE, HEATING_ROOM_HUMIDITY_RELATIVE, HEATING_ROOM_TEMPERATURE, SCHLAFZIMMER_HUMIDITY_ABSOLUTE, SCHLAFZIMMER_HUMIDITY_RELATIVE, SCHLAFZIMMER_TEMPERATURE} from "./constants";
export function returnUpdatedSeriesIfNameAndNewer(fresh: Series, old: Series, name: string): Series { export function returnUpdatedSeriesIfNameAndNewer(fresh: Series, old: Series, name: string): Series {
if (fresh.name !== name) { if (fresh.name !== name) {
@ -80,34 +81,34 @@ export class SeriesCacheService {
} }
private seriesUpdate(series: Series) { private seriesUpdate(series: Series) {
this.gridPurchased = returnUpdatedSeriesIfNameAndNewer(series, this.gridPurchased, 'electricity.grid.purchase.energy'); this.gridPurchased = returnUpdatedSeriesIfNameAndNewer(series, this.gridPurchased, ELECTRICITY_GRID_PURCHASED_ENERGY);
this.gridDelivered = returnUpdatedSeriesIfNameAndNewer(series, this.gridDelivered, 'electricity.grid.delivery.energy'); this.gridDelivered = returnUpdatedSeriesIfNameAndNewer(series, this.gridDelivered, ELECTRICITY_GRID_DELIVERED_ENERGY);
this.gridPower = returnUpdatedSeriesIfNameAndNewer(series, this.gridPower, 'electricity.grid.power'); this.gridPower = returnUpdatedSeriesIfNameAndNewer(series, this.gridPower, ELECTRICITY_GRID_POWER);
this.photovoltaicProduced = returnUpdatedSeriesIfNameAndNewer(series, this.photovoltaicProduced, 'electricity.photovoltaic.energy'); this.photovoltaicProduced = returnUpdatedSeriesIfNameAndNewer(series, this.photovoltaicProduced, ELECTRICITY_PHOTOVOLTAIC_PRODUCED);
this.photovoltaicPower = returnUpdatedSeriesIfNameAndNewer(series, this.photovoltaicPower, 'electricity.photovoltaic.power'); this.photovoltaicPower = returnUpdatedSeriesIfNameAndNewer(series, this.photovoltaicPower, ELECTRICITY_PHOTOVOLTAIC_POWER);
this.schlafzimmerTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerTemperature, 'schlafzimmer.temperature'); this.schlafzimmerTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerTemperature, SCHLAFZIMMER_TEMPERATURE);
this.schlafzimmerHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerHumidityRelative, 'schlafzimmer.humidity_relative'); this.schlafzimmerHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerHumidityRelative, SCHLAFZIMMER_HUMIDITY_RELATIVE);
this.schlafzimmerHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerHumidityAbsolute, 'schlafzimmer.humidity_absolute'); this.schlafzimmerHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerHumidityAbsolute, SCHLAFZIMMER_HUMIDITY_ABSOLUTE);
this.outdoorTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorTemperature, 'garten.temperature'); this.outdoorTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorTemperature, GARTEN_TEMPERATURE);
this.outdoorHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorHumidityRelative, 'garten.humidity_relative'); this.outdoorHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorHumidityRelative, GARTEN_HUMIDITY_RELATIVE);
this.outdoorHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorHumidityAbsolute, 'garten.humidity_absolute'); this.outdoorHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorHumidityAbsolute, GARTEN_HUMIDITY_ABSOLUTE);
this.heatingRoomTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomTemperature, 'heating.room.temperature'); this.heatingRoomTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomTemperature, HEATING_ROOM_TEMPERATURE);
this.heatingRoomHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomHumidityRelative, 'heating.room.humidity_relative'); this.heatingRoomHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomHumidityRelative, HEATING_ROOM_HUMIDITY_RELATIVE);
this.heatingRoomHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomHumidityAbsolute, 'heating.room.humidity_absolute'); this.heatingRoomHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomHumidityAbsolute, HEATING_ROOM_HUMIDITY_ABSOLUTE);
this.heatingExhaustTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingExhaustTemperature, 'heating.exhaust.temperature'); this.heatingExhaustTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingExhaustTemperature, HEATING_EXHAUST_TEMPERATURE);
this.heatingBufferSupplyTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferSupplyTemperature, 'heating.buffer.supply.temperature'); this.heatingBufferSupplyTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferSupplyTemperature, HEATING_BUFFER_SUPPLY_TEMPERATURE);
this.heatingBufferReturnTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferReturnTemperature, 'heating.buffer.return.temperature'); this.heatingBufferReturnTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferReturnTemperature, HEATING_BUFFER_RETURN_TEMPERATURE);
this.heatingBufferColdTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferColdTemperature, 'heating.buffer.cold.temperature'); this.heatingBufferColdTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferColdTemperature, HEATING_BUFFER_COLD_TEMPERATURE);
this.heatingBufferInnerTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferInnerTemperature, 'heating.buffer.inner.temperature'); this.heatingBufferInnerTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferInnerTemperature, HEATING_BUFFER_INNER_TEMPERATURE);
this.heatingBufferHotTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferHotTemperature, 'heating.buffer.hot.temperature'); this.heatingBufferHotTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferHotTemperature, HEATING_BUFFER_HOT_TEMPERATURE);
this.heatingBufferCirculationTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferCirculationTemperature, 'heating.buffer.circulation.temperature'); this.heatingBufferCirculationTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferCirculationTemperature, HEATING_BUFFER_CIRCULATION_TEMPERATURE);
this.heatingLoopSupplyTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingLoopSupplyTemperature, 'heating.loop.supply.temperature'); this.heatingLoopSupplyTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingLoopSupplyTemperature, HEATING_LOOP_SUPPLY_TEMPERATURE);
this.heatingLoopReturnTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingLoopReturnTemperature, 'heating.loop.return.temperature'); this.heatingLoopReturnTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingLoopReturnTemperature, HEATING_LOOP_RETURN_TEMPERATURE);
} }
} }

View File

@ -12,4 +12,4 @@ export class DisplayValue {
} }
export type Display = DisplayValue | null; export type Display = DisplayValue | string | null;

View File

@ -5,7 +5,7 @@ export class Value {
constructor( constructor(
readonly date: Date | null, readonly date: Date | null,
readonly value: number | null, readonly value: number | null,
readonly unit: string | null, readonly unit: string,
) { ) {
// - // -
} }
@ -56,17 +56,17 @@ export class Value {
unary(func: (v: number) => number): Value { unary(func: (v: number) => number): Value {
if (this.value === null) { if (this.value === null) {
return new Value(null, null, null); return new Value(null, null, '');
} }
const value = func(this.value); const value = func(this.value);
return new Value(this.date, value, this.unit); return new Value(this.date, value, this.unit);
} }
binary(other: Value, func: (a: number, b: number) => number): Value { binary(other: Value, func: (a: number, b: number) => number): Value {
if (this.date === null || this.value === null || this.unit === null) { if (this.date === null || this.value === null) {
return new Value(null, null, other.unit || null); return new Value(null, null, other.unit);
} }
if (other.date === null || other.value === null || other.unit === null) { if (other.date === null || other.value === null) {
return new Value(null, null, this.unit); return new Value(null, null, this.unit);
} }
const oldestDate = this.getOldestDate(other); const oldestDate = this.getOldestDate(other);
@ -75,10 +75,10 @@ export class Value {
} }
compare(other: Value, comparing?: (a: number, b: number) => number): number { compare(other: Value, comparing?: (a: number, b: number) => number): number {
if (this.date === null || this.value === null || this.unit === null) { if (this.date === null || this.value === null) {
return -1; return -1;
} }
if (other.date === null || other.value === null || other.unit === null) { if (other.date === null || other.value === null) {
return +1; return +1;
} }
if (comparing === undefined) { if (comparing === undefined) {

View File

@ -4,7 +4,7 @@ export class ValueConstant extends Value {
constructor( constructor(
value: number | null, value: number | null,
unit: string | null, unit: string,
) { ) {
super(new Date(Date.now()), value, unit); super(new Date(Date.now()), value, unit);
} }

View File

@ -1,6 +1,6 @@
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {ValueListComponent} from "../../../shared/value-list/value-list.component"; import {ValueListComponent} from "../../../shared/value-list/value-list.component";
import {Display, DisplayValue} from "../../../api/Value/Display"; import {Display, DisplayValue} from "../../../api/value/Display";
import {SeriesCacheService} from "../../../api/series/series-cache.service"; import {SeriesCacheService} from "../../../api/series/series-cache.service";
@Component({ @Component({

View File

@ -1,5 +1,5 @@
<div class="tile"> <div class="tile">
<app-dashboard-electricity-tile [now]="now"></app-dashboard-electricity-tile> <app-dashboard-electricity-tile [now]="now" [slowUpdate]="slowUpdate"></app-dashboard-electricity-tile>
</div> </div>
<div class="tile"> <div class="tile">

View File

@ -7,6 +7,7 @@ import {Subscription, timer} from "rxjs";
import {DashboardHeatingTileComponent} from "./heating/dashboard-heating-tile.component"; import {DashboardHeatingTileComponent} from "./heating/dashboard-heating-tile.component";
const UPDATE_INTERVAL_MILLIS = 1000; const UPDATE_INTERVAL_MILLIS = 1000;
const SLOW_UPDATE_INTERVAL_MILLIS = 60 * 1000;
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -26,10 +27,15 @@ export class DashboardComponent implements OnInit, OnDestroy {
protected now: Date = new Date(); protected now: Date = new Date();
protected slowUpdate: Date = new Date();
private timer?: Subscription; private timer?: Subscription;
private slowUpdateTimer?: Subscription;
ngOnInit(): void { ngOnInit(): void {
this.timer = timer(0, UPDATE_INTERVAL_MILLIS).subscribe(() => this.now = new Date()); this.timer = timer(0, UPDATE_INTERVAL_MILLIS).subscribe(() => this.now = new Date());
this.slowUpdateTimer = timer(0, SLOW_UPDATE_INTERVAL_MILLIS).subscribe(() => this.slowUpdate = new Date());
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@ -1,8 +1,12 @@
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {ValueListComponent} from "../../../shared/value-list/value-list.component"; import {ValueListComponent} from "../../../shared/value-list/value-list.component";
import {Display, DisplayValue} from "../../../api/Value/Display"; import {Display, DisplayValue} from "../../../api/value/Display";
import {ValueConstant} from "../../../api/Value/ValueConstant"; import {ValueConstant} from "../../../api/value/ValueConstant";
import {SeriesCacheService} from "../../../api/series/series-cache.service"; import {SeriesCacheService} from "../../../api/series/series-cache.service";
import {SliceService} from "../../../api/series/consumption/slice/slice.service";
import {Slice} from "../../../api/series/consumption/slice/Slice";
import {Interval} from "../../../api/series/consumption/interval/Interval";
import {ELECTRICITY_GRID_DELIVERED_ENERGY, ELECTRICITY_GRID_PURCHASED_ENERGY, ELECTRICITY_PHOTOVOLTAIC_PRODUCED} from "../../../api/series/constants";
const PURCHASING_MUCH = 200; const PURCHASING_MUCH = 200;
@ -22,30 +26,74 @@ export class DashboardElectricityTileComponent {
@Input() @Input()
now!: Date; now!: Date;
protected producedToday: Slice = Slice.EMPTY;
protected purchasedToday: Slice = Slice.EMPTY;
protected deliveredToday: Slice = Slice.EMPTY;
protected producedYesterday: Slice = Slice.EMPTY;
protected purchasedYesterday: Slice = Slice.EMPTY;
protected deliveredYesterday: Slice = Slice.EMPTY;
@Input()
set slowUpdate(_: Date) {
this.sliceService.at(ELECTRICITY_PHOTOVOLTAIC_PRODUCED, Interval.Day, 0, slice => this.producedToday = slice);
this.sliceService.at(ELECTRICITY_GRID_PURCHASED_ENERGY, Interval.Day, 0, slice => this.purchasedToday = slice);
this.sliceService.at(ELECTRICITY_GRID_DELIVERED_ENERGY, Interval.Day, 0, slice => this.deliveredToday = slice);
this.sliceService.at(ELECTRICITY_PHOTOVOLTAIC_PRODUCED, Interval.Day, 1, slice => this.producedYesterday = slice);
this.sliceService.at(ELECTRICITY_GRID_PURCHASED_ENERGY, Interval.Day, 1, slice => this.purchasedYesterday = slice);
this.sliceService.at(ELECTRICITY_GRID_DELIVERED_ENERGY, Interval.Day, 1, slice => this.deliveredYesterday = slice);
}
constructor( constructor(
protected readonly seriesCacheService: SeriesCacheService, private readonly seriesCacheService: SeriesCacheService,
private readonly sliceService: SliceService,
) { ) {
// - // -
} }
getDisplayList(): Display[] { getDisplayList(): Display[] {
const consumptionPower = this.seriesCacheService.photovoltaicPower.plus(this.seriesCacheService.gridPower).clampNonNegative();
const producedAfterChange = this.seriesCacheService.photovoltaicProduced.minus(PRODUCED_BEFORE_METER_CHANGE); const producedAfterChange = this.seriesCacheService.photovoltaicProduced.minus(PRODUCED_BEFORE_METER_CHANGE);
const selfAfterChange = producedAfterChange.minus(this.seriesCacheService.gridDelivered); const selfAfterChange = producedAfterChange.minus(this.seriesCacheService.gridDelivered);
const selfRatio = selfAfterChange.div(producedAfterChange); const selfRatio = selfAfterChange.div(producedAfterChange);
const selfConsumed = selfRatio.mul(this.seriesCacheService.photovoltaicProduced); const selfConsumed = selfRatio.mul(this.seriesCacheService.photovoltaicProduced);
const consumptionPower = this.seriesCacheService.photovoltaicPower.plus(this.seriesCacheService.gridPower).clampNonNegative();
const gridColor = this.getGridPowerColor(); const gridColor = this.getGridPowerColor();
const productionColor = this.getProductionPowerColor(); const productionColor = this.getProductionPowerColor();
const selfToday = this.producedToday.minus(this.deliveredToday);
const consumedToday = this.purchasedToday.plus(selfToday);
const selfYesterday = this.producedYesterday.minus(this.deliveredYesterday);
const consumedYesterday = this.purchasedYesterday.plus(selfYesterday);
return [ return [
'Zählerstände',
new DisplayValue('Bezogen', this.seriesCacheService.gridPurchased, ''), new DisplayValue('Bezogen', this.seriesCacheService.gridPurchased, ''),
new DisplayValue('Eingespeist', this.seriesCacheService.gridDelivered, ''), new DisplayValue('Eingespeist', this.seriesCacheService.gridDelivered, ''),
new DisplayValue('Produziert', this.seriesCacheService.photovoltaicProduced, ''), new DisplayValue('Produziert', this.seriesCacheService.photovoltaicProduced, ''),
new DisplayValue('Selbst verbraucht', selfConsumed, ''), new DisplayValue('Selbst verbraucht', selfConsumed, ''),
null, 'Leistung',
new DisplayValue('Produktion', this.seriesCacheService.photovoltaicPower, productionColor), new DisplayValue('Produktion', this.seriesCacheService.photovoltaicPower, productionColor),
new DisplayValue('Netz', this.seriesCacheService.gridPower, gridColor), new DisplayValue('Netz', this.seriesCacheService.gridPower, gridColor),
new DisplayValue('Verbrauch', consumptionPower, ''), new DisplayValue('Verbrauch', consumptionPower, ''),
'Heute',
new DisplayValue('Produziert', this.producedToday, ''),
new DisplayValue('Eingespeist', this.deliveredToday, ''),
new DisplayValue('Selbstverbraucht', selfToday, ''),
new DisplayValue('Bezogen', this.purchasedToday, ''),
new DisplayValue('Verbraucht', consumedToday, ''),
'Gestern',
new DisplayValue('Produziert', this.producedYesterday, ''),
new DisplayValue('Eingespeist', this.deliveredYesterday, ''),
new DisplayValue('Selbstverbraucht', selfYesterday, ''),
new DisplayValue('Bezogen', this.purchasedYesterday, ''),
new DisplayValue('Verbraucht', consumedYesterday, ''),
]; ];
} }

View File

@ -1,6 +1,6 @@
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {ValueListComponent} from "../../../shared/value-list/value-list.component"; import {ValueListComponent} from "../../../shared/value-list/value-list.component";
import {Display, DisplayValue} from "../../../api/Value/Display"; import {Display, DisplayValue} from "../../../api/value/Display";
import {SeriesCacheService} from "../../../api/series/series-cache.service"; import {SeriesCacheService} from "../../../api/series/series-cache.service";
const WARM = 25; const WARM = 25;

View File

@ -4,17 +4,22 @@
{{ title }} {{ title }}
</div> </div>
<table class="values"> <table class="values">
<ng-container *ngFor="let display of displayList"> <ng-container *ngFor="let item of displayList">
<tr class="rate" *ngIf="display" [style.color]="display.color"> <tr class="rate" *ngIf="asDisplay(item)" [style.color]="asDisplay(item)?.color">
<th>{{ display.title }}</th> <th>{{ asDisplay(item)?.title }}</th>
<td class="v"> <td class="v">
{{ display?.value?.value | number:'0.0-0' }} {{ asDisplay(item)?.value?.value | number:'0.1-1' }}
</td> </td>
<td class="u"> <td class="u">
{{ display?.value?.unit }} {{ asDisplay(item)?.value?.unit }}
</td> </td>
</tr> </tr>
<tr *ngIf="display === null"> <tr *ngIf="asString(item)">
<td colspan="3" class="header">
{{ asString(item) }}
</td>
</tr>
<tr *ngIf="item === null">
<td colspan="3" class="spacer"></td> <td colspan="3" class="spacer"></td>
</tr> </tr>
</ng-container> </ng-container>

View File

@ -49,6 +49,13 @@
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
} }
.header {
border-top: 1px solid #ddd;
text-align: center;
font-style: italic;
font-size: 80%;
}
} }
} }

View File

@ -1,6 +1,6 @@
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {DecimalPipe, NgForOf, NgIf} from "@angular/common"; import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
import {Display} from "../../api/Value/Display"; import {Display, DisplayValue} from "../../api/value/Display";
@Component({ @Component({
selector: 'app-value-list', selector: 'app-value-list',
@ -51,7 +51,23 @@ export class ValueListComponent {
maxAgeSeconds: number = 10; maxAgeSeconds: number = 10;
private displayUpdate() { private displayUpdate() {
this.valid = this.displayList.some(d => !!d && !!d.value && !!d.value.date && (this.now.getTime() - d.value.date.getTime()) <= this.maxAgeSeconds * 1000) this.valid = this.displayList
.filter(d => d instanceof DisplayValue)
.some(d => !!d && !!d.value && !!d.value.date && (this.now.getTime() - d.value.date.getTime()) <= this.maxAgeSeconds * 1000)
}
asDisplay(item: Display): DisplayValue | null {
if (item instanceof DisplayValue) {
return item;
}
return null;
}
asString(item: Display): string | null {
if (typeof item === 'string') {
return item;
}
return null;
} }
} }

View File

@ -1,6 +1,6 @@
package de.ph87.data.series; package de.ph87.data.series;
import de.ph87.data.series.consumption.unit.Unit; import de.ph87.data.series.interval.Interval;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Embeddable; import jakarta.persistence.Embeddable;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
@ -22,16 +22,16 @@ public class SeriesIntervalKey implements Serializable {
@NonNull @NonNull
@Column(nullable = false, updatable = false, columnDefinition = "CHAR(1)") @Column(nullable = false, updatable = false, columnDefinition = "CHAR(1)")
private Unit unit; private Interval interval;
@NonNull @NonNull
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private ZonedDateTime aligned; private ZonedDateTime aligned;
public SeriesIntervalKey(@NonNull final Series series, @NonNull final Unit unit, @NonNull final ZonedDateTime unaligned) { public SeriesIntervalKey(@NonNull final Series series, @NonNull final Interval interval, @NonNull final ZonedDateTime unaligned) {
this.series = series; this.series = series;
this.unit = unit; this.interval = interval;
this.aligned = unit.align(unaligned); this.aligned = interval.align(unaligned);
} }
} }

View File

@ -5,29 +5,44 @@ import lombok.NonNull;
import java.util.function.BiFunction; import java.util.function.BiFunction;
public enum SeriesMode { public enum SeriesMode {
MEASURE((first, second) -> second - first, (last, value) -> value), MEASURE(
COUNTER((first, second) -> second - first, Double::sum), (first, second) -> second - first,
INCREASING((first, second) -> second - first, (last, value) -> value), (last, value) -> value
DECREASING((first, second) -> first - second, (last, value) -> value), ),
COUNTER(
(first, second) -> second - first,
Double::sum
),
INCREASING(
(first, second) -> second - first,
(last, value) -> value
),
DECREASING(
(first, second) -> first - second,
(last, value) -> value
),
; ;
@NonNull @NonNull
private final BiFunction<Double, Double, Double> delta; private final BiFunction<Double, Double, Double> amount;
@NonNull @NonNull
private final BiFunction<Double, Double, Double> add; private final BiFunction<Double, Double, Double> plus;
SeriesMode(@NonNull final BiFunction<Double, Double, Double> delta, @NonNull final BiFunction<Double, Double, Double> add) { SeriesMode(
this.delta = delta; @NonNull final BiFunction<Double, Double, Double> amount,
this.add = add; @NonNull final BiFunction<Double, Double, Double> plus
) {
this.amount = amount;
this.plus = plus;
} }
public double getDelta(final double first, final double second) { public double amount(final double first, final double second) {
return delta.apply(first, second); return amount.apply(first, second);
} }
public double update(final double series, final double value) { public double update(final double series, final double value) {
return add.apply(series, value); return plus.apply(series, value);
} }
} }

View File

@ -4,8 +4,10 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
@ -37,4 +39,9 @@ public class SeriesService {
return seriesRepository.findAll().stream().map(SeriesDto::new).toList(); return seriesRepository.findAll().stream().map(SeriesDto::new).toList();
} }
@NonNull
public Series getByName(@NonNull final String name) {
return seriesRepository.findByNameOrAliasesContains(name, name).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
} }

View File

@ -1,14 +1,14 @@
package de.ph87.data.series.consumption; package de.ph87.data.series.consumption;
import de.ph87.data.series.consumption.period.Period; import de.ph87.data.series.consumption.period.Period;
import de.ph87.data.series.consumption.unit.Unit; import de.ph87.data.series.interval.Interval;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import java.io.Serializable; import java.io.Serializable;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import static de.ph87.data.series.consumption.slice.SliceService.DL; import static de.ph87.data.series.interval.IntervalHelper.DL;
@Entity @Entity
@Getter @Getter
@ -32,15 +32,13 @@ public class Consumption {
} }
@ToString.Include @ToString.Include
@SuppressWarnings("unused") // toString public Interval interval() {
public Unit unit() { return id.interval;
return id.unit;
} }
@ToString.Include @ToString.Include
@SuppressWarnings("unused") // toString
public String aligned() { public String aligned() {
return DL(id.unit, id.aligned); return DL(id.interval, id.aligned);
} }
@NonNull @NonNull
@ -59,8 +57,8 @@ public class Consumption {
@Column(nullable = false) @Column(nullable = false)
private double lastValue; private double lastValue;
public Consumption(@NonNull final Period period, @NonNull final Unit unit, @NonNull final ZonedDateTime aligned, @NonNull final ZonedDateTime date, final double value) { public Consumption(@NonNull final Period period, @NonNull final Interval interval, @NonNull final ZonedDateTime aligned, @NonNull final ZonedDateTime date, final double value) {
this.id = new Id(period, unit, aligned); this.id = new Id(period, interval, aligned);
this.firstDate = date; this.firstDate = date;
this.firstValue = value; this.firstValue = value;
this.lastDate = date; this.lastDate = date;
@ -81,7 +79,7 @@ public class Consumption {
@NonNull @NonNull
@Column(nullable = false, updatable = false, columnDefinition = "CHAR(1)") @Column(nullable = false, updatable = false, columnDefinition = "CHAR(1)")
private Unit unit; private Interval interval;
@NonNull @NonNull
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)

View File

@ -1,80 +0,0 @@
package de.ph87.data.series.consumption;
import de.ph87.data.common.DateTimeHelpers;
import de.ph87.data.series.consumption.slice.Slice;
import de.ph87.data.series.consumption.slice.SliceService;
import de.ph87.data.series.consumption.unit.Unit;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@CrossOrigin
@RestController
@RequiredArgsConstructor
@RequestMapping("Consumption")
public class ConsumptionController {
private static final int MAX_COUNT = 1500;
private final SliceService sliceService;
@NonNull
@GetMapping("{seriesId}/{unitName}/last/{count}")
public List<List<Number>> latest(@PathVariable final long seriesId, @PathVariable final String unitName, @PathVariable final int count) {
return offset(seriesId, unitName, count, 0);
}
@NonNull
@GetMapping("{seriesId}/{unitName}/last/{count}/{offset}")
public List<List<Number>> offset(@PathVariable final long seriesId, @PathVariable final String unitName, @PathVariable final int count, @PathVariable final int offset) {
if (count <= 0) {
log.error("'count' must at least be 1");
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
if (count > MAX_COUNT) {
log.error("'count' must at most be {}", MAX_COUNT);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
final Unit unit = Unit.valueOf(unitName);
final ZonedDateTime end = unit.plus(unit.align(ZonedDateTime.now()), -offset);
final ZonedDateTime begin = unit.plus(end, -(count - 1));
return between(seriesId, unit, begin, end);
}
@NonNull
@GetMapping("{seriesId}/{unitName}/between/{beginEpochSeconds}/{endEpochSeconds}")
public List<List<Number>> between(@PathVariable final long seriesId, @PathVariable final String unitName, @PathVariable final long beginEpochSeconds, @PathVariable final long endEpochSeconds) {
return between(seriesId, Unit.valueOf(unitName), DateTimeHelpers.ZDT(beginEpochSeconds), DateTimeHelpers.ZDT(endEpochSeconds));
}
@NonNull
private List<List<Number>> between(final long seriesId, @NonNull final Unit unit, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end) {
final long estimatedCount = unit.estimateCount(begin, end);
log.debug("estimatedCount: {}", estimatedCount);
if (estimatedCount > MAX_COUNT) {
log.error("'estimatedCount' must at most be {} but is {}", MAX_COUNT, estimatedCount);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
return sliceService.slice(seriesId, unit, begin, end)
.stream()
.map(this::map)
.toList();
}
@NonNull
private List<Number> map(@NonNull final Slice slice) {
final ArrayList<Number> numbers = new ArrayList<>();
numbers.add(slice.begin.toEpochSecond());
numbers.add(Double.isNaN(slice.getDelta()) ? null : slice.getDelta());
return numbers;
}
}

View File

@ -1,22 +1,21 @@
package de.ph87.data.series.consumption; package de.ph87.data.series.consumption;
import de.ph87.data.series.consumption.period.Period; import de.ph87.data.series.consumption.period.Period;
import de.ph87.data.series.consumption.unit.Unit; import de.ph87.data.series.interval.Interval;
import lombok.NonNull; import lombok.NonNull;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface ConsumptionRepository extends CrudRepository<Consumption, Consumption.Id> { public interface ConsumptionRepository extends CrudRepository<Consumption, Consumption.Id> {
Optional<Consumption> findByIdPeriodAndIdUnitAndIdAligned(@NonNull Period period, @NonNull Unit unit, @NonNull ZonedDateTime aligned); Optional<Consumption> findByIdPeriodAndIdIntervalAndIdAligned(@NonNull Period period, @NonNull Interval interval, @NonNull ZonedDateTime aligned);
Optional<Consumption> findFirstByIdPeriodAndIdUnitAndIdAlignedLessThanOrderByIdAlignedDesc(@NonNull Period period, @NonNull Unit unit, @NonNull ZonedDateTime begin); Optional<Consumption> findFirstByIdPeriodAndIdIntervalAndIdAlignedLessThanOrderByIdAlignedDesc(@NonNull Period period, @NonNull Interval interval, @NonNull ZonedDateTime begin);
Optional<Consumption> findFirstByIdPeriodAndIdUnitAndIdAlignedGreaterThanOrderByIdAlignedAsc(@NonNull Period period, @NonNull Unit unit, @NonNull ZonedDateTime begin); Optional<Consumption> findFirstByIdPeriodAndIdIntervalAndIdAlignedGreaterThanOrderByIdAlignedAsc(@NonNull Period period, @NonNull Interval interval, @NonNull ZonedDateTime begin);
List<Consumption> findAllByIdPeriodAndIdUnitAndIdAlignedGreaterThanEqualAndIdAlignedLessThanEqualOrderByIdAlignedAsc(@NonNull Period period, @NonNull Unit unit, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end); Optional<Consumption> findFirstByIdPeriodAndIdIntervalAndIdAligned(@NonNull Period period, @NonNull Interval interval, @NonNull ZonedDateTime date);
} }

View File

@ -5,7 +5,7 @@ import de.ph87.data.series.SeriesMode;
import de.ph87.data.series.SeriesService; import de.ph87.data.series.SeriesService;
import de.ph87.data.series.consumption.period.Period; import de.ph87.data.series.consumption.period.Period;
import de.ph87.data.series.consumption.period.PeriodService; import de.ph87.data.series.consumption.period.PeriodService;
import de.ph87.data.series.consumption.unit.Unit; import de.ph87.data.series.interval.Interval;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -37,16 +37,16 @@ public class ConsumptionService {
period.setLastDate(event.getDate()); period.setLastDate(event.getDate());
period.setLastValue(series.getMode().update(period.getLastValue(), event.getValue())); period.setLastValue(series.getMode().update(period.getLastValue(), event.getValue()));
for (final Unit unit : Unit.values()) { for (final Interval interval : Interval.values()) {
final ZonedDateTime aligned = unit.align(event.getDate()); final ZonedDateTime aligned = interval.align(event.getDate());
final Optional<Consumption> existingOptional = consumptionRepository.findByIdPeriodAndIdUnitAndIdAligned(period, unit, aligned); final Optional<Consumption> existingOptional = consumptionRepository.findByIdPeriodAndIdIntervalAndIdAligned(period, interval, aligned);
if (existingOptional.isPresent()) { if (existingOptional.isPresent()) {
final Consumption existing = existingOptional.get(); final Consumption existing = existingOptional.get();
existing.setLastDate(event.getDate()); existing.setLastDate(event.getDate());
existing.setLastValue(event.getValue()); existing.setLastValue(event.getValue());
log.debug("Existing Consumption updated: {}", existing); log.debug("Existing Consumption updated: {}", existing);
} else { } else {
final Consumption created = consumptionRepository.save(new Consumption(period, unit, aligned, event.getDate(), event.getValue())); final Consumption created = consumptionRepository.save(new Consumption(period, interval, aligned, event.getDate(), event.getValue()));
log.debug("New Consumption created: created={}", created); log.debug("New Consumption created: created={}", created);
} }
} }

View File

@ -1,5 +1,6 @@
package de.ph87.data.series.consumption.period; package de.ph87.data.series.consumption.period;
import de.ph87.data.series.Series;
import lombok.NonNull; import lombok.NonNull;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
@ -8,6 +9,6 @@ import java.util.List;
public interface PeriodRepository extends CrudRepository<Period, Long> { public interface PeriodRepository extends CrudRepository<Period, Long> {
List<Period> findAllBySeriesIdAndLastDateGreaterThanAndFirstDateLessThan(long seriesId, @NonNull ZonedDateTime wantedEnd, @NonNull ZonedDateTime wantedBegin); List<Period> findAllBySeriesAndLastDateGreaterThanAndFirstDateLessThan(@NonNull Series series, @NonNull ZonedDateTime wantedEnd, @NonNull ZonedDateTime wantedBegin);
} }

View File

@ -1,71 +0,0 @@
package de.ph87.data.series.consumption.slice;
import de.ph87.data.series.consumption.Consumption;
import de.ph87.data.series.consumption.unit.Unit;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import java.time.Duration;
import java.time.ZonedDateTime;
@Getter
@ToString
public class Slice {
@NonNull
public final ZonedDateTime begin;
@NonNull
public final ZonedDateTime end;
@Setter
private double delta;
public Slice(@NonNull final Consumption consumption) {
this(consumption.getFirstDate(), consumption.getLastDate(), consumption.getId().getPeriod().getSeries().getMode().getDelta(consumption.getFirstValue(), consumption.getLastValue()));
}
public Slice(@NonNull final Consumption first, @NonNull final Consumption second) {
this(first.getLastDate(), second.getFirstDate(), first.getId().getPeriod().getSeries().getMode().getDelta(first.getLastValue(), second.getFirstValue()));
}
private Slice(@NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end, final double delta) {
this.begin = begin;
this.end = end;
this.delta = delta;
}
public Slice(@NonNull final ZonedDateTime begin, @NonNull final Unit unit) {
this.begin = begin;
this.end = unit.plus(begin, 1);
this.delta = Double.NaN;
}
public double getDeltaPerMilli() {
return delta / Duration.between(begin, end).toMillis();
}
public void merge(@NonNull final Slice other) {
if (!this.begin.equals(other.begin)) {
throw new RuntimeException();
}
if (!this.end.equals(other.end)) {
throw new RuntimeException();
}
add(other.delta);
}
public void add(final double addDelta) {
if (Double.isNaN(addDelta)) {
return;
}
if (Double.isNaN(this.delta)) {
this.delta = addDelta;
} else {
this.delta += addDelta;
}
}
}

View File

@ -0,0 +1,34 @@
package de.ph87.data.series.consumption.slice;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.time.ZonedDateTime;
@Getter
@ToString
public class SliceAligned {
@NonNull
private final ZonedDateTime date;
public final double amount;
public SliceAligned(@NonNull final SliceAligned a, @NonNull final SliceAligned b) {
if (a.date.toEpochSecond() != b.date.toEpochSecond()) {
throw new RuntimeException();
}
this.date = a.date;
this.amount = a.amount + b.amount;
}
public SliceAligned(@NonNull final ZonedDateTime date, final double amount) {
if (amount < 0) {
throw new RuntimeException();
}
this.date = date;
this.amount = amount;
}
}

View File

@ -0,0 +1,29 @@
package de.ph87.data.series.consumption.slice;
import de.ph87.data.series.interval.Interval;
import jakarta.annotation.Nullable;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.time.ZonedDateTime;
@Slf4j
@CrossOrigin
@RestController
@RequiredArgsConstructor
@RequestMapping("Slice")
public class SliceController {
private final SliceService sliceService;
@Nullable
@GetMapping("seriesName/{seriesName}/interval/{intervalName}/offset/{offset}")
public SliceDto offset(@NonNull @PathVariable final String seriesName, @NonNull @PathVariable final String intervalName, @PathVariable final int offset) {
final Interval interval = Interval.valueOf(intervalName);
final ZonedDateTime date = interval.plus(interval.align(ZonedDateTime.now()), -offset);
return sliceService.at(seriesName, interval, date);
}
}

View File

@ -0,0 +1,25 @@
package de.ph87.data.series.consumption.slice;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.time.ZonedDateTime;
@Getter
@ToString
public class SliceDto {
public final ZonedDateTime date;
public final double amount;
public final String unit;
public SliceDto(@NonNull final SliceAligned slice, final @NonNull String unit) {
this.date = slice.getDate();
this.amount = slice.getAmount();
this.unit = unit;
}
}

View File

@ -1,10 +1,13 @@
package de.ph87.data.series.consumption.slice; package de.ph87.data.series.consumption.slice;
import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesMode;
import de.ph87.data.series.SeriesService;
import de.ph87.data.series.consumption.Consumption; import de.ph87.data.series.consumption.Consumption;
import de.ph87.data.series.consumption.ConsumptionRepository; import de.ph87.data.series.consumption.ConsumptionRepository;
import de.ph87.data.series.consumption.period.Period; import de.ph87.data.series.consumption.period.Period;
import de.ph87.data.series.consumption.period.PeriodRepository; import de.ph87.data.series.consumption.period.PeriodRepository;
import de.ph87.data.series.consumption.unit.Unit; import de.ph87.data.series.interval.Interval;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -14,11 +17,8 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.Duration; import java.time.Duration;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Objects;
@Slf4j @Slf4j
@Service @Service
@ -26,166 +26,67 @@ import java.util.Optional;
@RequiredArgsConstructor @RequiredArgsConstructor
public class SliceService { public class SliceService {
private final ConsumptionRepository consumptionRepository;
private final PeriodRepository periodRepository; private final PeriodRepository periodRepository;
@NonNull private final ConsumptionRepository consumptionRepository;
public List<Slice> slice(final long seriesId, @NonNull final Unit unit, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end) {
log.debug("slice:");
log.debug(" seriesId: {}", seriesId);
log.debug(" unit: {}", unit);
final ZonedDateTime wantedFirst = unit.align(begin); private final SeriesService seriesService;
final ZonedDateTime wantedLast = unit.align(end);
log.debug(" wantedFirst: {}", DL(unit, wantedFirst));
log.debug(" wantedLast: {}", DL(unit, wantedLast));
final List<Period> periods = periodRepository.findAllBySeriesIdAndLastDateGreaterThanAndFirstDateLessThan(seriesId, wantedFirst, unit.plus(wantedLast, 1)); @Nullable
log.debug(" periods: {}", periods.size()); public SliceDto at(@NonNull final String seriesName, @NonNull final Interval interval, @NonNull final ZonedDateTime date) {
final ZonedDateTime sliceBegin = interval.align(date);
final List<Slice> totalSlices = new ArrayList<>(); final Series series = seriesService.getByName(seriesName);
for (final Period period : periods) { final List<Period> periods = periodRepository.findAllBySeriesAndLastDateGreaterThanAndFirstDateLessThan(series, sliceBegin, sliceBegin);
log.debug(" {}", period); return periods.stream()
log.debug(" firstDate: {}", DL(unit, period.getFirstDate())); .map(period -> at(period, interval, sliceBegin))
log.debug(" lastDate: {}", DL(unit, period.getLastDate())); .filter(Objects::nonNull)
.reduce(SliceAligned::new)
final List<Slice> periodSlices = reslicePeriod(unit, period, wantedFirst, wantedLast); .map(slice -> new SliceDto(slice, series.getUnit()))
print("periodSlices", periodSlices, 3); .orElse(null);
periodSlices.forEach(merge -> merge(totalSlices, merge));
print("totalSlices", totalSlices, 3);
}
return totalSlices;
}
private static void merge(@NonNull final List<Slice> resultList, @NonNull final Slice merge) {
for (int resultIndex = 0; resultIndex < resultList.size(); resultIndex++) {
final Slice result = resultList.get(resultIndex);
final long compare = result.begin.toEpochSecond() - merge.begin.toEpochSecond();
if (compare == 0) {
result.merge(merge);
return;
}
if (compare > 0) {
resultList.add(resultIndex, merge);
return;
}
}
resultList.add(merge);
}
@NonNull
private List<Slice> reslicePeriod(@NonNull final Unit unit, @NonNull final Period period, @NonNull final ZonedDateTime firstBegin, @NonNull final ZonedDateTime lastBegin) {
final ZonedDateTime lastEnd = unit.plus(lastBegin, 1);
final List<Slice> sourceList = slicePeriod(period, unit, firstBegin, lastBegin);
final List<Slice> resultList = new ArrayList<>();
ZonedDateTime date = firstBegin;
Slice result = firstResult(firstBegin, unit, resultList);
Slice source = nextSourceIfNeeded(date, null, sourceList);
while (date.isBefore(lastEnd)) {
source = nextSourceIfNeeded(date, source, sourceList);
result = nextResultIfNeeded(date, result, resultList, unit, lastEnd);
if (source == null) {
date = result.end;
} else {
final ZonedDateTime earliestEnd = source.end.isBefore(result.end) ? source.end : result.end;
if (hasOverlap(source, earliestEnd, date)) {
final ZonedDateTime latestBegin = source.begin.isAfter(date) ? source.begin : date;
final long millis = Duration.between(latestBegin, earliestEnd).toMillis();
result.add(millis * source.getDeltaPerMilli());
}
date = earliestEnd;
}
}
return resultList;
}
private static boolean hasOverlap(@NonNull final Slice source, @NonNull final ZonedDateTime earliestEnd, @NonNull final ZonedDateTime date) {
return source.begin.isBefore(earliestEnd) && source.end.isAfter(date);
}
@NonNull
private static Slice firstResult(@NonNull final ZonedDateTime begin, @NonNull final Unit unit, @NonNull final List<Slice> resultList) {
final Slice newWanted = new Slice(begin, unit);
resultList.add(newWanted);
return newWanted;
} }
@Nullable @Nullable
private static Slice nextSourceIfNeeded(@NonNull final ZonedDateTime date, @Nullable final Slice source, @NonNull final List<Slice> sourceList) { private SliceAligned at(@NonNull final Period period, @NonNull final Interval interval, @NonNull final ZonedDateTime sliceBegin) {
if (source == null || !date.isBefore(source.end)) { final SeriesMode mode = period.getSeries().getMode();
return sourceList.isEmpty() ? null : sourceList.remove(0);
final ZonedDateTime sliceEnd = interval.plus(sliceBegin, 1);
final Consumption before = consumptionRepository.findFirstByIdPeriodAndIdIntervalAndIdAlignedLessThanOrderByIdAlignedDesc(period, interval, sliceBegin).orElse(null);
final Consumption wanted = consumptionRepository.findFirstByIdPeriodAndIdIntervalAndIdAligned(period, interval, sliceBegin).orElse(null);
final Consumption after = consumptionRepository.findFirstByIdPeriodAndIdIntervalAndIdAlignedGreaterThanOrderByIdAlignedAsc(period, interval, sliceBegin).orElse(null);
final double sliceAmount;
if (wanted == null) {
if (before == null || after == null) {
return null;
} }
return source; final long totalMillis = Duration.between(before.getLastDate(), after.getFirstDate()).toMillis();
final double totalAmount = mode.amount(before.getLastValue(), after.getFirstValue());
final long sliceMillis = Duration.between(sliceBegin, sliceEnd).toMillis();
sliceAmount = sliceMillis * totalAmount / totalMillis;
} else {
final double firstValue;
if (before != null) {
final long totalMillis = Duration.between(before.getLastDate(), wanted.getFirstDate()).toMillis();
final double totalDelta = wanted.getFirstValue() - before.getLastValue();
final long beforeMillis = Duration.between(before.getLastDate(), sliceBegin).toMillis();
firstValue = before.getLastValue() + beforeMillis * totalDelta / totalMillis;
} else {
firstValue = wanted.getFirstValue();
} }
@NonNull final double lastValue;
private static Slice nextResultIfNeeded(@NonNull final ZonedDateTime date, @NonNull final Slice result, @NonNull final List<Slice> resultList, @NonNull final Unit unit, @NonNull final ZonedDateTime lastEnd) { if (after != null) {
if (date.isBefore(lastEnd) && !date.isBefore(result.end)) { final long totalMillis = Duration.between(wanted.getLastDate(), after.getFirstDate()).toMillis();
final Slice slice = new Slice(result.end, unit); final double totalDelta = after.getFirstValue() - wanted.getLastValue();
resultList.add(slice); final long afterMillis = Duration.between(sliceEnd, after.getFirstDate()).toMillis();
return slice; lastValue = after.getFirstValue() - afterMillis * totalDelta / totalMillis;
} else {
lastValue = wanted.getLastValue();
} }
return result; sliceAmount = mode.amount(firstValue, lastValue);
} }
return new SliceAligned(sliceBegin, sliceAmount);
@NonNull
private List<Slice> slicePeriod(@NonNull final Period period, @NonNull final Unit unit, @NonNull final ZonedDateTime wantedFirst, @NonNull final ZonedDateTime wantedLast) {
final Optional<Consumption> firstOptional = consumptionRepository.findFirstByIdPeriodAndIdUnitAndIdAlignedLessThanOrderByIdAlignedDesc(period, unit, wantedFirst)
.or(() -> consumptionRepository.findFirstByIdPeriodAndIdUnitAndIdAlignedGreaterThanOrderByIdAlignedAsc(period, unit, wantedFirst));
if (firstOptional.isEmpty()) {
log.error(" No first Consumption for Period: {}", period);
return Collections.emptyList();
}
final Optional<Consumption> lastOptional = consumptionRepository.findFirstByIdPeriodAndIdUnitAndIdAlignedGreaterThanOrderByIdAlignedAsc(period, unit, wantedLast)
.or(() -> consumptionRepository.findFirstByIdPeriodAndIdUnitAndIdAlignedLessThanOrderByIdAlignedDesc(period, unit, wantedLast));
if (lastOptional.isEmpty()) {
log.error(" No last Consumption for Period: {}", period);
return Collections.emptyList();
}
final Consumption firstToFetch = firstOptional.get();
final Consumption lastToFetch = lastOptional.get();
final List<Consumption> consumptions = consumptionRepository.findAllByIdPeriodAndIdUnitAndIdAlignedGreaterThanEqualAndIdAlignedLessThanEqualOrderByIdAlignedAsc(period, unit, firstToFetch.getId().getAligned(), lastToFetch.getId().getAligned());
print("consumptions", consumptions, 3);
Consumption last = null;
final List<Slice> slices = new ArrayList<>();
for (final Consumption consumption : consumptions) {
if (last != null) {
slices.add(new Slice(last, consumption));
}
if (!consumption.getFirstDate().equals(consumption.getLastDate())) {
slices.add(new Slice(consumption));
}
last = consumption;
}
print("sourceSlices", slices, 3);
return slices;
}
@NonNull
@SuppressWarnings("SuspiciousDateFormat")
public static String DL(@NonNull final Unit unit, @NonNull final ZonedDateTime date) {
return switch (unit) {
case Quarterhour, Hour -> date.toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
case Day -> date.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
case Week -> date.toLocalDate().format(DateTimeFormatter.ofPattern("YYYY-'KW'w"));
case Month -> date.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy-LLLL"));
case Year -> date.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy"));
};
}
@SuppressWarnings("SameParameterValue")
private static void print(@NonNull final String name, @NonNull final List<?> list, final int indent) {
final String indentStr = " ".repeat(indent * 2);
log.debug("{}{}: {}", indentStr, name, list.size());
list.forEach(item -> log.debug("{}{}", indentStr + " ", item.toString()));
} }
} }

View File

@ -1,27 +0,0 @@
package de.ph87.data.series.consumption.unit;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.Arrays;
@Converter(autoApply = true)
public class UnitJpaConverter implements AttributeConverter<Unit, String> {
@Override
public String convertToDatabaseColumn(final Unit unit) {
if (unit == null) {
return null;
}
return unit.code;
}
@Override
public Unit convertToEntityAttribute(final String code) {
if (code == null) {
return null;
}
return Arrays.stream(Unit.values()).filter(u -> u.code.equals(code)).findFirst().orElse(null);
}
}

View File

@ -4,7 +4,7 @@ import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesIntervalKey; import de.ph87.data.series.SeriesIntervalKey;
import de.ph87.data.series.SeriesMode; import de.ph87.data.series.SeriesMode;
import de.ph87.data.series.SeriesService; import de.ph87.data.series.SeriesService;
import de.ph87.data.series.consumption.unit.Unit; import de.ph87.data.series.interval.Interval;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -27,8 +27,8 @@ public class CounterService {
log.debug("Handling CounterEvent: {}", event); log.debug("Handling CounterEvent: {}", event);
final Series series = seriesService.getOrCreateByName(event.getName(), SeriesMode.COUNTER, event.getDate(), event.getCount(), event.getUnit()); final Series series = seriesService.getOrCreateByName(event.getName(), SeriesMode.COUNTER, event.getDate(), event.getCount(), event.getUnit());
for (final Unit unit : Unit.values()) { for (final Interval interval : Interval.values()) {
final SeriesIntervalKey id = new SeriesIntervalKey(series, unit, event.getDate()); final SeriesIntervalKey id = new SeriesIntervalKey(series, interval, event.getDate());
counterRepository.findById(id) counterRepository.findById(id)
.stream() .stream()
.peek(existing -> existing.setCount(existing.getCount() + event.getCount())) .peek(existing -> existing.setCount(existing.getCount() + event.getCount()))

View File

@ -1,4 +1,4 @@
package de.ph87.data.series.consumption.unit; package de.ph87.data.series.interval;
import lombok.NonNull; import lombok.NonNull;
@ -9,7 +9,7 @@ import java.time.temporal.ChronoUnit;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
public enum Unit { public enum Interval {
Quarterhour("q", t -> t.truncatedTo(ChronoUnit.MINUTES).minusMinutes(t.getMinute() % 15), (t, count) -> t.plusMinutes(15 * count), (a, b) -> Duration.between(a, b).toMinutes() / 15), Quarterhour("q", t -> t.truncatedTo(ChronoUnit.MINUTES).minusMinutes(t.getMinute() % 15), (t, count) -> t.plusMinutes(15 * count), (a, b) -> Duration.between(a, b).toMinutes() / 15),
Hour("h", t -> t.truncatedTo(ChronoUnit.HOURS), ZonedDateTime::plusHours, (a, b) -> Duration.between(a, b).toHours()), Hour("h", t -> t.truncatedTo(ChronoUnit.HOURS), ZonedDateTime::plusHours, (a, b) -> Duration.between(a, b).toHours()),
Day("d", t -> t.truncatedTo(ChronoUnit.DAYS), ZonedDateTime::plusDays, (a, b) -> Duration.between(a, b).toDays()), Day("d", t -> t.truncatedTo(ChronoUnit.DAYS), ZonedDateTime::plusDays, (a, b) -> Duration.between(a, b).toDays()),
@ -30,7 +30,7 @@ public enum Unit {
@NonNull @NonNull
private final BiFunction<ZonedDateTime, ZonedDateTime, Long> estimateCount; private final BiFunction<ZonedDateTime, ZonedDateTime, Long> estimateCount;
Unit(@NonNull final String code, @NonNull final Function<ZonedDateTime, ZonedDateTime> align, @NonNull final BiFunction<ZonedDateTime, Long, ZonedDateTime> offset, @NonNull final BiFunction<ZonedDateTime, ZonedDateTime, Long> estimateCount) { Interval(@NonNull final String code, @NonNull final Function<ZonedDateTime, ZonedDateTime> align, @NonNull final BiFunction<ZonedDateTime, Long, ZonedDateTime> offset, @NonNull final BiFunction<ZonedDateTime, ZonedDateTime, Long> estimateCount) {
this.code = code; this.code = code;
this.align = align; this.align = align;
this.offset = offset; this.offset = offset;

View File

@ -0,0 +1,22 @@
package de.ph87.data.series.interval;
import lombok.NonNull;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class IntervalHelper {
@NonNull
@SuppressWarnings("SuspiciousDateFormat")
public static String DL(@NonNull final Interval interval, @NonNull final ZonedDateTime date) {
return switch (interval) {
case Quarterhour, Hour -> date.toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
case Day -> date.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
case Week -> date.toLocalDate().format(DateTimeFormatter.ofPattern("YYYY-'KW'w"));
case Month -> date.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy-LLLL"));
case Year -> date.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy"));
};
}
}

View File

@ -0,0 +1,27 @@
package de.ph87.data.series.interval;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.Arrays;
@Converter(autoApply = true)
public class IntervalJpaConverter implements AttributeConverter<Interval, String> {
@Override
public String convertToDatabaseColumn(final Interval interval) {
if (interval == null) {
return null;
}
return interval.code;
}
@Override
public Interval convertToEntityAttribute(final String code) {
if (code == null) {
return null;
}
return Arrays.stream(Interval.values()).filter(u -> u.code.equals(code)).findFirst().orElse(null);
}
}

View File

@ -4,7 +4,7 @@ import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesIntervalKey; import de.ph87.data.series.SeriesIntervalKey;
import de.ph87.data.series.SeriesMode; import de.ph87.data.series.SeriesMode;
import de.ph87.data.series.SeriesService; import de.ph87.data.series.SeriesService;
import de.ph87.data.series.consumption.unit.Unit; import de.ph87.data.series.interval.Interval;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -27,8 +27,8 @@ public class MeasureService {
log.debug("Handling MeasureEvent: {}", event); log.debug("Handling MeasureEvent: {}", event);
final Series series = seriesService.getOrCreateByName(event.getName(), SeriesMode.MEASURE, event.getDate(), event.getValue(), event.getUnit()); final Series series = seriesService.getOrCreateByName(event.getName(), SeriesMode.MEASURE, event.getDate(), event.getValue(), event.getUnit());
for (final Unit unit : Unit.values()) { for (final Interval interval : Interval.values()) {
final SeriesIntervalKey id = new SeriesIntervalKey(series, unit, event.getDate()); final SeriesIntervalKey id = new SeriesIntervalKey(series, interval, event.getDate());
measureRepository.findById(id) measureRepository.findById(id)
.stream() .stream()
.peek(existing -> existing.update(event)) .peek(existing -> existing.update(event))