Eltern SmartMeter Wattwächter JSON + Demo Data

This commit is contained in:
Patrick Haßel 2025-10-27 11:24:01 +01:00
parent 25c9c372a6
commit 524d03174a
3 changed files with 144 additions and 54 deletions

View File

@ -6,9 +6,11 @@ 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;
@ -64,6 +66,8 @@ public class DemoService {
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")
@ -75,6 +79,8 @@ public class DemoService {
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", TopicQueryFunction.ONLY_POSITIVE),
@ -82,6 +88,21 @@ public class DemoService {
new TopicQuery(electricityPowerDelivery, "$.powerW", TopicQueryFunction.ONLY_NEGATIVE_BUT_NEGATE)
);
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", 1),
new TopicQuery(elternElectricityPowerPurchase, "$.meter.power_w", TopicQueryFunction.ONLY_POSITIVE),
new TopicQuery(elternElectricityEnergyDelivery, "$.meter.energy_delivered_kwh", 1),
new TopicQuery(elternElectricityPowerDelivery, "$.meter.power_w", TopicQueryFunction.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);
@ -132,13 +153,17 @@ public class DemoService {
final Series cisternVolume = series("cistern/volume", "L", SeriesType.VARYING, 5);
topic("cistern/volume/PatrixJson", "$.date", new TopicQuery(cisternVolume, "$.value"));
plotRepository.deleteAll();
zuhauseEnergie();
zuhauseTemperatur();
eltern();
}
private void plots() {
plotRepository.deleteAll();
final Plot plot = plotRepository.save(new Plot());
plot.setName("Test");
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);
@ -148,13 +173,39 @@ public class DemoService {
final Series electricityEnergyPurchase = seriesRepository.findByName("electricity/energy/purchase").orElseThrow();
final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase));
electricityEnergyPurchaseGraph.setType(GraphType.BAR);
electricityEnergyPurchaseGraph.setStack("a");
electricityEnergyPurchaseGraph.setName("Bezug");
electricityEnergyPurchaseGraph.setColor("#FF8800");
energy.addGraph(electricityEnergyPurchaseGraph);
final Series electricityEnergyDelivery = seriesRepository.findByName("electricity/energy/delivery").orElseThrow();
final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery));
electricityEnergyDeliveryGraph.setType(GraphType.BAR);
electricityEnergyDeliveryGraph.setStack("a");
electricityEnergyDeliveryGraph.setName("Ü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.setType(GraphType.BAR);
electricityEnergyProduceGraph.setStack("a");
electricityEnergyProduceGraph.setName("Produktion");
electricityEnergyProduceGraph.setColor("#0000FF");
energy.addGraph(electricityEnergyProduceGraph);
}
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");
@ -167,34 +218,51 @@ public class DemoService {
bedroomTemperatureGraph.setMax(true);
temperature.addGraph(bedroomTemperatureGraph);
final Axis status = axisRepository.save(new Axis(plot));
plot.addAxis(status);
status.setVisible(false);
status.setRight(true);
status.setName("Status");
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 fallbackRelay0 = seriesRepository.findByName("fallback/relay0").orElseThrow();
final Graph fallbackRelay0Graph = graphRepository.save(new Graph(status, fallbackRelay0));
fallbackRelay0Graph.setName("FallbackRelay0");
fallbackRelay0Graph.setColor("#00FF00");
status.addGraph(fallbackRelay0Graph);
private void eltern() {
final Plot plot = plotRepository.save(new Plot(plotRepository.count()));
plot.setName("Eltern");
final Series infrared = seriesRepository.findByName("infraredHeater/state").orElseThrow();
final Graph infraredGraph = graphRepository.save(new Graph(status, infrared));
infraredGraph.setName("Infrarotheizung");
infraredGraph.setColor("#FF00FF");
status.addGraph(infraredGraph);
final Axis energy = axisRepository.save(new Axis(plot));
plot.addAxis(energy);
plot.setDashboard(true);
energy.setRight(true);
energy.setName("Energie");
energy.setUnit("kWh");
plotRepository.save(plot);
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);
axisRepository.save(energy);
axisRepository.save(temperature);
axisRepository.save(status);
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);
graphRepository.save(electricityEnergyPurchaseGraph);
graphRepository.save(bedroomTemperatureGraph);
graphRepository.save(fallbackRelay0Graph);
graphRepository.save(infraredGraph);
// final Series electricityEnergyProduce = seriesRepository.findByName("eltern/electricity/energy/produce").orElseThrow();
// final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce));
// electricityEnergyProduceGraph.setName("Produktion");
// electricityEnergyProduceGraph.setType(GraphType.BAR);
// electricityEnergyProduceGraph.setStack("a");
// electricityEnergyProduceGraph.setColor("#0000FF");
// energy.addGraph(electricityEnergyProduceGraph);
}
@NonNull
@ -222,10 +290,11 @@ public class DemoService {
topic.getQueries().addAll(List.of(queries));
}
private void topicMeterNumber(@NonNull final String name, @NonNull final String meterNumberQuery, @NonNull final TopicQuery... 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.setTimestampQuery("$.timestamp");
topic.setTimestampType(timestampType);
topic.setTimestampQuery(timestampQuery);
topic.getQueries().clear();
topic.getQueries().addAll(List.of(queries));
}

