This commit is contained in:
Patrick Haßel 2025-10-06 10:38:12 +02:00
parent e88ff035cf
commit e454ed610e
28 changed files with 376 additions and 104 deletions

View File

@ -16,4 +16,6 @@
</div> </div>
</div> </div>
<router-outlet/> <div class="bodyContent">
<router-outlet/>
</div>

View File

@ -1,7 +1,6 @@
@sidebarColor: #62b0ca; @sidebarColor: #62b0ca;
.sidebar { .sidebar {
margin-left: -2em;
position: fixed; position: fixed;
display: flex; display: flex;
height: 100%; height: 100%;
@ -49,3 +48,7 @@
} }
} }
.bodyContent {
margin-left: 2em;
}

View File

@ -8,13 +8,15 @@ import {NTU, UTN} from '../../COMMON';
import {Graph} from '../axis/graph/Graph'; import {Graph} from '../axis/graph/Graph';
import {Axis} from '../axis/Axis'; import {Axis} from '../axis/Axis';
import {Subscription} from 'rxjs'; import {Subscription} from 'rxjs';
import {Delta, DeltaService} from '../../series/delta/delta-service'; import {DeltaService} from '../../series/delta/delta-service';
import {Bool, BoolService} from '../../series/bool/bool-service'; import {BoolService} from '../../series/bool/bool-service';
import {VaryingService} from '../../series/varying/varying-service'; import {VaryingService} from '../../series/varying/varying-service';
import {Interval} from '../../series/Interval'; import {Interval} from '../../series/Interval';
import {SeriesService} from '../../series/series.service'; import {SeriesService} from '../../series/series.service';
import {GraphType} from '../axis/graph/GraphType'; import {GraphType} from '../axis/graph/GraphType';
import {Varying} from '../../series/varying/Varying'; import {Varying} from '../../series/varying/Varying';
import {Delta} from '../../series/delta/Delta';
import {Bool} from '../../series/bool/Bool';
type Dataset = ChartDataset<any, any>[][number]; type Dataset = ChartDataset<any, any>[][number];

View File

@ -49,7 +49,7 @@ export function toDelta(points: number[][], factor: number): Point[] {
for (const p of points) { for (const p of points) {
result.push({ result.push({
x: p[0] * 1000, x: p[0] * 1000,
y: (p[2] - p[1]) * factor, y: (p[1]) * factor,
}); });
} }
return result; return result;

View File

@ -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),
);
}
}

View File

@ -1,30 +1,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ApiService, CrudService, validateBoolean, validateDate} from '../../COMMON'; import {ApiService, CrudService} from '../../COMMON';
import {Series} from '../Series'; import {Bool} from './Bool';
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),
);
}
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

@ -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),
);
}
}

View File

@ -1,28 +1,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ApiService, CrudService, validateDate, validateNumber} from '../../COMMON'; import {ApiService, CrudService} from '../../COMMON';
import {Series} from '../Series'; import {Delta} from './Delta';
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),
);
}
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

@ -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),
);
}
}

View File

