diff --git a/pom.xml b/pom.xml
index bce4387..c7f3cb4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.4.2
+ 3.4.4
diff --git a/src/main/angular/colors.less b/src/main/angular/colors.less
index d3832e6..a28e0c0 100644
--- a/src/main/angular/colors.less
+++ b/src/main/angular/colors.less
@@ -2,6 +2,7 @@
@foreground: gray;
@background: white;
+@FONT_SELECTABLE: white;
@consumption: orange;
@purchase: orangered;
diff --git a/src/main/angular/src/app/View/View.ts b/src/main/angular/src/app/View/View.ts
new file mode 100644
index 0000000..24597bf
--- /dev/null
+++ b/src/main/angular/src/app/View/View.ts
@@ -0,0 +1,155 @@
+import {Series} from '../series/Series';
+import {validateNumber, validateString} from '../core/validators';
+
+export abstract class View {
+
+ protected constructor(
+ readonly _type_: string,
+ readonly uuid: string,
+ readonly name: string,
+ ) {
+ //
+ }
+
+ static fromJson(json: any, locale: string): View {
+ const type = validateString(json._type_);
+ switch (type) {
+ case 'literal':
+ return ViewLiteral.fromJson2(json);
+ case 'series':
+ return ViewSeries.fromJson2(json, locale);
+ case 'unary':
+ return ViewUnary.fromJson2(json, locale);
+ case 'binary':
+ return ViewBinary.fromJson2(json, locale);
+ default:
+ throw new Error(`View type '${type}' not implemented.`);
+ }
+ }
+
+}
+
+export class ViewLiteral extends View {
+
+ constructor(
+ _type_: string,
+ uuid: string,
+ name: string,
+ readonly value: number,
+ ) {
+ super(_type_, uuid, name);
+ }
+
+ static fromJson2(json: any): ViewLiteral {
+ return new ViewLiteral(
+ validateString(json._type_),
+ validateString(json.uuid),
+ validateString(json.name),
+ validateNumber(json.value),
+ );
+ }
+
+ static cast(view: View): ViewLiteral {
+ return view as ViewLiteral;
+ }
+
+}
+
+export class ViewSeries extends View {
+
+ constructor(
+ _type_: string,
+ uuid: string,
+ name: string,
+ readonly series: Series,
+ ) {
+ super(_type_, uuid, name);
+ }
+
+ static fromJson2(json: any, locale: string): ViewSeries {
+ return new ViewSeries(
+ validateString(json._type_),
+ validateString(json.uuid),
+ validateString(json.name),
+ Series.fromJson(json.series, locale),
+ );
+ }
+
+ static cast(view: View): ViewSeries {
+ return view as ViewSeries;
+ }
+
+}
+
+export enum ViewUnaryOperator {
+ NEG = "NEG",
+ REC = "REC",
+ NOT_NEG = "NOT_NEG",
+}
+
+export class ViewUnary extends View {
+
+ constructor(
+ _type_: string,
+ uuid: string,
+ name: string,
+ readonly operation: ViewUnaryOperator,
+ readonly view: View,
+ ) {
+ super(_type_, uuid, name);
+ }
+
+ static fromJson2(json: any, locale: string): ViewUnary {
+ return new ViewUnary(
+ validateString(json._type_),
+ validateString(json.uuid),
+ validateString(json.name),
+ validateString(json.operation) as ViewUnaryOperator,
+ View.fromJson(json.view, locale),
+ );
+ }
+
+ static cast(view: View): ViewUnary {
+ return view as ViewUnary;
+ }
+
+}
+
+export enum ViewBinaryOperator {
+ PLUS = 'PLUS',
+ MINUS = 'MINUS',
+ MULTIPLY = 'MULTIPLY',
+ DIVIDE = 'DIVIDE',
+ MODULO = 'MODULO',
+ PERCENT = 'PERCENT',
+}
+
+export class ViewBinary extends View {
+
+ constructor(
+ _type_: string,
+ uuid: string,
+ name: string,
+ readonly operation: ViewBinaryOperator,
+ readonly view0: View,
+ readonly view1: View,
+ ) {
+ super(_type_, uuid, name);
+ }
+
+ static fromJson2(json: any, locale: string): ViewBinary {
+ return new ViewBinary(
+ validateString(json._type_),
+ validateString(json.uuid),
+ validateString(json.name),
+ validateString(json.operation) as ViewBinaryOperator,
+ View.fromJson(json.view0, locale),
+ View.fromJson(json.view1, locale),
+ );
+ }
+
+ static cast(view: View): ViewBinary {
+ return view as ViewBinary;
+ }
+
+}
diff --git a/src/main/angular/src/app/View/view-body/view-body.component.html b/src/main/angular/src/app/View/view-body/view-body.component.html
new file mode 100644
index 0000000..465b624
--- /dev/null
+++ b/src/main/angular/src/app/View/view-body/view-body.component.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/angular/src/app/View/view-body/view-body.component.less b/src/main/angular/src/app/View/view-body/view-body.component.less
new file mode 100644
index 0000000..0b1a0b8
--- /dev/null
+++ b/src/main/angular/src/app/View/view-body/view-body.component.less
@@ -0,0 +1,19 @@
+@import "../../../../colors";
+
+.children {
+ margin-left: 0.5em;
+ border-left: 0.1em solid green;
+ overflow: visible;
+
+ .child {
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+ padding-left: 0.5em;
+ }
+
+}
+
+select.binary {
+ background-color: @background;
+ margin-left: -0.5em;
+}
diff --git a/src/main/angular/src/app/View/view-body/view-body.component.ts b/src/main/angular/src/app/View/view-body/view-body.component.ts
new file mode 100644
index 0000000..b0a0f2b
--- /dev/null
+++ b/src/main/angular/src/app/View/view-body/view-body.component.ts
@@ -0,0 +1,40 @@
+import {Component, Input} from '@angular/core';
+import {NgForOf, NgIf, NgSwitch, NgSwitchCase} from '@angular/common';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {View, ViewBinary, ViewBinaryOperator, ViewLiteral, ViewSeries, ViewUnary, ViewUnaryOperator} from '../View';
+import {Series} from '../../series/Series';
+
+@Component({
+ selector: 'app-view-body',
+ imports: [
+ NgForOf,
+ NgSwitchCase,
+ ReactiveFormsModule,
+ NgSwitch,
+ FormsModule,
+ NgIf
+ ],
+ templateUrl: './view-body.component.html',
+ styleUrl: './view-body.component.less'
+})
+export class ViewBodyComponent {
+
+ protected readonly ViewSeries = ViewSeries;
+
+ protected readonly ViewLiteral = ViewLiteral;
+
+ protected readonly ViewUnary = ViewUnary;
+
+ protected readonly ViewUnaryOperator = ViewUnaryOperator;
+
+ protected readonly ViewBinaryOperator = ViewBinaryOperator;
+
+ protected readonly ViewBinary = ViewBinary;
+
+ @Input()
+ view?: View;
+
+ @Input()
+ seriesList?: Series[] = [];
+
+}
diff --git a/src/main/angular/src/app/View/view-list/view-list.component.html b/src/main/angular/src/app/View/view-list/view-list.component.html
new file mode 100644
index 0000000..b0d49b7
--- /dev/null
+++ b/src/main/angular/src/app/View/view-list/view-list.component.html
@@ -0,0 +1,11 @@
+
diff --git a/src/main/angular/src/app/View/view-list/view-list.component.less b/src/main/angular/src/app/View/view-list/view-list.component.less
new file mode 100644
index 0000000..94a8f68
--- /dev/null
+++ b/src/main/angular/src/app/View/view-list/view-list.component.less
@@ -0,0 +1,24 @@
+.view {
+ font-size: 80%;
+
+ .labelPair {
+ display: flex;
+ white-space: nowrap;
+ margin: 0.5em;
+
+ .name {
+ padding-right: 0.5em;
+ }
+
+ input {
+ flex-grow: 1;
+ width: 0;
+ }
+
+ }
+
+ .body {
+ margin: 0.5em;
+ }
+
+}
diff --git a/src/main/angular/src/app/View/view-list/view-list.component.ts b/src/main/angular/src/app/View/view-list/view-list.component.ts
new file mode 100644
index 0000000..7c55600
--- /dev/null
+++ b/src/main/angular/src/app/View/view-list/view-list.component.ts
@@ -0,0 +1,46 @@
+import {Component, OnInit} from '@angular/core';
+import {ViewService} from '../view.service';
+import {View, ViewLiteral, ViewSeries, ViewUnary, ViewUnaryOperator} from '../View';
+import {NgForOf} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {Series} from '../../series/Series';
+import {SeriesService} from '../../series/series.service';
+import {ViewBodyComponent} from '../view-body/view-body.component';
+
+@Component({
+ selector: 'app-view-list',
+ imports: [
+ NgForOf,
+ FormsModule,
+ ViewBodyComponent
+ ],
+ templateUrl: './view-list.component.html',
+ styleUrl: './view-list.component.less'
+})
+export class ViewListComponent implements OnInit {
+
+ protected readonly ViewUnary = ViewUnary;
+
+ protected readonly ViewUnaryOperator = ViewUnaryOperator;
+
+ protected readonly ViewLiteral = ViewLiteral;
+
+ protected readonly ViewSeries = ViewSeries;
+
+ protected seriesList: Series[] = [];
+
+ protected rootList: View[] = [];
+
+ constructor(
+ readonly viewService: ViewService,
+ readonly seriesService: SeriesService,
+ ) {
+ //
+ }
+
+ ngOnInit(): void {
+ this.viewService.list(list => this.rootList = list);
+ this.seriesService.all(list => this.seriesList = list);
+ }
+
+}
diff --git a/src/main/angular/src/app/View/view.service.ts b/src/main/angular/src/app/View/view.service.ts
new file mode 100644
index 0000000..683ef23
--- /dev/null
+++ b/src/main/angular/src/app/View/view.service.ts
@@ -0,0 +1,28 @@
+import {Inject, Injectable, LOCALE_ID} from '@angular/core';
+import {ApiService} from '../core/api.service';
+import {FromJson, Next} from '../core/types';
+import {View} from './View';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ViewService {
+
+ readonly fromJson: FromJson = json => View.fromJson(json, this.locale);
+
+ constructor(
+ readonly api: ApiService,
+ @Inject(LOCALE_ID) readonly locale: string,
+ ) {
+ //
+ }
+
+ list(next: Next) {
+ return this.api.getList(['View', 'rootList'], this.fromJson, next);
+ }
+
+ byUuid(uuid: string, next: Next) {
+ return this.api.getSingle(['View', 'byUuid', uuid], this.fromJson, next);
+ }
+
+}
diff --git a/src/main/angular/src/app/app.component.less b/src/main/angular/src/app/app.component.less
index 69a3009..cb183f5 100644
--- a/src/main/angular/src/app/app.component.less
+++ b/src/main/angular/src/app/app.component.less
@@ -1,5 +1,5 @@
.menubar {
- border-bottom: 1px solid black;
+ border-bottom: 0.05em solid black;
background-color: #303d47;
.menuitem {
@@ -8,12 +8,12 @@
.menuitemLeft {
float: left;
- border-right: 1px solid black;
+ border-right: 0.05em solid black;
}
.menuitemRight {
float: right;
- border-left: 1px solid black;
+ border-left: 0.05em solid black;
}
.menuitemActive {
diff --git a/src/main/angular/src/app/app.routes.ts b/src/main/angular/src/app/app.routes.ts
index 6d1a114..9c1b5ce 100644
--- a/src/main/angular/src/app/app.routes.ts
+++ b/src/main/angular/src/app/app.routes.ts
@@ -2,6 +2,7 @@ import {Routes} from '@angular/router';
import {LiveComponent} from './live/live.component';
import {GreenhouseComponent} from './live/greenhouse/greenhouse/greenhouse.component';
import {HistoryComponent} from './history/history.component';
+import {ViewListComponent} from './View/view-list/view-list.component';
export class Path {
@@ -22,6 +23,7 @@ export class Path {
export const ROUTING = {
LIVE: new Path('Live', 'Live', true),
HISTORY: new Path('History', 'Historie', true),
+ VIEW_LIST: new Path('ViewList', 'Ansichten', true),
GREENHOUSE: new Path('Greenhouse', 'Gewächshaus', false),
}
@@ -33,5 +35,6 @@ export const routes: Routes = [
{path: ROUTING.LIVE.path, component: LiveComponent},
{path: ROUTING.HISTORY.path, component: HistoryComponent},
{path: ROUTING.GREENHOUSE.path, component: GreenhouseComponent},
+ {path: ROUTING.VIEW_LIST.path, component: ViewListComponent},
{path: '**', redirectTo: ROUTING.LIVE.path},
];
diff --git a/src/main/angular/src/app/core/api.service.ts b/src/main/angular/src/app/core/api.service.ts
index 49ff931..b6d864a 100644
--- a/src/main/angular/src/app/core/api.service.ts
+++ b/src/main/angular/src/app/core/api.service.ts
@@ -4,7 +4,7 @@ import {map, Subscription} from 'rxjs';
import {StompService} from '@stomp/ng2-stompjs';
import {FromJson, Next} from './types';
-const DEV_TO_PROD = true;
+const DEV_TO_PROD = false;
@Injectable({
providedIn: 'root'
diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less
index 8c0fc8c..66ff44c 100644
--- a/src/main/angular/src/styles.less
+++ b/src/main/angular/src/styles.less
@@ -10,6 +10,14 @@ body {
margin: 0;
}
+input, select {
+ background-color: transparent;
+ font-size: inherit;
+ color: @FONT_SELECTABLE;
+ border: 0.05em solid gray;
+ border-radius: 0.2em;
+}
+
button {
all: unset;
font-size: inherit;
diff --git a/src/main/java/de/ph87/data/series/graph/Graph.java b/src/main/java/de/ph87/data/graph/Graph.java
similarity index 81%
rename from src/main/java/de/ph87/data/series/graph/Graph.java
rename to src/main/java/de/ph87/data/graph/Graph.java
index fe5ef51..1be78a0 100644
--- a/src/main/java/de/ph87/data/series/graph/Graph.java
+++ b/src/main/java/de/ph87/data/graph/Graph.java
@@ -1,7 +1,7 @@
-package de.ph87.data.series.graph;
+package de.ph87.data.graph;
-import de.ph87.data.series.Aligned;
-import de.ph87.data.series.Alignment;
+import de.ph87.data.point.Point;
+import de.ph87.data.point.PointRequest;
import de.ph87.data.series.SeriesDto;
import de.ph87.data.series.SeriesType;
import de.ph87.data.value.Autoscale;
@@ -26,13 +26,7 @@ public class Graph {
public final SeriesDto series;
@NonNull
- public final Alignment innerAlignment;
-
- @NonNull
- public final Aligned begin;
-
- @NonNull
- public final Aligned end;
+ public final PointRequest request;
public final int width;
@@ -40,7 +34,7 @@ public class Graph {
public final int border;
- public final List points;
+ public final List points;
public final long minuteMin;
@@ -68,11 +62,9 @@ public class Graph {
public final Autoscale autoscale;
- public Graph(@NonNull final SeriesDto series, @NonNull final List points, @NonNull final Alignment innerAlignment, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) {
+ public Graph(@NonNull final SeriesDto series, @NonNull final List points, @NonNull final PointRequest request, final int width, final int height, final int border) {
this.series = series;
- this.innerAlignment = innerAlignment;
- this.begin = begin;
- this.end = end;
+ this.request = request;
this.width = width;
this.height = height;
this.border = border;
@@ -81,7 +73,7 @@ public class Graph {
double vSum = 0;
double vMin = series.getYMin() == null || Double.isNaN(series.getYMin()) ? Double.MIN_VALUE : series.getYMin();
double vMax = series.getYMax() == null || Double.isNaN(series.getYMax()) ? Double.MAX_VALUE : series.getYMax();
- for (final GraphPoint point : points) {
+ for (final Point point : points) {
vMin = Math.min(vMin, point.getValue());
vMax = max(vMax, point.getValue());
vSum += point.getValue();
@@ -96,7 +88,7 @@ public class Graph {
// find max label width
int __maxLabelWidth = 0;
final FontMetrics fontMetrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics().getFontMetrics();
- for (final GraphPoint point : points) {
+ for (final Point point : points) {
__maxLabelWidth = max(__maxLabelWidth, fontMetrics.stringWidth(autoscale.format(point.getValue() * autoscale.factor)));
}
this.maxLabelWidth = __maxLabelWidth;
@@ -104,10 +96,10 @@ public class Graph {
widthInner = width - 3 * border - this.maxLabelWidth;
heightInner = height - 2 * border;
- minuteMin = begin.date.toEpochSecond() / 60;
- minuteMax = end.date.toEpochSecond() / 60;
+ minuteMin = request.begin.date.toEpochSecond() / 60;
+ minuteMax = request.end.date.toEpochSecond() / 60;
minuteRange = minuteMax - minuteMin;
- minuteScale = (double) widthInner / (minuteRange + innerAlignment.maxDuration.toMinutes());
+ minuteScale = (double) widthInner / (minuteRange + request.inner.maxDuration.toMinutes());
valueMin = vMin;
valueMax = vMax;
@@ -137,14 +129,14 @@ public class Graph {
g.setColor(Color.WHITE);
if (series.type == SeriesType.METER) {
- final int space = (int) (minuteScale * innerAlignment.maxDuration.toMinutes());
+ final int space = (int) (minuteScale * request.inner.maxDuration.toMinutes());
final int width = (int) (space * 0.95);
- for (final Point point : points) {
+ for (final java.awt.Point point : points) {
g.fillRect(point.x + (space - width), 0, width, point.y);
}
} else {
- Point last = null;
- for (final Point current : points) {
+ java.awt.Point last = null;
+ for (final java.awt.Point current : points) {
if (last != null) {
g.drawLine(last.x, last.y, current.x, current.y);
}
@@ -167,7 +159,7 @@ public class Graph {
}
@NonNull
- private Function toPoint() {
+ private Function toPoint() {
return point -> {
final long minuteEpoch = point.getDate().toEpochSecond() / 60;
final long minuteRelative = minuteEpoch - minuteMin;
@@ -178,7 +170,7 @@ public class Graph {
final double valueScaled = valueRelative * valueScale;
final int y = (int) Math.round(valueScaled);
- return new Point(x, y);
+ return new java.awt.Point(x, y);
};
}
diff --git a/src/main/java/de/ph87/data/graph/GraphController.java b/src/main/java/de/ph87/data/graph/GraphController.java
new file mode 100644
index 0000000..c042ecf
--- /dev/null
+++ b/src/main/java/de/ph87/data/graph/GraphController.java
@@ -0,0 +1,49 @@
+package de.ph87.data.graph;
+
+import de.ph87.data.point.Point;
+import de.ph87.data.point.PointRequest;
+import de.ph87.data.point.PointService;
+import de.ph87.data.series.Alignment;
+import de.ph87.data.series.Series;
+import de.ph87.data.series.SeriesDto;
+import de.ph87.data.series.SeriesRepository;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("Series/Graph")
+public class GraphController {
+
+ private final SeriesRepository seriesRepository;
+
+ private final PointService pointService;
+
+ @GetMapping(path = "{seriesId}/{width}/{height}/{outerName}/{offset}/{duration}/{innerName}", produces = "image/png")
+ public void graph(@PathVariable final long seriesId, final HttpServletResponse response, @PathVariable final int width, @PathVariable final int height, @PathVariable final String outerName, @PathVariable final long offset, @PathVariable final long duration, @PathVariable final String innerName) throws IOException {
+ final Alignment outer = Alignment.valueOf(outerName);
+ final Alignment inner = Alignment.valueOf(innerName);
+ final PointRequest request = new PointRequest(outer, offset, duration, inner);
+
+ final Series series = seriesRepository.findById(seriesId).orElseThrow();
+ final List points = pointService.getPoints(series, request);
+ final Graph graph = new Graph(new SeriesDto(series), points, request, width, height, 10);
+
+ final BufferedImage image = graph.draw();
+ response.setContentType("image/png");
+ ImageIO.write(image, "PNG", response.getOutputStream());
+ response.getOutputStream().flush();
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/series/graph/GraphPoint.java b/src/main/java/de/ph87/data/point/Point.java
similarity index 63%
rename from src/main/java/de/ph87/data/series/graph/GraphPoint.java
rename to src/main/java/de/ph87/data/point/Point.java
index b171a54..b9abd5b 100644
--- a/src/main/java/de/ph87/data/series/graph/GraphPoint.java
+++ b/src/main/java/de/ph87/data/point/Point.java
@@ -1,22 +1,22 @@
-package de.ph87.data.series.graph;
+package de.ph87.data.point;
import lombok.*;
import java.time.*;
@Data
-public class GraphPoint {
+public class Point {
public final ZonedDateTime date;
public final double value;
@NonNull
- public GraphPoint plus(@NonNull final GraphPoint other) {
+ public Point plus(@NonNull final Point other) {
if (this.date.compareTo(other.date) != 0) {
throw new RuntimeException("Cannot 'add' GraphPoints with different dates: this=%s, other=%s".formatted(this, other));
}
- return new GraphPoint(date, value + other.value);
+ return new Point(date, value + other.value);
}
}
diff --git a/src/main/java/de/ph87/data/point/PointController.java b/src/main/java/de/ph87/data/point/PointController.java
new file mode 100644
index 0000000..8bdc945
--- /dev/null
+++ b/src/main/java/de/ph87/data/point/PointController.java
@@ -0,0 +1,26 @@
+package de.ph87.data.point;
+
+import de.ph87.data.view.ViewPointRequest;
+import de.ph87.data.view.ViewService;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("points")
+public class PointController {
+
+ private final ViewService viewService;
+
+ @PostMapping("fetch")
+ public List fetch(@RequestBody @NonNull final ViewPointRequest request) {
+ return viewService.getPoints(request);
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/point/PointRequest.java b/src/main/java/de/ph87/data/point/PointRequest.java
new file mode 100644
index 0000000..f60eb4c
--- /dev/null
+++ b/src/main/java/de/ph87/data/point/PointRequest.java
@@ -0,0 +1,41 @@
+package de.ph87.data.point;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import de.ph87.data.series.Aligned;
+import de.ph87.data.series.Alignment;
+import lombok.Data;
+import lombok.NonNull;
+
+import java.time.ZonedDateTime;
+
+@Data
+public class PointRequest {
+
+ @NonNull
+ public final Alignment outer;
+
+ public final long outerOffset;
+
+ public final long outerCount;
+
+ @NonNull
+ public final Alignment inner;
+
+ @NonNull
+ @JsonIgnore
+ public final Aligned begin;
+
+ @NonNull
+ @JsonIgnore
+ public final Aligned end;
+
+ public PointRequest(@NonNull final Alignment outer, final long outerOffset, final long outerCount, @NonNull final Alignment inner) {
+ this.outer = outer;
+ this.outerOffset = outerOffset;
+ this.outerCount = outerCount;
+ this.inner = inner;
+ this.end = outer.align(ZonedDateTime.now()).plus(1).minus(outerOffset);
+ this.begin = end.minus(outerCount);
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/point/PointService.java b/src/main/java/de/ph87/data/point/PointService.java
new file mode 100644
index 0000000..c6a34af
--- /dev/null
+++ b/src/main/java/de/ph87/data/point/PointService.java
@@ -0,0 +1,30 @@
+package de.ph87.data.point;
+
+import de.ph87.data.series.Series;
+import de.ph87.data.series.meter.MeterService;
+import de.ph87.data.series.varying.VaryingService;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PointService {
+
+ private final VaryingService varyingService;
+
+ private final MeterService meterService;
+
+ @NonNull
+ public List getPoints(@NonNull final Series series, @NonNull final PointRequest pointRequest) {
+ return switch (series.getType()) {
+ case METER -> meterService.getPoints(series, pointRequest);
+ case VARYING -> varyingService.getPoints(series, pointRequest);
+ };
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/series/SeriesDto.java b/src/main/java/de/ph87/data/series/SeriesDto.java
index 6235d55..49af668 100644
--- a/src/main/java/de/ph87/data/series/SeriesDto.java
+++ b/src/main/java/de/ph87/data/series/SeriesDto.java
@@ -1,5 +1,6 @@
package de.ph87.data.series;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import de.ph87.data.value.Unit;
import de.ph87.data.web.IWebSocketMessage;
import jakarta.annotation.Nullable;
@@ -14,6 +15,7 @@ import java.util.List;
@ToString
public class SeriesDto implements IWebSocketMessage {
+ @JsonIgnore
public final List