Slice, Unit->Interval, Today+Yesterday
This commit is contained in:
parent
f76b6cdec8
commit
fe8afcad29
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|
||||||
|
|||||||
27
src/main/angular/src/app/api/series/constants.ts
Normal file
27
src/main/angular/src/app/api/series/constants.ts
Normal 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';
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
export enum Interval {
|
||||||
|
Quarterhour = 'Quarterhour',
|
||||||
|
Hour = 'Hour',
|
||||||
|
Day = 'Day',
|
||||||
|
Week = 'Week',
|
||||||
|
Month = 'Month',
|
||||||
|
Year = 'Year',
|
||||||
|
}
|
||||||
@ -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']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,4 +12,4 @@ export class DisplayValue {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Display = DisplayValue | null;
|
export type Display = DisplayValue | string | null;
|
||||||
@ -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) {
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -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({
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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, ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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%;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
final ZonedDateTime sliceEnd = interval.plus(sliceBegin, 1);
|
||||||
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 (date.isBefore(lastEnd) && !date.isBefore(result.end)) {
|
|
||||||
final Slice slice = new Slice(result.end, unit);
|
|
||||||
resultList.add(slice);
|
|
||||||
return slice;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
final Consumption before = consumptionRepository.findFirstByIdPeriodAndIdIntervalAndIdAlignedLessThanOrderByIdAlignedDesc(period, interval, sliceBegin).orElse(null);
|
||||||
private List<Slice> slicePeriod(@NonNull final Period period, @NonNull final Unit unit, @NonNull final ZonedDateTime wantedFirst, @NonNull final ZonedDateTime wantedLast) {
|
final Consumption wanted = consumptionRepository.findFirstByIdPeriodAndIdIntervalAndIdAligned(period, interval, sliceBegin).orElse(null);
|
||||||
final Optional<Consumption> firstOptional = consumptionRepository.findFirstByIdPeriodAndIdUnitAndIdAlignedLessThanOrderByIdAlignedDesc(period, unit, wantedFirst)
|
final Consumption after = consumptionRepository.findFirstByIdPeriodAndIdIntervalAndIdAlignedGreaterThanOrderByIdAlignedAsc(period, interval, sliceBegin).orElse(null);
|
||||||
.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)
|
final double sliceAmount;
|
||||||
.or(() -> consumptionRepository.findFirstByIdPeriodAndIdUnitAndIdAlignedLessThanOrderByIdAlignedDesc(period, unit, wantedLast));
|
if (wanted == null) {
|
||||||
if (lastOptional.isEmpty()) {
|
if (before == null || after == null) {
|
||||||
log.error(" No last Consumption for Period: {}", period);
|
return null;
|
||||||
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())) {
|
final long totalMillis = Duration.between(before.getLastDate(), after.getFirstDate()).toMillis();
|
||||||
slices.add(new Slice(consumption));
|
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();
|
||||||
}
|
}
|
||||||
last = consumption;
|
|
||||||
|
final double lastValue;
|
||||||
|
if (after != null) {
|
||||||
|
final long totalMillis = Duration.between(wanted.getLastDate(), after.getFirstDate()).toMillis();
|
||||||
|
final double totalDelta = after.getFirstValue() - wanted.getLastValue();
|
||||||
|
final long afterMillis = Duration.between(sliceEnd, after.getFirstDate()).toMillis();
|
||||||
|
lastValue = after.getFirstValue() - afterMillis * totalDelta / totalMillis;
|
||||||
|
} else {
|
||||||
|
lastValue = wanted.getLastValue();
|
||||||
|
}
|
||||||
|
sliceAmount = mode.amount(firstValue, lastValue);
|
||||||
}
|
}
|
||||||
|
return new SliceAligned(sliceBegin, sliceAmount);
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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()))
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user