diff --git a/src/main/java/de/ph87/data/message/handler/EspHomeHandler.java b/src/main/java/de/ph87/data/message/handler/EspHomeHandler.java index 3c5f06a..5166071 100644 --- a/src/main/java/de/ph87/data/message/handler/EspHomeHandler.java +++ b/src/main/java/de/ph87/data/message/handler/EspHomeHandler.java @@ -4,9 +4,9 @@ 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.series.entry.*; import de.ph87.data.value.Value; +import de.ph87.data.value.*; import lombok.*; import lombok.extern.slf4j.*; import org.springframework.stereotype.*; @@ -36,15 +36,15 @@ public class EspHomeHandler implements IMessageHandler { final String area = matcher.group("area"); final String property = propertyReplace(matcher.group("property")); final String name = "%s/%s".formatted(area, property.replace("_", "/")); - final Value.Unit targetUnit = switch (property) { - case "iaq" -> Value.Unit.IAQ; - case "iaq_co2" -> Value.Unit.IAQ_CO2_EQUIVALENT; - case "iaq_voc" -> Value.Unit.IAQ_VOC_EQUIVALENT; - case "pressure" -> Value.Unit.PRESSURE_HPA; - case "temperature" -> Value.Unit.TEMPERATURE_C; - case "humidity_relative" -> Value.Unit.HUMIDITY_RELATIVE_PERCENT; - case "humidity_absolute" -> Value.Unit.HUMIDITY_ABSOLUTE_GM3; - case "sun" -> Value.Unit.SUN_DC; + final Unit targetUnit = switch (property) { + case "iaq" -> Unit.IAQ; + case "iaq_co2" -> Unit.IAQ_CO2_EQUIVALENT; + case "iaq_voc" -> 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) { @@ -52,13 +52,13 @@ public class EspHomeHandler implements IMessageHandler { return; } final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); - final Value.Unit unitFromPayload = inbound.units.stream().filter(ufp -> ufp.base == targetUnit.base).findFirst().orElse(null); + 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); - entryService.receive(new SeriesInbound(name, inbound.date, value.as(targetUnit))); + entryService.receive(name, inbound.date, value.as(targetUnit)); } private String propertyReplace(final String property) { @@ -81,10 +81,10 @@ public class EspHomeHandler implements IMessageHandler { public final double value; @NonNull - @JsonDeserialize(using = Value.Unit.ListDeserializer.class) - public final List units; + @JsonDeserialize(using = Unit.ListDeserializer.class) + public final List units; - public Inbound(final long timestamp, final double value, @JsonProperty("unit") @NonNull 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/HeizungHandler.java b/src/main/java/de/ph87/data/message/handler/HeizungHandler.java index ff09b31..739fd54 100644 --- a/src/main/java/de/ph87/data/message/handler/HeizungHandler.java +++ b/src/main/java/de/ph87/data/message/handler/HeizungHandler.java @@ -2,9 +2,9 @@ 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.series.entry.*; import de.ph87.data.value.Value; +import de.ph87.data.value.*; import lombok.*; import lombok.extern.slf4j.*; import org.springframework.stereotype.*; @@ -31,7 +31,7 @@ public class HeizungHandler implements IMessageHandler { } final String property = matcher.group("property"); final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); - entryService.receive(new SeriesInbound(property, inbound.date, inbound.value)); + entryService.receive(property, inbound.date, inbound.value); } @Getter @@ -45,7 +45,7 @@ public class HeizungHandler implements IMessageHandler { public Inbound(final long timestamp, final double sum, final int count) { this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); - this.value = new Value(sum / count, Value.Unit.TEMPERATURE_C); + this.value = new Value(sum / count, Unit.TEMPERATURE_C); } } diff --git a/src/main/java/de/ph87/data/message/handler/OpenDTUHandler.java b/src/main/java/de/ph87/data/message/handler/OpenDTUHandler.java index e73430d..d96b128 100644 --- a/src/main/java/de/ph87/data/message/handler/OpenDTUHandler.java +++ b/src/main/java/de/ph87/data/message/handler/OpenDTUHandler.java @@ -2,9 +2,10 @@ 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.meter.*; import de.ph87.data.series.entry.*; import de.ph87.data.value.Value; +import de.ph87.data.value.*; import lombok.*; import lombok.extern.slf4j.*; import org.springframework.stereotype.*; @@ -16,23 +17,27 @@ import java.time.*; @RequiredArgsConstructor public class OpenDTUHandler implements IMessageHandler { + private static final String METER_NUMBER = "114190515175"; + private final ObjectMapper objectMapper; private final EntryService entryService; + private final MeterService meterService; + @Override public void handle(final @NonNull Message message) throws Exception { if (!"openDTU/pv/patrix/json".equals(message.topic)) { return; } final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); - 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(Value.Unit.POWER_W))); + entryService.receive("power/produced", inbound.date, inbound.power.as(Unit.POWER_W)); + meterService.receive("energy/produced", METER_NUMBER, inbound.date, inbound.energy); } @Getter @ToString - private static class Inbound { + public static class Inbound { @NonNull public final ZonedDateTime date; @@ -43,8 +48,8 @@ public class OpenDTUHandler implements IMessageHandler { public Inbound(final long timestamp, final double energyProducedKWh, final double powerW) { this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault()); - this.energy = new Value(energyProducedKWh, Value.Unit.ENERGY_KWH); - this.power = new Value(powerW, Value.Unit.POWER_W); + this.energy = new Value(energyProducedKWh, Unit.ENERGY_KWH); + this.power = new Value(powerW, Unit.POWER_W); } } 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 b981cf7..90393c5 100644 --- a/src/main/java/de/ph87/data/message/handler/SimpleJsonHandler.java +++ b/src/main/java/de/ph87/data/message/handler/SimpleJsonHandler.java @@ -2,9 +2,9 @@ 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.series.entry.*; import de.ph87.data.value.Value; +import de.ph87.data.value.*; import lombok.*; import lombok.extern.slf4j.*; import org.springframework.stereotype.*; @@ -26,7 +26,7 @@ public class SimpleJsonHandler implements IMessageHandler { return; } final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class); - entryService.receive(new SeriesInbound(inbound.name, inbound.date, inbound.value)); + entryService.receive(inbound.name, inbound.date, inbound.value); } @Getter @@ -41,7 +41,7 @@ public class SimpleJsonHandler implements IMessageHandler { public final Value value; - public Inbound(@NonNull final String name, final long timestamp, final double value, @NonNull final Value.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 = new Value(value, 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 index 0456f7f..642a242 100644 --- a/src/main/java/de/ph87/data/message/handler/SmartMeterHandler.java +++ b/src/main/java/de/ph87/data/message/handler/SmartMeterHandler.java @@ -2,9 +2,10 @@ 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.meter.*; import de.ph87.data.series.entry.*; import de.ph87.data.value.Value; +import de.ph87.data.value.*; import lombok.*; import lombok.extern.slf4j.*; import org.springframework.stereotype.*; @@ -16,24 +17,28 @@ import java.time.*; @RequiredArgsConstructor public class SmartMeterHandler implements IMessageHandler { + private static final String METER_NUMBER = "1ZPA0020300305"; + private final ObjectMapper objectMapper; private final EntryService entryService; + private final MeterService meterService; + @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); - 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(Value.Unit.ENERGY_KWH))); - entryService.receive(new SeriesInbound("electricity/power/difference", inbound.date, inbound.powerDifference.as(Value.Unit.POWER_W))); + entryService.receive("power/balance", inbound.date, inbound.powerBalance.as(Unit.POWER_W)); + meterService.receive("energy/purchased", METER_NUMBER, inbound.date, inbound.energyPurchased); + meterService.receive("energy/delivered", METER_NUMBER, inbound.date, inbound.energyDelivered); } @Getter @ToString - private static class Inbound { + public static class Inbound { @NonNull public final ZonedDateTime date; @@ -42,13 +47,13 @@ public class SmartMeterHandler implements IMessageHandler { public final Value energyDelivered; - public final Value powerDifference; + public final Value powerBalance; 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, Value.Unit.ENERGY_WH); - this.energyDelivered = new Value(deliveryWh, Value.Unit.ENERGY_WH); - this.powerDifference = new Value(powerW, Value.Unit.POWER_W); + this.energyPurchased = new Value(purchaseWh, Unit.ENERGY_WH).as(Unit.ENERGY_KWH); + this.energyDelivered = new Value(deliveryWh, Unit.ENERGY_WH).as(Unit.ENERGY_KWH); + this.powerBalance = new Value(powerW, Unit.POWER_W); } } diff --git a/src/main/java/de/ph87/data/meter/Meter.java b/src/main/java/de/ph87/data/meter/Meter.java new file mode 100644 index 0000000..41b67a0 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/Meter.java @@ -0,0 +1,40 @@ +package de.ph87.data.meter; + +import de.ph87.data.series.*; +import jakarta.persistence.*; +import lombok.*; + +import java.time.*; + +@Entity +@Getter +@ToString +@NoArgsConstructor +@Table(uniqueConstraints = { + @UniqueConstraint(columnNames = {"series_id", "date"}) +}) +public class Meter { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @NonNull + @ManyToOne + private Series series; + + @NonNull + @Column(nullable = false) + private ZonedDateTime date; + + @NonNull + @Column(nullable = false) + private String number; + + public Meter(@NonNull final Series series, @NonNull final ZonedDateTime date, @NonNull final String number) { + this.series = series; + this.date = date; + this.number = number; + } + +} diff --git a/src/main/java/de/ph87/data/meter/MeterRepository.java b/src/main/java/de/ph87/data/meter/MeterRepository.java new file mode 100644 index 0000000..28f56c4 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/MeterRepository.java @@ -0,0 +1,12 @@ +package de.ph87.data.meter; + +import de.ph87.data.series.*; +import org.springframework.data.repository.*; + +import java.util.*; + +public interface MeterRepository extends ListCrudRepository { + + Optional findFirstBySeriesOrderByDateDesc(Series series); + +} diff --git a/src/main/java/de/ph87/data/meter/MeterService.java b/src/main/java/de/ph87/data/meter/MeterService.java new file mode 100644 index 0000000..0f38185 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/MeterService.java @@ -0,0 +1,93 @@ +package de.ph87.data.meter; + +import de.ph87.data.meter.day.*; +import de.ph87.data.meter.five.*; +import de.ph87.data.meter.month.*; +import de.ph87.data.meter.week.*; +import de.ph87.data.meter.year.*; +import de.ph87.data.series.*; +import de.ph87.data.value.Value; +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 MeterService { + + private final SeriesService seriesService; + + private final MeterRepository meterRepository; + + private final MeterDayRepository meterDayRepository; + + private final MeterWeekRepository meterWeekRepository; + + private final MeterMonthRepository meterMonthRepository; + + private final MeterYearRepository meterYearRepository; + + private final MeterFiveRepository meterFiveRepository; + + public void receive(@NonNull final String seriesName, @NonNull final String meterNumber, @NonNull final ZonedDateTime date, @NonNull final Value value) { + final Series series = seriesService.getOrCreate(seriesName, value.unit); + final Meter meter = meterRepository.findFirstBySeriesOrderByDateDesc(series) + .filter(m -> m.getNumber().equals(meterNumber)) + .orElseGet(() -> meterRepository.save(new Meter(series, date, meterNumber))); + + final MeterFive.Id five = new MeterFive.Id(meter, date.truncatedTo(ChronoUnit.MINUTES).minusMinutes(date.getMinute() % 5)); + meterFiveRepository.findById(five).ifPresentOrElse( + existing -> existing.update(value), + () -> meterFiveRepository.save(new MeterFive(five, value)) + ); + + final MeterDay.Id day = new MeterDay.Id(meter, date.toLocalDate()); + meterDayRepository.findById(day).ifPresentOrElse( + existing -> existing.update(value), + () -> meterDayRepository.save(new MeterDay(day, value)) + ); + + final MeterWeek.Id week = new MeterWeek.Id(meter, toWeek(date)); + meterWeekRepository.findById(week).ifPresentOrElse( + existing -> existing.update(value), + () -> meterWeekRepository.save(new MeterWeek(week, value)) + ); + + final MeterMonth.Id month = new MeterMonth.Id(meter, toMonth(date)); + meterMonthRepository.findById(month).ifPresentOrElse( + existing -> existing.update(value), + () -> meterMonthRepository.save(new MeterMonth(month, value)) + ); + + final MeterYear.Id year = new MeterYear.Id(meter, toYear(date)); + meterYearRepository.findById(year).ifPresentOrElse( + existing -> existing.update(value), + () -> meterYearRepository.save(new MeterYear(year, value)) + ); + } + + @NonNull + private static LocalDate toWeek(@NonNull final ZonedDateTime date) { + final LocalDate t = date.toLocalDate(); + return t.minusDays(t.getDayOfWeek().getValue() - 1); + } + + @NonNull + private static LocalDate toMonth(@NonNull final ZonedDateTime date) { + final LocalDate t = date.toLocalDate(); + return t.minusDays(t.getDayOfMonth() - 1); + } + + @NonNull + private static LocalDate toYear(@NonNull final ZonedDateTime date) { + final LocalDate t = date.toLocalDate(); + return t.minusDays(t.getDayOfYear() - 1); + } + +} diff --git a/src/main/java/de/ph87/data/meter/day/MeterDay.java b/src/main/java/de/ph87/data/meter/day/MeterDay.java new file mode 100644 index 0000000..e1a61e4 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/day/MeterDay.java @@ -0,0 +1,61 @@ +package de.ph87.data.meter.day; + +import de.ph87.data.meter.*; +import de.ph87.data.value.Value; +import jakarta.persistence.*; +import lombok.*; + +import java.time.*; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class MeterDay { + + @EmbeddedId + private Id id; + + @Column(nullable = false) + private double min; + + @Column(nullable = false) + private double max; + + public MeterDay(@NonNull final Id id, @NonNull final Value value) { + this.id = id; + this.min = value.as(id.getMeter().getSeries().getUnit()).value; + this.max = this.min; + } + + public void update(@NonNull final Value value) { + final double v = value.as(id.getMeter().getSeries().getUnit()).value; + if (v < this.max) { + throw new RuntimeException(); + } + this.max = v; + } + + @Getter + @ToString + @Embeddable + @EqualsAndHashCode + @NoArgsConstructor + public static class Id { + + @NonNull + @ManyToOne + private Meter meter; + + @NonNull + @Column(nullable = false) + private LocalDate date; + + public Id(@NonNull final Meter meter, @NonNull final LocalDate date) { + this.meter = meter; + this.date = date; + } + + } + +} diff --git a/src/main/java/de/ph87/data/meter/day/MeterDayRepository.java b/src/main/java/de/ph87/data/meter/day/MeterDayRepository.java new file mode 100644 index 0000000..290d42c --- /dev/null +++ b/src/main/java/de/ph87/data/meter/day/MeterDayRepository.java @@ -0,0 +1,7 @@ +package de.ph87.data.meter.day; + +import org.springframework.data.repository.*; + +public interface MeterDayRepository extends ListCrudRepository { + +} diff --git a/src/main/java/de/ph87/data/meter/five/MeterFive.java b/src/main/java/de/ph87/data/meter/five/MeterFive.java new file mode 100644 index 0000000..5a97719 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/five/MeterFive.java @@ -0,0 +1,61 @@ +package de.ph87.data.meter.five; + +import de.ph87.data.meter.*; +import de.ph87.data.value.Value; +import jakarta.persistence.*; +import lombok.*; + +import java.time.*; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class MeterFive { + + @EmbeddedId + private Id id; + + @Column(nullable = false) + private double min; + + @Column(nullable = false) + private double max; + + public MeterFive(@NonNull final Id id, @NonNull final Value value) { + this.id = id; + this.min = value.as(id.getMeter().getSeries().getUnit()).value; + this.max = this.min; + } + + public void update(@NonNull final Value value) { + final double v = value.as(id.getMeter().getSeries().getUnit()).value; + if (v < this.max) { + throw new RuntimeException(); + } + this.max = v; + } + + @Getter + @ToString + @Embeddable + @EqualsAndHashCode + @NoArgsConstructor + public static class Id { + + @NonNull + @ManyToOne + private Meter meter; + + @NonNull + @Column(nullable = false) + private ZonedDateTime date; + + public Id(@NonNull final Meter meter, @NonNull final ZonedDateTime date) { + this.meter = meter; + this.date = date; + } + + } + +} diff --git a/src/main/java/de/ph87/data/meter/five/MeterFiveRepository.java b/src/main/java/de/ph87/data/meter/five/MeterFiveRepository.java new file mode 100644 index 0000000..f49ca96 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/five/MeterFiveRepository.java @@ -0,0 +1,7 @@ +package de.ph87.data.meter.five; + +import org.springframework.data.repository.*; + +public interface MeterFiveRepository extends ListCrudRepository { + +} diff --git a/src/main/java/de/ph87/data/meter/month/MeterMonth.java b/src/main/java/de/ph87/data/meter/month/MeterMonth.java new file mode 100644 index 0000000..24428ac --- /dev/null +++ b/src/main/java/de/ph87/data/meter/month/MeterMonth.java @@ -0,0 +1,61 @@ +package de.ph87.data.meter.month; + +import de.ph87.data.meter.*; +import de.ph87.data.value.Value; +import jakarta.persistence.*; +import lombok.*; + +import java.time.*; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class MeterMonth { + + @EmbeddedId + private Id id; + + @Column(nullable = false) + private double min; + + @Column(nullable = false) + private double max; + + public MeterMonth(@NonNull final Id id, @NonNull final Value value) { + this.id = id; + this.min = value.as(id.getMeter().getSeries().getUnit()).value; + this.max = this.min; + } + + public void update(@NonNull final Value value) { + final double v = value.as(id.getMeter().getSeries().getUnit()).value; + if (v < this.max) { + throw new RuntimeException(); + } + this.max = v; + } + + @Getter + @ToString + @Embeddable + @EqualsAndHashCode + @NoArgsConstructor + public static class Id { + + @NonNull + @ManyToOne + private Meter meter; + + @NonNull + @Column(nullable = false) + private LocalDate date; + + public Id(@NonNull final Meter meter, @NonNull final LocalDate date) { + this.meter = meter; + this.date = date; + } + + } + +} diff --git a/src/main/java/de/ph87/data/meter/month/MeterMonthRepository.java b/src/main/java/de/ph87/data/meter/month/MeterMonthRepository.java new file mode 100644 index 0000000..8f6033d --- /dev/null +++ b/src/main/java/de/ph87/data/meter/month/MeterMonthRepository.java @@ -0,0 +1,7 @@ +package de.ph87.data.meter.month; + +import org.springframework.data.repository.*; + +public interface MeterMonthRepository extends ListCrudRepository { + +} diff --git a/src/main/java/de/ph87/data/meter/week/MeterWeek.java b/src/main/java/de/ph87/data/meter/week/MeterWeek.java new file mode 100644 index 0000000..7b4c4d5 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/week/MeterWeek.java @@ -0,0 +1,61 @@ +package de.ph87.data.meter.week; + +import de.ph87.data.meter.*; +import de.ph87.data.value.Value; +import jakarta.persistence.*; +import lombok.*; + +import java.time.*; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class MeterWeek { + + @EmbeddedId + private Id id; + + @Column(nullable = false) + private double min; + + @Column(nullable = false) + private double max; + + public MeterWeek(@NonNull final Id id, @NonNull final Value value) { + this.id = id; + this.min = value.as(id.getMeter().getSeries().getUnit()).value; + this.max = this.min; + } + + public void update(@NonNull final Value value) { + final double v = value.as(id.getMeter().getSeries().getUnit()).value; + if (v < this.max) { + throw new RuntimeException(); + } + this.max = v; + } + + @Getter + @ToString + @Embeddable + @EqualsAndHashCode + @NoArgsConstructor + public static class Id { + + @NonNull + @ManyToOne + private Meter meter; + + @NonNull + @Column(nullable = false) + private LocalDate date; + + public Id(@NonNull final Meter meter, @NonNull final LocalDate date) { + this.meter = meter; + this.date = date; + } + + } + +} diff --git a/src/main/java/de/ph87/data/meter/week/MeterWeekRepository.java b/src/main/java/de/ph87/data/meter/week/MeterWeekRepository.java new file mode 100644 index 0000000..267d514 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/week/MeterWeekRepository.java @@ -0,0 +1,7 @@ +package de.ph87.data.meter.week; + +import org.springframework.data.repository.*; + +public interface MeterWeekRepository extends ListCrudRepository { + +} diff --git a/src/main/java/de/ph87/data/meter/year/MeterYear.java b/src/main/java/de/ph87/data/meter/year/MeterYear.java new file mode 100644 index 0000000..8aaa0a7 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/year/MeterYear.java @@ -0,0 +1,61 @@ +package de.ph87.data.meter.year; + +import de.ph87.data.meter.*; +import de.ph87.data.value.Value; +import jakarta.persistence.*; +import lombok.*; + +import java.time.*; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class MeterYear { + + @EmbeddedId + private Id id; + + @Column(nullable = false) + private double min; + + @Column(nullable = false) + private double max; + + public MeterYear(@NonNull final Id id, @NonNull final Value value) { + this.id = id; + this.min = value.as(id.getMeter().getSeries().getUnit()).value; + this.max = this.min; + } + + public void update(@NonNull final Value value) { + final double v = value.as(id.getMeter().getSeries().getUnit()).value; + if (v < this.max) { + throw new RuntimeException(); + } + this.max = v; + } + + @Getter + @ToString + @Embeddable + @EqualsAndHashCode + @NoArgsConstructor + public static class Id { + + @NonNull + @ManyToOne + private Meter meter; + + @NonNull + @Column(nullable = false) + private LocalDate date; + + public Id(@NonNull final Meter meter, @NonNull final LocalDate date) { + this.meter = meter; + this.date = date; + } + + } + +} diff --git a/src/main/java/de/ph87/data/meter/year/MeterYearRepository.java b/src/main/java/de/ph87/data/meter/year/MeterYearRepository.java new file mode 100644 index 0000000..e01d074 --- /dev/null +++ b/src/main/java/de/ph87/data/meter/year/MeterYearRepository.java @@ -0,0 +1,7 @@ +package de.ph87.data.meter.year; + +import org.springframework.data.repository.*; + +public interface MeterYearRepository extends ListCrudRepository { + +} diff --git a/src/main/java/de/ph87/data/series/Series.java b/src/main/java/de/ph87/data/series/Series.java index 82329b0..9b3bdf5 100644 --- a/src/main/java/de/ph87/data/series/Series.java +++ b/src/main/java/de/ph87/data/series/Series.java @@ -1,6 +1,6 @@ package de.ph87.data.series; -import de.ph87.data.value.Value; +import de.ph87.data.value.*; import jakarta.persistence.*; import lombok.*; @@ -24,17 +24,16 @@ public class Series { @Column(nullable = false, unique = true) private String title; - @Setter @NonNull @Column(nullable = false) @Enumerated(EnumType.STRING) - private Value.Unit unit; + private Unit unit; @Setter @Column(nullable = false) private int decimals = 1; - public Series(@NonNull final String name, @NonNull final Value.Unit unit) { + 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 e6360c7..6838c60 100644 --- a/src/main/java/de/ph87/data/series/SeriesDto.java +++ b/src/main/java/de/ph87/data/series/SeriesDto.java @@ -1,6 +1,6 @@ package de.ph87.data.series; -import de.ph87.data.value.Value; +import de.ph87.data.value.*; import jakarta.annotation.*; import lombok.*; @@ -14,7 +14,7 @@ public class SeriesDto { public final String title; - public final Value.Unit unit; + public final Unit unit; public final int decimals; diff --git a/src/main/java/de/ph87/data/series/SeriesService.java b/src/main/java/de/ph87/data/series/SeriesService.java index e99cde9..473406f 100644 --- a/src/main/java/de/ph87/data/series/SeriesService.java +++ b/src/main/java/de/ph87/data/series/SeriesService.java @@ -1,7 +1,7 @@ package de.ph87.data.series; import de.ph87.data.*; -import de.ph87.data.value.Value; +import de.ph87.data.value.*; import lombok.*; import lombok.extern.slf4j.*; import org.springframework.stereotype.*; @@ -49,7 +49,7 @@ public class SeriesService { return new SeriesDto(series); } - public Series getOrCreate(@NonNull final String name, @NonNull final Value.Unit unit) { + public Series getOrCreate(@NonNull final String name, @NonNull final Unit unit) { return seriesRepository .findByName(name) .orElseGet(() -> { 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 ca667f3..71ec48b 100644 --- a/src/main/java/de/ph87/data/series/entry/Entry.java +++ b/src/main/java/de/ph87/data/series/entry/Entry.java @@ -12,33 +12,39 @@ import java.time.temporal.*; @Getter @ToString @NoArgsConstructor -@Table( - uniqueConstraints = { - @UniqueConstraint(columnNames = {"series_id", "date"}) - } -) public class Entry { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; + @EmbeddedId + private EntryId id; - @NonNull - @ManyToOne - private Series series; - - @NonNull - @Column(nullable = false) - private ZonedDateTime date; - - @Setter @Column(nullable = false, name = "`value`") private double value; - public Entry(@NonNull final Series series, @NonNull final ZonedDateTime date, final Value value) throws Value.Unit.NotConvertible { - this.series = series; - this.date = date.truncatedTo(ChronoUnit.MINUTES); - this.value = value.as(series.getUnit()).value; + public Entry(@NonNull final EntryId id, @NonNull final Value value) { + this.id = id; + this.value = value.as(id.getSeries().getUnit()).value; + } + + @Getter + @ToString + @Embeddable + @EqualsAndHashCode + @NoArgsConstructor + public static class EntryId { + + @NonNull + @ManyToOne + private Series series; + + @NonNull + @Column(nullable = false) + private ZonedDateTime date; + + public EntryId(@NonNull final Series series, final @NonNull ZonedDateTime date) { + this.series = series; + this.date = date.truncatedTo(ChronoUnit.MINUTES).minusMinutes(date.getMinute() % 5); + } + } } 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 142c43e..96a6a94 100644 --- a/src/main/java/de/ph87/data/series/entry/EntryRepository.java +++ b/src/main/java/de/ph87/data/series/entry/EntryRepository.java @@ -1,16 +1,15 @@ package de.ph87.data.series.entry; -import de.ph87.data.series.*; import lombok.*; import org.springframework.data.repository.*; import java.time.*; import java.util.*; -public interface EntryRepository extends ListCrudRepository { +public interface EntryRepository extends ListCrudRepository { - Optional findBySeriesAndDate(@NonNull Series series, @NonNull ZonedDateTime truncated); + Optional findById(@NonNull Entry.EntryId id); - List findAllBySeriesIdAndDateGreaterThanEqualAndDateLessThanEqual(long id, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end); + List findAllByIdSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqual(long id, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end); } 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 ad599b9..325e69a 100644 --- a/src/main/java/de/ph87/data/series/entry/EntryService.java +++ b/src/main/java/de/ph87/data/series/entry/EntryService.java @@ -8,7 +8,6 @@ import org.springframework.stereotype.*; import org.springframework.transaction.annotation.*; import java.time.*; -import java.time.temporal.*; @Slf4j @Service @@ -20,17 +19,17 @@ public class EntryService { private final SeriesService seriesService; - 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); - if (entryRepository.findBySeriesAndDate(series, truncated).isEmpty()) { - final Entry created = entryRepository.save(new Entry(series, truncated, measure.value)); + public void receive(final String name, final @NonNull ZonedDateTime date, final Value value) { + final Series series = seriesService.getOrCreate(name, value.unit); + write(series, date, value); + } + + public void write(@NonNull final Series series, final @NonNull ZonedDateTime date, final Value value) { + final Entry.EntryId id = new Entry.EntryId(series, date); + if (entryRepository.findById(id).isEmpty()) { + final Entry created = entryRepository.save(new Entry(id, value)); log.debug("Created: {}", created); } } - public void receive(@NonNull final SeriesInbound measure) throws Value.Unit.NotConvertible { - final Series series = seriesService.getOrCreate(measure.name, measure.value.unit); - write(series, measure); - } - } diff --git a/src/main/java/de/ph87/data/series/graph/Graph.java b/src/main/java/de/ph87/data/series/graph/Graph.java index 8e973cb..c8f09fd 100644 --- a/src/main/java/de/ph87/data/series/graph/Graph.java +++ b/src/main/java/de/ph87/data/series/graph/Graph.java @@ -145,7 +145,7 @@ public class Graph { private Function toPoint(final long minuteMin, final double minuteScale, final double valueMin, final double valueScale) { return entry -> { - final long minuteEpoch = entry.getDate().toEpochSecond() / 60; + final long minuteEpoch = entry.getId().getDate().toEpochSecond() / 60; final long minuteRelative = minuteEpoch - minuteMin; final double minuteScaled = minuteRelative * minuteScale; final int x = (int) Math.round(minuteScaled); diff --git a/src/main/java/de/ph87/data/series/graph/GraphService.java b/src/main/java/de/ph87/data/series/graph/GraphService.java index 8ce6a3b..53a5871 100644 --- a/src/main/java/de/ph87/data/series/graph/GraphService.java +++ b/src/main/java/de/ph87/data/series/graph/GraphService.java @@ -22,7 +22,7 @@ public class GraphService { public Graph getGraph(final long seriesId, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end, final int width, final int height, final int border) { final SeriesDto series = seriesService.getDtoById(seriesId); - final List entries = entryRepository.findAllBySeriesIdAndDateGreaterThanEqualAndDateLessThanEqual(seriesId, begin, end); + final List entries = entryRepository.findAllByIdSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqual(seriesId, begin, end); return new Graph(series, entries, begin, end, width, height, border); } diff --git a/src/main/java/de/ph87/data/value/Unit.java b/src/main/java/de/ph87/data/value/Unit.java new file mode 100644 index 0000000..a032321 --- /dev/null +++ b/src/main/java/de/ph87/data/value/Unit.java @@ -0,0 +1,65 @@ +package de.ph87.data.value; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; +import lombok.*; + +import java.io.*; +import java.util.*; + +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 RuntimeException { + + public NotConvertible(final @NonNull Unit source, final Unit target) { + super("Cannot convert Units: source=%s, target=%s".formatted(source, target)); + } + + } + + public static class ListDeserializer extends JsonDeserializer> { + + @Override + public List 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(); + } + + } +} diff --git a/src/main/java/de/ph87/data/value/Value.java b/src/main/java/de/ph87/data/value/Value.java index f1ba740..6e173fb 100644 --- a/src/main/java/de/ph87/data/value/Value.java +++ b/src/main/java/de/ph87/data/value/Value.java @@ -1,12 +1,7 @@ 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; @@ -18,7 +13,7 @@ public class Value { this.unit = unit; } - public Value as(@NonNull final Unit target) throws Unit.NotConvertible { + public Value as(@NonNull final Unit target) { if (this.unit == target) { return this; } @@ -28,61 +23,4 @@ public class Value { 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> { - - @Override - public List 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(); - } - - } - } - }