From 8f727572fad270ce2a31ddd9162a6b42679b4eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Sat, 15 Feb 2025 18:41:19 +0100 Subject: [PATCH] Value + EspHomeHandler + SmartMeterHandler --- application.properties | 4 +- .../de/ph87/data/{series => }/Action.java | 2 +- src/main/java/de/ph87/data/Backend.java | 8 +- .../de/ph87/data/message/IMessageHandler.java | 8 +- .../java/de/ph87/data/message/Message.java | 18 ++-- .../de/ph87/data/message/MessageService.java | 19 ++-- .../data/message/handler/EspHomeHandler.java | 92 +++++++++++++++++++ .../message/handler/SimpleJsonHandler.java | 63 ++++++------- .../message/handler/SmartMeterHandler.java | 56 +++++++++++ .../message/receive/mqtt/MqttReceiver.java | 8 +- src/main/java/de/ph87/data/series/Series.java | 31 ++++--- .../java/de/ph87/data/series/SeriesDto.java | 26 +++--- .../de/ph87/data/series/SeriesInbound.java | 33 +++---- .../de/ph87/data/series/SeriesRepository.java | 9 +- .../de/ph87/data/series/SeriesService.java | 89 +++++++----------- .../java/de/ph87/data/series/entry/Entry.java | 35 ++++--- .../data/series/entry/EntryRepository.java | 11 ++- .../ph87/data/series/entry/EntryService.java | 30 +++--- src/main/java/de/ph87/data/unit/Unit.java | 28 ++++-- .../ph87/data/unit/UnitListDeserializer.java | 17 ++++ src/main/java/de/ph87/data/unit/Value.java | 23 +++++ 21 files changed, 407 insertions(+), 203 deletions(-) rename src/main/java/de/ph87/data/{series => }/Action.java (64%) create mode 100644 src/main/java/de/ph87/data/message/handler/EspHomeHandler.java create mode 100644 src/main/java/de/ph87/data/message/handler/SmartMeterHandler.java create mode 100644 src/main/java/de/ph87/data/unit/UnitListDeserializer.java create mode 100644 src/main/java/de/ph87/data/unit/Value.java diff --git a/application.properties b/application.properties index 857de87..78c51d9 100644 --- a/application.properties +++ b/application.properties @@ -1,11 +1,11 @@ -logging.level.de.ph87=DEBUG +logging.level.de.ph87.data.series=DEBUG #- spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password #- -#spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=create #- de.ph87.data.message.receive.mqtt.host=10.0.0.50 de.ph87.data.message.receive.mqtt.topic=# diff --git a/src/main/java/de/ph87/data/series/Action.java b/src/main/java/de/ph87/data/Action.java similarity index 64% rename from src/main/java/de/ph87/data/series/Action.java rename to src/main/java/de/ph87/data/Action.java index e8fa431..6443e19 100644 --- a/src/main/java/de/ph87/data/series/Action.java +++ b/src/main/java/de/ph87/data/Action.java @@ -1,4 +1,4 @@ -package de.ph87.data.series; +package de.ph87.data; public enum Action { CREATED, CHANGED, DELETED diff --git a/src/main/java/de/ph87/data/Backend.java b/src/main/java/de/ph87/data/Backend.java index ad19ca1..e1625ca 100644 --- a/src/main/java/de/ph87/data/Backend.java +++ b/src/main/java/de/ph87/data/Backend.java @@ -1,13 +1,13 @@ package de.ph87.data; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.*; +import org.springframework.boot.autoconfigure.*; @SpringBootApplication public class Backend { - + public static void main(String[] args) { SpringApplication.run(Backend.class, args); } - + } \ No newline at end of file diff --git a/src/main/java/de/ph87/data/message/IMessageHandler.java b/src/main/java/de/ph87/data/message/IMessageHandler.java index 1a8a391..6c1201d 100644 --- a/src/main/java/de/ph87/data/message/IMessageHandler.java +++ b/src/main/java/de/ph87/data/message/IMessageHandler.java @@ -1,9 +1,9 @@ package de.ph87.data.message; -import lombok.NonNull; +import lombok.*; public interface IMessageHandler { - - void handle(@NonNull final Message message); - + + void handle(@NonNull final Message message) throws Exception; + } diff --git a/src/main/java/de/ph87/data/message/Message.java b/src/main/java/de/ph87/data/message/Message.java index 8b9f37d..c5fc0c5 100644 --- a/src/main/java/de/ph87/data/message/Message.java +++ b/src/main/java/de/ph87/data/message/Message.java @@ -1,26 +1,22 @@ package de.ph87.data.message; -import lombok.Getter; -import lombok.ToString; - -import java.time.ZonedDateTime; +import lombok.*; @Getter @ToString public class Message { - - public final ZonedDateTime date = ZonedDateTime.now(); - + public final String topic; - + + @ToString.Exclude public final String payload; - + public final String payloadLoggable; - + public Message(final String topic, final String payload) { this.topic = topic; this.payload = payload; this.payloadLoggable = payload.replace("\n", "\\n").replace("\r", "\\r"); } - + } diff --git a/src/main/java/de/ph87/data/message/MessageService.java b/src/main/java/de/ph87/data/message/MessageService.java index a93acb3..8d7aa63 100644 --- a/src/main/java/de/ph87/data/message/MessageService.java +++ b/src/main/java/de/ph87/data/message/MessageService.java @@ -1,27 +1,26 @@ package de.ph87.data.message; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import lombok.*; +import lombok.extern.slf4j.*; +import org.springframework.stereotype.*; -import java.util.List; +import java.util.*; @Slf4j @Service @RequiredArgsConstructor public class MessageService { - + private final List messageHandlers; - - public void onMessage(@NonNull final Message message) { + + public void handle(@NonNull final Message message) { messageHandlers.forEach(handler -> { try { handler.handle(message); } catch (Exception e) { - log.warn("Message handler error: message={}", e.getMessage(), e); + log.error("Failed handling message: message={}, error={}", message, e.toString()); } }); } - + } diff --git a/src/main/java/de/ph87/data/message/handler/EspHomeHandler.java b/src/main/java/de/ph87/data/message/handler/EspHomeHandler.java new file mode 100644 index 0000000..7f7a271 --- /dev/null +++ b/src/main/java/de/ph87/data/message/handler/EspHomeHandler.java @@ -0,0 +1,92 @@ +package de.ph87.data.message.handler; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.*; +import de.ph87.data.message.*; +import de.ph87.data.series.*; +import de.ph87.data.unit.Value; +import de.ph87.data.unit.*; +import lombok.*; +import lombok.extern.slf4j.*; +import org.springframework.stereotype.*; + +import java.time.*; +import java.util.*; +import java.util.regex.*; +import java.util.stream.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EspHomeHandler implements IMessageHandler { + + private static final Pattern REGEX = Pattern.compile("^(?\\w+)/sensor/(?\\w+)$"); + + private final ObjectMapper objectMapper; + + private final SeriesService seriesService; + + @Override + public void handle(@NonNull final Message message) throws Exception { + final Matcher matcher = REGEX.matcher(message.topic); + if (!matcher.find()) { + return; + } + final String area = matcher.group("area"); + final String property = propertyWorkaround(matcher.group("property")); + final String name = "%s/%s".formatted(area, property.replace("_", "/")); + final Unit targetUnit = switch (property) { + case "iaq" -> Unit.IAQ; + case "iaq_co2_equivalent" -> Unit.IAQ_CO2_EQUIVALENT; + case "iaq_voc_equivalent" -> Unit.IAQ_VOC_EQUIVALENT; + case "pressure" -> Unit.PRESSURE_HPA; + case "temperature" -> Unit.TEMPERATURE_C; + case "humidity_relative" -> Unit.HUMIDITY_RELATIVE_PERCENT; + case "humidity_absolute" -> Unit.HUMIDITY_ABSOLUTE_GM3; + case "sun" -> Unit.SUN_DC; + default -> null; + }; + if (targetUnit == null) { + log.debug("Skipping property: {}", name); + return; + } + final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); + final Unit unitFromPayload = inbound.units.stream().filter(ufp -> ufp.base == targetUnit.base).findFirst().orElse(null); + if (unitFromPayload == null) { + log.error("Unit mismatch: fromTopic={}, fromPayload=[{}]", targetUnit, inbound.getUnits().stream().map(Enum::name).collect(Collectors.joining(","))); + return; + } + final Value value = new Value(inbound.value, unitFromPayload); + seriesService.receive(new SeriesInbound(name, inbound.date, value.as(targetUnit))); + } + + private String propertyWorkaround(final String property) { + if ("iaq_equivalent_voc".equals(property)) { + return "iaq_voc_equivalent"; + } + return property; + } + + @Getter + @ToString + private static class Inbound { + + @NonNull + public final ZonedDateTime date; + + public final double value; + + @NonNull + @JsonDeserialize(using = UnitListDeserializer.class) + public final List units; + + public Inbound(final long timestamp, final double value, @JsonProperty("unit") @NonNull final List units) { + this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); + this.value = value; + this.units = units; + } + + } + +} diff --git a/src/main/java/de/ph87/data/message/handler/SimpleJsonHandler.java b/src/main/java/de/ph87/data/message/handler/SimpleJsonHandler.java index a81547a..700540f 100644 --- a/src/main/java/de/ph87/data/message/handler/SimpleJsonHandler.java +++ b/src/main/java/de/ph87/data/message/handler/SimpleJsonHandler.java @@ -1,58 +1,55 @@ package de.ph87.data.message.handler; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.ph87.data.message.IMessageHandler; -import de.ph87.data.message.Message; -import de.ph87.data.series.SeriesInbound; -import de.ph87.data.series.SeriesService; -import de.ph87.data.unit.Unit; -import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.ToString; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import com.fasterxml.jackson.databind.*; +import de.ph87.data.message.*; +import de.ph87.data.series.*; +import de.ph87.data.unit.*; +import lombok.*; +import lombok.extern.slf4j.*; +import org.springframework.stereotype.*; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; @Slf4j @Service @RequiredArgsConstructor public class SimpleJsonHandler implements IMessageHandler { - + private final ObjectMapper objectMapper; - + private final SeriesService seriesService; - + @Override - public void handle(@NonNull final Message message) { - try { - final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); - seriesService.receive(new SeriesInbound(message.topic, inbound.date, inbound.value, inbound.unit)); - } catch (JsonProcessingException e) { - log.debug("Failed to parse inbound message: topic={}, message={}, error={}", message.topic, message, e.toString()); + public void handle(@NonNull final Message message) throws Exception { + if (!message.topic.endsWith("/SimpleJson")) { + return; } + final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); + seriesService.receive(new SeriesInbound(inbound.name, inbound.date, inbound.value, inbound.unit)); } - + @Getter @ToString private static class Inbound { - + + @NonNull + public final String name; + + @NonNull public final ZonedDateTime date; - + public final double value; - + + @NonNull public final Unit unit; - - public Inbound(final long timestamp, final double value, final Unit unit) { + + public Inbound(@NonNull final String name, final long timestamp, final double value, @NonNull final Unit unit) { + this.name = name; this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); this.value = value; this.unit = unit; } - + } - + } diff --git a/src/main/java/de/ph87/data/message/handler/SmartMeterHandler.java b/src/main/java/de/ph87/data/message/handler/SmartMeterHandler.java new file mode 100644 index 0000000..635b9ed --- /dev/null +++ b/src/main/java/de/ph87/data/message/handler/SmartMeterHandler.java @@ -0,0 +1,56 @@ +package de.ph87.data.message.handler; + +import com.fasterxml.jackson.databind.*; +import de.ph87.data.message.*; +import de.ph87.data.series.*; +import de.ph87.data.unit.Value; +import de.ph87.data.unit.*; +import lombok.*; +import lombok.extern.slf4j.*; +import org.springframework.stereotype.*; + +import java.time.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SmartMeterHandler implements IMessageHandler { + + private final ObjectMapper objectMapper; + + private final SeriesService seriesService; + + @Override + public void handle(@NonNull final Message message) throws Exception { + if (!"electricity/grid/json".equals(message.topic)) { + return; + } + final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); + seriesService.receive(new SeriesInbound("energy/purchased", inbound.date, inbound.energyPurchased)); + seriesService.receive(new SeriesInbound("energy/delivered", inbound.date, inbound.energyDelivered)); + seriesService.receive(new SeriesInbound("power/difference", inbound.date, inbound.powerDifference)); + } + + @Getter + @ToString + private static class Inbound { + + @NonNull + public final ZonedDateTime date; + + public final Value energyPurchased; + + public final Value energyDelivered; + + public final Value powerDifference; + + public Inbound(final long timestamp, final double purchaseWh, final double deliveryWh, final double powerW) { + this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); + this.energyPurchased = new Value(purchaseWh, Unit.ENERGY_WH); + this.energyDelivered = new Value(deliveryWh, Unit.ENERGY_WH); + this.powerDifference = new Value(powerW, Unit.POWER_W); + } + + } + +} diff --git a/src/main/java/de/ph87/data/message/receive/mqtt/MqttReceiver.java b/src/main/java/de/ph87/data/message/receive/mqtt/MqttReceiver.java index 0a64539..49f38f8 100644 --- a/src/main/java/de/ph87/data/message/receive/mqtt/MqttReceiver.java +++ b/src/main/java/de/ph87/data/message/receive/mqtt/MqttReceiver.java @@ -1,5 +1,6 @@ package de.ph87.data.message.receive.mqtt; +import de.ph87.data.message.*; import jakarta.annotation.*; import lombok.*; import lombok.extern.slf4j.*; @@ -19,6 +20,8 @@ public class MqttReceiver { private final MqttConfig config; + private final MessageService messageService; + @Nullable private MqttClient client; @@ -108,8 +111,9 @@ public class MqttReceiver { private void _receive(final String topic, final MqttMessage mqttMessage) { Thread.currentThread().setName("MQTT-RECEIVE"); final String payload = new String(mqttMessage.getPayload(), StandardCharsets.UTF_8); - final String payloadLoggable = payload.replace("\n", "\\n").replace("\r", "\\r"); - log.debug("received: topic={}, message={}", topic, payloadLoggable); + final Message message = new Message(topic, payload); + log.debug("received: {}", message); + messageService.handle(message); } } diff --git a/src/main/java/de/ph87/data/series/Series.java b/src/main/java/de/ph87/data/series/Series.java index 2ee11cd..24c330c 100644 --- a/src/main/java/de/ph87/data/series/Series.java +++ b/src/main/java/de/ph87/data/series/Series.java @@ -1,34 +1,39 @@ package de.ph87.data.series; -import de.ph87.data.unit.Unit; +import de.ph87.data.unit.*; import jakarta.persistence.*; import lombok.*; -import java.util.UUID; - @Entity @Getter @ToString @NoArgsConstructor public class Series { - + @Id - private String uuid = UUID.randomUUID().toString(); - + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Setter + @NonNull + @Column(nullable = false, unique = true) + private String name; + @Setter @NonNull @Column(nullable = false, unique = true) private String title; - + @Setter @NonNull @Column(nullable = false) @Enumerated(EnumType.STRING) private Unit unit; - - @Setter - @NonNull - @Column(nullable = false, unique = true) - private String source; - + + public Series(@NonNull final String name, @NonNull final Unit unit) { + this.name = name; + this.title = name; + this.unit = unit; + } + } diff --git a/src/main/java/de/ph87/data/series/SeriesDto.java b/src/main/java/de/ph87/data/series/SeriesDto.java index b6727e9..0e4d598 100644 --- a/src/main/java/de/ph87/data/series/SeriesDto.java +++ b/src/main/java/de/ph87/data/series/SeriesDto.java @@ -1,27 +1,25 @@ package de.ph87.data.series; -import de.ph87.data.unit.Unit; -import lombok.Getter; -import lombok.NonNull; -import lombok.ToString; +import de.ph87.data.unit.*; +import lombok.*; @Getter @ToString public class SeriesDto { - - public final String uuid; - + + public final long id; + + public final String name; + public final String title; - + public final Unit unit; - - public final String source; - + public SeriesDto(@NonNull final Series series) { - this.uuid = series.getUuid(); + this.id = series.getId(); + this.name = series.getName(); this.title = series.getTitle(); this.unit = series.getUnit(); - this.source = series.getSource(); } - + } diff --git a/src/main/java/de/ph87/data/series/SeriesInbound.java b/src/main/java/de/ph87/data/series/SeriesInbound.java index 874641d..a5b4649 100644 --- a/src/main/java/de/ph87/data/series/SeriesInbound.java +++ b/src/main/java/de/ph87/data/series/SeriesInbound.java @@ -1,32 +1,33 @@ package de.ph87.data.series; -import de.ph87.data.unit.Unit; -import lombok.Getter; -import lombok.NonNull; -import lombok.ToString; +import de.ph87.data.unit.Value; +import de.ph87.data.unit.*; +import lombok.*; -import java.time.ZonedDateTime; +import java.time.*; @Getter @ToString public class SeriesInbound { - + @NonNull public final String name; - + @NonNull public final ZonedDateTime date; - - public final double value; - - @NonNull - public final Unit unit; - - public SeriesInbound(@NonNull final String name, @NonNull final ZonedDateTime date, final double value, @NonNull final Unit unit) { + + public final Value value; + + public SeriesInbound(@NonNull final String name, @NonNull final ZonedDateTime date, final Value value) { this.name = name; this.date = date; this.value = value; - this.unit = unit; } - + + public SeriesInbound(@NonNull final String name, @NonNull final ZonedDateTime date, final double value, @NonNull final Unit unit) { + this.name = name; + this.date = date; + this.value = new Value(value, unit); + } + } diff --git a/src/main/java/de/ph87/data/series/SeriesRepository.java b/src/main/java/de/ph87/data/series/SeriesRepository.java index 47d02cc..9d693c5 100644 --- a/src/main/java/de/ph87/data/series/SeriesRepository.java +++ b/src/main/java/de/ph87/data/series/SeriesRepository.java @@ -1,7 +1,12 @@ package de.ph87.data.series; -import org.springframework.data.repository.ListCrudRepository; +import lombok.*; +import org.springframework.data.repository.*; -public interface SeriesRepository extends ListCrudRepository { +import java.util.*; +public interface SeriesRepository extends ListCrudRepository { + + Optional findByName(@NonNull String name); + } diff --git a/src/main/java/de/ph87/data/series/SeriesService.java b/src/main/java/de/ph87/data/series/SeriesService.java index 0047537..7a5c716 100644 --- a/src/main/java/de/ph87/data/series/SeriesService.java +++ b/src/main/java/de/ph87/data/series/SeriesService.java @@ -1,81 +1,62 @@ package de.ph87.data.series; -import de.ph87.data.series.entry.EntryService; -import jakarta.annotation.PostConstruct; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import de.ph87.data.*; +import de.ph87.data.series.entry.*; +import de.ph87.data.unit.*; +import lombok.*; +import lombok.extern.slf4j.*; +import org.springframework.stereotype.*; +import org.springframework.transaction.annotation.*; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; +import java.util.function.*; @Slf4j @Service @Transactional @RequiredArgsConstructor public class SeriesService { - - private final Map sources = new HashMap<>(); - + private final SeriesRepository seriesRepository; - + private final EntryService entryService; - - @PostConstruct - public void cacheInit() { - synchronized (sources) { - seriesRepository.findAll().forEach(this::cachePut); - } - } - - private void cachePut(@NonNull final Series series) { - synchronized (sources) { - sources.put(series.getUuid(), series.getSource()); - } - } - - private void cacheRemove(final Series series) { - synchronized (sources) { - sources.remove(series.getUuid()); - } - } - - public SeriesDto modify(@NonNull final String uuid, @NonNull final Consumer modifier) { - final Series series = getByUuid(uuid); + + public SeriesDto modify(@NonNull final long id, @NonNull final Consumer modifier) { + final Series series = getById(id); modifier.accept(series); - cachePut(series); return publish(series, Action.CHANGED); } - - public void delete(@NonNull final String uuid) { - final Series series = getByUuid(uuid); - cacheRemove(series); + + public void delete(@NonNull final long id) { + final Series series = getById(id); } - - private Series getByUuid(@NonNull final String uuid) { - return seriesRepository.findById(uuid).orElseThrow(); + + private Series getById(@NonNull final long id) { + return seriesRepository.findById(id).orElseThrow(); } - + private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) { final SeriesDto dto = toDto(series); log.info("Series {}: {}", action, series); return dto; } - + private SeriesDto toDto(@NonNull final Series series) { return new SeriesDto(series); } - - public void receive(@NonNull final SeriesInbound measure) { - final String uuid = sources.get(measure.name); - if (uuid == null) { - return; - } - final Series series = getByUuid(uuid); + + public void receive(@NonNull final SeriesInbound measure) throws Unit.NotConvertible { + final Series series = getOrCreate(measure.name, measure.value.unit); entryService.write(series, measure); } - + + private Series getOrCreate(@NonNull final String name, @NonNull final Unit unit) { + return seriesRepository + .findByName(name) + .orElseGet(() -> { + final Series series = seriesRepository.save(new Series(name, unit)); + publish(series, Action.CREATED); + return series; + }); + } + } diff --git a/src/main/java/de/ph87/data/series/entry/Entry.java b/src/main/java/de/ph87/data/series/entry/Entry.java index e82ed77..1e50051 100644 --- a/src/main/java/de/ph87/data/series/entry/Entry.java +++ b/src/main/java/de/ph87/data/series/entry/Entry.java @@ -1,40 +1,45 @@ package de.ph87.data.series.entry; -import de.ph87.data.series.Series; +import de.ph87.data.series.*; +import de.ph87.data.unit.Value; +import de.ph87.data.unit.*; import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.ToString; +import lombok.*; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; +import java.time.*; +import java.time.temporal.*; @Entity @Getter @ToString @NoArgsConstructor +@Table( + uniqueConstraints = { + @UniqueConstraint(columnNames = {"series_id", "date"}) + } +) public class Entry { - + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; - + @NonNull @ManyToOne private Series series; - + @NonNull @Column(nullable = false) private ZonedDateTime date; - @Column(nullable = false,name = "`value`") + @Setter + @Column(nullable = false, name = "`value`") private double value; - - public Entry(@NonNull final Series series, @NonNull final ZonedDateTime date, final double value) { + + public Entry(@NonNull final Series series, @NonNull final ZonedDateTime date, final Value value) throws Unit.NotConvertible { this.series = series; this.date = date.truncatedTo(ChronoUnit.MINUTES); - this.value = value; + this.value = value.as(series.getUnit()).value; } - + } diff --git a/src/main/java/de/ph87/data/series/entry/EntryRepository.java b/src/main/java/de/ph87/data/series/entry/EntryRepository.java index 5b79c1b..c713b4f 100644 --- a/src/main/java/de/ph87/data/series/entry/EntryRepository.java +++ b/src/main/java/de/ph87/data/series/entry/EntryRepository.java @@ -1,7 +1,14 @@ package de.ph87.data.series.entry; -import org.springframework.data.repository.ListCrudRepository; +import de.ph87.data.series.*; +import lombok.*; +import org.springframework.data.repository.*; + +import java.time.*; +import java.util.*; public interface EntryRepository extends ListCrudRepository { - + + Optional findBySeriesAndDate(@NonNull Series series, @NonNull ZonedDateTime truncated); + } diff --git a/src/main/java/de/ph87/data/series/entry/EntryService.java b/src/main/java/de/ph87/data/series/entry/EntryService.java index e0d6426..2792a7a 100644 --- a/src/main/java/de/ph87/data/series/entry/EntryService.java +++ b/src/main/java/de/ph87/data/series/entry/EntryService.java @@ -1,23 +1,29 @@ package de.ph87.data.series.entry; -import de.ph87.data.series.Series; -import de.ph87.data.series.SeriesInbound; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import de.ph87.data.series.*; +import de.ph87.data.unit.*; +import lombok.*; +import lombok.extern.slf4j.*; +import org.springframework.stereotype.*; +import org.springframework.transaction.annotation.*; + +import java.time.*; +import java.time.temporal.*; @Slf4j @Service @Transactional @RequiredArgsConstructor public class EntryService { - + private final EntryRepository entryRepository; - - public void write(@NonNull final Series series, @NonNull final SeriesInbound measure) { - // TODO + + public void write(@NonNull final Series series, @NonNull final SeriesInbound measure) throws Unit.NotConvertible { + final ZonedDateTime truncated = measure.date.truncatedTo(ChronoUnit.MINUTES); + if (entryRepository.findBySeriesAndDate(series, truncated).isEmpty()) { + final Entry created = entryRepository.save(new Entry(series, truncated, measure.value)); + log.debug("Created: {}", created); + } } - + } diff --git a/src/main/java/de/ph87/data/unit/Unit.java b/src/main/java/de/ph87/data/unit/Unit.java index 0be711f..f6ecad4 100644 --- a/src/main/java/de/ph87/data/unit/Unit.java +++ b/src/main/java/de/ph87/data/unit/Unit.java @@ -1,6 +1,6 @@ package de.ph87.data.unit; -import lombok.NonNull; +import lombok.*; public enum Unit { TEMPERATURE_C("°C"), @@ -15,24 +15,36 @@ public enum Unit { POWER_KW("kW", 1000, POWER_W), ENERGY_WH("W"), ENERGY_KWH("kWh", 1000, ENERGY_WH), + IAQ("IAQ"), + IAQ_CO2_EQUIVALENT("ppm"), + IAQ_VOC_EQUIVALENT("ppm"), + SUN_DC("Δ°C"), ; - + public final String unit; - + public final double factor; - + public final Unit base; - + Unit(@NonNull final String unit) { this.unit = unit; this.factor = 1.0; this.base = this; } - - Unit(@NonNull final String unit, double factor, @NonNull Unit base) { + + Unit(@NonNull final String unit, final double factor, @NonNull final Unit base) { this.unit = unit; this.factor = factor; this.base = base; } - + + public static class NotConvertible extends Exception { + + public NotConvertible(final @NonNull Unit source, final Unit target) { + super("Cannot convert Units: source=%s, target=%s".formatted(source, target)); + } + + } + } diff --git a/src/main/java/de/ph87/data/unit/UnitListDeserializer.java b/src/main/java/de/ph87/data/unit/UnitListDeserializer.java new file mode 100644 index 0000000..09fcee2 --- /dev/null +++ b/src/main/java/de/ph87/data/unit/UnitListDeserializer.java @@ -0,0 +1,17 @@ +package de.ph87.data.unit; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; + +import java.io.*; +import java.util.*; + +public class UnitListDeserializer extends JsonDeserializer> { + + @Override + public List deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException { + final String name = jsonParser.getValueAsString(); + return Arrays.stream(Unit.values()).filter(unit -> unit.unit.equals(name)).toList(); + } + +} diff --git a/src/main/java/de/ph87/data/unit/Value.java b/src/main/java/de/ph87/data/unit/Value.java new file mode 100644 index 0000000..6ae55c3 --- /dev/null +++ b/src/main/java/de/ph87/data/unit/Value.java @@ -0,0 +1,23 @@ +package de.ph87.data.unit; + +import lombok.*; + +public class Value { + + public final double value; + + public final Unit unit; + + public Value(final double value, @NonNull final Unit unit) { + this.value = value; + this.unit = unit; + } + + public Value as(@NonNull final Unit target) throws Unit.NotConvertible { + if (this.unit.base != target.base) { + throw new Unit.NotConvertible(this.unit, target); + } + return new Value(value * this.unit.factor / target.factor, target); + } + +}