View File

@ -1,5 +1,7 @@
package de.ph87.data.topic;
public enum TimestampType {
EPOCH_MILLISECONDS, EPOCH_SECONDS
EPOCH_MILLISECONDS,
EPOCH_SECONDS,
ISO_LOCAL_DATE_TIME,
}

View File

@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Optional;
@ -27,6 +28,8 @@ import java.util.Optional;
@RequiredArgsConstructor
public class TopicReceiver {
public static final String SML_METER_NUMBER_RAW = "sml_meter_number_raw:";
private final TopicRepository topicRepository;
private final BoolService boolService;
@ -111,8 +114,7 @@ public class TopicReceiver {
}
final Object valueRaw = json.read(query.getValueQuery());
queryValue(valueRaw).ifPresentOrElse(
v -> {
queryValue(valueRaw).ifPresentOrElse(v -> {
final double value = query.getFunction().apply(v) * query.getFactor();
series.update(date, value);
applicationEventPublisher.publishEvent(new SeriesDto(series, false));
@ -129,9 +131,7 @@ public class TopicReceiver {
}
case VARYING -> varyingService.write(series, date, value);
}
},
() -> topic.error(log, "Failed to parse value: %s".formatted(valueRaw))
);
}, () -> topic.error(log, "Failed to parse value: %s".formatted(valueRaw)));
} catch (Exception e) {
topic.error(log, "Error executing TopicQuery: %s\n topic=%s\n query=%s\n inbound=%s".formatted(e.toString(), topic, query, inbound), e);
}
@ -139,10 +139,28 @@ public class TopicReceiver {
@NonNull
private static String queryMeterNumber(@NonNull final Topic topic, @NonNull final DocumentContext json) {
if (topic.getMeterNumberQuery().startsWith("\"") && topic.getMeterNumberQuery().endsWith("\"")) {
return topic.getMeterNumberQuery().substring(1, topic.getMeterNumberQuery().length() - 1);
final String query = topic.getMeterNumberQuery();
if (query.startsWith(SML_METER_NUMBER_RAW)) {
final String field = query.substring(SML_METER_NUMBER_RAW.length());
final String raw = json.read(field, String.class);
if (raw.isEmpty()) {
throw new NumberFormatException("Cannot parse Meter number: No Hex-chars read.");
}
return json.read(topic.getMeterNumberQuery(), String.class);
if (raw.length() % 2 != 0) {
throw new NumberFormatException("Cannot parse Meter number: Hex-char count must be multiple of 2.");
}
final int length = Integer.parseInt(raw.substring(0, 2), 16);
if (raw.length() != length * 2) {
throw new NumberFormatException("Cannot parse Meter number: Invalid length");
}
final int type = Integer.parseInt(raw.substring(2, 4), 16);
final String name = "" + (char) Integer.parseInt(raw.substring(4, 6), 16) + (char) Integer.parseInt(raw.substring(6, 8), 16) + (char) Integer.parseInt(raw.substring(8, 10), 16);
final int number = Integer.parseInt(raw.substring(10), 16);
return "%d%s%s".formatted(type, name, number);
} else if (query.startsWith("\"") && query.endsWith("\"")) {
return query.substring(1, query.length() - 1);
}
return json.read(query, String.class);
}
private static boolean queryBoolean(@NonNull final DocumentContext json, @NonNull final String terminatedQuery) {
@ -181,6 +199,7 @@ public class TopicReceiver {
return switch (type) {
case TimestampType.EPOCH_SECONDS -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(json.read(query, Long.class)), ZoneId.systemDefault());
case TimestampType.EPOCH_MILLISECONDS -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(json.read(query, Long.class)), ZoneId.systemDefault());
case TimestampType.ISO_LOCAL_DATE_TIME -> ZonedDateTime.of(LocalDateTime.parse(json.read(query, String.class)), ZoneId.systemDefault());
};
}