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.AxisRepository;
import de.ph87.data.plot.axis.graph.Graph; import de.ph87.data.plot.axis.graph.Graph;
import de.ph87.data.plot.axis.graph.GraphRepository; 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.Series;
import de.ph87.data.series.SeriesRepository; import de.ph87.data.series.SeriesRepository;
import de.ph87.data.series.SeriesType; import de.ph87.data.series.SeriesType;
import de.ph87.data.topic.TimestampType;
import de.ph87.data.topic.Topic; import de.ph87.data.topic.Topic;
import de.ph87.data.topic.TopicRepository; import de.ph87.data.topic.TopicRepository;
import de.ph87.data.topic.query.TopicQuery; 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); final Series electricityPowerProduce = series("electricity/power/produce", "W", SeriesType.VARYING, 5);
topicMeterNumber( topicMeterNumber(
"openDTU/pv/patrix/json2", "openDTU/pv/patrix/json2",
TimestampType.EPOCH_SECONDS,
"$.timestamp",
"$.inverter", "$.inverter",
new TopicQuery(electricityEnergyProduce, "$.totalKWh"), new TopicQuery(electricityEnergyProduce, "$.totalKWh"),
new TopicQuery(electricityPowerProduce, "$.totalW") new TopicQuery(electricityPowerProduce, "$.totalW")
@ -75,6 +79,8 @@ public class DemoService {
final Series electricityPowerDelivery = series("electricity/power/delivery", "W", SeriesType.VARYING, 5); final Series electricityPowerDelivery = series("electricity/power/delivery", "W", SeriesType.VARYING, 5);
topicMeterNumber( topicMeterNumber(
"electricity/grid/json", "electricity/grid/json",
TimestampType.EPOCH_SECONDS,
"$.timestamp",
"\"1ZPA0020300305\"", "\"1ZPA0020300305\"",
new TopicQuery(electricityEnergyPurchase, "$.purchaseWh", 0.001), new TopicQuery(electricityEnergyPurchase, "$.purchaseWh", 0.001),
new TopicQuery(electricityPowerPurchase, "$.powerW", TopicQueryFunction.ONLY_POSITIVE), new TopicQuery(electricityPowerPurchase, "$.powerW", TopicQueryFunction.ONLY_POSITIVE),
@ -82,6 +88,21 @@ public class DemoService {
new TopicQuery(electricityPowerDelivery, "$.powerW", TopicQueryFunction.ONLY_NEGATIVE_BUT_NEGATE) 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 gardenPressure = series("garden/pressure", "hPa", SeriesType.VARYING, 5);
final Series gardenTemperature = series("garden/temperature", "°C", 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 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); final Series cisternVolume = series("cistern/volume", "L", SeriesType.VARYING, 5);
topic("cistern/volume/PatrixJson", "$.date", new TopicQuery(cisternVolume, "$.value")); topic("cistern/volume/PatrixJson", "$.date", new TopicQuery(cisternVolume, "$.value"));
plotRepository.deleteAll();
zuhauseEnergie();
zuhauseTemperatur();
eltern();
} }
private void plots() { private void zuhauseEnergie() {
plotRepository.deleteAll(); final Plot plot = plotRepository.save(new Plot(plotRepository.count()));
plot.setName("Zuhause Energie");
final Plot plot = plotRepository.save(new Plot()); plot.setDashboard(true);
plot.setName("Test");
final Axis energy = axisRepository.save(new Axis(plot)); final Axis energy = axisRepository.save(new Axis(plot));
plot.addAxis(energy); plot.addAxis(energy);
@ -148,13 +173,39 @@ public class DemoService {
final Series electricityEnergyPurchase = seriesRepository.findByName("electricity/energy/purchase").orElseThrow(); final Series electricityEnergyPurchase = seriesRepository.findByName("electricity/energy/purchase").orElseThrow();
final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase)); final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase));
electricityEnergyPurchaseGraph.setType(GraphType.BAR);
electricityEnergyPurchaseGraph.setStack("a");
electricityEnergyPurchaseGraph.setName("Bezug"); electricityEnergyPurchaseGraph.setName("Bezug");
electricityEnergyPurchaseGraph.setColor("#FF8800"); electricityEnergyPurchaseGraph.setColor("#FF8800");
energy.addGraph(electricityEnergyPurchaseGraph); 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)); final Axis temperature = axisRepository.save(new Axis(plot));
plot.addAxis(temperature); plot.addAxis(temperature);
temperature.setRight(true); temperature.setRight(true);
temperature.setMin(0.0);
temperature.setName("Temperatur"); temperature.setName("Temperatur");
temperature.setUnit("°C"); temperature.setUnit("°C");
@ -167,34 +218,51 @@ public class DemoService {
bedroomTemperatureGraph.setMax(true); bedroomTemperatureGraph.setMax(true);
temperature.addGraph(bedroomTemperatureGraph); temperature.addGraph(bedroomTemperatureGraph);
final Axis status = axisRepository.save(new Axis(plot)); final Series gardenTemperature = seriesRepository.findByName("garden/temperature").orElseThrow();
plot.addAxis(status); final Graph gardenTemperatureGraph = graphRepository.save(new Graph(temperature, gardenTemperature));
status.setVisible(false); gardenTemperatureGraph.setName("Garten");
status.setRight(true); gardenTemperatureGraph.setColor("#00FF00");
status.setName("Status"); gardenTemperatureGraph.setMin(true);
gardenTemperatureGraph.setAvg(false);
gardenTemperatureGraph.setMax(true);
temperature.addGraph(gardenTemperatureGraph);
}
final Series fallbackRelay0 = seriesRepository.findByName("fallback/relay0").orElseThrow(); private void eltern() {
final Graph fallbackRelay0Graph = graphRepository.save(new Graph(status, fallbackRelay0)); final Plot plot = plotRepository.save(new Plot(plotRepository.count()));
fallbackRelay0Graph.setName("FallbackRelay0"); plot.setName("Eltern");
fallbackRelay0Graph.setColor("#00FF00");
status.addGraph(fallbackRelay0Graph);
final Series infrared = seriesRepository.findByName("infraredHeater/state").orElseThrow(); final Axis energy = axisRepository.save(new Axis(plot));
final Graph infraredGraph = graphRepository.save(new Graph(status, infrared)); plot.addAxis(energy);
infraredGraph.setName("Infrarotheizung"); plot.setDashboard(true);
infraredGraph.setColor("#FF00FF"); energy.setRight(true);
status.addGraph(infraredGraph); 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); final Series electricityEnergyDelivery = seriesRepository.findByName("eltern/electricity/energy/delivery").orElseThrow();
axisRepository.save(temperature); final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery));
axisRepository.save(status); electricityEnergyDeliveryGraph.setName("Überschuss");
electricityEnergyDeliveryGraph.setType(GraphType.BAR);
electricityEnergyDeliveryGraph.setFactor(-1);
electricityEnergyDeliveryGraph.setStack("a");
electricityEnergyDeliveryGraph.setColor("#FF00FF");
energy.addGraph(electricityEnergyDeliveryGraph);
graphRepository.save(electricityEnergyPurchaseGraph); // final Series electricityEnergyProduce = seriesRepository.findByName("eltern/electricity/energy/produce").orElseThrow();
graphRepository.save(bedroomTemperatureGraph); // final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce));
graphRepository.save(fallbackRelay0Graph); // electricityEnergyProduceGraph.setName("Produktion");
graphRepository.save(infraredGraph); // electricityEnergyProduceGraph.setType(GraphType.BAR);
// electricityEnergyProduceGraph.setStack("a");
// electricityEnergyProduceGraph.setColor("#0000FF");
// energy.addGraph(electricityEnergyProduceGraph);
} }
@NonNull @NonNull
@ -222,10 +290,11 @@ public class DemoService {
topic.getQueries().addAll(List.of(queries)); 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))); final Topic topic = topicRepository.findByName(name).orElseGet(() -> topicRepository.save(new Topic(name)));
topic.setMeterNumberQuery(meterNumberQuery); topic.setMeterNumberQuery(meterNumberQuery);
topic.setTimestampQuery("$.timestamp"); topic.setTimestampType(timestampType);
topic.setTimestampQuery(timestampQuery);
topic.getQueries().clear(); topic.getQueries().clear();
topic.getQueries().addAll(List.of(queries)); topic.getQueries().addAll(List.of(queries));
} }

