From 2ad140589c531d06a800fca7c3b71d46f537d5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Wed, 29 Oct 2025 10:13:49 +0100 Subject: [PATCH] new topic parsers: PatrixOpenDtu, PatrixSmartMeter, ShellyPlus1PM, TasmotaSmartMeter --- src/main/angular/src/app/location/Location.ts | 20 +- .../app/location/detail/location-detail.html | 10 +- .../src/app/location/location-service.ts | 25 +- .../src/app/series/select/series-select.html | 28 +- .../src/app/series/select/series-select.less | 29 ++ .../src/app/series/select/series-select.ts | 30 +- src/main/java/de/ph87/data/DemoService.java | 363 +----------------- .../java/de/ph87/data/location/Location.java | 18 +- .../data/location/LocationController.java | 42 +- .../de/ph87/data/location/LocationDto.java | 24 +- .../ph87/data/location/LocationService.java | 58 +-- .../de/ph87/data/plot/axis/AxisService.java | 7 +- .../de/ph87/data/plot/axis/graph/Graph.java | 7 +- .../data/plot/axis/graph/GraphController.java | 70 ++-- src/main/java/de/ph87/data/series/Series.java | 7 +- .../de/ph87/data/series/SeriesController.java | 2 +- .../de/ph87/data/series/SeriesService.java | 21 +- .../ph87/data/series/data/bool/BoolDto.java | 2 + .../data/series/data/bool/BoolService.java | 53 ++- .../ph87/data/series/data/delta/DeltaDto.java | 2 + .../data/series/data/delta/DeltaService.java | 32 +- .../data/series/data/delta/meter/Meter.java | 7 + .../series/data/delta/meter/MeterService.java | 28 -- .../data/series/data/varying/VaryingDto.java | 2 + .../series/data/varying/VaryingService.java | 12 +- .../data/series/point/SeriesPointService.java | 14 +- .../java/de/ph87/data/topic/ITopicParser.java | 10 + src/main/java/de/ph87/data/topic/Topic.java | 41 ++ .../de/ph87/data/topic/TopicController.java | 58 +++ .../java/de/ph87/data/topic/TopicDto.java | 24 ++ .../ph87/data/topic/TopicParserAbstract.java | 45 +++ .../de/ph87/data/topic/TopicReceiver.java | 61 ++- .../de/ph87/data/topic/TopicRepository.java | 8 +- .../java/de/ph87/data/topic/TopicService.java | 72 ++++ .../java/de/ph87/data/topic/TopicType.java | 23 ++ .../ph87/data/topic/parser/PatrixOpenDtu.java | 50 +++ .../data/topic/parser/PatrixSmartMeter.java | 60 +++ .../ph87/data/topic/parser/ShellyPlus1PM.java | 112 ++++++ .../data/topic/parser/TasmotaSmartMeter.java | 94 +++++ 39 files changed, 946 insertions(+), 625 deletions(-) delete mode 100644 src/main/java/de/ph87/data/series/data/delta/meter/MeterService.java create mode 100644 src/main/java/de/ph87/data/topic/ITopicParser.java create mode 100644 src/main/java/de/ph87/data/topic/TopicController.java create mode 100644 src/main/java/de/ph87/data/topic/TopicParserAbstract.java create mode 100644 src/main/java/de/ph87/data/topic/TopicService.java create mode 100644 src/main/java/de/ph87/data/topic/TopicType.java create mode 100644 src/main/java/de/ph87/data/topic/parser/PatrixOpenDtu.java create mode 100644 src/main/java/de/ph87/data/topic/parser/PatrixSmartMeter.java create mode 100644 src/main/java/de/ph87/data/topic/parser/ShellyPlus1PM.java create mode 100644 src/main/java/de/ph87/data/topic/parser/TasmotaSmartMeter.java diff --git a/src/main/angular/src/app/location/Location.ts b/src/main/angular/src/app/location/Location.ts index 11f5e46..5070ef9 100644 --- a/src/main/angular/src/app/location/Location.ts +++ b/src/main/angular/src/app/location/Location.ts @@ -8,10 +8,12 @@ export class Location { readonly name: string, readonly latitude: number, readonly longitude: number, - readonly purchase: Series | null, - readonly delivery: Series | null, - readonly produce: Series | null, - readonly power: Series | null, + readonly energyPurchase: Series | null, + readonly energyDeliver: Series | null, + readonly energyProduce: Series | null, + readonly powerPurchase: Series | null, + readonly powerDeliver: Series | null, + readonly powerProduce: Series | null, ) { // } @@ -22,10 +24,12 @@ export class Location { validateString(json.name), validateNumber(json.latitude), validateNumber(json.longitude), - or(json.purchase, Series.fromJson, null), - or(json.delivery, Series.fromJson, null), - or(json.produce, Series.fromJson, null), - or(json.power, Series.fromJson, null), + or(json.energyPurchase, Series.fromJson, null), + or(json.energyDeliver, Series.fromJson, null), + or(json.energyProduce, Series.fromJson, null), + or(json.powerPurchase, Series.fromJson, null), + or(json.powerDeliver, Series.fromJson, null), + or(json.powerProduce, Series.fromJson, null), ); } diff --git a/src/main/angular/src/app/location/detail/location-detail.html b/src/main/angular/src/app/location/detail/location-detail.html index ad8c0d8..fc0a571 100644 --- a/src/main/angular/src/app/location/detail/location-detail.html +++ b/src/main/angular/src/app/location/detail/location-detail.html @@ -2,8 +2,10 @@ - - - - + + + + + + } diff --git a/src/main/angular/src/app/location/location-service.ts b/src/main/angular/src/app/location/location-service.ts index 2f40049..9269e55 100644 --- a/src/main/angular/src/app/location/location-service.ts +++ b/src/main/angular/src/app/location/location-service.ts @@ -1,7 +1,6 @@ import {Injectable} from '@angular/core'; import {ApiService, CrudService, Next, WebsocketService} from '../common'; import {Location} from './Location' -import {Series} from '../series/Series'; @Injectable({ providedIn: 'root' @@ -32,20 +31,28 @@ export class LocationService extends CrudService { this.postSingle([location.id, 'longitude'], longitude, next); } - purchase(location: Location, purchase: Series | null, next?: Next) { - this.postSingle([location.id, 'purchase'], purchase?.id, next); + energyPurchase(location: Location, seriesId: number | null, next?: Next) { + this.postSingle([location.id, 'energyPurchase'], seriesId, next); } - delivery(location: Location, delivery: Series | null, next?: Next) { - this.postSingle([location.id, 'delivery'], delivery?.id, next); + energyDeliver(location: Location, seriesId: number | null, next?: Next) { + this.postSingle([location.id, 'energyDeliver'], seriesId, next); } - produce(location: Location, produce: Series | null, next?: Next) { - this.postSingle([location.id, 'produce'], produce?.id, next); + energyProduce(location: Location, seriesId: number | null, next?: Next) { + this.postSingle([location.id, 'energyProduce'], seriesId, next); } - power(location: Location, power: Series | null, next?: Next) { - this.postSingle([location.id, 'power'], power?.id, next); + powerPurchase(location: Location, seriesId: number | null, next?: Next) { + this.postSingle([location.id, 'powerPurchase'], seriesId, next); + } + + powerDeliver(location: Location, seriesId: number | null, next?: Next) { + this.postSingle([location.id, 'powerDeliver'], seriesId, next); + } + + powerProduce(location: Location, seriesId: number | null, next?: Next) { + this.postSingle([location.id, 'powerProduce'], seriesId, next); } } diff --git a/src/main/angular/src/app/series/select/series-select.html b/src/main/angular/src/app/series/select/series-select.html index f4985d1..592c64a 100644 --- a/src/main/angular/src/app/series/select/series-select.html +++ b/src/main/angular/src/app/series/select/series-select.html @@ -4,24 +4,18 @@ (mouseenter)="showPen = true" (mouseleave)="showPen = false" > -
- @if (editing) { - - } @else { - {{ initial?.name || ' ' }} + + @if (showPen) { }
diff --git a/src/main/angular/src/app/series/select/series-select.less b/src/main/angular/src/app/series/select/series-select.less index e69de29..5546c96 100644 --- a/src/main/angular/src/app/series/select/series-select.less +++ b/src/main/angular/src/app/series/select/series-select.less @@ -0,0 +1,29 @@ +.container { + display: flex; + + .value { + flex: 1; + } + +} + +.container:hover { + background-color: #0002; +} + +select { + all: unset; + width: 100%; +} + +.unchanged { + background-color: lightgreen !important; +} + +.invalid { + background-color: red !important; +} + +.changed { + background-color: yellow !important; +} diff --git a/src/main/angular/src/app/series/select/series-select.ts b/src/main/angular/src/app/series/select/series-select.ts index 61af75b..147d1c8 100644 --- a/src/main/angular/src/app/series/select/series-select.ts +++ b/src/main/angular/src/app/series/select/series-select.ts @@ -4,6 +4,7 @@ import {FormsModule} from '@angular/forms'; import {NgClass} from '@angular/common'; import {faPen} from '@fortawesome/free-solid-svg-icons'; import {Series} from '../Series'; +import {or} from '../../common'; export enum SeriesType { BOOL = 'BOOL', @@ -35,47 +36,36 @@ export class SeriesSelect { list: Series[] = []; @Output() - readonly onChange = new EventEmitter(); + readonly onChange = new EventEmitter(); protected showPen: boolean = false; - protected model: Series | null = null; - - protected editing: boolean = false; + protected model: number | null = null; @Input() set initial(value: Series | null) { this._initial = value; - if (!this.editing) { - this.model = this.initial; - } + this.reset(); + } + + private reset() { + this.model = or(this.initial, i => i.id, null); } get initial(): Series | null { return this._initial; } - protected start() { - this.editing = true; - setTimeout(() => this.input.nativeElement.focus(), 0); - } - protected apply() { if (this.model !== this.initial) { this.onChange.emit(this.model); } - this.editing = false; - } - - protected cancel() { - this.model = this.initial; - this.editing = false; } protected classes(): {} { return { - "unchanged": this.editing && this.model === this.initial, - "changed": this.model !== this.initial, + "unchanged": this.model === this.initial, + "changed": this.model !== or(this.initial, i => i.id, null), "invalid": !this.allowEmpty && this.model === null, }; } diff --git a/src/main/java/de/ph87/data/DemoService.java b/src/main/java/de/ph87/data/DemoService.java index 3ac1e88..50842e4 100644 --- a/src/main/java/de/ph87/data/DemoService.java +++ b/src/main/java/de/ph87/data/DemoService.java @@ -1,22 +1,8 @@ package de.ph87.data; -import de.ph87.data.location.Location; -import de.ph87.data.location.LocationRepository; -import de.ph87.data.plot.Plot; -import de.ph87.data.plot.PlotRepository; -import de.ph87.data.plot.axis.Axis; -import de.ph87.data.plot.axis.AxisRepository; -import de.ph87.data.plot.axis.graph.Graph; -import de.ph87.data.plot.axis.graph.GraphRepository; -import de.ph87.data.plot.axis.graph.GraphType; -import de.ph87.data.series.Series; -import de.ph87.data.series.SeriesRepository; -import de.ph87.data.series.SeriesType; -import de.ph87.data.topic.TimestampType; -import de.ph87.data.topic.Topic; -import de.ph87.data.topic.TopicRepository; -import de.ph87.data.topic.query.TopicQuery; -import lombok.NonNull; +import de.ph87.data.topic.TopicDto; +import de.ph87.data.topic.TopicType; +import de.ph87.data.topic.TopicService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -24,26 +10,14 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor public class DemoService { - private final SeriesRepository seriesRepository; - - private final TopicRepository topicRepository; - - private final PlotRepository plotRepository; - - private final AxisRepository axisRepository; - - private final GraphRepository graphRepository; - private final DemoConfig demoConfig; - private final LocationRepository locationRepository; + private final TopicService topicService; @Transactional @EventListener(ApplicationReadyEvent.class) @@ -51,331 +25,12 @@ public class DemoService { if (!demoConfig.isEnabled()) { return; } - location("Eppelborn", 49.4086, 6.9645); - location("Friedrichsthal", 49.3270, 7.0947); - topics(); - } - - private void location(@NonNull final String name, final double latitude, final double longitude) { - locationRepository.findByName(name).orElseGet(() -> { - final Location created = new Location(); - created.setName(name); - created.setLatitude(latitude); - created.setLongitude(longitude); - return locationRepository.save(created); + final TopicDto dto = topicService.create(); + topicService.modify(dto.id, topic -> { + topic.setEnabled(true); + topic.setName("openDTU/pv/patrix/json2"); + topic.setType(TopicType.PatrixOpenDtu); }); } - private void topics() { - final Series fallbackRelay0 = series("fallback/relay0", "", SeriesType.BOOL, 5); - topic( - "fallback/relay0", - "$.timestamp", - new TopicQuery(fallbackRelay0, "$.state", "$.stateEpoch", "true") - ); - - final Series infraredHeater = series("infraredHeater/state", "", SeriesType.BOOL, 5); - topic( - "Infrarotheizung", - "now", - new TopicQuery(infraredHeater, "$.state", "timestamp", "true") - ); - - final Series electricityEnergyProduce = series("electricity/energy/produce", "kWh", SeriesType.DELTA, 5); - final Series electricityPowerProduce = series("electricity/power/produce", "W", SeriesType.VARYING, 5); - topicMeterNumber( - "openDTU/pv/patrix/json2", - TimestampType.EPOCH_SECONDS, - "$.timestamp", - "$.inverter", - new TopicQuery(electricityEnergyProduce, "$.totalKWh"), - new TopicQuery(electricityPowerProduce, "$.totalW") - ); - - final Series electricityEnergyPurchase = series("electricity/energy/purchase", "kWh", SeriesType.DELTA, 5); - final Series electricityPowerPurchase = series("electricity/power/purchase", "W", SeriesType.VARYING, 5); - final Series electricityEnergyDelivery = series("electricity/energy/delivery", "kWh", SeriesType.DELTA, 5); - final Series electricityPowerDelivery = series("electricity/power/delivery", "W", SeriesType.VARYING, 5); - topicMeterNumber( - "electricity/grid/json", - TimestampType.EPOCH_SECONDS, - "$.timestamp", - "\"1ZPA0020300305\"", - new TopicQuery(electricityEnergyPurchase, "$.purchaseWh", 0.001), - new TopicQuery(electricityPowerPurchase, "$.powerW", 1, TopicQuery.Function.ONLY_POSITIVE), - new TopicQuery(electricityEnergyDelivery, "$.deliveryWh", 0.001), - new TopicQuery(electricityPowerDelivery, "$.powerW", 1, TopicQuery.Function.ONLY_NEGATIVE_BUT_NEGATE) - ); - - final Series elternElectricityEnergyProduce = series("eltern/electricity/energy/produce", "kWh", SeriesType.DELTA, 60); - final Series elternElectricityPowerProduce = series("eltern/electricity/power/produce", "W", SeriesType.VARYING, 60); - topicMeterNumber( - "Eltern/Solar/Shelly/status/switch:0", - TimestampType.EPOCH_SECONDS, - "$.aenergy.minute_ts", - "\"2025-10-27-shelly\"", - new TopicQuery(elternElectricityEnergyProduce, "$.aenergy.total", 0.001), - new TopicQuery(elternElectricityPowerProduce, "$.apower") - ); - - final Series elternElectricityEnergyPurchase = series("eltern/electricity/energy/purchase", "kWh", SeriesType.DELTA, 10); - final Series elternElectricityPowerPurchase = series("eltern/electricity/power/purchase", "W", SeriesType.VARYING, 10); - final Series elternElectricityEnergyDelivery = series("eltern/electricity/energy/delivery", "kWh", SeriesType.DELTA, 10); - final Series elternElectricityPowerDelivery = series("eltern/electricity/power/delivery", "W", SeriesType.VARYING, 10); - topicMeterNumber( - "Eltern/SmartMeter/SENSOR", - TimestampType.ISO_LOCAL_DATE_TIME, - "$.Time", - "sml_meter_number_raw:$.meter.number", - new TopicQuery(elternElectricityEnergyPurchase, "$.meter.energy_purchased_kwh"), - new TopicQuery(elternElectricityPowerPurchase, "$.meter.power_w", 1, TopicQuery.Function.ONLY_POSITIVE), - new TopicQuery(elternElectricityEnergyDelivery, "$.meter.energy_delivered_kwh"), - new TopicQuery(elternElectricityPowerDelivery, "$.meter.power_w", 1, TopicQuery.Function.ONLY_NEGATIVE_BUT_NEGATE) - ); - - final Series gardenPressure = series("garden/pressure", "hPa", SeriesType.VARYING, 5); - final Series gardenTemperature = series("garden/temperature", "°C", SeriesType.VARYING, 5); - final Series gardenHumidityAbsolute = series("garden/humidity/absolute", "mg/L", SeriesType.VARYING, 5); - final Series gardenHumidityRelative = series("garden/humidity/relative", "%", SeriesType.VARYING, 5); - topic("garten/sensor/pressure", new TopicQuery(gardenPressure, "$.value")); - topic("garten/sensor/temperature", new TopicQuery(gardenTemperature, "$.value")); - topic("garten/sensor/humidity_absolute", new TopicQuery(gardenHumidityAbsolute, "$.value")); - topic("garten/sensor/humidity_relative", new TopicQuery(gardenHumidityRelative, "$.value")); - - final Series bedroomPressure = series("bedroom/pressure", "hPa", SeriesType.VARYING, 5); - final Series bedroomTemperature = series("bedroom/temperature", "°C", SeriesType.VARYING, 5); - final Series bedroomHumidityAbsolute = series("bedroom/humidity/absolute", "mg/L", SeriesType.VARYING, 5); - final Series bedroomHumidityRelative = series("bedroom/humidity/relative", "%", SeriesType.VARYING, 5); - topic("schlafzimmer/sensor/pressure", new TopicQuery(bedroomPressure, "$.value")); - topic("schlafzimmer/sensor/temperature", new TopicQuery(bedroomTemperature, "$.value")); - topic("schlafzimmer/sensor/humidity_absolute", new TopicQuery(bedroomHumidityAbsolute, "$.value")); - topic("schlafzimmer/sensor/humidity_relative", new TopicQuery(bedroomHumidityRelative, "$.value")); - - final Series basementTemperature = series("basement/temperature", "°C", SeriesType.VARYING, 60); - final Series basementHumidityAbsolute = series("basement/humidity/absolute", "mg/L", SeriesType.VARYING, 60); - final Series basementHumidityRelative = series("basement/humidity/relative", "%", SeriesType.VARYING, 60); - topic("aggregation/heizraum/luftfeuchte/absolut", "$.lastTime", new TopicQuery(basementTemperature, "$.lastValue")); - topic("aggregation/heizraum/luftfeuchte/relativ", "$.lastTime", new TopicQuery(basementHumidityAbsolute, "$.lastValue")); - topic("aggregation/heizraum/temperatur", "$.lastTime", new TopicQuery(basementHumidityRelative, "$.lastValue")); - - final Series heatingExhaustTemperature = series("heating/exhaust/temperature", "°C", SeriesType.VARYING, 60); - topic("aggregation/heizung/abgas/temperatur", "$.lastTime", new TopicQuery(heatingExhaustTemperature, "$.lastValue")); - - final Series heatingCircuitReturnTemperature = series("heating/circuit/return/temperature", "°C", SeriesType.VARYING, 60); - final Series heatingCircuitSupplyTemperature = series("heating/circuit/supply/temperature", "°C", SeriesType.VARYING, 60); - topic("aggregation/heizung/heizkreis/ruecklauf/temperatur", "$.lastTime", new TopicQuery(heatingCircuitReturnTemperature, "$.lastValue")); - topic("aggregation/heizung/heizkreis/vorlauf/temperatur", "$.lastTime", new TopicQuery(heatingCircuitSupplyTemperature, "$.lastValue")); - - final Series heatingBufferInletTemperature = series("heating/buffer/inlet/temperature", "°C", SeriesType.VARYING, 60); - final Series heatingBufferOutletTemperature = series("heating/buffer/outlet/temperature", "°C", SeriesType.VARYING, 60); - final Series heatingBufferCirculationTemperature = series("heating/buffer/circulation/temperature", "°C", SeriesType.VARYING, 60); - topic("aggregation/heizung/puffer/eingang/temperatur", "$.lastTime", new TopicQuery(heatingBufferInletTemperature, "$.lastValue")); - topic("aggregation/heizung/puffer/ausgang/temperatur", "$.lastTime", new TopicQuery(heatingBufferOutletTemperature, "$.lastValue")); - topic("aggregation/heizung/puffer/zirkulation/temperatur", "$.lastTime", new TopicQuery(heatingBufferCirculationTemperature, "$.lastValue")); - - final Series heatingBufferInsideTemperature = series("heating/buffer/inside/temperature", "°C", SeriesType.VARYING, 60); - topic("aggregation/heizung/puffer/speicher/temperatur", "$.lastTime", new TopicQuery(heatingBufferInsideTemperature, "$.lastValue")); - - final Series heatingBufferSupplyTemperature = series("heating/buffer/supply/temperature", "°C", SeriesType.VARYING, 60); - final Series heatingBufferReturnTemperature = series("heating/buffer/return/temperature", "°C", SeriesType.VARYING, 60); - topic("aggregation/heizung/puffer/vorlauf/temperatur", "$.lastTime", new TopicQuery(heatingBufferSupplyTemperature, "$.lastValue")); - topic("aggregation/heizung/puffer/ruecklauf/temperatur", "$.lastTime", new TopicQuery(heatingBufferReturnTemperature, "$.lastValue")); - - final Series cisternVolume = series("cistern/volume", "L", SeriesType.VARYING, 5); - topic("cistern/volume/PatrixJson", "$.date", new TopicQuery(cisternVolume, "$.value")); - - plotRepository.deleteAll(); - zuhauseEnergie(); - zuhauseTemperatur(); - eltern(); - leistungVergleich(); - } - - private void zuhauseEnergie() { - final Plot plot = plotRepository.save(new Plot(plotRepository.count())); - plot.setName("Zuhause Energie"); - plot.setDashboard(true); - - final Axis energy = axisRepository.save(new Axis(plot)); - plot.addAxis(energy); - energy.setRight(true); - energy.setName("Energie"); - energy.setUnit("kWh"); - - final String stack = "a"; - - final Series electricityEnergyDelivery = seriesRepository.findByName("electricity/energy/delivery").orElseThrow(); - final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery)); - electricityEnergyDeliveryGraph.setType(GraphType.BAR); - electricityEnergyDeliveryGraph.setStack(stack); - electricityEnergyDeliveryGraph.setName("Zuhause Überschuss"); - electricityEnergyDeliveryGraph.setColor("#FF00FF"); - electricityEnergyDeliveryGraph.setFactor(-1); - energy.addGraph(electricityEnergyDeliveryGraph); - - final Series electricityEnergyProduce = seriesRepository.findByName("electricity/energy/produce").orElseThrow(); - final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce)); - electricityEnergyProduceGraph.setSeries2(electricityEnergyDelivery); - electricityEnergyProduceGraph.setName("Zuhause Eigenverbrauch"); - electricityEnergyProduceGraph.setType(GraphType.BAR); - electricityEnergyProduceGraph.setStack(stack); - electricityEnergyProduceGraph.setColor("#008800"); - energy.addGraph(electricityEnergyProduceGraph); - - final Series electricityEnergyPurchase = seriesRepository.findByName("electricity/energy/purchase").orElseThrow(); - final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase)); - electricityEnergyPurchaseGraph.setType(GraphType.BAR); - electricityEnergyPurchaseGraph.setStack(stack); - electricityEnergyPurchaseGraph.setName("Zuhause Bezug"); - electricityEnergyPurchaseGraph.setColor("#FF8800"); - energy.addGraph(electricityEnergyPurchaseGraph); - } - - private void zuhauseTemperatur() { - final Plot plot = plotRepository.save(new Plot(plotRepository.count())); - plot.setName("Zuhause Temperaturen"); - plot.setDashboard(true); - - final Axis temperature = axisRepository.save(new Axis(plot)); - plot.addAxis(temperature); - temperature.setRight(true); - temperature.setMin(0.0); - temperature.setName("Temperatur"); - temperature.setUnit("°C"); - - final Series bedroomTemperature = seriesRepository.findByName("bedroom/temperature").orElseThrow(); - final Graph bedroomTemperatureGraph = graphRepository.save(new Graph(temperature, bedroomTemperature)); - bedroomTemperatureGraph.setName("Schlafzimmer"); - bedroomTemperatureGraph.setColor("#0000FF"); - bedroomTemperatureGraph.setMin(true); - bedroomTemperatureGraph.setAvg(false); - bedroomTemperatureGraph.setMax(true); - temperature.addGraph(bedroomTemperatureGraph); - - final Series gardenTemperature = seriesRepository.findByName("garden/temperature").orElseThrow(); - final Graph gardenTemperatureGraph = graphRepository.save(new Graph(temperature, gardenTemperature)); - gardenTemperatureGraph.setName("Garten"); - gardenTemperatureGraph.setColor("#00FF00"); - gardenTemperatureGraph.setMin(true); - gardenTemperatureGraph.setAvg(false); - gardenTemperatureGraph.setMax(true); - temperature.addGraph(gardenTemperatureGraph); - - final Series bufferTemperature = seriesRepository.findByName("heating/buffer/inside/temperature").orElseThrow(); - final Graph bufferTemperatureGraph = graphRepository.save(new Graph(temperature, bufferTemperature)); - bufferTemperatureGraph.setName("Puffer"); - bufferTemperatureGraph.setColor("#FF00FF"); - bufferTemperatureGraph.setMin(true); - bufferTemperatureGraph.setAvg(false); - bufferTemperatureGraph.setMax(true); - temperature.addGraph(bufferTemperatureGraph); - - final Series circuitTemperature = seriesRepository.findByName("heating/circuit/supply/temperature").orElseThrow(); - final Graph circuitTemperatureGraph = graphRepository.save(new Graph(temperature, circuitTemperature)); - circuitTemperatureGraph.setName("Heizkreis"); - circuitTemperatureGraph.setColor("#FF0000"); - circuitTemperatureGraph.setMin(true); - circuitTemperatureGraph.setAvg(false); - circuitTemperatureGraph.setMax(true); - temperature.addGraph(circuitTemperatureGraph); - } - - private void eltern() { - final Plot plot = plotRepository.save(new Plot(plotRepository.count())); - plot.setName("Eltern Energie"); - - final Axis energy = axisRepository.save(new Axis(plot)); - plot.addAxis(energy); - plot.setDashboard(true); - energy.setRight(true); - energy.setName("Energie"); - energy.setUnit("kWh"); - - final Series electricityEnergyDelivery = seriesRepository.findByName("eltern/electricity/energy/delivery").orElseThrow(); - final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery)); - electricityEnergyDeliveryGraph.setName("Überschuss"); - electricityEnergyDeliveryGraph.setType(GraphType.BAR); - electricityEnergyDeliveryGraph.setFactor(-1); - electricityEnergyDeliveryGraph.setStack("a"); - electricityEnergyDeliveryGraph.setColor("#FF00FF"); - energy.addGraph(electricityEnergyDeliveryGraph); - - final Series electricityEnergyProduce = seriesRepository.findByName("eltern/electricity/energy/produce").orElseThrow(); - final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce)); - electricityEnergyProduceGraph.setSeries2(electricityEnergyDelivery); - electricityEnergyProduceGraph.setName("Eigenverbrauch"); - electricityEnergyProduceGraph.setType(GraphType.BAR); - electricityEnergyProduceGraph.setStack("a"); - electricityEnergyProduceGraph.setColor("#008800"); - energy.addGraph(electricityEnergyProduceGraph); - - final Series electricityEnergyPurchase = seriesRepository.findByName("eltern/electricity/energy/purchase").orElseThrow(); - final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase)); - electricityEnergyPurchaseGraph.setName("Bezug"); - electricityEnergyPurchaseGraph.setType(GraphType.BAR); - electricityEnergyPurchaseGraph.setStack("a"); - electricityEnergyPurchaseGraph.setColor("#FF8800"); - energy.addGraph(electricityEnergyPurchaseGraph); - } - - private void leistungVergleich() { - final Plot plot = plotRepository.save(new Plot(plotRepository.count())); - plot.setName("Leistung Vergleich"); - - final Axis power = axisRepository.save(new Axis(plot)); - plot.addAxis(power); - plot.setDashboard(true); - power.setRight(true); - power.setName("Leistung"); - power.setUnit("W"); - - final Series electricityPowerProduce = seriesRepository.findByName("electricity/power/produce").orElseThrow(); - final Graph electricityPowerProduceGraph = graphRepository.save(new Graph(power, electricityPowerProduce)); - electricityPowerProduceGraph.setName("Zuhause"); - electricityPowerProduceGraph.setType(GraphType.BAR); - electricityPowerProduceGraph.setColor("#008800"); - power.addGraph(electricityPowerProduceGraph); - - final Series elternElectricityPowerProduce = seriesRepository.findByName("eltern/electricity/power/produce").orElseThrow(); - final Graph elternElectricityPowerProduceGraph = graphRepository.save(new Graph(power, elternElectricityPowerProduce)); - elternElectricityPowerProduceGraph.setName("Eltern"); - elternElectricityPowerProduceGraph.setType(GraphType.BAR); - elternElectricityPowerProduceGraph.setColor("#0088FF"); - power.addGraph(elternElectricityPowerProduceGraph); - } - - @NonNull - private Series series(@NonNull final String name, @NonNull final String unit, @NonNull final SeriesType type, final int seconds) { - return seriesRepository - .findByName(name) - .stream() - .peek(existing -> { - existing.setUnit(unit); - existing.setType(type); - existing.setSeconds(seconds); - }) - .findFirst() - .orElseGet(() -> seriesRepository.save(new Series(name, unit, 1, seconds, type))); - } - - private void topic(@NonNull final String name, @NonNull final TopicQuery... queries) { - topic(name, "$.timestamp", queries); - } - - private void topic(@NonNull final String name, @NonNull final String timestampQuery, @NonNull final TopicQuery... queries) { - final Topic topic = topicRepository.findByName(name).orElseGet(() -> topicRepository.save(new Topic(name))); - topic.setTimestampQuery(timestampQuery); - topic.getQueries().clear(); - topic.getQueries().addAll(List.of(queries)); - } - - private void topicMeterNumber(@NonNull final String name, @NonNull final TimestampType timestampType, @NonNull final String timestampQuery, @NonNull final String meterNumberQuery, @NonNull final TopicQuery... queries) { - final Topic topic = topicRepository.findByName(name).orElseGet(() -> topicRepository.save(new Topic(name))); - topic.setMeterNumberQuery(meterNumberQuery); - topic.setTimestampType(timestampType); - topic.setTimestampQuery(timestampQuery); - topic.getQueries().clear(); - topic.getQueries().addAll(List.of(queries)); - } - } diff --git a/src/main/java/de/ph87/data/location/Location.java b/src/main/java/de/ph87/data/location/Location.java index 4de284f..515f903 100644 --- a/src/main/java/de/ph87/data/location/Location.java +++ b/src/main/java/de/ph87/data/location/Location.java @@ -44,21 +44,31 @@ public class Location { @Setter @Nullable @ManyToOne - private Series purchase; + private Series energyPurchase; @Setter @Nullable @ManyToOne - private Series delivery; + private Series energyDeliver; @Setter @Nullable @ManyToOne - private Series produce; + private Series energyProduce; @Setter @Nullable @ManyToOne - private Series power; + private Series powerPurchase; + + @Setter + @Nullable + @ManyToOne + private Series powerDeliver; + + @Setter + @Nullable + @ManyToOne + private Series powerProduce; } diff --git a/src/main/java/de/ph87/data/location/LocationController.java b/src/main/java/de/ph87/data/location/LocationController.java index 84df576..91ea3e1 100644 --- a/src/main/java/de/ph87/data/location/LocationController.java +++ b/src/main/java/de/ph87/data/location/LocationController.java @@ -39,42 +39,52 @@ public class LocationController { @GetMapping("{id}/delete") public LocationDto delete(@PathVariable final long id) { - return locationService.delete(id); + return locationService.set(id, locationRepository::delete); } @PostMapping("{id}/name") public LocationDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String name) { - return locationService.name(id, name == null ? "" : name); + return locationService.set(id, location -> location.setName(name == null ? "" : name)); } @PostMapping("{id}/latitude") public LocationDto latitude(@PathVariable final long id, @RequestBody final double latitude) { - return locationService.latitude(id, latitude); + return locationService.set(id, location -> location.setLatitude(latitude)); } @PostMapping("{id}/longitude") public LocationDto longitude(@PathVariable final long id, @RequestBody final double longitude) { - return locationService.longitude(id, longitude); + return locationService.set(id, location -> location.setLongitude(longitude)); } - @PostMapping("{id}/purchase") - public LocationDto purchase(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { - return locationService.purchase(id, seriesId); + @PostMapping("{id}/energyPurchase") + public LocationDto energyPurchase(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { + return locationService.setSeries(id, seriesId, Location::setEnergyPurchase); } - @PostMapping("{id}/delivery") - public LocationDto delivery(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { - return locationService.delivery(id, seriesId); + @PostMapping("{id}/energyDeliver") + public LocationDto energyDeliver(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { + return locationService.setSeries(id, seriesId, Location::setEnergyDeliver); } - @PostMapping("{id}/produce") - public LocationDto produce(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { - return locationService.produce(id, seriesId); + @PostMapping("{id}/energyProduce") + public LocationDto energyProduce(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { + return locationService.setSeries(id, seriesId, Location::setEnergyProduce); } - @PostMapping("{id}/power") - public LocationDto power(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { - return locationService.power(id, seriesId); + @PostMapping("{id}/powerPurchase") + public LocationDto powerPurchase(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { + return locationService.setSeries(id, seriesId, Location::setPowerPurchase); + } + + @PostMapping("{id}/powerDeliver") + public LocationDto powerDeliver(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { + return locationService.setSeries(id, seriesId, Location::setPowerDeliver); + } + + @PostMapping("{id}/powerProduce") + public LocationDto powerProduce(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) { + return locationService.setSeries(id, seriesId, Location::setPowerProduce); } } diff --git a/src/main/java/de/ph87/data/location/LocationDto.java b/src/main/java/de/ph87/data/location/LocationDto.java index 9ca60b2..9d06cdc 100644 --- a/src/main/java/de/ph87/data/location/LocationDto.java +++ b/src/main/java/de/ph87/data/location/LocationDto.java @@ -21,16 +21,22 @@ public class LocationDto { public final double longitude; @Nullable - public final SeriesDto purchase; + public final SeriesDto energyPurchase; @Nullable - public final SeriesDto delivery; + public final SeriesDto energyDeliver; @Nullable - public final SeriesDto produce; + public final SeriesDto energyProduce; @Nullable - public final SeriesDto power; + public final SeriesDto powerPurchase; + + @Nullable + public final SeriesDto powerDeliver; + + @Nullable + public final SeriesDto powerProduce; public LocationDto(@NonNull final Location location) { this.id = location.getId(); @@ -38,10 +44,12 @@ public class LocationDto { this.name = location.getName(); this.latitude = location.getLatitude(); this.longitude = location.getLongitude(); - this.purchase = map(location.getPurchase(), SeriesDto::new); - this.delivery = map(location.getDelivery(), SeriesDto::new); - this.produce = map(location.getProduce(), SeriesDto::new); - this.power = map(location.getPower(), SeriesDto::new); + this.energyPurchase = map(location.getEnergyPurchase(), SeriesDto::new); + this.energyDeliver = map(location.getEnergyDeliver(), SeriesDto::new); + this.energyProduce = map(location.getEnergyProduce(), SeriesDto::new); + this.powerPurchase = map(location.getPowerPurchase(), SeriesDto::new); + this.powerDeliver = map(location.getPowerDeliver(), SeriesDto::new); + this.powerProduce = map(location.getPowerProduce(), SeriesDto::new); } } diff --git a/src/main/java/de/ph87/data/location/LocationService.java b/src/main/java/de/ph87/data/location/LocationService.java index 0fb560b..5cb2d6b 100644 --- a/src/main/java/de/ph87/data/location/LocationService.java +++ b/src/main/java/de/ph87/data/location/LocationService.java @@ -1,7 +1,7 @@ package de.ph87.data.location; import de.ph87.data.series.Series; -import de.ph87.data.series.SeriesRepository; +import de.ph87.data.series.SeriesService; import jakarta.annotation.Nullable; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -21,7 +21,7 @@ public class LocationService { private final LocationRepository locationRepository; - private final SeriesRepository seriesRepository; + private final SeriesService seriesService; @NonNull @Transactional @@ -31,60 +31,14 @@ public class LocationService { @NonNull @Transactional - public LocationDto delete(final long id) { - return _set(id, locationRepository::delete); + public LocationDto setSeries(final long id, @Nullable final Long seriesId, @NonNull final BiConsumer setter) { + final Series series = seriesId == null ? null : seriesService.getById(seriesId); + return set(id, location -> setter.accept(location, series)); } @NonNull @Transactional - public LocationDto name(final long id, @NonNull final String name) { - return _set(id, location -> location.setName(name)); - } - - @NonNull - @Transactional - public LocationDto latitude(final long id, final double latitude) { - return _set(id, location -> location.setLatitude(latitude)); - } - - @NonNull - @Transactional - public LocationDto longitude(final long id, final double longitude) { - return _set(id, location -> location.setLongitude(longitude)); - } - - @NonNull - @Transactional - public LocationDto purchase(final long id, final Long seriesId) { - return _setSeries(id, seriesId, Location::setPurchase); - } - - @NonNull - @Transactional - public LocationDto delivery(final long id, final Long seriesId) { - return _setSeries(id, seriesId, Location::setDelivery); - } - - @NonNull - @Transactional - public LocationDto produce(final long id, final Long seriesId) { - return _setSeries(id, seriesId, Location::setProduce); - } - - @NonNull - @Transactional - public LocationDto power(final long id, final Long seriesId) { - return _setSeries(id, seriesId, Location::setPower); - } - - @NonNull - private LocationDto _setSeries(final long id, @Nullable final Long seriesId, @NonNull final BiConsumer setter) { - final Series series = seriesId == null ? null : seriesRepository.findById(seriesId).orElseThrow(notFound(Series.class, "id", seriesId)); - return _set(id, location -> setter.accept(location, series)); - } - - @NonNull - private LocationDto _set(final long id, @NonNull final Consumer setter) { + public LocationDto set(final long id, @NonNull final Consumer setter) { final Location location = locationRepository.findById(id).orElseThrow(notFound(Location.class, "id", id)); setter.accept(location); return new LocationDto(location); diff --git a/src/main/java/de/ph87/data/plot/axis/AxisService.java b/src/main/java/de/ph87/data/plot/axis/AxisService.java index 40cf438..b8a961c 100644 --- a/src/main/java/de/ph87/data/plot/axis/AxisService.java +++ b/src/main/java/de/ph87/data/plot/axis/AxisService.java @@ -4,8 +4,6 @@ import de.ph87.data.plot.PlotDto; import de.ph87.data.plot.PlotService; import de.ph87.data.plot.axis.graph.Graph; import de.ph87.data.plot.axis.graph.GraphRepository; -import de.ph87.data.series.Series; -import de.ph87.data.series.SeriesRepository; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,8 +21,6 @@ public class AxisService { private final GraphRepository graphRepository; - private final SeriesRepository seriesRepository; - private final PlotService plotService; @NonNull @@ -40,8 +36,7 @@ public class AxisService { @NonNull @Transactional public PlotDto addGraph(final long axisId) { - final Series series = seriesRepository.findFirstByOrderByNameAsc().orElseThrow(); - return set(axisId, axis -> axis.addGraph(graphRepository.save(new Graph(axis, series)))); + return set(axisId, axis -> axis.addGraph(graphRepository.save(new Graph(axis)))); } @NonNull diff --git a/src/main/java/de/ph87/data/plot/axis/graph/Graph.java b/src/main/java/de/ph87/data/plot/axis/graph/Graph.java index d0fc775..06eb24a 100644 --- a/src/main/java/de/ph87/data/plot/axis/graph/Graph.java +++ b/src/main/java/de/ph87/data/plot/axis/graph/Graph.java @@ -38,8 +38,8 @@ public class Graph { private Axis axis; @Setter - @NonNull - @ManyToOne(optional = false) + @Nullable + @ManyToOne private Series series; @Setter @@ -108,9 +108,8 @@ public class Graph { @Column(nullable = false) private boolean avg = true; - public Graph(@NonNull final Axis axis, @NonNull final Series series) { + public Graph(@NonNull final Axis axis) { this.axis = axis; - this.series = series; } public Graph(@NonNull final Axis axis, @NonNull final Graph graph) { diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java index e8cb18c..382f955 100644 --- a/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java @@ -2,7 +2,7 @@ package de.ph87.data.plot.axis.graph; import de.ph87.data.plot.PlotDto; import de.ph87.data.plot.axis.AxisRepository; -import de.ph87.data.series.SeriesRepository; +import de.ph87.data.series.SeriesService; import jakarta.annotation.Nullable; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -26,10 +26,10 @@ public class GraphController { private final GraphService graphService; - private final SeriesRepository seriesRepository; - private final AxisRepository axisRepository; + private final SeriesService seriesService; + @NonNull @GetMapping("{id}/delete") public PlotDto delete(@PathVariable final long id) { @@ -37,86 +37,86 @@ public class GraphController { } @PostMapping("{id}/visible") - public PlotDto visible(@PathVariable final long id, @RequestBody final boolean value) { - return graphService.set(id, graph -> graph.setVisible(value)); + public PlotDto visible(@PathVariable final long id, @RequestBody final boolean visible) { + return graphService.set(id, graph -> graph.setVisible(visible)); } @PostMapping("{id}/type") - public PlotDto type(@PathVariable final long id, @RequestBody @NonNull final String value) { - return graphService.set(id, graph -> graph.setType(GraphType.valueOf(value))); + public PlotDto type(@PathVariable final long id, @RequestBody @NonNull final String typeName) { + return graphService.set(id, graph -> graph.setType(GraphType.valueOf(typeName))); } @PostMapping("{id}/name") - public PlotDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) { - return graphService.set(id, graph -> graph.setName(or(value, ""))); + public PlotDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String name) { + return graphService.set(id, graph -> graph.setName(or(name, ""))); } @PostMapping("{id}/series") - public PlotDto series(@PathVariable final long id, @RequestBody final long value) { - return graphService.set(id, graph -> graph.setSeries(seriesRepository.findById(value).orElseThrow())); + public PlotDto series(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long series) { + return graphService.set(id, graph -> graph.setSeries(series == null ? null : seriesService.getById(series))); } @PostMapping("{id}/factor") - public PlotDto factor(@PathVariable final long id, @RequestBody final double value) { - return graphService.set(id, graph -> graph.setFactor(value)); + public PlotDto factor(@PathVariable final long id, @RequestBody final double factor) { + return graphService.set(id, graph -> graph.setFactor(factor)); } @PostMapping("{id}/operation") - public PlotDto operation(@PathVariable final long id, @RequestBody @NonNull final String value) { - return graphService.set(id, graph -> graph.setOperation(GraphOperation.valueOf(value))); + public PlotDto operation(@PathVariable final long id, @RequestBody @NonNull final String operationName) { + return graphService.set(id, graph -> graph.setOperation(GraphOperation.valueOf(operationName))); } @PostMapping("{id}/series2") - public PlotDto series2(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long value) { - return graphService.set(id, graph -> graph.setSeries2(value == null ? null : seriesRepository.findById(value).orElseThrow())); + public PlotDto series2(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long series2) { + return graphService.set(id, graph -> graph.setSeries2(series2 == null ? null : seriesService.getById(series2))); } @PostMapping("{id}/factor2") - public PlotDto factor2(@PathVariable final long id, @RequestBody final double value) { - return graphService.set(id, graph -> graph.setFactor2(value)); + public PlotDto factor2(@PathVariable final long id, @RequestBody final double factor2) { + return graphService.set(id, graph -> graph.setFactor2(factor2)); } @PostMapping("{id}/color") - public PlotDto color(@PathVariable final long id, @RequestBody @NonNull final String value) { - return graphService.set(id, graph -> graph.setColor(value)); + public PlotDto color(@PathVariable final long id, @RequestBody @NonNull final String color) { + return graphService.set(id, graph -> graph.setColor(color)); } @PostMapping("{id}/group") - public PlotDto group(@PathVariable final long id, @RequestBody @NonNull final String value) { - return graphService.set(id, graph -> graph.setGroup(Group.valueOf(value))); + public PlotDto group(@PathVariable final long id, @RequestBody @NonNull final String group) { + return graphService.set(id, graph -> graph.setGroup(Group.valueOf(group))); } @PostMapping("{id}/stack") - public PlotDto stack(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) { - return graphService.set(id, graph -> graph.setStack(or(value, ""))); + public PlotDto stack(@PathVariable final long id, @RequestBody(required = false) @Nullable final String stack) { + return graphService.set(id, graph -> graph.setStack(or(stack, ""))); } @PostMapping("{id}/min") - public PlotDto min(@PathVariable final long id, @RequestBody final boolean value) { - return graphService.set(id, graph -> graph.setMin(value)); + public PlotDto min(@PathVariable final long id, @RequestBody final boolean min) { + return graphService.set(id, graph -> graph.setMin(min)); } @PostMapping("{id}/max") - public PlotDto max(@PathVariable final long id, @RequestBody final boolean value) { - return graphService.set(id, graph -> graph.setMax(value)); + public PlotDto max(@PathVariable final long id, @RequestBody final boolean max) { + return graphService.set(id, graph -> graph.setMax(max)); } @PostMapping("{id}/avg") - public PlotDto avg(@PathVariable final long id, @RequestBody final boolean value) { - return graphService.set(id, graph -> graph.setAvg(value)); + public PlotDto avg(@PathVariable final long id, @RequestBody final boolean avg) { + return graphService.set(id, graph -> graph.setAvg(avg)); } @PostMapping("{id}/axis") - public PlotDto axis(@PathVariable final long id, @RequestBody final long value) { - return graphService.set(id, graph -> graph.setAxis(axisRepository.findById(value).orElseThrow())); + public PlotDto axis(@PathVariable final long id, @RequestBody final long axis) { + return graphService.set(id, graph -> graph.setAxis(axisRepository.findById(axis).orElseThrow())); } @PostMapping("{id}/position") - public PlotDto position(@PathVariable final long id, @RequestBody final int value) { + public PlotDto position(@PathVariable final long id, @RequestBody final int position) { return graphService.set(id, graph -> { final List list = graph.getAxis().getGraphs(); list.remove(graph); - list.add(Math.max(0, Math.min(value, list.size())), graph); + list.add(Math.max(0, Math.min(position, list.size())), graph); }); } diff --git a/src/main/java/de/ph87/data/series/Series.java b/src/main/java/de/ph87/data/series/Series.java index ebd35a8..d44ff4a 100644 --- a/src/main/java/de/ph87/data/series/Series.java +++ b/src/main/java/de/ph87/data/series/Series.java @@ -64,7 +64,7 @@ public class Series { @NonNull @Column(nullable = false) @Enumerated(EnumType.STRING) - private SeriesType type; + private SeriesType type = SeriesType.VARYING; public Series(@NonNull final String name, @NonNull final String unit, final int decimals, final int seconds, @NonNull final SeriesType type) { this.name = name; @@ -85,4 +85,9 @@ public class Series { } } + @NonNull + public String toShortString() { + return "#%d \"%s\"".formatted(id, name); + } + } diff --git a/src/main/java/de/ph87/data/series/SeriesController.java b/src/main/java/de/ph87/data/series/SeriesController.java index be0a207..f4dbe3f 100644 --- a/src/main/java/de/ph87/data/series/SeriesController.java +++ b/src/main/java/de/ph87/data/series/SeriesController.java @@ -29,7 +29,7 @@ public class SeriesController { private final SeriesPointService seriesPointService; - @PostMapping("create") + @GetMapping("create") public SeriesDto create() { return seriesService.create(); } diff --git a/src/main/java/de/ph87/data/series/SeriesService.java b/src/main/java/de/ph87/data/series/SeriesService.java index 8d8a508..316465c 100644 --- a/src/main/java/de/ph87/data/series/SeriesService.java +++ b/src/main/java/de/ph87/data/series/SeriesService.java @@ -7,6 +7,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.ZonedDateTime; +import java.util.List; import java.util.function.Consumer; import static de.ph87.data.location.NotFoundException.notFound; @@ -29,7 +31,7 @@ public class SeriesService { private String _generateUniqueName() { int index = 0; while (true) { - final String name = "series" + index; + final String name = "series/unnamed/" + index++; if (!seriesRepository.existsByName(name)) { return name; } @@ -38,14 +40,19 @@ public class SeriesService { @NonNull @Transactional - SeriesDto modify(final long id, @NonNull Consumer modifier) { + public SeriesDto modify(final long id, @NonNull Consumer modifier) { final Series series = getById(id); modifier.accept(series); return publish(series, CrudAction.MODIFIED); } @NonNull - private Series getById(final long id) { + public List findAll() { + return seriesRepository.findAll(); + } + + @NonNull + public Series getById(final long id) { return seriesRepository.findById(id).orElseThrow(notFound(Series.class, "id", id)); } @@ -56,4 +63,12 @@ public class SeriesService { return dto; } + @NonNull + @Transactional + public Series update(final long seriesId, @NonNull final ZonedDateTime date, final double value) { + final Series series = getById(seriesId); + series.update(date, value); + return series; + } + } diff --git a/src/main/java/de/ph87/data/series/data/bool/BoolDto.java b/src/main/java/de/ph87/data/series/data/bool/BoolDto.java index 192a805..069fa6a 100644 --- a/src/main/java/de/ph87/data/series/data/bool/BoolDto.java +++ b/src/main/java/de/ph87/data/series/data/bool/BoolDto.java @@ -1,5 +1,6 @@ package de.ph87.data.series.data.bool; +import com.fasterxml.jackson.annotation.JsonIgnore; import de.ph87.data.series.SeriesDto; import de.ph87.data.websocket.IWebsocketMessage; import lombok.Data; @@ -33,6 +34,7 @@ public class BoolDto implements IWebsocketMessage { @NonNull @Override + @JsonIgnore public String getWebsocketTopic() { return "Bool/%d".formatted(series.id); } diff --git a/src/main/java/de/ph87/data/series/data/bool/BoolService.java b/src/main/java/de/ph87/data/series/data/bool/BoolService.java index c7885f3..d49d2fa 100644 --- a/src/main/java/de/ph87/data/series/data/bool/BoolService.java +++ b/src/main/java/de/ph87/data/series/data/bool/BoolService.java @@ -1,8 +1,9 @@ package de.ph87.data.series.data.bool; -import de.ph87.data.series.point.ISeriesPointRequest; import de.ph87.data.series.Series; +import de.ph87.data.series.SeriesService; import de.ph87.data.series.data.DataId; +import de.ph87.data.series.point.ISeriesPointRequest; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,12 +19,16 @@ import java.util.List; @RequiredArgsConstructor public class BoolService { - private final BoolRepo boolRepo; - private final ApplicationEventPublisher applicationEventPublisher; + private final SeriesService seriesService; + + private final BoolRepo boolRepo; + @Transactional - public void write(@NonNull final Series series, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end, final boolean state, final boolean terminated) { + @SuppressWarnings("unused") + public void write(@NonNull final long seriesId, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end, final boolean state, final boolean terminated) { + final Series series = seriesService.update(seriesId, end, state ? 1 : 0); final Bool bool = updateOrCreate(series, begin, end, state, terminated); log.debug("Bool written: {}", bool); applicationEventPublisher.publishEvent(new BoolDto(bool)); @@ -32,30 +37,22 @@ public class BoolService { @NonNull private Bool updateOrCreate(@NonNull final Series series, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end, final boolean state, final boolean terminated) { final DataId id = new DataId(series, begin); - return boolRepo - .findById(id) - .stream() - .peek( - existing -> { - if (existing.isState() != state) { - log.error("Differing states: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing); - return; - } - if (existing.getEnd().isAfter(end)) { - log.error("End ran backwards: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing); - return; - } - if (existing.isTerminated() && (!terminated || !existing.getEnd().equals(end))) { - log.error("Already terminated: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing); - return; - } - existing.setEnd(end); - existing.setTerminated(terminated); - }) - .findFirst() - .orElseGet( - () -> boolRepo.save(new Bool(id, end, state, terminated)) - ); + return boolRepo.findById(id).stream().peek(existing -> { + if (existing.isState() != state) { + log.error("Differing states: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing); + return; + } + if (existing.getEnd().isAfter(end)) { + log.error("End ran backwards: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing); + return; + } + if (existing.isTerminated() && (!terminated || !existing.getEnd().equals(end))) { + log.error("Already terminated: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing); + return; + } + existing.setEnd(end); + existing.setTerminated(terminated); + }).findFirst().orElseGet(() -> boolRepo.save(new Bool(id, end, state, terminated))); } @NonNull diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaDto.java b/src/main/java/de/ph87/data/series/data/delta/DeltaDto.java index 05f3627..c34dd0e 100644 --- a/src/main/java/de/ph87/data/series/data/delta/DeltaDto.java +++ b/src/main/java/de/ph87/data/series/data/delta/DeltaDto.java @@ -1,5 +1,6 @@ package de.ph87.data.series.data.delta; +import com.fasterxml.jackson.annotation.JsonIgnore; import de.ph87.data.series.data.Interval; import de.ph87.data.series.data.delta.meter.MeterDto; import de.ph87.data.websocket.IWebsocketMessage; @@ -36,6 +37,7 @@ public abstract class DeltaDto implements IWebsocketMessage { @NonNull @Override + @JsonIgnore public String getWebsocketTopic() { return "Delta/%d/%s".formatted(meter.series.id, interval); } diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaService.java b/src/main/java/de/ph87/data/series/data/delta/DeltaService.java index 52d4b39..02fc31a 100644 --- a/src/main/java/de/ph87/data/series/data/delta/DeltaService.java +++ b/src/main/java/de/ph87/data/series/data/delta/DeltaService.java @@ -1,10 +1,11 @@ package de.ph87.data.series.data.delta; -import de.ph87.data.series.point.ISeriesPointRequest; import de.ph87.data.series.Series; +import de.ph87.data.series.SeriesService; import de.ph87.data.series.data.Interval; import de.ph87.data.series.data.delta.meter.Meter; -import de.ph87.data.series.data.delta.meter.MeterService; +import de.ph87.data.series.data.delta.meter.MeterRepository; +import de.ph87.data.series.point.ISeriesPointRequest; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -21,6 +22,12 @@ import java.util.function.BiFunction; @RequiredArgsConstructor public class DeltaService { + private final ApplicationEventPublisher applicationEventPublisher; + + private final SeriesService seriesService; + + private final MeterRepository meterRepository; + private final DeltaRepoFive five; private final DeltaRepoHour hour; @@ -33,13 +40,10 @@ public class DeltaService { private final DeltaRepoYear year; - private final ApplicationEventPublisher applicationEventPublisher; - - private final MeterService meterService; - @Transactional - public void write(@NonNull final Series series, @NonNull final String meterNumber, @NonNull final ZonedDateTime date, final double value) { - final Meter meter = meterService.getLastValidBySeriesAndNumberOrCreate(series, meterNumber, date); + public void write(final long seriesId, @NonNull final String meterNumber, @NonNull final ZonedDateTime date, final double value) { + final Series series = seriesService.update(seriesId, date, value); + final Meter meter = getOrCreateMeter(series, meterNumber, date); write(meter, five, Interval.FIVE, date, value, Delta.Five::new, DeltaDto.Five::new); write(meter, hour, Interval.HOUR, date, value, Delta.Hour::new, DeltaDto.Hour::new); write(meter, day, Interval.DAY, date, value, Delta.Day::new, DeltaDto.Day::new); @@ -55,6 +59,18 @@ public class DeltaService { applicationEventPublisher.publishEvent(toDto.apply(delta, interval)); } + @NonNull + private Meter getOrCreateMeter(@NonNull final Series series, @NonNull final String number, @NonNull final ZonedDateTime date) { + return meterRepository + .findFirstBySeriesOrderByFirstDesc(series) + .filter(meter -> meter.getNumber().equals(number)) + .orElseGet(() -> { + final Meter created = meterRepository.save(new Meter(series, number, date)); + log.info("Meter created: {}", created); + return created; + }); + } + @NonNull public List points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { return switch (request.getInterval()) { diff --git a/src/main/java/de/ph87/data/series/data/delta/meter/Meter.java b/src/main/java/de/ph87/data/series/data/delta/meter/Meter.java index 64e5ab9..0445266 100644 --- a/src/main/java/de/ph87/data/series/data/delta/meter/Meter.java +++ b/src/main/java/de/ph87/data/series/data/delta/meter/Meter.java @@ -29,6 +29,7 @@ public class Meter { private long version; @NonNull + @ToString.Exclude @ManyToOne(optional = false) private Series series; @@ -36,6 +37,12 @@ public class Meter { @Column(nullable = false) private String number; + @NonNull + @ToString.Include + public String series() { + return series.toShortString(); + } + @NonNull @Column(nullable = false) private ZonedDateTime first; diff --git a/src/main/java/de/ph87/data/series/data/delta/meter/MeterService.java b/src/main/java/de/ph87/data/series/data/delta/meter/MeterService.java deleted file mode 100644 index 4c49e15..0000000 --- a/src/main/java/de/ph87/data/series/data/delta/meter/MeterService.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.ph87.data.series.data.delta.meter; - -import de.ph87.data.series.Series; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.ZonedDateTime; - -@Slf4j -@Service -@RequiredArgsConstructor -public class MeterService { - - private final MeterRepository meterRepository; - - @NonNull - @Transactional - public Meter getLastValidBySeriesAndNumberOrCreate(@NonNull final Series series, @NonNull final String number, @NonNull final ZonedDateTime date) { - return meterRepository - .findFirstBySeriesOrderByFirstDesc(series) - .filter(meter -> meter.getNumber().equals(number)) - .orElseGet(() -> meterRepository.save(new Meter(series, number, date))); - } - -} diff --git a/src/main/java/de/ph87/data/series/data/varying/VaryingDto.java b/src/main/java/de/ph87/data/series/data/varying/VaryingDto.java index 1017cbf..aa600d0 100644 --- a/src/main/java/de/ph87/data/series/data/varying/VaryingDto.java +++ b/src/main/java/de/ph87/data/series/data/varying/VaryingDto.java @@ -1,5 +1,6 @@ package de.ph87.data.series.data.varying; +import com.fasterxml.jackson.annotation.JsonIgnore; import de.ph87.data.series.SeriesDto; import de.ph87.data.series.data.Interval; import de.ph87.data.websocket.IWebsocketMessage; @@ -42,6 +43,7 @@ public abstract class VaryingDto implements IWebsocketMessage { @NonNull @Override + @JsonIgnore public String getWebsocketTopic() { return "Varying/%d/%s".formatted(series.id, interval); } diff --git a/src/main/java/de/ph87/data/series/data/varying/VaryingService.java b/src/main/java/de/ph87/data/series/data/varying/VaryingService.java index 72ab50b..2751685 100644 --- a/src/main/java/de/ph87/data/series/data/varying/VaryingService.java +++ b/src/main/java/de/ph87/data/series/data/varying/VaryingService.java @@ -1,9 +1,10 @@ package de.ph87.data.series.data.varying; -import de.ph87.data.series.point.ISeriesPointRequest; import de.ph87.data.series.Series; +import de.ph87.data.series.SeriesService; import de.ph87.data.series.data.DataId; import de.ph87.data.series.data.Interval; +import de.ph87.data.series.point.ISeriesPointRequest; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,6 +21,10 @@ import java.util.function.BiFunction; @RequiredArgsConstructor public class VaryingService { + private final ApplicationEventPublisher applicationEventPublisher; + + private final SeriesService seriesService; + private final VaryingRepoFive five; private final VaryingRepoHour hour; @@ -32,10 +37,9 @@ public class VaryingService { private final VaryingRepoYear year; - private final ApplicationEventPublisher applicationEventPublisher; - @Transactional - public void write(@NonNull final Series series, @NonNull final ZonedDateTime date, final double value) { + public void write(@NonNull final long seriesId, @NonNull final ZonedDateTime date, final double value) { + final Series series = seriesService.update(seriesId, date, value); write(series, five, Interval.FIVE, date, value, Varying.Five::new, VaryingDto.Five::new); write(series, hour, Interval.HOUR, date, value, Varying.Hour::new, VaryingDto.Hour::new); write(series, day, Interval.DAY, date, value, Varying.Day::new, VaryingDto.Day::new); diff --git a/src/main/java/de/ph87/data/series/point/SeriesPointService.java b/src/main/java/de/ph87/data/series/point/SeriesPointService.java index 2d3311d..3eb86c8 100644 --- a/src/main/java/de/ph87/data/series/point/SeriesPointService.java +++ b/src/main/java/de/ph87/data/series/point/SeriesPointService.java @@ -2,7 +2,7 @@ package de.ph87.data.series.point; import de.ph87.data.series.Series; import de.ph87.data.series.SeriesDto; -import de.ph87.data.series.SeriesRepository; +import de.ph87.data.series.SeriesService; import de.ph87.data.series.data.bool.BoolService; import de.ph87.data.series.data.delta.DeltaService; import de.ph87.data.series.data.varying.VaryingService; @@ -10,9 +10,7 @@ import jakarta.annotation.Nullable; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; import java.util.List; @@ -21,20 +19,20 @@ import java.util.List; @RequiredArgsConstructor public class SeriesPointService { - private final SeriesRepository seriesRepository; - private final BoolService boolService; private final DeltaService deltaService; private final VaryingService varyingService; + private final SeriesService seriesService; + @NonNull public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) { - final Series series1 = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + final Series series1 = seriesService.getById(request.id); final List> points1 = getSeriesPoints(series1, request, request.factor); if (request.id2 != null) { - final Series series2 = seriesRepository.findById(request.id2).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + final Series series2 = seriesService.getById(request.id2); final List> points2 = getSeriesPoints(series2, request, request.factor2); return new OneSeriesPointsResponse(SeriesPoint.combine(points1, points2, request.operation)); } @@ -43,7 +41,7 @@ public class SeriesPointService { @NonNull public AllSeriesPointResponse allSeriesPoint(@NonNull final AllSeriesPointRequest request) { - final List seriesPoints = seriesRepository.findAll().stream().map(series -> map(series, request)).toList(); + final List seriesPoints = seriesService.findAll().stream().map(series -> map(series, request)).toList(); return new AllSeriesPointResponse(request, seriesPoints); } diff --git a/src/main/java/de/ph87/data/topic/ITopicParser.java b/src/main/java/de/ph87/data/topic/ITopicParser.java new file mode 100644 index 0000000..9da9f62 --- /dev/null +++ b/src/main/java/de/ph87/data/topic/ITopicParser.java @@ -0,0 +1,10 @@ +package de.ph87.data.topic; + +import de.ph87.data.mqtt.MqttMessage; +import lombok.NonNull; + +public interface ITopicParser { + + void handle(@NonNull final TopicDto topic, @NonNull final MqttMessage message); + +} diff --git a/src/main/java/de/ph87/data/topic/Topic.java b/src/main/java/de/ph87/data/topic/Topic.java index 0b6bd5e..21d2f7d 100644 --- a/src/main/java/de/ph87/data/topic/Topic.java +++ b/src/main/java/de/ph87/data/topic/Topic.java @@ -1,14 +1,20 @@ package de.ph87.data.topic; +import de.ph87.data.series.Series; +import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import jakarta.persistence.Version; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; +import lombok.Setter; import lombok.ToString; @Entity @@ -24,8 +30,43 @@ public class Topic { @Version private long version; + @Setter @NonNull @Column(nullable = false, unique = true) private String name; + @Setter + @Column(nullable = false) + private boolean enabled; + + @Setter + @NonNull + @Enumerated(EnumType.STRING) + @Column(nullable = false, columnDefinition = "varchar(100)") + private TopicType type = TopicType.PatrixOpenDtu; + + @Setter + @Nullable + @ManyToOne + private Series series0; + + @Setter + @Nullable + @ManyToOne + private Series series1; + + @Setter + @Nullable + @ManyToOne + private Series series2; + + @Setter + @Nullable + @ManyToOne + private Series series3; + + public Topic(@NonNull final String name) { + this.name = name; + } + } diff --git a/src/main/java/de/ph87/data/topic/TopicController.java b/src/main/java/de/ph87/data/topic/TopicController.java new file mode 100644 index 0000000..46cb4d8 --- /dev/null +++ b/src/main/java/de/ph87/data/topic/TopicController.java @@ -0,0 +1,58 @@ +package de.ph87.data.topic; + +import jakarta.annotation.Nullable; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@CrossOrigin +@RestController +@RequiredArgsConstructor +@RequestMapping("Topic") +public class TopicController { + + private final TopicService topicService; + + @GetMapping("create") + public TopicDto create() { + return topicService.create(); + } + + @PostMapping("{id}/name") + public TopicDto name(@PathVariable final long id, @RequestBody @NonNull final String name) { + return topicService.modify(id, topic -> topic.setName(name)); + } + + @PostMapping("{id}/enabled") + public TopicDto enabled(@PathVariable final long id, @RequestBody @NonNull final boolean enabled) { + return topicService.modify(id, topic -> topic.setEnabled(enabled)); + } + + @PostMapping("{id}/type") + public TopicDto type(@PathVariable final long id, @RequestBody @NonNull final String typeName) { + final TopicType type = TopicType.valueOf(typeName); + return topicService.modify(id, topic -> topic.setType(type)); + } + + @PostMapping("{id}/series0") + public TopicDto series0(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long series0) { + return topicService.modifySeries(id, series0, Topic::setSeries0); + } + + @PostMapping("{id}/series1") + public TopicDto series1(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long series1) { + return topicService.modifySeries(id, series1, Topic::setSeries1); + } + + @PostMapping("{id}/series2") + public TopicDto series2(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long series2) { + return topicService.modifySeries(id, series2, Topic::setSeries2); + } + +} diff --git a/src/main/java/de/ph87/data/topic/TopicDto.java b/src/main/java/de/ph87/data/topic/TopicDto.java index 2c233cd..f6e73c4 100644 --- a/src/main/java/de/ph87/data/topic/TopicDto.java +++ b/src/main/java/de/ph87/data/topic/TopicDto.java @@ -1,9 +1,13 @@ package de.ph87.data.topic; +import de.ph87.data.series.SeriesDto; import de.ph87.data.websocket.IWebsocketMessage; +import jakarta.annotation.Nullable; import lombok.Data; import lombok.NonNull; +import static de.ph87.data.Helpers.map; + @Data public class TopicDto implements IWebsocketMessage { @@ -12,9 +16,29 @@ public class TopicDto implements IWebsocketMessage { @NonNull public final String name; + @NonNull + public final TopicType type; + + @Nullable + public final SeriesDto series0; + + @Nullable + public final SeriesDto series1; + + @Nullable + public final SeriesDto series2; + + @Nullable + public final SeriesDto series3; + public TopicDto(@NonNull final Topic topic) { this.id = topic.getId(); this.name = topic.getName(); + this.type = topic.getType(); + this.series0 = map(topic.getSeries0(), SeriesDto::new); + this.series1 = map(topic.getSeries1(), SeriesDto::new); + this.series2 = map(topic.getSeries2(), SeriesDto::new); + this.series3 = map(topic.getSeries3(), SeriesDto::new); } } diff --git a/src/main/java/de/ph87/data/topic/TopicParserAbstract.java b/src/main/java/de/ph87/data/topic/TopicParserAbstract.java new file mode 100644 index 0000000..5b1a16b --- /dev/null +++ b/src/main/java/de/ph87/data/topic/TopicParserAbstract.java @@ -0,0 +1,45 @@ +package de.ph87.data.topic; + +import de.ph87.data.mqtt.MqttMessage; +import de.ph87.data.series.SeriesDto; +import de.ph87.data.series.data.delta.DeltaService; +import de.ph87.data.series.data.varying.VaryingService; +import jakarta.annotation.Nullable; +import lombok.Data; +import lombok.NonNull; +import tools.jackson.databind.ObjectMapper; + +import java.time.ZonedDateTime; + +@Data +public abstract class TopicParserAbstract implements ITopicParser { + + protected final Class clazz; + + protected final ObjectMapper mapper; + + protected final DeltaService deltaService; + + protected final VaryingService varyingService; + + @Override + public void handle(@NonNull final TopicDto topic, @NonNull final MqttMessage message) { + final INBOUND inbound = mapper.readValue(message.getPayload(), clazz); + handle2(topic, inbound); + } + + protected abstract void handle2(@NonNull final TopicDto topic, @NonNull final INBOUND inbound); + + protected void delta(@Nullable final SeriesDto series, @NonNull final String meter, @NonNull final ZonedDateTime date, final double value) { + if (series != null) { + deltaService.write(series.id, meter, date, value); + } + } + + protected void varying(@Nullable final SeriesDto series, @NonNull final ZonedDateTime date, final double value) { + if (series != null) { + varyingService.write(series.id, date, value); + } + } + +} diff --git a/src/main/java/de/ph87/data/topic/TopicReceiver.java b/src/main/java/de/ph87/data/topic/TopicReceiver.java index 59d1f38..40e098a 100644 --- a/src/main/java/de/ph87/data/topic/TopicReceiver.java +++ b/src/main/java/de/ph87/data/topic/TopicReceiver.java @@ -7,14 +7,71 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Optional; +import java.util.Set; + @Slf4j @Service @RequiredArgsConstructor public class TopicReceiver { - @EventListener(MqttMessage.class) - public void receive(@NonNull final MqttMessage mqttMessage) { + private final TopicRepository topicRepository; + private final Set parsers; + + @EventListener(MqttMessage.class) + public void receive(@NonNull final MqttMessage message) { + try { + receive2(message); + } catch (Exception e) { + log.error("Failed to handle message:"); + log.error(" message: {}", message); + log.error(" error: {}", e.toString().replaceAll("\r*\n\r*", " ")); + log.error(" stacktrace:\n{}", stackTraceToString(e)); + } + } + + private void receive2(final MqttMessage message) { + final Optional topicOptional = topicRepository.findDtoByEnabledTrueAndName(message.getTopic()); + if (topicOptional.isEmpty()) { + return; + } + final TopicDto topic = topicOptional.get(); + final Optional parserOptional = parsers.stream().filter(p -> p.getClass() == topic.type.getClazz()).findFirst(); + if (parserOptional.isEmpty()) { + log.error("No parser found:"); + log.error(" message: {}", message); + log.error(" topic: {}", topic); + log.error(" type: {}", topic.type); + return; + } + final ITopicParser parser = parserOptional.get(); + handle(message, parser, topic); + } + + private static void handle(@NonNull final MqttMessage message, @NonNull final ITopicParser parser, @NonNull final TopicDto topic) { + try { + parser.handle(topic, message); + } catch (Exception e) { + log.error("Failed to parse message:"); + log.error(" message: {}", message); + log.error(" topic: {}", topic); + log.error(" parser: {}", parser.getClass().getSimpleName()); + log.error(" error: {}", e.toString().replaceAll("\r*\n\r*", " ")); + if (log.isDebugEnabled()) { + log.debug(stackTraceToString(e)); + } + } + } + + @NonNull + public static String stackTraceToString(@NonNull final Throwable throwable) { + final StringWriter stringWriter = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(stringWriter); + throwable.printStackTrace(printWriter); + return stringWriter.toString(); } } diff --git a/src/main/java/de/ph87/data/topic/TopicRepository.java b/src/main/java/de/ph87/data/topic/TopicRepository.java index c325c77..1e22d70 100644 --- a/src/main/java/de/ph87/data/topic/TopicRepository.java +++ b/src/main/java/de/ph87/data/topic/TopicRepository.java @@ -4,15 +4,13 @@ import lombok.NonNull; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.ListCrudRepository; -import java.util.List; import java.util.Optional; public interface TopicRepository extends ListCrudRepository { - @NonNull - Optional findByName(@NonNull String name); + boolean existsByName(@NonNull String name); - @Query("select new de.ph87.data.topic.TopicDto(t) from Topic t") - List findAllDto(); + @Query("select new de.ph87.data.topic.TopicDto(t) from Topic t where t.name = :name") + Optional findDtoByEnabledTrueAndName(@NonNull String name); } diff --git a/src/main/java/de/ph87/data/topic/TopicService.java b/src/main/java/de/ph87/data/topic/TopicService.java new file mode 100644 index 0000000..43e5eb6 --- /dev/null +++ b/src/main/java/de/ph87/data/topic/TopicService.java @@ -0,0 +1,72 @@ +package de.ph87.data.topic; + +import de.ph87.data.common.CrudAction; +import de.ph87.data.series.Series; +import de.ph87.data.series.SeriesService; +import jakarta.annotation.Nullable; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static de.ph87.data.location.NotFoundException.notFound; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TopicService { + + private final TopicRepository topicRepository; + + private final SeriesService seriesService; + + @NonNull + @Transactional + public TopicDto create() { + final String name = _generateUniqueName(); + return publish(topicRepository.save(new Topic(name)), CrudAction.CREATED); + } + + @NonNull + private String _generateUniqueName() { + int index = 0; + while (true) { + final String name = "topic/unnamed/" + index++; + if (!topicRepository.existsByName(name)) { + return name; + } + } + } + + @NonNull + @Transactional + public TopicDto modify(final long id, @NonNull final Consumer modifier) { + final Topic topic = getById(id); + modifier.accept(topic); + return publish(topic, CrudAction.MODIFIED); + } + + @NonNull + @Transactional + public TopicDto modifySeries(final long topicId, @Nullable final Long seriesId, @NonNull final BiConsumer modifier) { + final Series series = seriesId == null ? null : seriesService.getById(seriesId); + return modify(topicId, topic -> modifier.accept(topic, series)); + } + + @NonNull + private Topic getById(final long id) { + return topicRepository.findById(id).orElseThrow(notFound(Topic.class, "id", id)); + } + + @NonNull + private TopicDto publish(@NonNull final Topic topic, @NonNull final CrudAction action) { + final TopicDto dto = new TopicDto(topic); + log.info("{} {}: {}", Topic.class.getSimpleName(), action, dto); + return dto; + } + +} diff --git a/src/main/java/de/ph87/data/topic/TopicType.java b/src/main/java/de/ph87/data/topic/TopicType.java new file mode 100644 index 0000000..2f27d13 --- /dev/null +++ b/src/main/java/de/ph87/data/topic/TopicType.java @@ -0,0 +1,23 @@ +package de.ph87.data.topic; + +import de.ph87.data.topic.parser.PatrixOpenDtu; +import de.ph87.data.topic.parser.ShellyPlus1PM; +import de.ph87.data.topic.parser.PatrixSmartMeter; +import de.ph87.data.topic.parser.TasmotaSmartMeter; +import lombok.Getter; + +@Getter +public enum TopicType { + PatrixOpenDtu(PatrixOpenDtu.class), + PatrixSmartMeter(PatrixSmartMeter.class), + TasmotaSmartMeter(TasmotaSmartMeter.class), + ShellyPlus1PM(ShellyPlus1PM.class), + ; + + public final Class clazz; + + TopicType(final Class clazz) { + this.clazz = clazz; + } + +} diff --git a/src/main/java/de/ph87/data/topic/parser/PatrixOpenDtu.java b/src/main/java/de/ph87/data/topic/parser/PatrixOpenDtu.java new file mode 100644 index 0000000..863ea74 --- /dev/null +++ b/src/main/java/de/ph87/data/topic/parser/PatrixOpenDtu.java @@ -0,0 +1,50 @@ +package de.ph87.data.topic.parser; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.ph87.data.series.data.delta.DeltaService; +import de.ph87.data.series.data.varying.VaryingService; +import de.ph87.data.topic.TopicDto; +import de.ph87.data.topic.TopicParserAbstract; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +@Slf4j +@Service +public class PatrixOpenDtu extends TopicParserAbstract { + + public PatrixOpenDtu(@NonNull final ObjectMapper mapper, @NonNull final DeltaService deltaService, @NonNull final VaryingService varyingService) { + super(Dto.class, mapper, deltaService, varyingService); + } + + @Override + protected void handle2(@NonNull final TopicDto topic, @NonNull final Dto dto) { + delta(topic.series0, dto.meter, dto.date, dto.energyKWh); + varying(topic.series1, dto.date, dto.powerW); + } + + public static class Dto { + + public final String meter; + + public final ZonedDateTime date; + + public final double energyKWh; + + public final double powerW; + + public Dto(@JsonProperty(value = "inverter", required = true) @NonNull final String inverter, @JsonProperty(value = "timestamp", required = true) @NonNull final long epochSeconds, @JsonProperty(value = "totalKWh", required = true) final double energyKWh, @JsonProperty(value = "totalW", required = true) final double powerW) { + this.meter = inverter; + this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault()); + this.energyKWh = energyKWh; + this.powerW = powerW; + } + + } + +} diff --git a/src/main/java/de/ph87/data/topic/parser/PatrixSmartMeter.java b/src/main/java/de/ph87/data/topic/parser/PatrixSmartMeter.java new file mode 100644 index 0000000..e990eca --- /dev/null +++ b/src/main/java/de/ph87/data/topic/parser/PatrixSmartMeter.java @@ -0,0 +1,60 @@ +package de.ph87.data.topic.parser; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.ph87.data.series.data.delta.DeltaService; +import de.ph87.data.series.data.varying.VaryingService; +import de.ph87.data.topic.TopicDto; +import de.ph87.data.topic.TopicParserAbstract; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +@Slf4j +@Service +public class PatrixSmartMeter extends TopicParserAbstract { + + public PatrixSmartMeter(@NonNull final ObjectMapper mapper, @NonNull final DeltaService deltaService, @NonNull final VaryingService varyingService) { + super(Dto.class, mapper, deltaService, varyingService); + } + + @Override + protected void handle2(final @NonNull TopicDto topic, @NonNull final Dto dto) { + delta(topic.series0, dto.meter, dto.date, dto.purchaseKWh); + delta(topic.series1, dto.meter, dto.date, dto.deliverKWh); + varying(topic.series2, dto.date, Math.max(0, dto.powerW)); + varying(topic.series3, dto.date, Math.max(0, -dto.powerW)); + } + + public static class Dto { + + public final String meter; + + public final ZonedDateTime date; + + public final double purchaseKWh; + + public final double deliverKWh; + + public final double powerW; + + public Dto( + @JsonProperty(value = "timestamp", required = true) @NonNull final long epochSeconds, + @JsonProperty(value = "purchaseWh", required = true) final double purchaseWh, + @JsonProperty(value = "deliveryWh", required = true) final double deliverWh, + @JsonProperty(value = "powerW", required = true) final double powerW + ) { + this.meter = "1ZPA0020300305"; + this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault()); + this.purchaseKWh = purchaseWh / 1000; + this.deliverKWh = deliverWh / 1000; + this.powerW = powerW; + } + + } + +} diff --git a/src/main/java/de/ph87/data/topic/parser/ShellyPlus1PM.java b/src/main/java/de/ph87/data/topic/parser/ShellyPlus1PM.java new file mode 100644 index 0000000..f21ab14 --- /dev/null +++ b/src/main/java/de/ph87/data/topic/parser/ShellyPlus1PM.java @@ -0,0 +1,112 @@ +package de.ph87.data.topic.parser; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.ph87.data.series.data.delta.DeltaService; +import de.ph87.data.series.data.varying.VaryingService; +import de.ph87.data.topic.TopicDto; +import de.ph87.data.topic.TopicParserAbstract; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; + +@Slf4j +@Service +public class ShellyPlus1PM extends TopicParserAbstract { + + public ShellyPlus1PM(@NonNull final ObjectMapper mapper, @NonNull final DeltaService deltaService, @NonNull final VaryingService varyingService) { + super(Dto.class, mapper, deltaService, varyingService); + } + + @Override + protected void handle2(@NonNull final TopicDto topic, @NonNull final Dto dto) { + delta(topic.series0, "ShellyPlus1PM", dto.energy.date, dto.energy.totalKWh); + varying(topic.series1, dto.energy.date, dto.powerW); + } + + @Data + public static class Dto { + + public final int id; + + public final String source; + + public final boolean relayState; + + public final double powerW; + + public final double voltage; + + public final double current; + + public final Energy energy; + + public final Temperature temperature; + + public Dto( + @JsonProperty("id") int id, + @JsonProperty("source") String source, + @JsonProperty("output") boolean relayState, + @JsonProperty("apower") double powerW, + @JsonProperty("voltage") double voltageV, + @JsonProperty("current") double currentA, + @JsonProperty("aenergy") Energy energy, + @JsonProperty("temperature") Temperature temperature + ) { + this.id = id; + this.source = source; + this.relayState = relayState; + this.powerW = powerW; + this.voltage = voltageV; + this.current = currentA; + this.energy = energy; + this.temperature = temperature; + } + + @Data + public static class Energy { + + public final double totalKWh; + + public final List byMinute; + + public final ZonedDateTime date; + + public Energy( + @JsonProperty("total") double totalWh, + @JsonProperty("by_minute") List byMinute, + @JsonProperty("minute_ts") long epochSeconds + ) { + this.totalKWh = totalWh / 1000; + this.byMinute = byMinute; + this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault()); + } + + } + + @Data + public static class Temperature { + + public final double tC; + + public final double tF; + + public Temperature( + @JsonProperty("tC") double tC, + @JsonProperty("tF") double tF + ) { + this.tC = tC; + this.tF = tF; + } + + } + + } + +} diff --git a/src/main/java/de/ph87/data/topic/parser/TasmotaSmartMeter.java b/src/main/java/de/ph87/data/topic/parser/TasmotaSmartMeter.java new file mode 100644 index 0000000..d28e98d --- /dev/null +++ b/src/main/java/de/ph87/data/topic/parser/TasmotaSmartMeter.java @@ -0,0 +1,94 @@ +package de.ph87.data.topic.parser; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.ph87.data.series.data.delta.DeltaService; +import de.ph87.data.series.data.varying.VaryingService; +import de.ph87.data.topic.TopicDto; +import de.ph87.data.topic.TopicParserAbstract; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +@Slf4j +@Service +public class TasmotaSmartMeter extends TopicParserAbstract { + + public TasmotaSmartMeter(@NonNull final ObjectMapper mapper, @NonNull final DeltaService deltaService, @NonNull final VaryingService varyingService) { + super(Dto.class, mapper, deltaService, varyingService); + } + + @Override + protected void handle2(final @NonNull TopicDto topic, @NonNull final Dto dto) { + delta(topic.series0, dto.meter.number, dto.date, dto.meter.purchaseKWh); + delta(topic.series1, dto.meter.number, dto.date, dto.meter.deliverKWh); + varying(topic.series2, dto.date, Math.max(0, dto.meter.powerW)); + varying(topic.series3, dto.date, Math.max(0, -dto.meter.powerW)); + } + + public static class Dto { + + @NonNull + public final ZonedDateTime date; + + @NonNull + public final Meter meter; + + public Dto( + @JsonProperty(value = "Time", required = true) @NonNull final String localDateTimeString, + @JsonProperty(value = "meter", required = true) @NonNull final Meter meter + ) { + this.date = ZonedDateTime.of(LocalDateTime.parse(localDateTimeString), ZoneId.systemDefault()); + this.meter = meter; + } + + public static class Meter { + + @NonNull + public final String number; + + public final double purchaseKWh; + + public final double deliverKWh; + + public final double powerW; + + public Meter( + @JsonProperty(value = "number", required = true) @NonNull final String number, + @JsonProperty(value = "energy_purchased_kwh", required = true) final double purchaseWh, + @JsonProperty(value = "energy_delivered_kwh", required = true) final double deliverWh, + @JsonProperty(value = "power_w", required = true) final double powerW + ) { + this.number = parseMeterNumber(number); + this.purchaseKWh = purchaseWh; + this.deliverKWh = deliverWh; + this.powerW = powerW; + } + + @NonNull + private static String parseMeterNumber(@NonNull final String input) { + if (input.isEmpty()) { + throw new NumberFormatException("Cannot parse Meter number: No Hex-chars read."); + } + if (input.length() % 2 != 0) { + throw new NumberFormatException("Cannot parse Meter number: Hex-char count must be multiple of 2."); + } + final int length = Integer.parseInt(input.substring(0, 2), 16); + if (input.length() != length * 2) { + throw new NumberFormatException("Cannot parse Meter number: Invalid length"); + } + final int type = Integer.parseInt(input.substring(2, 4), 16); + final String name = "" + (char) Integer.parseInt(input.substring(4, 6), 16) + (char) Integer.parseInt(input.substring(6, 8), 16) + (char) Integer.parseInt(input.substring(8, 10), 16); + final int number = Integer.parseInt(input.substring(10), 16); + return "%d%s%s".formatted(type, name, number); + } + + } + + } + +}