new topic parsers: PatrixOpenDtu, PatrixSmartMeter, ShellyPlus1PM, TasmotaSmartMeter

This commit is contained in:
Patrick Haßel 2025-10-29 10:13:49 +01:00
parent 278fe60906
commit 2ad140589c
39 changed files with 946 additions and 625 deletions

View File

@ -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),
);
}

View File

@ -2,8 +2,10 @@
<app-text [initial]="location.name" (onChange)="locationService.name(location, $event, update)"></app-text>
<app-number [initial]="location.latitude" (onChange)="locationService.latitude(location, $event, update)" unit="°"></app-number>
<app-number [initial]="location.longitude" (onChange)="locationService.longitude(location, $event, update)" unit="°"></app-number>
<app-series-select [initial]="location.purchase" (onChange)="locationService.purchase(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.delivery" (onChange)="locationService.delivery(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.produce" (onChange)="locationService.produce(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.power" (onChange)="locationService.power(location, $event, update)" [list]="filterPower()"></app-series-select>
<app-series-select [initial]="location.energyPurchase" (onChange)="locationService.energyPurchase(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.energyDeliver" (onChange)="locationService.energyDeliver(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.energyProduce" (onChange)="locationService.energyProduce(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.powerPurchase" (onChange)="locationService.powerPurchase(location, $event, update)" [list]="filterPower()"></app-series-select>
<app-series-select [initial]="location.powerDeliver" (onChange)="locationService.powerDeliver(location, $event, update)" [list]="filterPower()"></app-series-select>
<app-series-select [initial]="location.powerProduce" (onChange)="locationService.powerProduce(location, $event, update)" [list]="filterPower()"></app-series-select>
}

View File

@ -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<Location> {
this.postSingle([location.id, 'longitude'], longitude, next);
}
purchase(location: Location, purchase: Series | null, next?: Next<Location>) {
this.postSingle([location.id, 'purchase'], purchase?.id, next);
energyPurchase(location: Location, seriesId: number | null, next?: Next<Location>) {
this.postSingle([location.id, 'energyPurchase'], seriesId, next);
}
delivery(location: Location, delivery: Series | null, next?: Next<Location>) {
this.postSingle([location.id, 'delivery'], delivery?.id, next);
energyDeliver(location: Location, seriesId: number | null, next?: Next<Location>) {
this.postSingle([location.id, 'energyDeliver'], seriesId, next);
}
produce(location: Location, produce: Series | null, next?: Next<Location>) {
this.postSingle([location.id, 'produce'], produce?.id, next);
energyProduce(location: Location, seriesId: number | null, next?: Next<Location>) {
this.postSingle([location.id, 'energyProduce'], seriesId, next);
}
power(location: Location, power: Series | null, next?: Next<Location>) {
this.postSingle([location.id, 'power'], power?.id, next);
powerPurchase(location: Location, seriesId: number | null, next?: Next<Location>) {
this.postSingle([location.id, 'powerPurchase'], seriesId, next);
}
powerDeliver(location: Location, seriesId: number | null, next?: Next<Location>) {
this.postSingle([location.id, 'powerDeliver'], seriesId, next);
}
powerProduce(location: Location, seriesId: number | null, next?: Next<Location>) {
this.postSingle([location.id, 'powerProduce'], seriesId, next);
}
}

View File

@ -4,24 +4,18 @@
(mouseenter)="showPen = true"
(mouseleave)="showPen = false"
>
<div class="value" (click)="start()">
@if (editing) {
<select
#input
[(ngModel)]="model"
(blur)="apply()"
(keydown.enter)="apply()"
(keydown.escape)="cancel()"
>
<option [ngValue]="null">-</option>
@for (series of list; track series.id) {
<option [ngValue]="series">{{ series.name }}</option>
<option [ngValue]="series.id">{{ series.name }}</option>
}
</select>
} @else {
{{ initial?.name || '&nbsp;' }}
}
</div>
@if (editing || showPen) {
@if (showPen) {
<fa-icon [icon]="faPen"></fa-icon>
}
</div>

View File

@ -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;
}

View File

@ -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<Series | null>();
readonly onChange = new EventEmitter<number | null>();
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,
};
}

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<Location, Series> 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<Location, Series> 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<Location> setter) {
public LocationDto set(final long id, @NonNull final Consumer<Location> setter) {
final Location location = locationRepository.findById(id).orElseThrow(notFound(Location.class, "id", id));
setter.accept(location);
return new LocationDto(location);

View File

@ -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

View File

@ -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) {

View File

@ -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<Graph> 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);
});
}

