diff --git a/src/main/angular/src/app/app.html b/src/main/angular/src/app/app.html
index 74ba95d..59202e0 100644
--- a/src/main/angular/src/app/app.html
+++ b/src/main/angular/src/app/app.html
@@ -16,4 +16,6 @@
-
+
+
+
diff --git a/src/main/angular/src/app/app.less b/src/main/angular/src/app/app.less
index 3fd6624..21bce2f 100644
--- a/src/main/angular/src/app/app.less
+++ b/src/main/angular/src/app/app.less
@@ -1,7 +1,6 @@
@sidebarColor: #62b0ca;
.sidebar {
- margin-left: -2em;
position: fixed;
display: flex;
height: 100%;
@@ -49,3 +48,7 @@
}
}
+
+.bodyContent {
+ margin-left: 2em;
+}
diff --git a/src/main/angular/src/app/plot/plot/plot.component.ts b/src/main/angular/src/app/plot/plot/plot.component.ts
index 787fec3..1f4f262 100644
--- a/src/main/angular/src/app/plot/plot/plot.component.ts
+++ b/src/main/angular/src/app/plot/plot/plot.component.ts
@@ -8,13 +8,15 @@ import {NTU, UTN} from '../../COMMON';
import {Graph} from '../axis/graph/Graph';
import {Axis} from '../axis/Axis';
import {Subscription} from 'rxjs';
-import {Delta, DeltaService} from '../../series/delta/delta-service';
-import {Bool, BoolService} from '../../series/bool/bool-service';
+import {DeltaService} from '../../series/delta/delta-service';
+import {BoolService} from '../../series/bool/bool-service';
import {VaryingService} from '../../series/varying/varying-service';
import {Interval} from '../../series/Interval';
import {SeriesService} from '../../series/series.service';
import {GraphType} from '../axis/graph/GraphType';
import {Varying} from '../../series/varying/Varying';
+import {Delta} from '../../series/delta/Delta';
+import {Bool} from '../../series/bool/Bool';
type Dataset = ChartDataset[][number];
diff --git a/src/main/angular/src/app/series/MinMaxAvg.ts b/src/main/angular/src/app/series/MinMaxAvg.ts
index 147d3cb..068c2b3 100644
--- a/src/main/angular/src/app/series/MinMaxAvg.ts
+++ b/src/main/angular/src/app/series/MinMaxAvg.ts
@@ -49,7 +49,7 @@ export function toDelta(points: number[][], factor: number): Point[] {
for (const p of points) {
result.push({
x: p[0] * 1000,
- y: (p[2] - p[1]) * factor,
+ y: (p[1]) * factor,
});
}
return result;
diff --git a/src/main/angular/src/app/series/bool/Bool.ts b/src/main/angular/src/app/series/bool/Bool.ts
new file mode 100644
index 0000000..a14b8a2
--- /dev/null
+++ b/src/main/angular/src/app/series/bool/Bool.ts
@@ -0,0 +1,26 @@
+import {Series} from "../Series";
+import {validateBoolean, validateDate} from "../../COMMON";
+
+export class Bool {
+
+ constructor(
+ readonly series: Series,
+ readonly date: Date,
+ readonly end: Date,
+ readonly state: boolean,
+ readonly terminated: boolean,
+ ) {
+ //
+ }
+
+ static fromJson(json: any): Bool {
+ return new Bool(
+ Series.fromJson(json.series),
+ validateDate(json.date),
+ validateDate(json.end),
+ validateBoolean(json.state),
+ validateBoolean(json.terminated),
+ );
+ }
+
+}
diff --git a/src/main/angular/src/app/series/bool/bool-service.ts b/src/main/angular/src/app/series/bool/bool-service.ts
index 371dba1..309a0ca 100644
--- a/src/main/angular/src/app/series/bool/bool-service.ts
+++ b/src/main/angular/src/app/series/bool/bool-service.ts
@@ -1,30 +1,6 @@
import {Injectable} from '@angular/core';
-import {ApiService, CrudService, validateBoolean, validateDate} from '../../COMMON';
-import {Series} from '../Series';
-
-export class Bool {
-
- constructor(
- readonly series: Series,
- readonly date: Date,
- readonly end: Date,
- readonly state: boolean,
- readonly terminated: boolean,
- ) {
- //
- }
-
- static fromJson(json: any): Bool {
- return new Bool(
- Series.fromJson(json.series),
- validateDate(json.date),
- validateDate(json.end),
- validateBoolean(json.state),
- validateBoolean(json.terminated),
- );
- }
-
-}
+import {ApiService, CrudService} from '../../COMMON';
+import {Bool} from './Bool';
@Injectable({
providedIn: 'root'
diff --git a/src/main/angular/src/app/series/delta/Delta.ts b/src/main/angular/src/app/series/delta/Delta.ts
new file mode 100644
index 0000000..629f191
--- /dev/null
+++ b/src/main/angular/src/app/series/delta/Delta.ts
@@ -0,0 +1,24 @@
+import {validateDate, validateNumber} from "../../COMMON";
+import {Meter} from './meter/Meter';
+
+export class Delta {
+
+ constructor(
+ readonly meter: Meter,
+ readonly date: Date,
+ readonly first: number,
+ readonly last: number,
+ ) {
+ //
+ }
+
+ static fromJson(json: any): Delta {
+ return new Delta(
+ Meter.fromJson(json.meter),
+ validateDate(json.date),
+ validateNumber(json.first),
+ validateNumber(json.last),
+ );
+ }
+
+}
diff --git a/src/main/angular/src/app/series/delta/delta-service.ts b/src/main/angular/src/app/series/delta/delta-service.ts
index 7a44717..9ef4736 100644
--- a/src/main/angular/src/app/series/delta/delta-service.ts
+++ b/src/main/angular/src/app/series/delta/delta-service.ts
@@ -1,28 +1,6 @@
import {Injectable} from '@angular/core';
-import {ApiService, CrudService, validateDate, validateNumber} from '../../COMMON';
-import {Series} from '../Series';
-
-export class Delta {
-
- constructor(
- readonly series: Series,
- readonly date: Date,
- readonly first: number,
- readonly last: number,
- ) {
- //
- }
-
- static fromJson(json: any): Delta {
- return new Delta(
- Series.fromJson(json.series),
- validateDate(json.date),
- validateNumber(json.first),
- validateNumber(json.last),
- );
- }
-
-}
+import {ApiService, CrudService} from '../../COMMON';
+import {Delta} from './Delta';
@Injectable({
providedIn: 'root'
diff --git a/src/main/angular/src/app/series/delta/meter/Meter.ts b/src/main/angular/src/app/series/delta/meter/Meter.ts
new file mode 100644
index 0000000..bba5d5b
--- /dev/null
+++ b/src/main/angular/src/app/series/delta/meter/Meter.ts
@@ -0,0 +1,24 @@
+import {Series} from "../../Series";
+import {validateDate, validateNumber, validateString} from "../../../COMMON";
+
+export class Meter {
+
+ constructor(
+ readonly id: number,
+ readonly series: Series,
+ readonly number: string,
+ readonly first: Date,
+ ) {
+ //
+ }
+
+ static fromJson(json: any): Meter {
+ return new Meter(
+ validateNumber(json.id),
+ Series.fromJson(json.series),
+ validateString(json.number),
+ validateDate(json.first),
+ );
+ }
+
+}
diff --git a/src/main/angular/src/app/topic/Topic.ts b/src/main/angular/src/app/topic/Topic.ts
index b68f2c8..0b8c0a1 100644
--- a/src/main/angular/src/app/topic/Topic.ts
+++ b/src/main/angular/src/app/topic/Topic.ts
@@ -17,6 +17,8 @@ export class Topic {
readonly timestampType: TimestampType,
readonly timestampQuery: string,
readonly timestampLast: Date | null,
+ readonly meterNumberQuery: string,
+ readonly meterNumberLast: string,
readonly queries: TopicQuery[],
readonly payload: string,
readonly error: string,
@@ -35,6 +37,8 @@ export class Topic {
validateString(json.timestampType) as TimestampType,
validateString(json.timestampQuery),
mapNotNull(json.timestampLast, validateDate),
+ validateString(json.meterNumberQuery),
+ validateString(json.meterNumberLast),
validateList(json.queries, TopicQuery.fromJson),
validateString(json.payload),
validateString(json.error),
diff --git a/src/main/angular/src/app/topic/list/topic-list.component.html b/src/main/angular/src/app/topic/list/topic-list.component.html
index 5fcebc2..68a6f39 100644
--- a/src/main/angular/src/app/topic/list/topic-list.component.html
+++ b/src/main/angular/src/app/topic/list/topic-list.component.html
@@ -1,3 +1,8 @@
+
+
+
+
+
@for (topic of sorted(); track topic.id) {
@@ -10,6 +15,14 @@
| {{ topic.timestampType }} |
{{ topic.timestampLast | date:'long':'':'de-DE' }} |
+ @if (topic.queries[0]?.series?.type === SeriesType.DELTA) {
+
+ | {{ topic.meterNumberQuery }} |
+ |
+ {{ topic.meterNumberLast }} |
+ {{ topic.timestampLast | date:'long':'':'de-DE' }} |
+
+ }
@for (query of topic.queries; track $index) {
| {{ query.valueQuery }} |
diff --git a/src/main/angular/src/app/topic/list/topic-list.component.less b/src/main/angular/src/app/topic/list/topic-list.component.less
index fe6ebcb..5f528fd 100644
--- a/src/main/angular/src/app/topic/list/topic-list.component.less
+++ b/src/main/angular/src/app/topic/list/topic-list.component.less
@@ -22,6 +22,10 @@ table.TopicList {
background-color: darkseagreen;
}
+ tr.meter {
+ background-color: #ffe7bd;
+ }
+
tr.spacer {
th, td {
border: none;
@@ -53,7 +57,11 @@ table.TopicList {
}
.timestampLast {
+ text-align: right;
+ }
+ .meterNumberLast {
+ text-align: right;
}
.valueQuery {
diff --git a/src/main/angular/src/app/topic/list/topic-list.component.ts b/src/main/angular/src/app/topic/list/topic-list.component.ts
index d7f2035..ce4aafd 100644
--- a/src/main/angular/src/app/topic/list/topic-list.component.ts
+++ b/src/main/angular/src/app/topic/list/topic-list.component.ts
@@ -11,6 +11,7 @@ import {SeriesService} from '../../series/series.service';
import {Config} from '../../config/Config';
import {ageString} from '../../COMMON';
import {ConfigService} from '../../config/config.service';
+import {SeriesType} from '../../series/SeriesType';
@Component({
selector: 'app-topic-list',
@@ -25,6 +26,8 @@ export class TopicListComponent implements OnInit, OnDestroy {
protected readonly ageString = ageString;
+ protected readonly SeriesType = SeriesType;
+
protected now: Date = new Date();
protected topicList: Topic[] = [];
@@ -55,10 +58,6 @@ export class TopicListComponent implements OnInit, OnDestroy {
this.subs.forEach(sub => sub.unsubscribe());
}
- sorted() {
- return this.topicList.filter(this.filter).sort(this.compare);
- }
-
private readonly update = (topic: Topic) => {
const index = this.topicList.findIndex(t => t.id === topic.id);
if (index >= 0) {
@@ -68,6 +67,10 @@ export class TopicListComponent implements OnInit, OnDestroy {
}
};
+ sorted() {
+ return this.topicList.filter(this.filter).sort(this.compare);
+ }
+
private readonly filter = (topic: Topic) => {
return ((!topic.used && this.config.topicList.unused) || (topic.used && this.config.topicList.used));
}
diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less
index 9a6147b..704217f 100644
--- a/src/main/angular/src/styles.less
+++ b/src/main/angular/src/styles.less
@@ -5,7 +5,6 @@ html, body {
body {
font-family: sans-serif;
- margin-left: 2em;
}
table {
@@ -23,7 +22,7 @@ input, select, textarea {
font-size: inherit;
}
-input, select {
+input:not([type=checkbox]), select {
width: 100%;
margin: 0;
outline: none;
diff --git a/src/main/java/de/ph87/data/DemoService.java b/src/main/java/de/ph87/data/DemoService.java
index 40bf903..668c1fa 100644
--- a/src/main/java/de/ph87/data/DemoService.java
+++ b/src/main/java/de/ph87/data/DemoService.java
@@ -62,8 +62,9 @@ public class DemoService {
final Series electricityEnergyProduce = series("electricity/energy/produce", "kWh", SeriesType.DELTA, 5);
final Series electricityPowerProduce = series("electricity/power/produce", "W", SeriesType.VARYING, 5);
- topic(
+ topicMeterNumber(
"openDTU/pv/patrix/json2",
+ "$.inverter",
new TopicQuery(electricityEnergyProduce, "$.totalKWh"),
new TopicQuery(electricityPowerProduce, "$.totalW")
);
@@ -72,8 +73,9 @@ public class DemoService {
final Series electricityPowerPurchase = series("electricity/power/purchase", "W", SeriesType.VARYING, 5);
final Series electricityEnergyDelivery = series("electricity/energy/delivery", "kWh", SeriesType.DELTA, 5);
final Series electricityPowerDelivery = series("electricity/power/delivery", "W", SeriesType.VARYING, 5);
- topic(
+ topicMeterNumber(
"electricity/grid/json",
+ "\"1ZPA0020300305\"",
new TopicQuery(electricityEnergyPurchase, "$.purchaseWh", 0.001),
new TopicQuery(electricityPowerPurchase, "$.powerW", TopicQueryFunction.ONLY_POSITIVE),
new TopicQuery(electricityEnergyDelivery, "$.deliveryWh", 0.001),
@@ -220,4 +222,12 @@ public class DemoService {
topic.getQueries().addAll(List.of(queries));
}
+ private void topicMeterNumber(@NonNull final String name, @NonNull final String meterNumberQuery, @NonNull final TopicQuery... queries) {
+ final Topic topic = topicRepository.findByName(name).orElseGet(() -> topicRepository.save(new Topic(name)));
+ topic.setMeterNumberQuery(meterNumberQuery);
+ topic.setTimestampQuery("$.timestamp");
+ topic.getQueries().clear();
+ topic.getQueries().addAll(List.of(queries));
+ }
+
}
diff --git a/src/main/java/de/ph87/data/series/data/delta/Delta.java b/src/main/java/de/ph87/data/series/data/delta/Delta.java
index 18a50a2..add7470 100644
--- a/src/main/java/de/ph87/data/series/data/delta/Delta.java
+++ b/src/main/java/de/ph87/data/series/data/delta/Delta.java
@@ -1,6 +1,5 @@
package de.ph87.data.series.data.delta;
-import de.ph87.data.series.data.DataId;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@@ -19,7 +18,7 @@ public abstract class Delta {
@Id
@NonNull
- private DataId id;
+ private DeltaId id;
@Version
private long version;
@@ -31,7 +30,7 @@ public abstract class Delta {
@Column(nullable = false)
private double last;
- protected Delta(@NonNull final DataId id, final double value) {
+ protected Delta(@NonNull final DeltaId id, final double value) {
this.id = id;
this.first = value;
this.last = value;
@@ -47,7 +46,7 @@ public abstract class Delta {
@Entity(name = "DeltaFive")
public static class Five extends Delta {
- public Five(@NonNull final DataId id, @NonNull final double value) {
+ public Five(@NonNull final DeltaId id, @NonNull final double value) {
super(id, value);
}
@@ -59,7 +58,7 @@ public abstract class Delta {
@Entity(name = "DeltaHour")
public static class Hour extends Delta {
- public Hour(@NonNull final DataId id, @NonNull final double value) {
+ public Hour(@NonNull final DeltaId id, @NonNull final double value) {
super(id, value);
}
@@ -71,7 +70,7 @@ public abstract class Delta {
@Entity(name = "DeltaDay")
public static class Day extends Delta {
- public Day(@NonNull final DataId id, @NonNull final double value) {
+ public Day(@NonNull final DeltaId id, @NonNull final double value) {
super(id, value);
}
@@ -83,7 +82,7 @@ public abstract class Delta {
@Entity(name = "DeltaWeek")
public static class Week extends Delta {
- public Week(@NonNull final DataId id, @NonNull final double value) {
+ public Week(@NonNull final DeltaId id, @NonNull final double value) {
super(id, value);
}
@@ -95,7 +94,7 @@ public abstract class Delta {
@Entity(name = "DeltaMonth")
public static class Month extends Delta {
- public Month(@NonNull final DataId id, @NonNull final double value) {
+ public Month(@NonNull final DeltaId id, @NonNull final double value) {
super(id, value);
}
@@ -107,7 +106,7 @@ public abstract class Delta {
@Entity(name = "DeltaYear")
public static class Year extends Delta {
- public Year(@NonNull final DataId id, @NonNull final double value) {
+ public Year(@NonNull final DeltaId id, @NonNull final double value) {
super(id, value);
}
diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaDto.java b/src/main/java/de/ph87/data/series/data/delta/DeltaDto.java
index 175d57a..05f3627 100644
--- a/src/main/java/de/ph87/data/series/data/delta/DeltaDto.java
+++ b/src/main/java/de/ph87/data/series/data/delta/DeltaDto.java
@@ -1,7 +1,7 @@
package de.ph87.data.series.data.delta;
-import de.ph87.data.series.SeriesDto;
import de.ph87.data.series.data.Interval;
+import de.ph87.data.series.data.delta.meter.MeterDto;
import de.ph87.data.websocket.IWebsocketMessage;
import lombok.Data;
import lombok.Getter;
@@ -14,21 +14,20 @@ import java.time.ZonedDateTime;
public abstract class DeltaDto implements IWebsocketMessage {
@NonNull
- private SeriesDto series;
+ private MeterDto meter;
@NonNull
private ZonedDateTime date;
public final double first;
- @NonNull
public final double last;
@NonNull
public final Interval interval;
protected DeltaDto(@NonNull final Delta delta, @NonNull final Interval interval) {
- this.series = new SeriesDto(delta.getId().getSeries(), false);
+ this.meter = new MeterDto(delta.getId().getMeter());
this.date = delta.getId().getDate();
this.first = delta.getFirst();
this.last = delta.getLast();
@@ -38,7 +37,7 @@ public abstract class DeltaDto implements IWebsocketMessage {
@NonNull
@Override
public String getWebsocketTopic() {
- return "Delta/%d/%s".formatted(series.id, interval);
+ return "Delta/%d/%s".formatted(meter.series.id, interval);
}
@Getter
diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaId.java b/src/main/java/de/ph87/data/series/data/delta/DeltaId.java
new file mode 100644
index 0000000..dfead24
--- /dev/null
+++ b/src/main/java/de/ph87/data/series/data/delta/DeltaId.java
@@ -0,0 +1,36 @@
+package de.ph87.data.series.data.delta;
+
+import de.ph87.data.series.data.Interval;
+import de.ph87.data.series.data.delta.meter.Meter;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.ManyToOne;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+import lombok.ToString;
+
+import java.time.ZonedDateTime;
+
+@Getter
+@ToString
+@Embeddable
+@EqualsAndHashCode
+@NoArgsConstructor
+public class DeltaId {
+
+ @NonNull
+ @ManyToOne(optional = false)
+ private Meter meter;
+
+ @NonNull
+ @Column(nullable = false)
+ private ZonedDateTime date;
+
+ public DeltaId(@NonNull final Meter meter, @NonNull final ZonedDateTime date, @NonNull final Interval interval) {
+ this.meter = meter;
+ this.date = interval.align.apply(date);
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java b/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java
index 9ee466f..23100c8 100644
--- a/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java
+++ b/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java
@@ -10,23 +10,20 @@ import java.time.ZonedDateTime;
@SuppressWarnings("unused") // used by repository query
public class DeltaPoint implements SeriesPoint {
+ @NonNull
public final ZonedDateTime date;
- public final double first;
+ public final double delta;
- public final double last;
-
- public DeltaPoint(@NonNull final Delta delta) {
- this.date = delta.getId().getDate();
- this.first = delta.getFirst();
- this.last = delta.getLast();
+ public DeltaPoint(@NonNull final ZonedDateTime date, final double delta) {
+ this.date = date;
+ this.delta = delta;
}
@Override
public void toJson(final JsonGenerator jsonGenerator) throws IOException {
jsonGenerator.writeNumber(date.toEpochSecond());
- jsonGenerator.writeNumber(first);
- jsonGenerator.writeNumber(last);
+ jsonGenerator.writeNumber(delta);
}
}
diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java b/src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java
index 3d7de5c..a545df5 100644
--- a/src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java
+++ b/src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java
@@ -1,7 +1,6 @@
package de.ph87.data.series.data.delta;
import de.ph87.data.series.Series;
-import de.ph87.data.series.data.DataId;
import lombok.NonNull;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
@@ -11,10 +10,10 @@ import java.time.ZonedDateTime;
import java.util.List;
@NoRepositoryBean
-public interface DeltaRepo extends CrudRepository {
+public interface DeltaRepo extends CrudRepository {
@NonNull
- @Query("select new de.ph87.data.series.data.delta.DeltaPoint(e) from #{#entityName} e where e.id.series = :series and e.id.date >= :first and e.id.date < :after")
+ @Query("select new de.ph87.data.series.data.delta.DeltaPoint(e.id.date, sum(e.last - e.first)) from #{#entityName} e where e.id.meter.series = :series and e.id.date >= :first and e.id.date < :after group by e.id.date")
List points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after);
}
diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaService.java b/src/main/java/de/ph87/data/series/data/delta/DeltaService.java
index 7f06802..7554f59 100644
--- a/src/main/java/de/ph87/data/series/data/delta/DeltaService.java
+++ b/src/main/java/de/ph87/data/series/data/delta/DeltaService.java
@@ -2,8 +2,9 @@ package de.ph87.data.series.data.delta;
import de.ph87.data.series.ISeriesPointRequest;
import de.ph87.data.series.Series;
-import de.ph87.data.series.data.DataId;
import de.ph87.data.series.data.Interval;
+import de.ph87.data.series.data.delta.meter.Meter;
+import de.ph87.data.series.data.delta.meter.MeterService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -34,18 +35,21 @@ public class DeltaService {
private final ApplicationEventPublisher applicationEventPublisher;
+ private final MeterService meterService;
+
@Transactional
- public void write(@NonNull final Series series, @NonNull final ZonedDateTime date, final double value) {
- write(series, five, Interval.FIVE, date, value, Delta.Five::new, DeltaDto.Five::new);
- write(series, hour, Interval.HOUR, date, value, Delta.Hour::new, DeltaDto.Hour::new);
- write(series, day, Interval.DAY, date, value, Delta.Day::new, DeltaDto.Day::new);
- write(series, week, Interval.WEEK, date, value, Delta.Week::new, DeltaDto.Week::new);
- write(series, month, Interval.MONTH, date, value, Delta.Month::new, DeltaDto.Month::new);
- write(series, year, Interval.YEAR, date, value, Delta.Year::new, DeltaDto.Year::new);
+ public void write(@NonNull final Series series, @NonNull final String meterNumber, @NonNull final ZonedDateTime date, final double value) {
+ final Meter meter = meterService.getLastValidBySeriesAndNumberOrCreate(series, meterNumber, date);
+ write(meter, five, Interval.FIVE, date, value, Delta.Five::new, DeltaDto.Five::new);
+ write(meter, hour, Interval.HOUR, date, value, Delta.Hour::new, DeltaDto.Hour::new);
+ write(meter, day, Interval.DAY, date, value, Delta.Day::new, DeltaDto.Day::new);
+ write(meter, week, Interval.WEEK, date, value, Delta.Week::new, DeltaDto.Week::new);
+ write(meter, month, Interval.MONTH, date, value, Delta.Month::new, DeltaDto.Month::new);
+ write(meter, year, Interval.YEAR, date, value, Delta.Year::new, DeltaDto.Year::new);
}
- private void write(@NonNull final Series series, @NonNull final DeltaRepo repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction create, @NonNull final BiFunction toDto) {
- final DataId id = new DataId(series, date, interval);
+ private void write(@NonNull final Meter meter, @NonNull final DeltaRepo repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction create, @NonNull final BiFunction toDto) {
+ final DeltaId id = new DeltaId(meter, date, interval);
final DELTA delta = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
log.debug("Delta written: {}", delta);
applicationEventPublisher.publishEvent(toDto.apply(delta, interval));
diff --git a/src/main/java/de/ph87/data/series/data/delta/meter/Meter.java b/src/main/java/de/ph87/data/series/data/delta/meter/Meter.java
new file mode 100644
index 0000000..64e5ab9
--- /dev/null
+++ b/src/main/java/de/ph87/data/series/data/delta/meter/Meter.java
@@ -0,0 +1,49 @@
+package de.ph87.data.series.data.delta.meter;
+
+import de.ph87.data.series.Series;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Version;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+import lombok.ToString;
+
+import java.time.ZonedDateTime;
+
+@Entity
+@Getter
+@ToString
+@NoArgsConstructor
+public class Meter {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Version
+ private long version;
+
+ @NonNull
+ @ManyToOne(optional = false)
+ private Series series;
+
+ @NonNull
+ @Column(nullable = false)
+ private String number;
+
+ @NonNull
+ @Column(nullable = false)
+ private ZonedDateTime first;
+
+ public Meter(@NonNull final Series series, @NonNull final String number, @NonNull final ZonedDateTime date) {
+ this.series = series;
+ this.number = number;
+ this.first = date;
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/series/data/delta/meter/MeterDto.java b/src/main/java/de/ph87/data/series/data/delta/meter/MeterDto.java
new file mode 100644
index 0000000..6084880
--- /dev/null
+++ b/src/main/java/de/ph87/data/series/data/delta/meter/MeterDto.java
@@ -0,0 +1,30 @@
+package de.ph87.data.series.data.delta.meter;
+
+import de.ph87.data.series.SeriesDto;
+import lombok.Data;
+import lombok.NonNull;
+
+import java.time.ZonedDateTime;
+
+@Data
+public class MeterDto {
+
+ private final long id;
+
+ @NonNull
+ public final SeriesDto series;
+
+ @NonNull
+ public final String number;
+
+ @NonNull
+ public final ZonedDateTime first;
+
+ public MeterDto(@NonNull final Meter meter) {
+ this.id = meter.getId();
+ this.series = new SeriesDto(meter.getSeries(), false);
+ this.number = meter.getNumber();
+ this.first = meter.getFirst();
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/series/data/delta/meter/MeterRepository.java b/src/main/java/de/ph87/data/series/data/delta/meter/MeterRepository.java
new file mode 100644
index 0000000..cf4c711
--- /dev/null
+++ b/src/main/java/de/ph87/data/series/data/delta/meter/MeterRepository.java
@@ -0,0 +1,14 @@
+package de.ph87.data.series.data.delta.meter;
+
+import de.ph87.data.series.Series;
+import lombok.NonNull;
+import org.springframework.data.repository.ListCrudRepository;
+
+import java.util.Optional;
+
+public interface MeterRepository extends ListCrudRepository {
+
+ @NonNull
+ Optional findFirstBySeriesOrderByFirstDesc(@NonNull Series series);
+
+}
diff --git a/src/main/java/de/ph87/data/series/data/delta/meter/MeterService.java b/src/main/java/de/ph87/data/series/data/delta/meter/MeterService.java
new file mode 100644
index 0000000..4c49e15
--- /dev/null
+++ b/src/main/java/de/ph87/data/series/data/delta/meter/MeterService.java
@@ -0,0 +1,28 @@
+package de.ph87.data.series.data.delta.meter;
+
+import de.ph87.data.series.Series;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.ZonedDateTime;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MeterService {
+
+ private final MeterRepository meterRepository;
+
+ @NonNull
+ @Transactional
+ public Meter getLastValidBySeriesAndNumberOrCreate(@NonNull final Series series, @NonNull final String number, @NonNull final ZonedDateTime date) {
+ return meterRepository
+ .findFirstBySeriesOrderByFirstDesc(series)
+ .filter(meter -> meter.getNumber().equals(number))
+ .orElseGet(() -> meterRepository.save(new Meter(series, number, date)));
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/topic/Topic.java b/src/main/java/de/ph87/data/topic/Topic.java
index 4ea42db..7cd271c 100644
--- a/src/main/java/de/ph87/data/topic/Topic.java
+++ b/src/main/java/de/ph87/data/topic/Topic.java
@@ -72,6 +72,16 @@ public class Topic extends AbstractEntityLog {
@Column(nullable = false)
private String timestampQuery = "";
+ @Setter
+ @NonNull
+ @Column(nullable = false)
+ private String meterNumberQuery = "";
+
+ @Setter
+ @NonNull
+ @Column(nullable = false)
+ private String meterNumberLast = "";
+
@NonNull
@ToString.Exclude
@ElementCollection(fetch = FetchType.EAGER)
diff --git a/src/main/java/de/ph87/data/topic/TopicDto.java b/src/main/java/de/ph87/data/topic/TopicDto.java
index 1a52f8b..dc62df4 100644
--- a/src/main/java/de/ph87/data/topic/TopicDto.java
+++ b/src/main/java/de/ph87/data/topic/TopicDto.java
@@ -36,6 +36,12 @@ public class TopicDto implements IWebsocketMessage {
@Nullable
public final ZonedDateTime timestampLast;
+ @NonNull
+ public final String meterNumberQuery;
+
+ @Nullable
+ public final String meterNumberLast;
+
@NonNull
public final List queries;
@@ -55,6 +61,8 @@ public class TopicDto implements IWebsocketMessage {
this.timestampType = topic.getTimestampType();
this.timestampQuery = topic.getTimestampQuery();
this.timestampLast = topic.getTimestampLast();
+ this.meterNumberQuery = topic.getMeterNumberQuery();
+ this.meterNumberLast = topic.getMeterNumberLast();
this.queries = topic.getQueries().stream().map(TopicQueryDto::new).toList();
this.error = topic.getError();
this.payload = topic.getPayload();
diff --git a/src/main/java/de/ph87/data/topic/TopicReceiver.java b/src/main/java/de/ph87/data/topic/TopicReceiver.java
index 6b9e5d0..b396779 100644
--- a/src/main/java/de/ph87/data/topic/TopicReceiver.java
+++ b/src/main/java/de/ph87/data/topic/TopicReceiver.java
@@ -103,6 +103,12 @@ public class TopicReceiver {
return;
}
}
+ if (series.getType() == SeriesType.DELTA) {
+ if (topic.getMeterNumberQuery().isEmpty()) {
+ log.debug("TopicQuery meterNumberQuery not set: topic={}", topic);
+ return;
+ }
+ }
final Object valueRaw = json.read(query.getValueQuery());
queryValue(valueRaw).ifPresentOrElse(
@@ -110,14 +116,17 @@ public class TopicReceiver {
final double value = query.getFunction().apply(v) * query.getFactor();
series.update(date, value);
applicationEventPublisher.publishEvent(new SeriesDto(series, false));
-
switch (series.getType()) {
case BOOL -> {
- final ZonedDateTime begin = "timestamp".equals(query.getBeginQuery()) ? date : queryTimestamp(json, query.getBeginQuery(), topic.getTimestampType());
- final boolean terminated = !"true".equals(query.getTerminatedQuery()) && json.read(query.getTerminatedQuery(), Boolean.class);
+ final ZonedDateTime begin = queryTimestamp(json, query.getBeginQuery(), topic.getTimestampType());
+ final boolean terminated = queryBoolean(json, query.getTerminatedQuery());
boolService.write(series, begin, date, value > 0, terminated);
}
- case DELTA -> deltaService.write(series, date, value);
+ case DELTA -> {
+ final String meterNumber = queryMeterNumber(topic, json);
+ topic.setMeterNumberLast(meterNumber);
+ deltaService.write(series, meterNumber, date, value);
+ }
case VARYING -> varyingService.write(series, date, value);
}
},
@@ -128,6 +137,24 @@ public class TopicReceiver {
}
}
+ @NonNull
+ private static String queryMeterNumber(@NonNull final Topic topic, @NonNull final DocumentContext json) {
+ if (topic.getMeterNumberQuery().startsWith("\"") && topic.getMeterNumberQuery().endsWith("\"")) {
+ return topic.getMeterNumberQuery().substring(1, topic.getMeterNumberQuery().length() - 1);
+ }
+ return json.read(topic.getMeterNumberQuery(), String.class);
+ }
+
+ private static boolean queryBoolean(@NonNull final DocumentContext json, @NonNull final String terminatedQuery) {
+ if ("true".equals(terminatedQuery)) {
+ return true;
+ }
+ if ("false".equals(terminatedQuery)) {
+ return false;
+ }
+ return json.read(terminatedQuery, Boolean.class);
+ }
+
private static Optional queryValue(final Object valueRaw) {
if (valueRaw instanceof final Double n) {
return Optional.of(n);
@@ -148,7 +175,7 @@ public class TopicReceiver {
@NonNull
private static ZonedDateTime queryTimestamp(@NonNull final DocumentContext json, @NonNull final String query, @NonNull final TimestampType type) {
- if ("now".equals(query)) {
+ if ("now".equals(query) || "timestamp".equals(query)) {
return ZonedDateTime.now();
}
return switch (type) {