EntryController + Graph
This commit is contained in:
parent
551db4b93d
commit
908134600c
@ -5,7 +5,7 @@ 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=#
|
||||
|
||||
@ -5,6 +5,7 @@ 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.unit.Value;
|
||||
import de.ph87.data.unit.*;
|
||||
import lombok.*;
|
||||
@ -25,7 +26,7 @@ public class EspHomeHandler implements IMessageHandler {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final SeriesService seriesService;
|
||||
private final EntryService entryService;
|
||||
|
||||
@Override
|
||||
public void handle(@NonNull final Message message) throws Exception {
|
||||
@ -58,7 +59,7 @@ public class EspHomeHandler implements IMessageHandler {
|
||||
return;
|
||||
}
|
||||
final Value value = new Value(inbound.value, unitFromPayload);
|
||||
seriesService.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) {
|
||||
|
||||
@ -3,6 +3,7 @@ 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.unit.Value;
|
||||
import de.ph87.data.unit.*;
|
||||
import lombok.*;
|
||||
@ -21,7 +22,7 @@ public class HeizungHandler implements IMessageHandler {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final SeriesService seriesService;
|
||||
private final EntryService entryService;
|
||||
|
||||
@Override
|
||||
public void handle(@NonNull final Message message) throws Exception {
|
||||
@ -31,7 +32,7 @@ public class HeizungHandler implements IMessageHandler {
|
||||
}
|
||||
final String property = matcher.group("property");
|
||||
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||
seriesService.receive(new SeriesInbound(property, inbound.date, inbound.value));
|
||||
entryService.receive(new SeriesInbound(property, inbound.date, inbound.value));
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
||||
@ -3,6 +3,7 @@ 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.unit.Value;
|
||||
import de.ph87.data.unit.*;
|
||||
import lombok.*;
|
||||
@ -18,7 +19,7 @@ public class OpenDTUHandler implements IMessageHandler {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final SeriesService seriesService;
|
||||
private final EntryService entryService;
|
||||
|
||||
@Override
|
||||
public void handle(final @NonNull Message message) throws Exception {
|
||||
@ -26,11 +27,10 @@ public class OpenDTUHandler implements IMessageHandler {
|
||||
return;
|
||||
}
|
||||
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||
seriesService.receive(new SeriesInbound("electricity/energy/produced", inbound.date, inbound.energy.as(Unit.ENERGY_KWH)));
|
||||
seriesService.receive(new SeriesInbound("electricity/power/produced", inbound.date, inbound.power.as(Unit.POWER_W)));
|
||||
entryService.receive(new SeriesInbound("electricity/energy/produced", inbound.date, inbound.energy.as(Unit.ENERGY_KWH)));
|
||||
entryService.receive(new SeriesInbound("electricity/power/produced", inbound.date, inbound.power.as(Unit.POWER_W)));
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
private static class Inbound {
|
||||
|
||||
@ -3,6 +3,7 @@ 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.unit.Value;
|
||||
import de.ph87.data.unit.*;
|
||||
import lombok.*;
|
||||
@ -18,7 +19,7 @@ public class SimpleJsonHandler implements IMessageHandler {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final SeriesService seriesService;
|
||||
private final EntryService entryService;
|
||||
|
||||
@Override
|
||||
public void handle(@NonNull final Message message) throws Exception {
|
||||
@ -26,7 +27,7 @@ public class SimpleJsonHandler implements IMessageHandler {
|
||||
return;
|
||||
}
|
||||
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||
seriesService.receive(new SeriesInbound(inbound.name, inbound.date, inbound.value));
|
||||
entryService.receive(new SeriesInbound(inbound.name, inbound.date, inbound.value));
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
||||
@ -3,6 +3,7 @@ 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.unit.Value;
|
||||
import de.ph87.data.unit.*;
|
||||
import lombok.*;
|
||||
@ -18,7 +19,7 @@ public class SmartMeterHandler implements IMessageHandler {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final SeriesService seriesService;
|
||||
private final EntryService entryService;
|
||||
|
||||
@Override
|
||||
public void handle(@NonNull final Message message) throws Exception {
|
||||
@ -26,9 +27,9 @@ public class SmartMeterHandler implements IMessageHandler {
|
||||
return;
|
||||
}
|
||||
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||
seriesService.receive(new SeriesInbound("electricity/energy/purchased", inbound.date, inbound.energyPurchased.as(Unit.ENERGY_KWH)));
|
||||
seriesService.receive(new SeriesInbound("electricity/energy/delivered", inbound.date, inbound.energyDelivered.as(Unit.ENERGY_KWH)));
|
||||
seriesService.receive(new SeriesInbound("electricity/power/difference", inbound.date, inbound.powerDifference.as(Unit.POWER_W)));
|
||||
entryService.receive(new SeriesInbound("electricity/energy/purchased", inbound.date, inbound.energyPurchased.as(Unit.ENERGY_KWH)));
|
||||
entryService.receive(new SeriesInbound("electricity/energy/delivered", inbound.date, inbound.energyDelivered.as(Unit.ENERGY_KWH)));
|
||||
entryService.receive(new SeriesInbound("electricity/power/difference", inbound.date, inbound.powerDifference.as(Unit.POWER_W)));
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
||||
@ -30,6 +30,10 @@ public class Series {
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Unit unit;
|
||||
|
||||
@Setter
|
||||
@Column(nullable = false)
|
||||
private int decimals = 1;
|
||||
|
||||
public Series(@NonNull final String name, @NonNull final Unit unit) {
|
||||
this.name = name;
|
||||
this.title = name;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import de.ph87.data.unit.*;
|
||||
import jakarta.annotation.*;
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@ -15,11 +16,21 @@ public class SeriesDto {
|
||||
|
||||
public final Unit unit;
|
||||
|
||||
public final int decimals;
|
||||
|
||||
public SeriesDto(@NonNull final Series series) {
|
||||
this.id = series.getId();
|
||||
this.name = series.getName();
|
||||
this.title = series.getTitle();
|
||||
this.unit = series.getUnit();
|
||||
this.decimals = series.getDecimals();
|
||||
}
|
||||
|
||||
public String format(@Nullable final Double value) {
|
||||
if (value == null || Double.isNaN(value)) {
|
||||
return "--- %s".formatted(unit.unit);
|
||||
}
|
||||
return "%%.%df %%s".formatted(decimals).formatted(value, unit.unit);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import de.ph87.data.*;
|
||||
import de.ph87.data.series.entry.*;
|
||||
import de.ph87.data.unit.*;
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.*;
|
||||
@ -18,22 +17,28 @@ public class SeriesService {
|
||||
|
||||
private final SeriesRepository seriesRepository;
|
||||
|
||||
private final EntryService entryService;
|
||||
|
||||
public SeriesDto modify(@NonNull final long id, @NonNull final Consumer<Series> modifier) {
|
||||
@SuppressWarnings("unused")
|
||||
public SeriesDto modify(final long id, @NonNull final Consumer<Series> modifier) {
|
||||
final Series series = getById(id);
|
||||
modifier.accept(series);
|
||||
return publish(series, Action.CHANGED);
|
||||
}
|
||||
|
||||
public void delete(@NonNull final long id) {
|
||||
@SuppressWarnings("unused")
|
||||
public void delete(final long id) {
|
||||
final Series series = getById(id);
|
||||
seriesRepository.delete(series);
|
||||
publish(series, Action.DELETED);
|
||||
}
|
||||
|
||||
private Series getById(@NonNull final long id) {
|
||||
private Series getById(final long id) {
|
||||
return seriesRepository.findById(id).orElseThrow();
|
||||
}
|
||||
|
||||
public SeriesDto getDtoById(final long id) {
|
||||
return toDto(getById(id));
|
||||
}
|
||||
|
||||
private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) {
|
||||
final SeriesDto dto = toDto(series);
|
||||
log.info("Series {}: {}", action, series);
|
||||
@ -44,12 +49,7 @@ public class SeriesService {
|
||||
return new SeriesDto(series);
|
||||
}
|
||||
|
||||
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) {
|
||||
public Series getOrCreate(@NonNull final String name, @NonNull final Unit unit) {
|
||||
return seriesRepository
|
||||
.findByName(name)
|
||||
.orElseGet(() -> {
|
||||
|
||||
@ -11,4 +11,6 @@ public interface EntryRepository extends ListCrudRepository<Entry, Long> {
|
||||
|
||||
Optional<Entry> findBySeriesAndDate(@NonNull Series series, @NonNull ZonedDateTime truncated);
|
||||
|
||||
List<Entry> findAllBySeriesIdAndDateGreaterThanEqualAndDateLessThanEqual(long id, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end);
|
||||
|
||||
}
|
||||
|
||||
@ -18,12 +18,19 @@ public class EntryService {
|
||||
|
||||
private final EntryRepository entryRepository;
|
||||
|
||||
private final SeriesService seriesService;
|
||||
|
||||
public void write(@NonNull final Series series, @NonNull final SeriesInbound measure) throws Unit.NotConvertible {
|
||||
final ZonedDateTime truncated = measure.date.truncatedTo(ChronoUnit.MINUTES);
|
||||
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));
|
||||
log.debug("Created: {}", created);
|
||||
}
|
||||
}
|
||||
|
||||
public void receive(@NonNull final SeriesInbound measure) throws Unit.NotConvertible {
|
||||
final Series series = seriesService.getOrCreate(measure.name, measure.value.unit);
|
||||
write(series, measure);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
161
src/main/java/de/ph87/data/series/graph/Graph.java
Normal file
161
src/main/java/de/ph87/data/series/graph/Graph.java
Normal file
@ -0,0 +1,161 @@
|
||||
package de.ph87.data.series.graph;
|
||||
|
||||
import de.ph87.data.series.*;
|
||||
import de.ph87.data.series.entry.*;
|
||||
import jakarta.annotation.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.*;
|
||||
import java.awt.image.*;
|
||||
import java.time.*;
|
||||
import java.util.List;
|
||||
import java.util.function.*;
|
||||
|
||||
import static java.lang.Math.*;
|
||||
|
||||
public class Graph {
|
||||
|
||||
public static final Stroke NORMAL = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1);
|
||||
|
||||
public static final Stroke DASHED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[]{2, 2}, 0);
|
||||
|
||||
@NonNull
|
||||
public final SeriesDto series;
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime begin;
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime end;
|
||||
|
||||
public final int width;
|
||||
|
||||
public final int height;
|
||||
|
||||
public final int border;
|
||||
|
||||
public final List<Point> points;
|
||||
|
||||
public final long minuteMin;
|
||||
|
||||
public final long minuteMax;
|
||||
|
||||
public final long minuteRange;
|
||||
|
||||
public final double minuteScale;
|
||||
|
||||
public final double valueMin;
|
||||
|
||||
public final double valueMax;
|
||||
|
||||
public final double valueAvg;
|
||||
|
||||
public final double valueRange;
|
||||
|
||||
public final double valueScale;
|
||||
|
||||
public final int heightInner;
|
||||
|
||||
public final int widthInner;
|
||||
|
||||
public final int maxLabelWidth;
|
||||
|
||||
public Graph(@NonNull final SeriesDto series, @NonNull final List<Entry> entries, final @NonNull ZonedDateTime begin, final @NonNull ZonedDateTime end, final int width, final int height, final int border) {
|
||||
this.series = series;
|
||||
this.begin = begin;
|
||||
this.end = end;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.border = border;
|
||||
|
||||
double vSum = 0;
|
||||
double vMin = Double.MAX_VALUE;
|
||||
double vMax = Double.MIN_VALUE;
|
||||
int maxLabelWidth = 80;
|
||||
final FontMetrics fontMetrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics().getFontMetrics();
|
||||
for (final Entry entry : entries) {
|
||||
vMin = Math.min(vMin, entry.getValue());
|
||||
vMax = max(vMax, entry.getValue());
|
||||
vSum += entry.getValue();
|
||||
maxLabelWidth = max(maxLabelWidth, fontMetrics.stringWidth(series.format(entry.getValue())));
|
||||
}
|
||||
this.valueAvg = vSum / entries.size();
|
||||
this.maxLabelWidth = maxLabelWidth;
|
||||
|
||||
widthInner = width - 3 * border - maxLabelWidth;
|
||||
heightInner = height - 2 * border;
|
||||
|
||||
minuteMin = begin.toEpochSecond() / 60;
|
||||
minuteMax = end.toEpochSecond() / 60;
|
||||
minuteRange = minuteMax - minuteMin;
|
||||
minuteScale = (double) widthInner / minuteRange;
|
||||
|
||||
valueMin = vMin;
|
||||
valueMax = vMax;
|
||||
valueRange = vMax - vMin;
|
||||
valueScale = heightInner / valueRange;
|
||||
|
||||
points = entries.stream().map(toPoint(minuteMin, minuteScale, vMin, valueScale)).toList();
|
||||
}
|
||||
|
||||
public BufferedImage draw() {
|
||||
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
final Graphics2D g = (Graphics2D) image.getGraphics();
|
||||
final int fontH3_4 = (int) Math.round(g.getFontMetrics().getHeight() * 0.75);
|
||||
|
||||
g.setColor(Color.gray);
|
||||
final String string = "%s [%s]".formatted(series.getTitle(), series.unit.unit);
|
||||
g.drawString(string, border, border + fontH3_4);
|
||||
|
||||
yLabel(g, valueMax, DASHED, Color.red);
|
||||
yLabel(g, valueAvg, DASHED, new Color(0, 127, 0));
|
||||
yLabel(g, 0, NORMAL, Color.BLACK);
|
||||
yLabel(g, valueMin, DASHED, Color.blue);
|
||||
|
||||
g.translate(border, height - border);
|
||||
g.scale(1, -1);
|
||||
|
||||
g.setStroke(NORMAL);
|
||||
g.setColor(Color.BLACK);
|
||||
g.drawLine(widthInner, 0, widthInner, heightInner); // y-axis
|
||||
|
||||
Point last = null;
|
||||
g.setColor(Color.RED);
|
||||
for (final Point current : points) {
|
||||
if (last != null && current.x - last.x <= minuteScale * 2) {
|
||||
g.drawLine(last.x, last.y, current.x, current.y);
|
||||
}
|
||||
last = current;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private void yLabel(final Graphics2D g, final double value, @Nullable final Stroke stroke, final @Nullable Color color) {
|
||||
final String string = series.format(value);
|
||||
final int offset = maxLabelWidth - g.getFontMetrics().stringWidth(string);
|
||||
final int y = height - ((int) Math.round((value - valueMin) * valueScale) + border);
|
||||
g.setColor(color);
|
||||
g.drawString(string, widthInner + 2 * border + offset, y + (int) Math.round(g.getFontMetrics().getHeight() * 0.25));
|
||||
if (stroke != null && color != null) {
|
||||
g.setStroke(stroke);
|
||||
g.draw(new Line2D.Double(border, y, width - maxLabelWidth - border * 1.5, y));
|
||||
}
|
||||
}
|
||||
|
||||
private Function<Entry, Point> toPoint(final long minuteMin, final double minuteScale, final double valueMin, final double valueScale) {
|
||||
return entry -> {
|
||||
final long minuteEpoch = entry.getDate().toEpochSecond() / 60;
|
||||
final long minuteRelative = minuteEpoch - minuteMin;
|
||||
final double minuteScaled = minuteRelative * minuteScale;
|
||||
final int x = (int) Math.round(minuteScaled);
|
||||
|
||||
final double valueRelative = entry.getValue() - valueMin;
|
||||
final double valueScaled = valueRelative * valueScale;
|
||||
final int y = (int) Math.round(valueScaled);
|
||||
|
||||
return new Point(x, y);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/java/de/ph87/data/series/graph/GraphController.java
Normal file
32
src/main/java/de/ph87/data/series/graph/GraphController.java
Normal file
@ -0,0 +1,32 @@
|
||||
package de.ph87.data.series.graph;
|
||||
|
||||
import jakarta.servlet.http.*;
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.imageio.*;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.time.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("Series/Graph")
|
||||
public class GraphController {
|
||||
|
||||
private final GraphService graphService;
|
||||
|
||||
@GetMapping(path = "{seriesId}/{width}/{height}/{offset}/{duration}", produces = "image/png")
|
||||
public void graph(@PathVariable final long seriesId, final HttpServletResponse response, @PathVariable final int width, @PathVariable final int height, @PathVariable final String offset, @PathVariable final String duration) throws IOException {
|
||||
final ZonedDateTime end = ZonedDateTime.now().minus(Duration.parse(offset));
|
||||
final ZonedDateTime begin = end.minus(Duration.parse(duration));
|
||||
final Graph graph = graphService.getGraph(seriesId, begin, end, width, height, 10);
|
||||
final BufferedImage image = graph.draw();
|
||||
response.setContentType("image/png");
|
||||
ImageIO.write(image, "PNG", response.getOutputStream());
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
}
|
||||
29
src/main/java/de/ph87/data/series/graph/GraphService.java
Normal file
29
src/main/java/de/ph87/data/series/graph/GraphService.java
Normal file
@ -0,0 +1,29 @@
|
||||
package de.ph87.data.series.graph;
|
||||
|
||||
import de.ph87.data.series.*;
|
||||
import de.ph87.data.series.entry.*;
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.*;
|
||||
import org.springframework.stereotype.*;
|
||||
import org.springframework.transaction.annotation.*;
|
||||
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class GraphService {
|
||||
|
||||
private final EntryRepository entryRepository;
|
||||
|
||||
private final SeriesService seriesService;
|
||||
|
||||
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<Entry> entries = entryRepository.findAllBySeriesIdAndDateGreaterThanEqualAndDateLessThanEqual(seriesId, begin, end);
|
||||
return new Graph(series, entries, begin, end, width, height, border);
|
||||
}
|
||||
|
||||
}
|
||||
@ -19,6 +19,7 @@ public enum Unit {
|
||||
IAQ_CO2_EQUIVALENT("ppm"),
|
||||
IAQ_VOC_EQUIVALENT("ppm"),
|
||||
SUN_DC("Δ°C"),
|
||||
UNIT_PERCENT("%"),
|
||||
;
|
||||
|
||||
public final String unit;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user