diff --git a/src/main/angular/README.md b/src/main/angular/README.md index 754e3c0..050909e 100644 --- a/src/main/angular/README.md +++ b/src/main/angular/README.md @@ -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. -## 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 diff --git a/src/main/angular/src/app/api/series/Series.ts b/src/main/angular/src/app/api/series/Series.ts index 7246b39..06657d3 100644 --- a/src/main/angular/src/app/api/series/Series.ts +++ b/src/main/angular/src/app/api/series/Series.ts @@ -1,6 +1,6 @@ import {Period} from "./consumption/period/Period"; import {validateDate, validateNumber, validateString} from "../validators"; -import {Value} from "../Value/Value"; +import {Value} from "../value/Value"; export class Series extends Value { diff --git a/src/main/angular/src/app/api/series/constants.ts b/src/main/angular/src/app/api/series/constants.ts new file mode 100644 index 0000000..e0802d5 --- /dev/null +++ b/src/main/angular/src/app/api/series/constants.ts @@ -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'; diff --git a/src/main/angular/src/app/api/series/consumption/interval/Interval.ts b/src/main/angular/src/app/api/series/consumption/interval/Interval.ts new file mode 100644 index 0000000..af0ff66 --- /dev/null +++ b/src/main/angular/src/app/api/series/consumption/interval/Interval.ts @@ -0,0 +1,8 @@ +export enum Interval { + Quarterhour = 'Quarterhour', + Hour = 'Hour', + Day = 'Day', + Week = 'Week', + Month = 'Month', + Year = 'Year', +} diff --git a/src/main/angular/src/app/api/series/consumption/slice/Slice.ts b/src/main/angular/src/app/api/series/consumption/slice/Slice.ts new file mode 100644 index 0000000..eafa023 --- /dev/null +++ b/src/main/angular/src/app/api/series/consumption/slice/Slice.ts @@ -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']), + ); + } + +} diff --git a/src/main/angular/src/app/api/series/consumption/slice/slice.service.ts b/src/main/angular/src/app/api/series/consumption/slice/slice.service.ts new file mode 100644 index 0000000..7c348d5 --- /dev/null +++ b/src/main/angular/src/app/api/series/consumption/slice/slice.service.ts @@ -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): void { + this.api.getSingle(['Slice', 'seriesName', seriesName, 'interval', interval, 'offset', offset], Slice.fromJson, next); + } + +} diff --git a/src/main/angular/src/app/api/series/series-cache.service.ts b/src/main/angular/src/app/api/series/series-cache.service.ts index 176ccce..cffe0c5 100644 --- a/src/main/angular/src/app/api/series/series-cache.service.ts +++ b/src/main/angular/src/app/api/series/series-cache.service.ts @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core'; import {Series} from "./Series"; import {ApiService} from "../api.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 { if (fresh.name !== name) { @@ -80,34 +81,34 @@ export class SeriesCacheService { } private seriesUpdate(series: Series) { - this.gridPurchased = returnUpdatedSeriesIfNameAndNewer(series, this.gridPurchased, 'electricity.grid.purchase.energy'); - this.gridDelivered = returnUpdatedSeriesIfNameAndNewer(series, this.gridDelivered, 'electricity.grid.delivery.energy'); - this.gridPower = returnUpdatedSeriesIfNameAndNewer(series, this.gridPower, 'electricity.grid.power'); + this.gridPurchased = returnUpdatedSeriesIfNameAndNewer(series, this.gridPurchased, ELECTRICITY_GRID_PURCHASED_ENERGY); + this.gridDelivered = returnUpdatedSeriesIfNameAndNewer(series, this.gridDelivered, ELECTRICITY_GRID_DELIVERED_ENERGY); + this.gridPower = returnUpdatedSeriesIfNameAndNewer(series, this.gridPower, ELECTRICITY_GRID_POWER); - this.photovoltaicProduced = returnUpdatedSeriesIfNameAndNewer(series, this.photovoltaicProduced, 'electricity.photovoltaic.energy'); - this.photovoltaicPower = returnUpdatedSeriesIfNameAndNewer(series, this.photovoltaicPower, 'electricity.photovoltaic.power'); + this.photovoltaicProduced = returnUpdatedSeriesIfNameAndNewer(series, this.photovoltaicProduced, ELECTRICITY_PHOTOVOLTAIC_PRODUCED); + this.photovoltaicPower = returnUpdatedSeriesIfNameAndNewer(series, this.photovoltaicPower, ELECTRICITY_PHOTOVOLTAIC_POWER); - this.schlafzimmerTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerTemperature, 'schlafzimmer.temperature'); - this.schlafzimmerHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerHumidityRelative, 'schlafzimmer.humidity_relative'); - this.schlafzimmerHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerHumidityAbsolute, 'schlafzimmer.humidity_absolute'); + this.schlafzimmerTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerTemperature, SCHLAFZIMMER_TEMPERATURE); + this.schlafzimmerHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerHumidityRelative, SCHLAFZIMMER_HUMIDITY_RELATIVE); + this.schlafzimmerHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.schlafzimmerHumidityAbsolute, SCHLAFZIMMER_HUMIDITY_ABSOLUTE); - this.outdoorTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorTemperature, 'garten.temperature'); - this.outdoorHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorHumidityRelative, 'garten.humidity_relative'); - this.outdoorHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorHumidityAbsolute, 'garten.humidity_absolute'); + this.outdoorTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorTemperature, GARTEN_TEMPERATURE); + this.outdoorHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorHumidityRelative, GARTEN_HUMIDITY_RELATIVE); + this.outdoorHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.outdoorHumidityAbsolute, GARTEN_HUMIDITY_ABSOLUTE); - this.heatingRoomTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomTemperature, 'heating.room.temperature'); - this.heatingRoomHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomHumidityRelative, 'heating.room.humidity_relative'); - this.heatingRoomHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomHumidityAbsolute, 'heating.room.humidity_absolute'); + this.heatingRoomTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomTemperature, HEATING_ROOM_TEMPERATURE); + this.heatingRoomHumidityRelative = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomHumidityRelative, HEATING_ROOM_HUMIDITY_RELATIVE); + this.heatingRoomHumidityAbsolute = returnUpdatedSeriesIfNameAndNewer(series, this.heatingRoomHumidityAbsolute, HEATING_ROOM_HUMIDITY_ABSOLUTE); - this.heatingExhaustTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingExhaustTemperature, 'heating.exhaust.temperature'); - this.heatingBufferSupplyTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferSupplyTemperature, 'heating.buffer.supply.temperature'); - this.heatingBufferReturnTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferReturnTemperature, 'heating.buffer.return.temperature'); - this.heatingBufferColdTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferColdTemperature, 'heating.buffer.cold.temperature'); - this.heatingBufferInnerTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferInnerTemperature, 'heating.buffer.inner.temperature'); - this.heatingBufferHotTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferHotTemperature, 'heating.buffer.hot.temperature'); - this.heatingBufferCirculationTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferCirculationTemperature, 'heating.buffer.circulation.temperature'); - this.heatingLoopSupplyTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingLoopSupplyTemperature, 'heating.loop.supply.temperature'); - this.heatingLoopReturnTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingLoopReturnTemperature, 'heating.loop.return.temperature'); + this.heatingExhaustTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingExhaustTemperature, HEATING_EXHAUST_TEMPERATURE); + this.heatingBufferSupplyTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferSupplyTemperature, HEATING_BUFFER_SUPPLY_TEMPERATURE); + this.heatingBufferReturnTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferReturnTemperature, HEATING_BUFFER_RETURN_TEMPERATURE); + this.heatingBufferColdTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferColdTemperature, HEATING_BUFFER_COLD_TEMPERATURE); + this.heatingBufferInnerTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferInnerTemperature, HEATING_BUFFER_INNER_TEMPERATURE); + this.heatingBufferHotTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferHotTemperature, HEATING_BUFFER_HOT_TEMPERATURE); + this.heatingBufferCirculationTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingBufferCirculationTemperature, HEATING_BUFFER_CIRCULATION_TEMPERATURE); + this.heatingLoopSupplyTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingLoopSupplyTemperature, HEATING_LOOP_SUPPLY_TEMPERATURE); + this.heatingLoopReturnTemperature = returnUpdatedSeriesIfNameAndNewer(series, this.heatingLoopReturnTemperature, HEATING_LOOP_RETURN_TEMPERATURE); } } diff --git a/src/main/angular/src/app/api/Value/Display.ts b/src/main/angular/src/app/api/value/Display.ts similarity index 77% rename from src/main/angular/src/app/api/Value/Display.ts rename to src/main/angular/src/app/api/value/Display.ts index 51f6ab2..ef2ff5f 100644 --- a/src/main/angular/src/app/api/Value/Display.ts +++ b/src/main/angular/src/app/api/value/Display.ts @@ -12,4 +12,4 @@ export class DisplayValue { } -export type Display = DisplayValue | null; +export type Display = DisplayValue | string | null; diff --git a/src/main/angular/src/app/api/Value/Value.ts b/src/main/angular/src/app/api/value/Value.ts similarity index 85% rename from src/main/angular/src/app/api/Value/Value.ts rename to src/main/angular/src/app/api/value/Value.ts index bdab72c..178bdd4 100644 --- a/src/main/angular/src/app/api/Value/Value.ts +++ b/src/main/angular/src/app/api/value/Value.ts @@ -5,7 +5,7 @@ export class Value { constructor( readonly date: Date | 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 { if (this.value === null) { - return new Value(null, null, null); + return new Value(null, null, ''); } const value = func(this.value); return new Value(this.date, value, this.unit); } binary(other: Value, func: (a: number, b: number) => number): Value { - if (this.date === null || this.value === null || this.unit === null) { - return new Value(null, null, other.unit || null); + if (this.date === null || this.value === 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); } const oldestDate = this.getOldestDate(other); @@ -75,10 +75,10 @@ export class Value { } 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; } - if (other.date === null || other.value === null || other.unit === null) { + if (other.date === null || other.value === null) { return +1; } if (comparing === undefined) { diff --git a/src/main/angular/src/app/api/Value/ValueConstant.ts b/src/main/angular/src/app/api/value/ValueConstant.ts similarity index 87% rename from src/main/angular/src/app/api/Value/ValueConstant.ts rename to src/main/angular/src/app/api/value/ValueConstant.ts index 11a1536..d2e09da 100644 --- a/src/main/angular/src/app/api/Value/ValueConstant.ts +++ b/src/main/angular/src/app/api/value/ValueConstant.ts @@ -4,7 +4,7 @@ export class ValueConstant extends Value { constructor( value: number | null, - unit: string | null, + unit: string, ) { super(new Date(Date.now()), value, unit); } diff --git a/src/main/angular/src/app/pages/dashboard/air/dashboard-air-tile.component.ts b/src/main/angular/src/app/pages/dashboard/air/dashboard-air-tile.component.ts index 5c12f22..166c6b0 100644 --- a/src/main/angular/src/app/pages/dashboard/air/dashboard-air-tile.component.ts +++ b/src/main/angular/src/app/pages/dashboard/air/dashboard-air-tile.component.ts @@ -1,6 +1,6 @@ import {Component, Input} from '@angular/core'; 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"; @Component({ diff --git a/src/main/angular/src/app/pages/dashboard/dashboard.component.html b/src/main/angular/src/app/pages/dashboard/dashboard.component.html index d286322..a6bf3a6 100644 --- a/src/main/angular/src/app/pages/dashboard/dashboard.component.html +++ b/src/main/angular/src/app/pages/dashboard/dashboard.component.html @@ -1,5 +1,5 @@
- +
diff --git a/src/main/angular/src/app/pages/dashboard/dashboard.component.ts b/src/main/angular/src/app/pages/dashboard/dashboard.component.ts index 1029f65..a5cb7fe 100644 --- a/src/main/angular/src/app/pages/dashboard/dashboard.component.ts +++ b/src/main/angular/src/app/pages/dashboard/dashboard.component.ts @@ -7,6 +7,7 @@ import {Subscription, timer} from "rxjs"; import {DashboardHeatingTileComponent} from "./heating/dashboard-heating-tile.component"; const UPDATE_INTERVAL_MILLIS = 1000; +const SLOW_UPDATE_INTERVAL_MILLIS = 60 * 1000; @Component({ selector: 'app-dashboard', @@ -26,10 +27,15 @@ export class DashboardComponent implements OnInit, OnDestroy { protected now: Date = new Date(); + protected slowUpdate: Date = new Date(); + private timer?: Subscription; + private slowUpdateTimer?: Subscription; + ngOnInit(): void { 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 { diff --git a/src/main/angular/src/app/pages/dashboard/electricity/dashboard-electricity-tile.component.ts b/src/main/angular/src/app/pages/dashboard/electricity/dashboard-electricity-tile.component.ts index a34c877..23ca998 100644 --- a/src/main/angular/src/app/pages/dashboard/electricity/dashboard-electricity-tile.component.ts +++ b/src/main/angular/src/app/pages/dashboard/electricity/dashboard-electricity-tile.component.ts @@ -1,8 +1,12 @@ import {Component, Input} from '@angular/core'; import {ValueListComponent} from "../../../shared/value-list/value-list.component"; -import {Display, DisplayValue} from "../../../api/Value/Display"; -import {ValueConstant} from "../../../api/Value/ValueConstant"; +import {Display, DisplayValue} from "../../../api/value/Display"; +import {ValueConstant} from "../../../api/value/ValueConstant"; 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; @@ -22,30 +26,74 @@ export class DashboardElectricityTileComponent { @Input() 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( - protected readonly seriesCacheService: SeriesCacheService, + private readonly seriesCacheService: SeriesCacheService, + private readonly sliceService: SliceService, ) { // - } getDisplayList(): Display[] { - const consumptionPower = this.seriesCacheService.photovoltaicPower.plus(this.seriesCacheService.gridPower).clampNonNegative(); const producedAfterChange = this.seriesCacheService.photovoltaicProduced.minus(PRODUCED_BEFORE_METER_CHANGE); const selfAfterChange = producedAfterChange.minus(this.seriesCacheService.gridDelivered); const selfRatio = selfAfterChange.div(producedAfterChange); const selfConsumed = selfRatio.mul(this.seriesCacheService.photovoltaicProduced); + const consumptionPower = this.seriesCacheService.photovoltaicPower.plus(this.seriesCacheService.gridPower).clampNonNegative(); + const gridColor = this.getGridPowerColor(); 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 [ + 'Zählerstände', new DisplayValue('Bezogen', this.seriesCacheService.gridPurchased, ''), new DisplayValue('Eingespeist', this.seriesCacheService.gridDelivered, ''), new DisplayValue('Produziert', this.seriesCacheService.photovoltaicProduced, ''), new DisplayValue('Selbst verbraucht', selfConsumed, ''), - null, + 'Leistung', new DisplayValue('Produktion', this.seriesCacheService.photovoltaicPower, productionColor), new DisplayValue('Netz', this.seriesCacheService.gridPower, gridColor), 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, ''), ]; } diff --git a/src/main/angular/src/app/pages/dashboard/heating/dashboard-heating-tile.component.ts b/src/main/angular/src/app/pages/dashboard/heating/dashboard-heating-tile.component.ts index b5c9789..b35bf41 100644 --- a/src/main/angular/src/app/pages/dashboard/heating/dashboard-heating-tile.component.ts +++ b/src/main/angular/src/app/pages/dashboard/heating/dashboard-heating-tile.component.ts @@ -1,6 +1,6 @@ import {Component, Input} from '@angular/core'; 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"; const WARM = 25; diff --git a/src/main/angular/src/app/shared/value-list/value-list.component.html b/src/main/angular/src/app/shared/value-list/value-list.component.html index 449c1a0..03074ec 100644 --- a/src/main/angular/src/app/shared/value-list/value-list.component.html +++ b/src/main/angular/src/app/shared/value-list/value-list.component.html @@ -4,17 +4,22 @@ {{ title }}
- - - + + + - + + + + diff --git a/src/main/angular/src/app/shared/value-list/value-list.component.less b/src/main/angular/src/app/shared/value-list/value-list.component.less index 5d49c83..def2db2 100644 --- a/src/main/angular/src/app/shared/value-list/value-list.component.less +++ b/src/main/angular/src/app/shared/value-list/value-list.component.less @@ -49,6 +49,13 @@ border-top: 1px solid #ddd; } + .header { + border-top: 1px solid #ddd; + text-align: center; + font-style: italic; + font-size: 80%; + } + } } diff --git a/src/main/angular/src/app/shared/value-list/value-list.component.ts b/src/main/angular/src/app/shared/value-list/value-list.component.ts index a8087e6..7d7b3ba 100644 --- a/src/main/angular/src/app/shared/value-list/value-list.component.ts +++ b/src/main/angular/src/app/shared/value-list/value-list.component.ts @@ -1,6 +1,6 @@ import {Component, Input} from '@angular/core'; import {DecimalPipe, NgForOf, NgIf} from "@angular/common"; -import {Display} from "../../api/Value/Display"; +import {Display, DisplayValue} from "../../api/value/Display"; @Component({ selector: 'app-value-list', @@ -51,7 +51,23 @@ export class ValueListComponent { maxAgeSeconds: number = 10; 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; } } diff --git a/src/main/java/de/ph87/data/series/SeriesIntervalKey.java b/src/main/java/de/ph87/data/series/SeriesIntervalKey.java index 680ffc9..9fc9a42 100644 --- a/src/main/java/de/ph87/data/series/SeriesIntervalKey.java +++ b/src/main/java/de/ph87/data/series/SeriesIntervalKey.java @@ -1,6 +1,6 @@ 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.Embeddable; import jakarta.persistence.ManyToOne; @@ -22,16 +22,16 @@ public class SeriesIntervalKey implements Serializable { @NonNull @Column(nullable = false, updatable = false, columnDefinition = "CHAR(1)") - private Unit unit; + private Interval interval; @NonNull @Column(nullable = false, updatable = false) 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.unit = unit; - this.aligned = unit.align(unaligned); + this.interval = interval; + this.aligned = interval.align(unaligned); } } diff --git a/src/main/java/de/ph87/data/series/SeriesMode.java b/src/main/java/de/ph87/data/series/SeriesMode.java index e82e1bd..d85bbf4 100644 --- a/src/main/java/de/ph87/data/series/SeriesMode.java +++ b/src/main/java/de/ph87/data/series/SeriesMode.java @@ -5,29 +5,44 @@ import lombok.NonNull; import java.util.function.BiFunction; public enum SeriesMode { - MEASURE((first, second) -> second - first, (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), + MEASURE( + (first, second) -> second - first, + (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 - private final BiFunction delta; + private final BiFunction amount; @NonNull - private final BiFunction add; + private final BiFunction plus; - SeriesMode(@NonNull final BiFunction delta, @NonNull final BiFunction add) { - this.delta = delta; - this.add = add; + SeriesMode( + @NonNull final BiFunction amount, + @NonNull final BiFunction plus + ) { + this.amount = amount; + this.plus = plus; } - public double getDelta(final double first, final double second) { - return delta.apply(first, second); + public double amount(final double first, final double second) { + return amount.apply(first, second); } public double update(final double series, final double value) { - return add.apply(series, value); + return plus.apply(series, value); } } diff --git a/src/main/java/de/ph87/data/series/SeriesService.java b/src/main/java/de/ph87/data/series/SeriesService.java index 0d04b64..e5e9aa5 100644 --- a/src/main/java/de/ph87/data/series/SeriesService.java +++ b/src/main/java/de/ph87/data/series/SeriesService.java @@ -4,8 +4,10 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; import java.time.ZonedDateTime; import java.util.List; @@ -37,4 +39,9 @@ public class SeriesService { 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)); + } + } diff --git a/src/main/java/de/ph87/data/series/consumption/Consumption.java b/src/main/java/de/ph87/data/series/consumption/Consumption.java index 5e29507..3e66dc8 100644 --- a/src/main/java/de/ph87/data/series/consumption/Consumption.java +++ b/src/main/java/de/ph87/data/series/consumption/Consumption.java @@ -1,14 +1,14 @@ package de.ph87.data.series.consumption; 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 lombok.*; import java.io.Serializable; import java.time.ZonedDateTime; -import static de.ph87.data.series.consumption.slice.SliceService.DL; +import static de.ph87.data.series.interval.IntervalHelper.DL; @Entity @Getter @@ -32,15 +32,13 @@ public class Consumption { } @ToString.Include - @SuppressWarnings("unused") // toString - public Unit unit() { - return id.unit; + public Interval interval() { + return id.interval; } @ToString.Include - @SuppressWarnings("unused") // toString public String aligned() { - return DL(id.unit, id.aligned); + return DL(id.interval, id.aligned); } @NonNull @@ -59,8 +57,8 @@ public class Consumption { @Column(nullable = false) private double lastValue; - public Consumption(@NonNull final Period period, @NonNull final Unit unit, @NonNull final ZonedDateTime aligned, @NonNull final ZonedDateTime date, final double value) { - this.id = new Id(period, unit, aligned); + 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, interval, aligned); this.firstDate = date; this.firstValue = value; this.lastDate = date; @@ -81,7 +79,7 @@ public class Consumption { @NonNull @Column(nullable = false, updatable = false, columnDefinition = "CHAR(1)") - private Unit unit; + private Interval interval; @NonNull @Column(nullable = false, updatable = false) diff --git a/src/main/java/de/ph87/data/series/consumption/ConsumptionController.java b/src/main/java/de/ph87/data/series/consumption/ConsumptionController.java deleted file mode 100644 index a95e497..0000000 --- a/src/main/java/de/ph87/data/series/consumption/ConsumptionController.java +++ /dev/null @@ -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> 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> 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> 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> 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 map(@NonNull final Slice slice) { - final ArrayList numbers = new ArrayList<>(); - numbers.add(slice.begin.toEpochSecond()); - numbers.add(Double.isNaN(slice.getDelta()) ? null : slice.getDelta()); - return numbers; - } - -} diff --git a/src/main/java/de/ph87/data/series/consumption/ConsumptionRepository.java b/src/main/java/de/ph87/data/series/consumption/ConsumptionRepository.java index 4b031de..00b85ce 100644 --- a/src/main/java/de/ph87/data/series/consumption/ConsumptionRepository.java +++ b/src/main/java/de/ph87/data/series/consumption/ConsumptionRepository.java @@ -1,22 +1,21 @@ package de.ph87.data.series.consumption; 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 org.springframework.data.repository.CrudRepository; import java.time.ZonedDateTime; -import java.util.List; import java.util.Optional; public interface ConsumptionRepository extends CrudRepository { - Optional findByIdPeriodAndIdUnitAndIdAligned(@NonNull Period period, @NonNull Unit unit, @NonNull ZonedDateTime aligned); + Optional findByIdPeriodAndIdIntervalAndIdAligned(@NonNull Period period, @NonNull Interval interval, @NonNull ZonedDateTime aligned); - Optional findFirstByIdPeriodAndIdUnitAndIdAlignedLessThanOrderByIdAlignedDesc(@NonNull Period period, @NonNull Unit unit, @NonNull ZonedDateTime begin); + Optional findFirstByIdPeriodAndIdIntervalAndIdAlignedLessThanOrderByIdAlignedDesc(@NonNull Period period, @NonNull Interval interval, @NonNull ZonedDateTime begin); - Optional findFirstByIdPeriodAndIdUnitAndIdAlignedGreaterThanOrderByIdAlignedAsc(@NonNull Period period, @NonNull Unit unit, @NonNull ZonedDateTime begin); + Optional findFirstByIdPeriodAndIdIntervalAndIdAlignedGreaterThanOrderByIdAlignedAsc(@NonNull Period period, @NonNull Interval interval, @NonNull ZonedDateTime begin); - List findAllByIdPeriodAndIdUnitAndIdAlignedGreaterThanEqualAndIdAlignedLessThanEqualOrderByIdAlignedAsc(@NonNull Period period, @NonNull Unit unit, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end); + Optional findFirstByIdPeriodAndIdIntervalAndIdAligned(@NonNull Period period, @NonNull Interval interval, @NonNull ZonedDateTime date); } diff --git a/src/main/java/de/ph87/data/series/consumption/ConsumptionService.java b/src/main/java/de/ph87/data/series/consumption/ConsumptionService.java index a34a9ff..3449459 100644 --- a/src/main/java/de/ph87/data/series/consumption/ConsumptionService.java +++ b/src/main/java/de/ph87/data/series/consumption/ConsumptionService.java @@ -5,7 +5,7 @@ import de.ph87.data.series.SeriesMode; import de.ph87.data.series.SeriesService; import de.ph87.data.series.consumption.period.Period; 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.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,16 +37,16 @@ public class ConsumptionService { period.setLastDate(event.getDate()); period.setLastValue(series.getMode().update(period.getLastValue(), event.getValue())); - for (final Unit unit : Unit.values()) { - final ZonedDateTime aligned = unit.align(event.getDate()); - final Optional existingOptional = consumptionRepository.findByIdPeriodAndIdUnitAndIdAligned(period, unit, aligned); + for (final Interval interval : Interval.values()) { + final ZonedDateTime aligned = interval.align(event.getDate()); + final Optional existingOptional = consumptionRepository.findByIdPeriodAndIdIntervalAndIdAligned(period, interval, aligned); if (existingOptional.isPresent()) { final Consumption existing = existingOptional.get(); existing.setLastDate(event.getDate()); existing.setLastValue(event.getValue()); log.debug("Existing Consumption updated: {}", existing); } 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); } } diff --git a/src/main/java/de/ph87/data/series/consumption/period/PeriodRepository.java b/src/main/java/de/ph87/data/series/consumption/period/PeriodRepository.java index 4563ef4..6f3894a 100644 --- a/src/main/java/de/ph87/data/series/consumption/period/PeriodRepository.java +++ b/src/main/java/de/ph87/data/series/consumption/period/PeriodRepository.java @@ -1,5 +1,6 @@ package de.ph87.data.series.consumption.period; +import de.ph87.data.series.Series; import lombok.NonNull; import org.springframework.data.repository.CrudRepository; @@ -8,6 +9,6 @@ import java.util.List; public interface PeriodRepository extends CrudRepository { - List findAllBySeriesIdAndLastDateGreaterThanAndFirstDateLessThan(long seriesId, @NonNull ZonedDateTime wantedEnd, @NonNull ZonedDateTime wantedBegin); + List findAllBySeriesAndLastDateGreaterThanAndFirstDateLessThan(@NonNull Series series, @NonNull ZonedDateTime wantedEnd, @NonNull ZonedDateTime wantedBegin); } diff --git a/src/main/java/de/ph87/data/series/consumption/slice/Slice.java b/src/main/java/de/ph87/data/series/consumption/slice/Slice.java deleted file mode 100644 index 7785a3f..0000000 --- a/src/main/java/de/ph87/data/series/consumption/slice/Slice.java +++ /dev/null @@ -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; - } - } - -} diff --git a/src/main/java/de/ph87/data/series/consumption/slice/SliceAligned.java b/src/main/java/de/ph87/data/series/consumption/slice/SliceAligned.java new file mode 100644 index 0000000..0387f05 --- /dev/null +++ b/src/main/java/de/ph87/data/series/consumption/slice/SliceAligned.java @@ -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; + } + +} diff --git a/src/main/java/de/ph87/data/series/consumption/slice/SliceController.java b/src/main/java/de/ph87/data/series/consumption/slice/SliceController.java new file mode 100644 index 0000000..5cd6eb2 --- /dev/null +++ b/src/main/java/de/ph87/data/series/consumption/slice/SliceController.java @@ -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); + } + +} diff --git a/src/main/java/de/ph87/data/series/consumption/slice/SliceDto.java b/src/main/java/de/ph87/data/series/consumption/slice/SliceDto.java new file mode 100644 index 0000000..653f2f1 --- /dev/null +++ b/src/main/java/de/ph87/data/series/consumption/slice/SliceDto.java @@ -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; + } + +} diff --git a/src/main/java/de/ph87/data/series/consumption/slice/SliceService.java b/src/main/java/de/ph87/data/series/consumption/slice/SliceService.java index 3b2e953..4e523c2 100644 --- a/src/main/java/de/ph87/data/series/consumption/slice/SliceService.java +++ b/src/main/java/de/ph87/data/series/consumption/slice/SliceService.java @@ -1,10 +1,13 @@ 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.ConsumptionRepository; import de.ph87.data.series.consumption.period.Period; 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 lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -14,11 +17,8 @@ import org.springframework.transaction.annotation.Transactional; import java.time.Duration; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.Objects; @Slf4j @Service @@ -26,166 +26,67 @@ import java.util.Optional; @RequiredArgsConstructor public class SliceService { - private final ConsumptionRepository consumptionRepository; - private final PeriodRepository periodRepository; - @NonNull - public List 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); + private final ConsumptionRepository consumptionRepository; - final ZonedDateTime wantedFirst = unit.align(begin); - final ZonedDateTime wantedLast = unit.align(end); - log.debug(" wantedFirst: {}", DL(unit, wantedFirst)); - log.debug(" wantedLast: {}", DL(unit, wantedLast)); + private final SeriesService seriesService; - final List periods = periodRepository.findAllBySeriesIdAndLastDateGreaterThanAndFirstDateLessThan(seriesId, wantedFirst, unit.plus(wantedLast, 1)); - log.debug(" periods: {}", periods.size()); - - final List totalSlices = new ArrayList<>(); - for (final Period period : periods) { - log.debug(" {}", period); - log.debug(" firstDate: {}", DL(unit, period.getFirstDate())); - log.debug(" lastDate: {}", DL(unit, period.getLastDate())); - - final List periodSlices = reslicePeriod(unit, period, wantedFirst, wantedLast); - print("periodSlices", periodSlices, 3); - periodSlices.forEach(merge -> merge(totalSlices, merge)); - - print("totalSlices", totalSlices, 3); - } - return totalSlices; - } - - private static void merge(@NonNull final List 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 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 sourceList = slicePeriod(period, unit, firstBegin, lastBegin); - final List 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 resultList) { - final Slice newWanted = new Slice(begin, unit); - resultList.add(newWanted); - return newWanted; + @Nullable + public SliceDto at(@NonNull final String seriesName, @NonNull final Interval interval, @NonNull final ZonedDateTime date) { + final ZonedDateTime sliceBegin = interval.align(date); + final Series series = seriesService.getByName(seriesName); + final List periods = periodRepository.findAllBySeriesAndLastDateGreaterThanAndFirstDateLessThan(series, sliceBegin, sliceBegin); + return periods.stream() + .map(period -> at(period, interval, sliceBegin)) + .filter(Objects::nonNull) + .reduce(SliceAligned::new) + .map(slice -> new SliceDto(slice, series.getUnit())) + .orElse(null); } @Nullable - private static Slice nextSourceIfNeeded(@NonNull final ZonedDateTime date, @Nullable final Slice source, @NonNull final List sourceList) { - if (source == null || !date.isBefore(source.end)) { - return sourceList.isEmpty() ? null : sourceList.remove(0); - } - return source; - } + private SliceAligned at(@NonNull final Period period, @NonNull final Interval interval, @NonNull final ZonedDateTime sliceBegin) { + final SeriesMode mode = period.getSeries().getMode(); - @NonNull - private static Slice nextResultIfNeeded(@NonNull final ZonedDateTime date, @NonNull final Slice result, @NonNull final List 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; - } + final ZonedDateTime sliceEnd = interval.plus(sliceBegin, 1); - @NonNull - private List slicePeriod(@NonNull final Period period, @NonNull final Unit unit, @NonNull final ZonedDateTime wantedFirst, @NonNull final ZonedDateTime wantedLast) { - final Optional 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 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 Optional 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 consumptions = consumptionRepository.findAllByIdPeriodAndIdUnitAndIdAlignedGreaterThanEqualAndIdAlignedLessThanEqualOrderByIdAlignedAsc(period, unit, firstToFetch.getId().getAligned(), lastToFetch.getId().getAligned()); - - print("consumptions", consumptions, 3); - Consumption last = null; - final List slices = new ArrayList<>(); - for (final Consumption consumption : consumptions) { - if (last != null) { - slices.add(new Slice(last, consumption)); + final double sliceAmount; + if (wanted == null) { + if (before == null || after == null) { + return null; } - if (!consumption.getFirstDate().equals(consumption.getLastDate())) { - slices.add(new Slice(consumption)); + 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(); } - 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); } - - 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())); + return new SliceAligned(sliceBegin, sliceAmount); } } diff --git a/src/main/java/de/ph87/data/series/consumption/unit/UnitJpaConverter.java b/src/main/java/de/ph87/data/series/consumption/unit/UnitJpaConverter.java deleted file mode 100644 index 48a9d20..0000000 --- a/src/main/java/de/ph87/data/series/consumption/unit/UnitJpaConverter.java +++ /dev/null @@ -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 { - - @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); - } - -} diff --git a/src/main/java/de/ph87/data/series/counter/CounterService.java b/src/main/java/de/ph87/data/series/counter/CounterService.java index 8ab8f4a..26728fb 100644 --- a/src/main/java/de/ph87/data/series/counter/CounterService.java +++ b/src/main/java/de/ph87/data/series/counter/CounterService.java @@ -4,7 +4,7 @@ import de.ph87.data.series.Series; import de.ph87.data.series.SeriesIntervalKey; import de.ph87.data.series.SeriesMode; 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.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,8 +27,8 @@ public class CounterService { log.debug("Handling CounterEvent: {}", event); final Series series = seriesService.getOrCreateByName(event.getName(), SeriesMode.COUNTER, event.getDate(), event.getCount(), event.getUnit()); - for (final Unit unit : Unit.values()) { - final SeriesIntervalKey id = new SeriesIntervalKey(series, unit, event.getDate()); + for (final Interval interval : Interval.values()) { + final SeriesIntervalKey id = new SeriesIntervalKey(series, interval, event.getDate()); counterRepository.findById(id) .stream() .peek(existing -> existing.setCount(existing.getCount() + event.getCount())) diff --git a/src/main/java/de/ph87/data/series/consumption/unit/Unit.java b/src/main/java/de/ph87/data/series/interval/Interval.java similarity index 86% rename from src/main/java/de/ph87/data/series/consumption/unit/Unit.java rename to src/main/java/de/ph87/data/series/interval/Interval.java index 6c29898..a7c6922 100644 --- a/src/main/java/de/ph87/data/series/consumption/unit/Unit.java +++ b/src/main/java/de/ph87/data/series/interval/Interval.java @@ -1,4 +1,4 @@ -package de.ph87.data.series.consumption.unit; +package de.ph87.data.series.interval; import lombok.NonNull; @@ -9,7 +9,7 @@ import java.time.temporal.ChronoUnit; import java.util.function.BiFunction; 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), 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()), @@ -30,7 +30,7 @@ public enum Unit { @NonNull private final BiFunction estimateCount; - Unit(@NonNull final String code, @NonNull final Function align, @NonNull final BiFunction offset, @NonNull final BiFunction estimateCount) { + Interval(@NonNull final String code, @NonNull final Function align, @NonNull final BiFunction offset, @NonNull final BiFunction estimateCount) { this.code = code; this.align = align; this.offset = offset; diff --git a/src/main/java/de/ph87/data/series/interval/IntervalHelper.java b/src/main/java/de/ph87/data/series/interval/IntervalHelper.java new file mode 100644 index 0000000..b7b5677 --- /dev/null +++ b/src/main/java/de/ph87/data/series/interval/IntervalHelper.java @@ -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")); + }; + } + +} diff --git a/src/main/java/de/ph87/data/series/interval/IntervalJpaConverter.java b/src/main/java/de/ph87/data/series/interval/IntervalJpaConverter.java new file mode 100644 index 0000000..c7d0f63 --- /dev/null +++ b/src/main/java/de/ph87/data/series/interval/IntervalJpaConverter.java @@ -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 { + + @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); + } + +} diff --git a/src/main/java/de/ph87/data/series/measure/MeasureService.java b/src/main/java/de/ph87/data/series/measure/MeasureService.java index 992a88a..c0166d8 100644 --- a/src/main/java/de/ph87/data/series/measure/MeasureService.java +++ b/src/main/java/de/ph87/data/series/measure/MeasureService.java @@ -4,7 +4,7 @@ import de.ph87.data.series.Series; import de.ph87.data.series.SeriesIntervalKey; import de.ph87.data.series.SeriesMode; 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.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,8 +27,8 @@ public class MeasureService { log.debug("Handling MeasureEvent: {}", event); final Series series = seriesService.getOrCreateByName(event.getName(), SeriesMode.MEASURE, event.getDate(), event.getValue(), event.getUnit()); - for (final Unit unit : Unit.values()) { - final SeriesIntervalKey id = new SeriesIntervalKey(series, unit, event.getDate()); + for (final Interval interval : Interval.values()) { + final SeriesIntervalKey id = new SeriesIntervalKey(series, interval, event.getDate()); measureRepository.findById(id) .stream() .peek(existing -> existing.update(event))
{{ display.title }}
{{ asDisplay(item)?.title }} - {{ display?.value?.value | number:'0.0-0' }} + {{ asDisplay(item)?.value?.value | number:'0.1-1' }} - {{ display?.value?.unit }} + {{ asDisplay(item)?.value?.unit }}
+ {{ asString(item) }} +