@ -17,6 +17,8 @@ export class Topic {
readonly timestampType: TimestampType, readonly timestampType: TimestampType,
readonly timestampQuery: string, readonly timestampQuery: string,
readonly timestampLast: Date | null, readonly timestampLast: Date | null,
readonly meterNumberQuery: string,
readonly meterNumberLast: string,
readonly queries: TopicQuery[], readonly queries: TopicQuery[],
readonly payload: string, readonly payload: string,
readonly error: string, readonly error: string,
@ -35,6 +37,8 @@ export class Topic {
validateString(json.timestampType) as TimestampType, validateString(json.timestampType) as TimestampType,
validateString(json.timestampQuery), validateString(json.timestampQuery),
mapNotNull(json.timestampLast, validateDate), mapNotNull(json.timestampLast, validateDate),
validateString(json.meterNumberQuery),
validateString(json.meterNumberLast),
validateList(json.queries, TopicQuery.fromJson), validateList(json.queries, TopicQuery.fromJson),
validateString(json.payload), validateString(json.payload),
validateString(json.error), validateString(json.error),

View File

@ -1,3 +1,8 @@
<div class="options">
<label><input type="checkbox" [(ngModel)]="config.topicList.unused">Ungenutzte</label>
<label><input type="checkbox" [(ngModel)]="config.topicList.used">Genutzte</label>
</div>
<table class="TopicList"> <table class="TopicList">
@for (topic of sorted(); track topic.id) { @for (topic of sorted(); track topic.id) {
<tr class="head"> <tr class="head">
@ -10,6 +15,14 @@
<td class="timestampType" [class.empty]="topic.timestampType === null">{{ topic.timestampType }}</td> <td class="timestampType" [class.empty]="topic.timestampType === null">{{ topic.timestampType }}</td>
<td class="timestampLast" [class.empty]="topic.timestampLast === null" colspan="6">{{ topic.timestampLast | date:'long':'':'de-DE' }}</td> <td class="timestampLast" [class.empty]="topic.timestampLast === null" colspan="6">{{ topic.timestampLast | date:'long':'':'de-DE' }}</td>
</tr> </tr>
@if (topic.queries[0]?.series?.type === SeriesType.DELTA) {
<tr class="meter">
<td class="meterNumberQuery" [class.empty]="!topic.meterNumberQuery">{{ topic.meterNumberQuery }}</td>
<td class="meterNumberLast" [class.empty]="topic.meterNumberLast === null"></td>
<td class="meterNumberLast" [class.empty]="topic.meterNumberLast === null" colspan="5">{{ topic.meterNumberLast }}</td>
<td class="meterNumberLast" [class.empty]="topic.timestampLast === null">{{ topic.timestampLast | date:'long':'':'de-DE' }}</td>
</tr>
}
@for (query of topic.queries; track $index) { @for (query of topic.queries; track $index) {
<tr class="query"> <tr class="query">
<td class="valueQuery" [class.empty]="query.valueQuery === null">{{ query.valueQuery }}</td> <td class="valueQuery" [class.empty]="query.valueQuery === null">{{ query.valueQuery }}</td>

View File

@ -22,6 +22,10 @@ table.TopicList {
background-color: darkseagreen; background-color: darkseagreen;
} }
tr.meter {
background-color: #ffe7bd;
}
tr.spacer { tr.spacer {
th, td { th, td {
border: none; border: none;
@ -53,7 +57,11 @@ table.TopicList {
} }
.timestampLast { .timestampLast {
text-align: right;
}
.meterNumberLast {
text-align: right;
} }
.valueQuery { .valueQuery {

View File

@ -11,6 +11,7 @@ import {SeriesService} from '../../series/series.service';
import {Config} from '../../config/Config'; import {Config} from '../../config/Config';
import {ageString} from '../../COMMON'; import {ageString} from '../../COMMON';
import {ConfigService} from '../../config/config.service'; import {ConfigService} from '../../config/config.service';
import {SeriesType} from '../../series/SeriesType';
@Component({ @Component({
selector: 'app-topic-list', selector: 'app-topic-list',
@ -25,6 +26,8 @@ export class TopicListComponent implements OnInit, OnDestroy {
protected readonly ageString = ageString; protected readonly ageString = ageString;
protected readonly SeriesType = SeriesType;
protected now: Date = new Date(); protected now: Date = new Date();
protected topicList: Topic[] = []; protected topicList: Topic[] = [];
@ -55,10 +58,6 @@ export class TopicListComponent implements OnInit, OnDestroy {
this.subs.forEach(sub => sub.unsubscribe()); this.subs.forEach(sub => sub.unsubscribe());
} }
sorted() {
return this.topicList.filter(this.filter).sort(this.compare);
}
private readonly update = (topic: Topic) => { private readonly update = (topic: Topic) => {
const index = this.topicList.findIndex(t => t.id === topic.id); const index = this.topicList.findIndex(t => t.id === topic.id);
if (index >= 0) { 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) => { private readonly filter = (topic: Topic) => {
return ((!topic.used && this.config.topicList.unused) || (topic.used && this.config.topicList.used)); return ((!topic.used && this.config.topicList.unused) || (topic.used && this.config.topicList.used));
} }

View File

@ -5,7 +5,6 @@ html, body {
body { body {
font-family: sans-serif; font-family: sans-serif;
margin-left: 2em;
} }
table { table {
@ -23,7 +22,7 @@ input, select, textarea {
font-size: inherit; font-size: inherit;
} }
input, select { input:not([type=checkbox]), select {
width: 100%; width: 100%;
margin: 0; margin: 0;
outline: none; outline: none;

View File

@ -62,8 +62,9 @@ public class DemoService {
final Series electricityEnergyProduce = series("electricity/energy/produce", "kWh", SeriesType.DELTA, 5); final Series electricityEnergyProduce = series("electricity/energy/produce", "kWh", SeriesType.DELTA, 5);
final Series electricityPowerProduce = series("electricity/power/produce", "W", SeriesType.VARYING, 5); final Series electricityPowerProduce = series("electricity/power/produce", "W", SeriesType.VARYING, 5);
topic( topicMeterNumber(
"openDTU/pv/patrix/json2", "openDTU/pv/patrix/json2",
"$.inverter",
new TopicQuery(electricityEnergyProduce, "$.totalKWh"), new TopicQuery(electricityEnergyProduce, "$.totalKWh"),
new TopicQuery(electricityPowerProduce, "$.totalW") new TopicQuery(electricityPowerProduce, "$.totalW")
); );
@ -72,8 +73,9 @@ public class DemoService {
final Series electricityPowerPurchase = series("electricity/power/purchase", "W", SeriesType.VARYING, 5); final Series electricityPowerPurchase = series("electricity/power/purchase", "W", SeriesType.VARYING, 5);
final Series electricityEnergyDelivery = series("electricity/energy/delivery", "kWh", SeriesType.DELTA, 5); final Series electricityEnergyDelivery = series("electricity/energy/delivery", "kWh", SeriesType.DELTA, 5);
final Series electricityPowerDelivery = series("electricity/power/delivery", "W", SeriesType.VARYING, 5); final Series electricityPowerDelivery = series("electricity/power/delivery", "W", SeriesType.VARYING, 5);
topic( topicMeterNumber(
"electricity/grid/json", "electricity/grid/json",
"\"1ZPA0020300305\"",
new TopicQuery(electricityEnergyPurchase, "$.purchaseWh", 0.001), new TopicQuery(electricityEnergyPurchase, "$.purchaseWh", 0.001),
new TopicQuery(electricityPowerPurchase, "$.powerW", TopicQueryFunction.ONLY_POSITIVE), new TopicQuery(electricityPowerPurchase, "$.powerW", TopicQueryFunction.ONLY_POSITIVE),
new TopicQuery(electricityEnergyDelivery, "$.deliveryWh", 0.001), new TopicQuery(electricityEnergyDelivery, "$.deliveryWh", 0.001),
@ -220,4 +222,12 @@ public class DemoService {
topic.getQueries().addAll(List.of(queries)); 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));
}
} }

View File

@ -1,6 +1,5 @@
package de.ph87.data.series.data.delta; package de.ph87.data.series.data.delta;
import de.ph87.data.series.data.DataId;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Id; import jakarta.persistence.Id;
@ -19,7 +18,7 @@ public abstract class Delta {
@Id @Id
@NonNull @NonNull
private DataId id; private DeltaId id;
@Version @Version
private long version; private long version;
@ -31,7 +30,7 @@ public abstract class Delta {
@Column(nullable = false) @Column(nullable = false)
private double last; 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.id = id;
this.first = value; this.first = value;
this.last = value; this.last = value;
@ -47,7 +46,7 @@ public abstract class Delta {
@Entity(name = "DeltaFive") @Entity(name = "DeltaFive")
public static class Five extends Delta { 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); super(id, value);
} }
@ -59,7 +58,7 @@ public abstract class Delta {
@Entity(name = "DeltaHour") @Entity(name = "DeltaHour")
public static class Hour extends Delta { 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); super(id, value);
} }
@ -71,7 +70,7 @@ public abstract class Delta {
@Entity(name = "DeltaDay") @Entity(name = "DeltaDay")
public static class Day extends Delta { 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); super(id, value);
} }
@ -83,7 +82,7 @@ public abstract class Delta {
@Entity(name = "DeltaWeek") @Entity(name = "DeltaWeek")
public static class Week extends Delta { 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); super(id, value);
} }
@ -95,7 +94,7 @@ public abstract class Delta {
@Entity(name = "DeltaMonth") @Entity(name = "DeltaMonth")
public static class Month extends Delta { 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); super(id, value);
} }
@ -107,7 +106,7 @@ public abstract class Delta {
@Entity(name = "DeltaYear") @Entity(name = "DeltaYear")
public static class Year extends Delta { 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); super(id, value);
} }

View File

@ -1,7 +1,7 @@
package de.ph87.data.series.data.delta; 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.Interval;
import de.ph87.data.series.data.delta.meter.MeterDto;
import de.ph87.data.websocket.IWebsocketMessage; import de.ph87.data.websocket.IWebsocketMessage;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
@ -14,21 +14,20 @@ import java.time.ZonedDateTime;
public abstract class DeltaDto implements IWebsocketMessage { public abstract class DeltaDto implements IWebsocketMessage {
@NonNull @NonNull
private SeriesDto series; private MeterDto meter;
@NonNull @NonNull
private ZonedDateTime date; private ZonedDateTime date;
public final double first; public final double first;
@NonNull
public final double last; public final double last;
@NonNull @NonNull
public final Interval interval; public final Interval interval;
protected DeltaDto(@NonNull final Delta delta, @NonNull 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.date = delta.getId().getDate();
this.first = delta.getFirst(); this.first = delta.getFirst();
this.last = delta.getLast(); this.last = delta.getLast();
@ -38,7 +37,7 @@ public abstract class DeltaDto implements IWebsocketMessage {
@NonNull @NonNull
@Override @Override
public String getWebsocketTopic() { public String getWebsocketTopic() {
return "Delta/%d/%s".formatted(series.id, interval); return "Delta/%d/%s".formatted(meter.series.id, interval);
} }
@Getter @Getter

View File

@ -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);
}
}

View File

@ -10,23 +10,20 @@ import java.time.ZonedDateTime;
@SuppressWarnings("unused") // used by repository query @SuppressWarnings("unused") // used by repository query
public class DeltaPoint implements SeriesPoint { public class DeltaPoint implements SeriesPoint {
@NonNull
public final ZonedDateTime date; public final ZonedDateTime date;
public final double first; public final double delta;
public final double last; public DeltaPoint(@NonNull final ZonedDateTime date, final double delta) {
this.date = date;
public DeltaPoint(@NonNull final Delta delta) { this.delta = delta;
this.date = delta.getId().getDate();
this.first = delta.getFirst();
this.last = delta.getLast();
} }
@Override @Override
public void toJson(final JsonGenerator jsonGenerator) throws IOException { public void toJson(final JsonGenerator jsonGenerator) throws IOException {
jsonGenerator.writeNumber(date.toEpochSecond()); jsonGenerator.writeNumber(date.toEpochSecond());
jsonGenerator.writeNumber(first); jsonGenerator.writeNumber(delta);
jsonGenerator.writeNumber(last);
} }
} }

View File

@ -1,7 +1,6 @@
package de.ph87.data.series.data.delta; package de.ph87.data.series.data.delta;
import de.ph87.data.series.Series; import de.ph87.data.series.Series;
import de.ph87.data.series.data.DataId;
import lombok.NonNull; import lombok.NonNull;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
@ -11,10 +10,10 @@ import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
@NoRepositoryBean @NoRepositoryBean
public interface DeltaRepo<T extends Delta> extends CrudRepository<T, DataId> { public interface DeltaRepo<T extends Delta> extends CrudRepository<T, DeltaId> {
@NonNull @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<DeltaPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after); List<DeltaPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after);
} }

View File

@ -2,8 +2,9 @@ package de.ph87.data.series.data.delta;
import de.ph87.data.series.ISeriesPointRequest; import de.ph87.data.series.ISeriesPointRequest;
import de.ph87.data.series.Series; 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.Interval;
import de.ph87.data.series.data.delta.meter.Meter;
import de.ph87.data.series.data.delta.meter.MeterService;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -34,18 +35,21 @@ public class DeltaService {
private final ApplicationEventPublisher applicationEventPublisher; private final ApplicationEventPublisher applicationEventPublisher;
private final MeterService meterService;
@Transactional @Transactional
public void write(@NonNull final Series series, @NonNull final ZonedDateTime date, final double value) { public void write(@NonNull final Series series, @NonNull final String meterNumber, @NonNull final ZonedDateTime date, final double value) {
write(series, five, Interval.FIVE, date, value, Delta.Five::new, DeltaDto.Five::new); final Meter meter = meterService.getLastValidBySeriesAndNumberOrCreate(series, meterNumber, date);
write(series, hour, Interval.HOUR, date, value, Delta.Hour::new, DeltaDto.Hour::new); write(meter, five, Interval.FIVE, date, value, Delta.Five::new, DeltaDto.Five::new);
write(series, day, Interval.DAY, date, value, Delta.Day::new, DeltaDto.Day::new); write(meter, hour, Interval.HOUR, date, value, Delta.Hour::new, DeltaDto.Hour::new);
write(series, week, Interval.WEEK, date, value, Delta.Week::new, DeltaDto.Week::new); write(meter, day, Interval.DAY, date, value, Delta.Day::new, DeltaDto.Day::new);
write(series, month, Interval.MONTH, date, value, Delta.Month::new, DeltaDto.Month::new); write(meter, week, Interval.WEEK, date, value, Delta.Week::new, DeltaDto.Week::new);
write(series, year, Interval.YEAR, date, value, Delta.Year::new, DeltaDto.Year::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 <DELTA extends Delta, DTO extends DeltaDto> void write(@NonNull final Series series, @NonNull final DeltaRepo<DELTA> repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction<DataId, Double, DELTA> create, @NonNull final BiFunction<DELTA, Interval, DTO> toDto) { private <DELTA extends Delta, DTO extends DeltaDto> void write(@NonNull final Meter meter, @NonNull final DeltaRepo<DELTA> repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction<DeltaId, Double, DELTA> create, @NonNull final BiFunction<DELTA, Interval, DTO> toDto) {
final DataId id = new DataId(series, date, interval); 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))); 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); log.debug("Delta written: {}", delta);
applicationEventPublisher.publishEvent(toDto.apply(delta, interval)); applicationEventPublisher.publishEvent(toDto.apply(delta, interval));

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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<Meter, Long> {
@NonNull
Optional<Meter> findFirstBySeriesOrderByFirstDesc(@NonNull Series series);
}

View File

@ -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)));
}
}

View File

@ -72,6 +72,16 @@ public class Topic extends AbstractEntityLog {
@Column(nullable = false) @Column(nullable = false)
private String timestampQuery = ""; private String timestampQuery = "";
@Setter
@NonNull
@Column(nullable = false)
private String meterNumberQuery = "";
@Setter
@NonNull
@Column(nullable = false)
private String meterNumberLast = "";
@NonNull @NonNull
@ToString.Exclude @ToString.Exclude
@ElementCollection(fetch = FetchType.EAGER) @ElementCollection(fetch = FetchType.EAGER)

View File

@ -36,6 +36,12 @@ public class TopicDto implements IWebsocketMessage {
@Nullable @Nullable
public final ZonedDateTime timestampLast; public final ZonedDateTime timestampLast;
@NonNull
public final String meterNumberQuery;
@Nullable
public final String meterNumberLast;
@NonNull @NonNull
public final List<TopicQueryDto> queries; public final List<TopicQueryDto> queries;
@ -55,6 +61,8 @@ public class TopicDto implements IWebsocketMessage {
this.timestampType = topic.getTimestampType(); this.timestampType = topic.getTimestampType();
this.timestampQuery = topic.getTimestampQuery(); this.timestampQuery = topic.getTimestampQuery();
this.timestampLast = topic.getTimestampLast(); this.timestampLast = topic.getTimestampLast();
this.meterNumberQuery = topic.getMeterNumberQuery();
this.meterNumberLast = topic.getMeterNumberLast();
this.queries = topic.getQueries().stream().map(TopicQueryDto::new).toList(); this.queries = topic.getQueries().stream().map(TopicQueryDto::new).toList();
this.error = topic.getError(); this.error = topic.getError();
this.payload = topic.getPayload(); this.payload = topic.getPayload();

View File

@ -103,6 +103,12 @@ public class TopicReceiver {
return; 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()); final Object valueRaw = json.read(query.getValueQuery());
queryValue(valueRaw).ifPresentOrElse( queryValue(valueRaw).ifPresentOrElse(
@ -110,14 +116,17 @@ public class TopicReceiver {
final double value = query.getFunction().apply(v) * query.getFactor(); final double value = query.getFunction().apply(v) * query.getFactor();
series.update(date, value); series.update(date, value);
applicationEventPublisher.publishEvent(new SeriesDto(series, false)); applicationEventPublisher.publishEvent(new SeriesDto(series, false));
switch (series.getType()) { switch (series.getType()) {
case BOOL -> { case BOOL -> {
final ZonedDateTime begin = "timestamp".equals(query.getBeginQuery()) ? date : queryTimestamp(json, query.getBeginQuery(), topic.getTimestampType()); final ZonedDateTime begin = queryTimestamp(json, query.getBeginQuery(), topic.getTimestampType());
final boolean terminated = !"true".equals(query.getTerminatedQuery()) && json.read(query.getTerminatedQuery(), Boolean.class); final boolean terminated = queryBoolean(json, query.getTerminatedQuery());
boolService.write(series, begin, date, value > 0, terminated); 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); 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<Double> queryValue(final Object valueRaw) { private static Optional<Double> queryValue(final Object valueRaw) {
if (valueRaw instanceof final Double n) { if (valueRaw instanceof final Double n) {
return Optional.of(n); return Optional.of(n);
@ -148,7 +175,7 @@ public class TopicReceiver {
@NonNull @NonNull
private static ZonedDateTime queryTimestamp(@NonNull final DocumentContext json, @NonNull final String query, @NonNull final TimestampType type) { 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 ZonedDateTime.now();
} }
return switch (type) { return switch (type) {