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 @@ + @if (topic.queries[0]?.series?.type === SeriesType.DELTA) { + + + + + + + } @for (query of topic.queries; track $index) { 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) {
{{ topic.timestampType }} {{ topic.timestampLast | date:'long':'':'de-DE' }}
{{ topic.meterNumberQuery }}{{ topic.meterNumberLast }}{{ topic.timestampLast | date:'long':'':'de-DE' }}
{{ query.valueQuery }}