View File

@ -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);
}
}

View File

@ -29,7 +29,7 @@ public class SeriesController {
private final SeriesPointService seriesPointService;
@PostMapping("create")
@GetMapping("create")
public SeriesDto create() {
return seriesService.create();
}

View File

@ -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<Series> modifier) {
public SeriesDto modify(final long id, @NonNull Consumer<Series> modifier) {
final Series series = getById(id);
modifier.accept(series);
return publish(series, CrudAction.MODIFIED);
}
@NonNull
private Series getById(final long id) {
public List<Series> 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;
}
}

View File

@ -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);
}

View File

@ -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,11 +37,7 @@ 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 -> {
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;
@ -51,11 +52,7 @@ public class BoolService {
}
existing.setEnd(end);
existing.setTerminated(terminated);
})
.findFirst()
.orElseGet(
() -> boolRepo.save(new Bool(id, end, state, terminated))
);
}).findFirst().orElseGet(() -> boolRepo.save(new Bool(id, end, state, terminated)));
}
@NonNull

View File

@ -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);
}

View File

@ -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<DeltaPoint> points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
return switch (request.getInterval()) {

View File

@ -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;

View File

@ -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)));
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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<? extends SeriesPoint<?>> 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<? extends SeriesPoint<?>> 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<AllSeriesPointResponse.Entry> seriesPoints = seriesRepository.findAll().stream().map(series -> map(series, request)).toList();
final List<AllSeriesPointResponse.Entry> seriesPoints = seriesService.findAll().stream().map(series -> map(series, request)).toList();
return new AllSeriesPointResponse(request, seriesPoints);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<INBOUND> implements ITopicParser {
protected final Class<INBOUND> 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);
}
}
}

View File

@ -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 {
private final TopicRepository topicRepository;
private final Set<ITopicParser> parsers;
@EventListener(MqttMessage.class)
public void receive(@NonNull final MqttMessage mqttMessage) {
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<TopicDto> topicOptional = topicRepository.findDtoByEnabledTrueAndName(message.getTopic());
if (topicOptional.isEmpty()) {
return;
}
final TopicDto topic = topicOptional.get();
final Optional<ITopicParser> 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();
}
}

View File

@ -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<Topic, Long> {
@NonNull
Optional<Topic> findByName(@NonNull String name);
boolean existsByName(@NonNull String name);
@Query("select new de.ph87.data.topic.TopicDto(t) from Topic t")
List<TopicDto> findAllDto();
@Query("select new de.ph87.data.topic.TopicDto(t) from Topic t where t.name = :name")
Optional<TopicDto> findDtoByEnabledTrueAndName(@NonNull String name);
}

View File

@ -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<Topic> 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<Topic, Series> 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;
}
}

View File

@ -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<? extends ITopicParser> clazz;
TopicType(final Class<? extends ITopicParser> clazz) {
this.clazz = clazz;
}
}

View File

@ -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<PatrixOpenDtu.Dto> {
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;
}
}
}

View File

@ -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<PatrixSmartMeter.Dto> {
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;
}
}
}

View File

@ -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<ShellyPlus1PM.Dto> {
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<Double> byMinute;
public final ZonedDateTime date;
public Energy(
@JsonProperty("total") double totalWh,
@JsonProperty("by_minute") List<Double> 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;
}
}
}
}

View File

@ -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<TasmotaSmartMeter.Dto> {
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);
}
}
}
}