View File

@ -1,5 +1,7 @@
package de.ph87.data.topic; package de.ph87.data.topic;
public enum TimestampType { 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 org.springframework.transaction.annotation.Transactional;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Optional; import java.util.Optional;
@ -27,6 +28,8 @@ import java.util.Optional;
@RequiredArgsConstructor @RequiredArgsConstructor
public class TopicReceiver { public class TopicReceiver {
public static final String SML_METER_NUMBER_RAW = "sml_meter_number_raw:";
private final TopicRepository topicRepository; private final TopicRepository topicRepository;
private final BoolService boolService; private final BoolService boolService;
@ -111,27 +114,24 @@ public class TopicReceiver {
} }
final Object valueRaw = json.read(query.getValueQuery()); final Object valueRaw = json.read(query.getValueQuery());
queryValue(valueRaw).ifPresentOrElse( queryValue(valueRaw).ifPresentOrElse(v -> {
v -> { final double value = query.getFunction().apply(v) * query.getFactor();
final double value = query.getFunction().apply(v) * query.getFactor(); series.update(date, value);
series.update(date, value); applicationEventPublisher.publishEvent(new SeriesDto(series, false));
applicationEventPublisher.publishEvent(new SeriesDto(series, false)); switch (series.getType()) {
switch (series.getType()) { case BOOL -> {
case BOOL -> { final ZonedDateTime begin = queryTimestamp(json, query.getBeginQuery(), topic.getTimestampType());
final ZonedDateTime begin = queryTimestamp(json, query.getBeginQuery(), topic.getTimestampType()); final boolean terminated = queryBoolean(json, query.getTerminatedQuery());
final boolean terminated = queryBoolean(json, query.getTerminatedQuery()); boolService.write(series, begin, date, value > 0, terminated);
boolService.write(series, begin, date, value > 0, terminated);
}
case DELTA -> {
final String meterNumber = queryMeterNumber(topic, json);
topic.setMeterNumberLast(meterNumber);
deltaService.write(series, meterNumber, date, value);
}
case VARYING -> varyingService.write(series, date, value);
} }
}, case DELTA -> {
() -> topic.error(log, "Failed to parse value: %s".formatted(valueRaw)) final String meterNumber = queryMeterNumber(topic, json);
); topic.setMeterNumberLast(meterNumber);
deltaService.write(series, meterNumber, date, value);
}
case VARYING -> varyingService.write(series, date, value);
}
}, () -> topic.error(log, "Failed to parse value: %s".formatted(valueRaw)));
} catch (Exception e) { } 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); 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 @NonNull
private static String queryMeterNumber(@NonNull final Topic topic, @NonNull final DocumentContext json) { private static String queryMeterNumber(@NonNull final Topic topic, @NonNull final DocumentContext json) {
if (topic.getMeterNumberQuery().startsWith("\"") && topic.getMeterNumberQuery().endsWith("\"")) { final String query = topic.getMeterNumberQuery();
return topic.getMeterNumberQuery().substring(1, topic.getMeterNumberQuery().length() - 1); 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.");
}
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(topic.getMeterNumberQuery(), String.class); return json.read(query, String.class);
} }
private static boolean queryBoolean(@NonNull final DocumentContext json, @NonNull final String terminatedQuery) { private static boolean queryBoolean(@NonNull final DocumentContext json, @NonNull final String terminatedQuery) {
@ -181,6 +199,7 @@ public class TopicReceiver {
return switch (type) { return switch (type) {
case TimestampType.EPOCH_SECONDS -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(json.read(query, Long.class)), ZoneId.systemDefault()); 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.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());
}; };
} }