code clean

This commit is contained in:
Patrick Haßel 2025-02-24 11:45:52 +01:00
parent 908134600c
commit e005f1ef8c
23 changed files with 256 additions and 268 deletions

View File

@ -5,9 +5,9 @@ import org.springframework.boot.autoconfigure.*;
@SpringBootApplication @SpringBootApplication
public class Backend { public class Backend {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(Backend.class, args); SpringApplication.run(Backend.class, args);
} }
} }

View File

@ -3,7 +3,7 @@ package de.ph87.data.message;
import lombok.*; import lombok.*;
public interface IMessageHandler { public interface IMessageHandler {
void handle(@NonNull final Message message) throws Exception; void handle(@NonNull final Message message) throws Exception;
} }

View File

@ -5,18 +5,18 @@ import lombok.*;
@Getter @Getter
@ToString @ToString
public class Message { public class Message {
public final String topic; public final String topic;
@ToString.Exclude @ToString.Exclude
public final String payload; public final String payload;
public final String payloadLoggable; public final String payloadLoggable;
public Message(final String topic, final String payload) { public Message(final String topic, final String payload) {
this.topic = topic; this.topic = topic;
this.payload = payload; this.payload = payload;
this.payloadLoggable = payload.replace("\n", "\\n").replace("\r", "\\r"); this.payloadLoggable = payload.replace("\n", "\\n").replace("\r", "\\r");
} }
} }

View File

@ -10,9 +10,9 @@ import java.util.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class MessageService { public class MessageService {
private final List<IMessageHandler> messageHandlers; private final List<IMessageHandler> messageHandlers;
public void handle(@NonNull final Message message) { public void handle(@NonNull final Message message) {
messageHandlers.forEach(handler -> { messageHandlers.forEach(handler -> {
try { try {
@ -22,5 +22,5 @@ public class MessageService {
} }
}); });
} }
} }

View File

@ -6,8 +6,7 @@ import com.fasterxml.jackson.databind.annotation.*;
import de.ph87.data.message.*; import de.ph87.data.message.*;
import de.ph87.data.series.*; import de.ph87.data.series.*;
import de.ph87.data.series.entry.*; import de.ph87.data.series.entry.*;
import de.ph87.data.unit.Value; import de.ph87.data.value.Value;
import de.ph87.data.unit.*;
import lombok.*; import lombok.*;
import lombok.extern.slf4j.*; import lombok.extern.slf4j.*;
import org.springframework.stereotype.*; import org.springframework.stereotype.*;
@ -21,13 +20,13 @@ import java.util.stream.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class EspHomeHandler implements IMessageHandler { public class EspHomeHandler implements IMessageHandler {
private static final Pattern REGEX = Pattern.compile("^(?<area>\\w+)/sensor/(?<property>\\w+)$"); private static final Pattern REGEX = Pattern.compile("^(?<area>\\w+)/sensor/(?<property>\\w+)$");
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final EntryService entryService; private final EntryService entryService;
@Override @Override
public void handle(@NonNull final Message message) throws Exception { public void handle(@NonNull final Message message) throws Exception {
final Matcher matcher = REGEX.matcher(message.topic); final Matcher matcher = REGEX.matcher(message.topic);
@ -37,15 +36,15 @@ public class EspHomeHandler implements IMessageHandler {
final String area = matcher.group("area"); final String area = matcher.group("area");
final String property = propertyReplace(matcher.group("property")); final String property = propertyReplace(matcher.group("property"));
final String name = "%s/%s".formatted(area, property.replace("_", "/")); final String name = "%s/%s".formatted(area, property.replace("_", "/"));
final Unit targetUnit = switch (property) { final Value.Unit targetUnit = switch (property) {
case "iaq" -> Unit.IAQ; case "iaq" -> Value.Unit.IAQ;
case "iaq_co2" -> Unit.IAQ_CO2_EQUIVALENT; case "iaq_co2" -> Value.Unit.IAQ_CO2_EQUIVALENT;
case "iaq_voc" -> Unit.IAQ_VOC_EQUIVALENT; case "iaq_voc" -> Value.Unit.IAQ_VOC_EQUIVALENT;
case "pressure" -> Unit.PRESSURE_HPA; case "pressure" -> Value.Unit.PRESSURE_HPA;
case "temperature" -> Unit.TEMPERATURE_C; case "temperature" -> Value.Unit.TEMPERATURE_C;
case "humidity_relative" -> Unit.HUMIDITY_RELATIVE_PERCENT; case "humidity_relative" -> Value.Unit.HUMIDITY_RELATIVE_PERCENT;
case "humidity_absolute" -> Unit.HUMIDITY_ABSOLUTE_GM3; case "humidity_absolute" -> Value.Unit.HUMIDITY_ABSOLUTE_GM3;
case "sun" -> Unit.SUN_DC; case "sun" -> Value.Unit.SUN_DC;
default -> null; default -> null;
}; };
if (targetUnit == null) { if (targetUnit == null) {
@ -53,7 +52,7 @@ public class EspHomeHandler implements IMessageHandler {
return; return;
} }
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
final Unit unitFromPayload = inbound.units.stream().filter(ufp -> ufp.base == targetUnit.base).findFirst().orElse(null); final Value.Unit unitFromPayload = inbound.units.stream().filter(ufp -> ufp.base == targetUnit.base).findFirst().orElse(null);
if (unitFromPayload == null) { if (unitFromPayload == null) {
log.error("Unit mismatch: fromTopic={}, fromPayload=[{}]", targetUnit, inbound.getUnits().stream().map(Enum::name).collect(Collectors.joining(","))); log.error("Unit mismatch: fromTopic={}, fromPayload=[{}]", targetUnit, inbound.getUnits().stream().map(Enum::name).collect(Collectors.joining(",")));
return; return;
@ -61,7 +60,7 @@ public class EspHomeHandler implements IMessageHandler {
final Value value = new Value(inbound.value, unitFromPayload); final Value value = new Value(inbound.value, unitFromPayload);
entryService.receive(new SeriesInbound(name, inbound.date, value.as(targetUnit))); entryService.receive(new SeriesInbound(name, inbound.date, value.as(targetUnit)));
} }
private String propertyReplace(final String property) { private String propertyReplace(final String property) {
if ("iaq_co2_equivalent".equals(property)) { if ("iaq_co2_equivalent".equals(property)) {
return "iaq_co2"; return "iaq_co2";
@ -71,26 +70,26 @@ public class EspHomeHandler implements IMessageHandler {
} }
return property; return property;
} }
@Getter @Getter
@ToString @ToString
private static class Inbound { private static class Inbound {
@NonNull @NonNull
public final ZonedDateTime date; public final ZonedDateTime date;
public final double value; public final double value;
@NonNull @NonNull
@JsonDeserialize(using = UnitListDeserializer.class) @JsonDeserialize(using = Value.Unit.ListDeserializer.class)
public final List<Unit> units; public final List<Value.Unit> units;
public Inbound(final long timestamp, final double value, @JsonProperty("unit") @NonNull final List<Unit> units) { public Inbound(final long timestamp, final double value, @JsonProperty("unit") @NonNull final List<Value.Unit> units) {
this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
this.value = value; this.value = value;
this.units = units; this.units = units;
} }
} }
} }

View File

@ -4,8 +4,7 @@ import com.fasterxml.jackson.databind.*;
import de.ph87.data.message.*; import de.ph87.data.message.*;
import de.ph87.data.series.*; import de.ph87.data.series.*;
import de.ph87.data.series.entry.*; import de.ph87.data.series.entry.*;
import de.ph87.data.unit.Value; import de.ph87.data.value.Value;
import de.ph87.data.unit.*;
import lombok.*; import lombok.*;
import lombok.extern.slf4j.*; import lombok.extern.slf4j.*;
import org.springframework.stereotype.*; import org.springframework.stereotype.*;
@ -17,13 +16,13 @@ import java.util.regex.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class HeizungHandler implements IMessageHandler { public class HeizungHandler implements IMessageHandler {
private static final Pattern REGEX = Pattern.compile("^aggregation/(?<property>heizung/.+)/temperatur$"); private static final Pattern REGEX = Pattern.compile("^aggregation/(?<property>heizung/.+)/temperatur$");
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final EntryService entryService; private final EntryService entryService;
@Override @Override
public void handle(@NonNull final Message message) throws Exception { public void handle(@NonNull final Message message) throws Exception {
final Matcher matcher = REGEX.matcher(message.topic); final Matcher matcher = REGEX.matcher(message.topic);
@ -34,21 +33,21 @@ public class HeizungHandler implements IMessageHandler {
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
entryService.receive(new SeriesInbound(property, inbound.date, inbound.value)); entryService.receive(new SeriesInbound(property, inbound.date, inbound.value));
} }
@Getter @Getter
@ToString @ToString
private static class Inbound { private static class Inbound {
@NonNull @NonNull
public final ZonedDateTime date; public final ZonedDateTime date;
public final Value value; public final Value value;
public Inbound(final long timestamp, final double sum, final int count) { public Inbound(final long timestamp, final double sum, final int count) {
this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
this.value = new Value(sum / count, Unit.TEMPERATURE_C); this.value = new Value(sum / count, Value.Unit.TEMPERATURE_C);
} }
} }
} }

View File

@ -4,8 +4,7 @@ import com.fasterxml.jackson.databind.*;
import de.ph87.data.message.*; import de.ph87.data.message.*;
import de.ph87.data.series.*; import de.ph87.data.series.*;
import de.ph87.data.series.entry.*; import de.ph87.data.series.entry.*;
import de.ph87.data.unit.Value; import de.ph87.data.value.Value;
import de.ph87.data.unit.*;
import lombok.*; import lombok.*;
import lombok.extern.slf4j.*; import lombok.extern.slf4j.*;
import org.springframework.stereotype.*; import org.springframework.stereotype.*;
@ -16,38 +15,38 @@ import java.time.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class OpenDTUHandler implements IMessageHandler { public class OpenDTUHandler implements IMessageHandler {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final EntryService entryService; private final EntryService entryService;
@Override @Override
public void handle(final @NonNull Message message) throws Exception { public void handle(final @NonNull Message message) throws Exception {
if (!"openDTU/pv/patrix/json".equals(message.topic)) { if (!"openDTU/pv/patrix/json".equals(message.topic)) {
return; return;
} }
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
entryService.receive(new SeriesInbound("electricity/energy/produced", inbound.date, inbound.energy.as(Unit.ENERGY_KWH))); entryService.receive(new SeriesInbound("electricity/energy/produced", inbound.date, inbound.energy.as(Value.Unit.ENERGY_KWH)));
entryService.receive(new SeriesInbound("electricity/power/produced", inbound.date, inbound.power.as(Unit.POWER_W))); entryService.receive(new SeriesInbound("electricity/power/produced", inbound.date, inbound.power.as(Value.Unit.POWER_W)));
} }
@Getter @Getter
@ToString @ToString
private static class Inbound { private static class Inbound {
@NonNull @NonNull
public final ZonedDateTime date; public final ZonedDateTime date;
public final Value energy; public final Value energy;
public final Value power; public final Value power;
public Inbound(final long timestamp, final double energyProducedKWh, final double powerW) { public Inbound(final long timestamp, final double energyProducedKWh, final double powerW) {
this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
this.energy = new Value(energyProducedKWh, Unit.ENERGY_KWH); this.energy = new Value(energyProducedKWh, Value.Unit.ENERGY_KWH);
this.power = new Value(powerW, Unit.POWER_W); this.power = new Value(powerW, Value.Unit.POWER_W);
} }
} }
} }

View File

@ -4,8 +4,7 @@ import com.fasterxml.jackson.databind.*;
import de.ph87.data.message.*; import de.ph87.data.message.*;
import de.ph87.data.series.*; import de.ph87.data.series.*;
import de.ph87.data.series.entry.*; import de.ph87.data.series.entry.*;
import de.ph87.data.unit.Value; import de.ph87.data.value.Value;
import de.ph87.data.unit.*;
import lombok.*; import lombok.*;
import lombok.extern.slf4j.*; import lombok.extern.slf4j.*;
import org.springframework.stereotype.*; import org.springframework.stereotype.*;
@ -16,11 +15,11 @@ import java.time.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SimpleJsonHandler implements IMessageHandler { public class SimpleJsonHandler implements IMessageHandler {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final EntryService entryService; private final EntryService entryService;
@Override @Override
public void handle(@NonNull final Message message) throws Exception { public void handle(@NonNull final Message message) throws Exception {
if (!message.topic.endsWith("/SimpleJson")) { if (!message.topic.endsWith("/SimpleJson")) {
@ -29,25 +28,25 @@ public class SimpleJsonHandler implements IMessageHandler {
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
entryService.receive(new SeriesInbound(inbound.name, inbound.date, inbound.value)); entryService.receive(new SeriesInbound(inbound.name, inbound.date, inbound.value));
} }
@Getter @Getter
@ToString @ToString
private static class Inbound { private static class Inbound {
@NonNull @NonNull
public final String name; public final String name;
@NonNull @NonNull
public final ZonedDateTime date; public final ZonedDateTime date;
public final Value value; public final Value value;
public Inbound(@NonNull final String name, final long timestamp, final double value, @NonNull final Unit unit) { public Inbound(@NonNull final String name, final long timestamp, final double value, @NonNull final Value.Unit unit) {
this.name = name; this.name = name;
this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
this.value = new Value(value, unit); this.value = new Value(value, unit);
} }
} }
} }

View File

@ -4,8 +4,7 @@ import com.fasterxml.jackson.databind.*;
import de.ph87.data.message.*; import de.ph87.data.message.*;
import de.ph87.data.series.*; import de.ph87.data.series.*;
import de.ph87.data.series.entry.*; import de.ph87.data.series.entry.*;
import de.ph87.data.unit.Value; import de.ph87.data.value.Value;
import de.ph87.data.unit.*;
import lombok.*; import lombok.*;
import lombok.extern.slf4j.*; import lombok.extern.slf4j.*;
import org.springframework.stereotype.*; import org.springframework.stereotype.*;
@ -16,42 +15,42 @@ import java.time.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SmartMeterHandler implements IMessageHandler { public class SmartMeterHandler implements IMessageHandler {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final EntryService entryService; private final EntryService entryService;
@Override @Override
public void handle(@NonNull final Message message) throws Exception { public void handle(@NonNull final Message message) throws Exception {
if (!"electricity/grid/json".equals(message.topic)) { if (!"electricity/grid/json".equals(message.topic)) {
return; return;
} }
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
entryService.receive(new SeriesInbound("electricity/energy/purchased", inbound.date, inbound.energyPurchased.as(Unit.ENERGY_KWH))); entryService.receive(new SeriesInbound("electricity/energy/purchased", inbound.date, inbound.energyPurchased.as(Value.Unit.ENERGY_KWH)));
entryService.receive(new SeriesInbound("electricity/energy/delivered", inbound.date, inbound.energyDelivered.as(Unit.ENERGY_KWH))); entryService.receive(new SeriesInbound("electricity/energy/delivered", inbound.date, inbound.energyDelivered.as(Value.Unit.ENERGY_KWH)));
entryService.receive(new SeriesInbound("electricity/power/difference", inbound.date, inbound.powerDifference.as(Unit.POWER_W))); entryService.receive(new SeriesInbound("electricity/power/difference", inbound.date, inbound.powerDifference.as(Value.Unit.POWER_W)));
} }
@Getter @Getter
@ToString @ToString
private static class Inbound { private static class Inbound {
@NonNull @NonNull
public final ZonedDateTime date; public final ZonedDateTime date;
public final Value energyPurchased; public final Value energyPurchased;
public final Value energyDelivered; public final Value energyDelivered;
public final Value powerDifference; public final Value powerDifference;
public Inbound(final long timestamp, final double purchaseWh, final double deliveryWh, final double powerW) { public Inbound(final long timestamp, final double purchaseWh, final double deliveryWh, final double powerW) {
this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
this.energyPurchased = new Value(purchaseWh, Unit.ENERGY_WH); this.energyPurchased = new Value(purchaseWh, Value.Unit.ENERGY_WH);
this.energyDelivered = new Value(deliveryWh, Unit.ENERGY_WH); this.energyDelivered = new Value(deliveryWh, Value.Unit.ENERGY_WH);
this.powerDifference = new Value(powerW, Unit.POWER_W); this.powerDifference = new Value(powerW, Value.Unit.POWER_W);
} }
} }
} }

View File

@ -8,19 +8,19 @@ import org.springframework.stereotype.*;
@Component @Component
@ConfigurationProperties(prefix = "de.ph87.data.message.receive.mqtt") @ConfigurationProperties(prefix = "de.ph87.data.message.receive.mqtt")
public class MqttConfig { public class MqttConfig {
private String host; private String host;
private int port = 1883; private int port = 1883;
private String topic; private String topic;
private String clientId; private String clientId;
private int connectTimeoutSec = 3; private int connectTimeoutSec = 3;
private long retrySec = 3; private long retrySec = 3;
private int connectKeepAliveSec = 3; private int connectKeepAliveSec = 3;
} }

View File

@ -17,23 +17,23 @@ import java.util.*;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class MqttReceiver { public class MqttReceiver {
private final MqttConfig config; private final MqttConfig config;
private final MessageService messageService; private final MessageService messageService;
@Nullable @Nullable
private MqttClient client; private MqttClient client;
private final Thread thread = new Thread(this::run, "MQTT-WATCH"); private final Thread thread = new Thread(this::run, "MQTT-WATCH");
private boolean stop = false; private boolean stop = false;
@EventListener(ApplicationStartedEvent.class) @EventListener(ApplicationStartedEvent.class)
public void startup() { public void startup() {
thread.start(); thread.start();
} }
@PreDestroy @PreDestroy
public void preDestroy() { public void preDestroy() {
synchronized (thread) { synchronized (thread) {
@ -42,7 +42,7 @@ public class MqttReceiver {
log.debug("stopping..."); log.debug("stopping...");
} }
} }
private void run() { private void run() {
try { try {
log.debug("started"); log.debug("started");
@ -63,7 +63,7 @@ public class MqttReceiver {
log.debug("terminated"); log.debug("terminated");
} }
} }
private void _connect() throws InterruptedException { private void _connect() throws InterruptedException {
final String clientId; final String clientId;
final boolean cleanSession; final boolean cleanSession;
@ -97,7 +97,7 @@ public class MqttReceiver {
} }
} }
} }
private void _disconnect() { private void _disconnect() {
if (client != null && client.isConnected()) { if (client != null && client.isConnected()) {
try { try {
@ -107,7 +107,7 @@ public class MqttReceiver {
} }
} }
} }
private void _receive(final String topic, final MqttMessage mqttMessage) { private void _receive(final String topic, final MqttMessage mqttMessage) {
Thread.currentThread().setName("MQTT-RECEIVE"); Thread.currentThread().setName("MQTT-RECEIVE");
final String payload = new String(mqttMessage.getPayload(), StandardCharsets.UTF_8); final String payload = new String(mqttMessage.getPayload(), StandardCharsets.UTF_8);
@ -115,5 +115,5 @@ public class MqttReceiver {
log.debug("received: {}", message); log.debug("received: {}", message);
messageService.handle(message); messageService.handle(message);
} }
} }

View File

@ -1,6 +1,6 @@
package de.ph87.data.series; package de.ph87.data.series;
import de.ph87.data.unit.*; import de.ph87.data.value.Value;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@ -9,35 +9,35 @@ import lombok.*;
@ToString @ToString
@NoArgsConstructor @NoArgsConstructor
public class Series { public class Series {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id; private long id;
@Setter @Setter
@NonNull @NonNull
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
private String name; private String name;
@Setter @Setter
@NonNull @NonNull
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
private String title; private String title;
@Setter @Setter
@NonNull @NonNull
@Column(nullable = false) @Column(nullable = false)
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private Unit unit; private Value.Unit unit;
@Setter @Setter
@Column(nullable = false) @Column(nullable = false)
private int decimals = 1; private int decimals = 1;
public Series(@NonNull final String name, @NonNull final Unit unit) { public Series(@NonNull final String name, @NonNull final Value.Unit unit) {
this.name = name; this.name = name;
this.title = name; this.title = name;
this.unit = unit; this.unit = unit;
} }
} }

View File

@ -1,23 +1,23 @@
package de.ph87.data.series; package de.ph87.data.series;
import de.ph87.data.unit.*; import de.ph87.data.value.Value;
import jakarta.annotation.*; import jakarta.annotation.*;
import lombok.*; import lombok.*;
@Getter @Getter
@ToString @ToString
public class SeriesDto { public class SeriesDto {
public final long id; public final long id;
public final String name; public final String name;
public final String title; public final String title;
public final Unit unit; public final Value.Unit unit;
public final int decimals; public final int decimals;
public SeriesDto(@NonNull final Series series) { public SeriesDto(@NonNull final Series series) {
this.id = series.getId(); this.id = series.getId();
this.name = series.getName(); this.name = series.getName();
@ -25,12 +25,12 @@ public class SeriesDto {
this.unit = series.getUnit(); this.unit = series.getUnit();
this.decimals = series.getDecimals(); this.decimals = series.getDecimals();
} }
public String format(@Nullable final Double value) { public String format(@Nullable final Double value) {
if (value == null || Double.isNaN(value)) { if (value == null || Double.isNaN(value)) {
return "--- %s".formatted(unit.unit); return "--- %s".formatted(unit.unit);
} }
return "%%.%df %%s".formatted(decimals).formatted(value, unit.unit); return "%%.%df %%s".formatted(decimals).formatted(value, unit.unit);
} }
} }

View File

@ -1,6 +1,6 @@
package de.ph87.data.series; package de.ph87.data.series;
import de.ph87.data.unit.Value; import de.ph87.data.value.Value;
import lombok.*; import lombok.*;
import java.time.*; import java.time.*;
@ -8,19 +8,19 @@ import java.time.*;
@Getter @Getter
@ToString @ToString
public class SeriesInbound { public class SeriesInbound {
@NonNull @NonNull
public final String name; public final String name;
@NonNull @NonNull
public final ZonedDateTime date; public final ZonedDateTime date;
public final Value value; public final Value value;
public SeriesInbound(@NonNull final String name, @NonNull final ZonedDateTime date, final Value value) { public SeriesInbound(@NonNull final String name, @NonNull final ZonedDateTime date, final Value value) {
this.name = name; this.name = name;
this.date = date; this.date = date;
this.value = value; this.value = value;
} }
} }

View File

@ -6,7 +6,7 @@ import org.springframework.data.repository.*;
import java.util.*; import java.util.*;
public interface SeriesRepository extends ListCrudRepository<Series, Long> { public interface SeriesRepository extends ListCrudRepository<Series, Long> {
Optional<Series> findByName(@NonNull String name); Optional<Series> findByName(@NonNull String name);
} }

View File

@ -1,7 +1,7 @@
package de.ph87.data.series; package de.ph87.data.series;
import de.ph87.data.*; import de.ph87.data.*;
import de.ph87.data.unit.*; import de.ph87.data.value.Value;
import lombok.*; import lombok.*;
import lombok.extern.slf4j.*; import lombok.extern.slf4j.*;
import org.springframework.stereotype.*; import org.springframework.stereotype.*;
@ -14,42 +14,42 @@ import java.util.function.*;
@Transactional @Transactional
@RequiredArgsConstructor @RequiredArgsConstructor
public class SeriesService { public class SeriesService {
private final SeriesRepository seriesRepository; private final SeriesRepository seriesRepository;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public SeriesDto modify(final long id, @NonNull final Consumer<Series> modifier) { public SeriesDto modify(final long id, @NonNull final Consumer<Series> modifier) {
final Series series = getById(id); final Series series = getById(id);
modifier.accept(series); modifier.accept(series);
return publish(series, Action.CHANGED); return publish(series, Action.CHANGED);
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
public void delete(final long id) { public void delete(final long id) {
final Series series = getById(id); final Series series = getById(id);
seriesRepository.delete(series); seriesRepository.delete(series);
publish(series, Action.DELETED); publish(series, Action.DELETED);
} }
private Series getById(final long id) { private Series getById(final long id) {
return seriesRepository.findById(id).orElseThrow(); return seriesRepository.findById(id).orElseThrow();
} }
public SeriesDto getDtoById(final long id) { public SeriesDto getDtoById(final long id) {
return toDto(getById(id)); return toDto(getById(id));
} }
private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) { private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) {
final SeriesDto dto = toDto(series); final SeriesDto dto = toDto(series);
log.info("Series {}: {}", action, series); log.info("Series {}: {}", action, series);
return dto; return dto;
} }
private SeriesDto toDto(@NonNull final Series series) { private SeriesDto toDto(@NonNull final Series series) {
return new SeriesDto(series); return new SeriesDto(series);
} }
public Series getOrCreate(@NonNull final String name, @NonNull final Unit unit) { public Series getOrCreate(@NonNull final String name, @NonNull final Value.Unit unit) {
return seriesRepository return seriesRepository
.findByName(name) .findByName(name)
.orElseGet(() -> { .orElseGet(() -> {
@ -58,5 +58,5 @@ public class SeriesService {
return series; return series;
}); });
} }
} }

View File

@ -1,8 +1,7 @@
package de.ph87.data.series.entry; package de.ph87.data.series.entry;
import de.ph87.data.series.*; import de.ph87.data.series.*;
import de.ph87.data.unit.Value; import de.ph87.data.value.Value;
import de.ph87.data.unit.*;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@ -19,27 +18,27 @@ import java.time.temporal.*;
} }
) )
public class Entry { public class Entry {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id; private long id;
@NonNull @NonNull
@ManyToOne @ManyToOne
private Series series; private Series series;
@NonNull @NonNull
@Column(nullable = false) @Column(nullable = false)
private ZonedDateTime date; private ZonedDateTime date;
@Setter @Setter
@Column(nullable = false, name = "`value`") @Column(nullable = false, name = "`value`")
private double value; private double value;
public Entry(@NonNull final Series series, @NonNull final ZonedDateTime date, final Value value) throws Unit.NotConvertible { public Entry(@NonNull final Series series, @NonNull final ZonedDateTime date, final Value value) throws Value.Unit.NotConvertible {
this.series = series; this.series = series;
this.date = date.truncatedTo(ChronoUnit.MINUTES); this.date = date.truncatedTo(ChronoUnit.MINUTES);
this.value = value.as(series.getUnit()).value; this.value = value.as(series.getUnit()).value;
} }
} }

View File

@ -8,9 +8,9 @@ import java.time.*;
import java.util.*; import java.util.*;
public interface EntryRepository extends ListCrudRepository<Entry, Long> { public interface EntryRepository extends ListCrudRepository<Entry, Long> {
Optional<Entry> findBySeriesAndDate(@NonNull Series series, @NonNull ZonedDateTime truncated); Optional<Entry> findBySeriesAndDate(@NonNull Series series, @NonNull ZonedDateTime truncated);
List<Entry> findAllBySeriesIdAndDateGreaterThanEqualAndDateLessThanEqual(long id, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end); List<Entry> findAllBySeriesIdAndDateGreaterThanEqualAndDateLessThanEqual(long id, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end);
} }

View File

@ -1,7 +1,7 @@
package de.ph87.data.series.entry; package de.ph87.data.series.entry;
import de.ph87.data.series.*; import de.ph87.data.series.*;
import de.ph87.data.unit.*; import de.ph87.data.value.Value;
import lombok.*; import lombok.*;
import lombok.extern.slf4j.*; import lombok.extern.slf4j.*;
import org.springframework.stereotype.*; import org.springframework.stereotype.*;
@ -20,7 +20,7 @@ public class EntryService {
private final SeriesService seriesService; private final SeriesService seriesService;
public void write(@NonNull final Series series, @NonNull final SeriesInbound measure) throws Unit.NotConvertible { public void write(@NonNull final Series series, @NonNull final SeriesInbound measure) throws Value.Unit.NotConvertible {
final ZonedDateTime truncated = measure.date.truncatedTo(ChronoUnit.MINUTES).minusMinutes(measure.date.getMinute() % 5); final ZonedDateTime truncated = measure.date.truncatedTo(ChronoUnit.MINUTES).minusMinutes(measure.date.getMinute() % 5);
if (entryRepository.findBySeriesAndDate(series, truncated).isEmpty()) { if (entryRepository.findBySeriesAndDate(series, truncated).isEmpty()) {
final Entry created = entryRepository.save(new Entry(series, truncated, measure.value)); final Entry created = entryRepository.save(new Entry(series, truncated, measure.value));
@ -28,7 +28,7 @@ public class EntryService {
} }
} }
public void receive(@NonNull final SeriesInbound measure) throws Unit.NotConvertible { public void receive(@NonNull final SeriesInbound measure) throws Value.Unit.NotConvertible {
final Series series = seriesService.getOrCreate(measure.name, measure.value.unit); final Series series = seriesService.getOrCreate(measure.name, measure.value.unit);
write(series, measure); write(series, measure);
} }

View File

@ -1,51 +0,0 @@
package de.ph87.data.unit;
import lombok.*;
public enum Unit {
TEMPERATURE_C("°C"),
PRESSURE_HPA("hPa"),
HUMIDITY_RELATIVE_PERCENT("%"),
HUMIDITY_ABSOLUTE_MGL("mg/L"),
HUMIDITY_ABSOLUTE_GM3("g/m³", 1, HUMIDITY_ABSOLUTE_MGL),
ILLUMINANCE_LUX("lux"),
RESISTANCE_OHMS("Ω"),
ALTITUDE_M("m"),
POWER_W("W"),
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"),
UNIT_PERCENT("%"),
;
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, 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));
}
}
}

View File

@ -1,17 +0,0 @@
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<List<Unit>> {
@Override
public List<Unit> 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();
}
}

View File

@ -1,26 +0,0 @@
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 == target) {
return this;
}
if (this.unit.base != target.base) {
throw new Unit.NotConvertible(this.unit, target);
}
return new Value(value * this.unit.factor / target.factor, target);
}
}

View File

@ -0,0 +1,88 @@
package de.ph87.data.value;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import lombok.*;
import java.io.*;
import java.util.*;
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 == target) {
return this;
}
if (this.unit.base != target.base) {
throw new Unit.NotConvertible(this.unit, target);
}
return new Value(value * this.unit.factor / target.factor, target);
}
public enum Unit {
TEMPERATURE_C("°C"),
PRESSURE_HPA("hPa"),
HUMIDITY_RELATIVE_PERCENT("%"),
HUMIDITY_ABSOLUTE_MGL("mg/L"),
HUMIDITY_ABSOLUTE_GM3("g/m³", 1, HUMIDITY_ABSOLUTE_MGL),
ILLUMINANCE_LUX("lux"),
RESISTANCE_OHMS("Ω"),
ALTITUDE_M("m"),
POWER_W("W"),
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"),
UNIT_PERCENT("%"),
;
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, final double factor, @NonNull final Value.Unit base) {
this.unit = unit;
this.factor = factor;
this.base = base;
}
public static class NotConvertible extends Exception {
public NotConvertible(final @NonNull Value.Unit source, final Unit target) {
super("Cannot convert Units: source=%s, target=%s".formatted(source, target));
}
}
public static class ListDeserializer extends JsonDeserializer<List<Unit>> {
@Override
public List<Unit> deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
final String name = jsonParser.getValueAsString();
return Arrays.stream(values()).filter(unit -> unit.unit.equals(name)).toList();
}
}
}
}