REFACTOR: points (request, response)

This commit is contained in:
Patrick Haßel 2025-10-29 16:45:28 +01:00
parent 03ad1615d2
commit 320729647e
53 changed files with 470 additions and 615 deletions

View File

@ -0,0 +1,9 @@
<div class="segments">
@for (segment of segments; track segment) {
<div class="segment">
<!-- @for (graph of graphs; track graph) {-->
<!-- <div class="" [style.background-color]="graph[0]" [style.height.%]="graph[1][segment] / total(segment) * 100"></div>-->
<!-- }-->
</div>
}
</div>

View File

@ -0,0 +1,9 @@
.segments {
display: flex;
height: 4em;
.segment {
flex: 1;
height: 100%;
}
}

View File

@ -0,0 +1,70 @@
import {AfterViewInit, Component, Input} from '@angular/core';
import {Location} from '../../../Location';
import {Interval} from '../../../../series/Interval';
import {PointService} from '../../../../point/point-service';
@Component({
selector: 'app-series-history-graph',
imports: [],
templateUrl: './simple-plot.component.html',
styleUrl: './simple-plot.component.less',
})
export class SeriesHistoryGraph implements AfterViewInit {
protected segments = Array.from(Array(288).keys());
protected totals: number[] = [];
protected historyEnergyPurchase: number[] | null = null;
protected historyEnergyDeliver: number[] | null = null;
protected historyEnergyProduce: number[] | null = null;
protected readonly Interval = Interval;
@Input()
heading!: string;
@Input()
date!: Date;
@Input()
interval!: Interval;
@Input()
location!: Location;
constructor(
readonly pointService: PointService,
) {
//
}
ngAfterViewInit(): void {
// this.history(this.location?.energyPurchase, history => this.historyEnergyPurchase = history);
// this.history(this.location?.energyDeliver, history => this.historyEnergyDeliver = history);
// this.history(this.location?.energyProduce, history => this.historyEnergyProduce = history);
}
// public readonly updateSeries = (fresh: Series): void => {
// if (fresh.id === this.location?.energyPurchase?.id) {
// this.history(this.location?.energyPurchase, history => this.historyEnergyPurchase = history);
// }
// if (fresh.id === this.location?.energyDeliver?.id) {
// this.history(this.location?.energyDeliver, history => this.historyEnergyDeliver = history);
// }
// if (fresh.id === this.location?.energyProduce?.id) {
// this.history(this.location?.energyProduce, history => this.historyEnergyProduce = history);
// }
// };
//
// private history(series: Series | null | undefined, next: Next<number[] | null>) {
// if (!series || !this.interval) {
// next(null);
// return
// }
// this.pointService.points(series, this.date, this.interval, next);
// }
}

View File

@ -30,4 +30,5 @@
</div>
</div>
</div>
<app-series-history-graph></app-series-history-graph>
</div>

View File

@ -1,23 +1,27 @@
import {AfterViewInit, Component, Input} from '@angular/core';
import {History} from '../../../series/History';
import {Location} from '../../Location';
import {Series} from '../../../series/Series';
import {Interval, SeriesService} from '../../../series/series-service';
import {Next} from '../../../common';
import {SeriesHistoryGraph} from './graph/simple-plot.component';
import {Interval} from '../../../series/Interval';
import {PointService} from '../../../point/point-service';
import {PointSeries} from '../../../point/PointSeries';
@Component({
selector: 'app-series-history',
imports: [],
imports: [
SeriesHistoryGraph
],
templateUrl: './series-history.html',
styleUrl: './series-history.less',
})
export class SeriesHistory implements AfterViewInit {
protected historyEnergyPurchase: History | null = null;
protected historyEnergyPurchase: PointSeries | null = null;
protected historyEnergyDeliver: History | null = null;
protected historyEnergyDeliver: PointSeries | null = null;
protected historyEnergyProduce: History | null = null;
protected historyEnergyProduce: PointSeries | null = null;
protected readonly Interval = Interval;
@ -25,7 +29,7 @@ export class SeriesHistory implements AfterViewInit {
heading!: string;
@Input()
date!: Date;
offset: number = 0;
@Input()
interval!: Interval;
@ -34,7 +38,7 @@ export class SeriesHistory implements AfterViewInit {
location!: Location;
constructor(
readonly seriesService: SeriesService,
readonly pointService: PointService,
) {
//
}
@ -57,12 +61,12 @@ export class SeriesHistory implements AfterViewInit {
}
};
private history(series: Series | null | undefined, next: Next<History | null>) {
private history(series: Series | null | undefined, next: Next<PointSeries | null>) {
if (!series || !this.interval) {
next(null);
return
}
this.seriesService.history(series, this.date, this.interval, next);
this.pointService.relative([series], this.interval, this.offset, 1, response => next(response.series[0]));
}
}

View File

@ -1,8 +1,8 @@
@if (location) {
<app-series-history [location]="location" [interval]="Interval.DAY" [date]="now" heading="Heute" #today></app-series-history>
<app-series-history [location]="location" [interval]="Interval.DAY" [offset]="0" heading="Heute" #today></app-series-history>
<app-series-history [location]="location" [interval]="Interval.DAY" [date]="yesterday" heading="Gestern"></app-series-history>
<app-series-history [location]="location" [interval]="Interval.DAY" [offset]="1" heading="Gestern"></app-series-history>
<div class="Section">
<div class="SectionHeading">

View File

@ -6,10 +6,11 @@ import {Text} from '../../shared/text/text';
import {Number} from '../../shared/number/number';
import {SeriesSelect} from '../../series/select/series-select';
import {Series} from '../../series/Series';
import {Interval, SeriesService} from '../../series/series-service';
import {SeriesService} from '../../series/series-service';
import {SeriesType} from '../../series/SeriesType';
import {Subscription, timer} from 'rxjs';
import {SeriesHistory} from './history/series-history';
import {Interval} from '../../series/Interval';
function yesterday(now: any) {
const yesterday = new Date(now.getTime());

View File

@ -0,0 +1,23 @@
import {validateDate, validateList} from "../common";
import {PointSeries} from './PointSeries';
export class PointResponse {
constructor(
readonly begin: Date,
readonly end: Date,
readonly series: PointSeries[],
) {
//
}
static fromJson(json: any): PointResponse {
return new PointResponse(
validateDate(json.begin),
validateDate(json.end),
validateList(json.series, PointSeries.fromJson),
);
}
}

View File

@ -0,0 +1,22 @@
import {Series} from "../series/Series";
import {validateList, validateNumber} from "../common";
export class PointSeries {
readonly valueString: string;
constructor(
readonly series: Series,
readonly points: number[][],
) {
this.valueString = series.getValueString(points.length === 0 ? null : points[0][1]);
}
static fromJson(json: any): PointSeries {
return new PointSeries(
Series.fromJson(json.series),
validateList(json.points, list => list.map(validateNumber)),
);
}
}

View File

@ -0,0 +1,29 @@
import {Injectable} from '@angular/core';
import {ApiService, CrudService, Next, WebsocketService} from '../common';
import {PointResponse} from './PointResponse';
import {Series} from '../series/Series';
import {Interval} from '../series/Interval';
@Injectable({
providedIn: 'root'
})
export class PointService extends CrudService<PointResponse> {
constructor(
api: ApiService,
ws: WebsocketService,
) {
super(api, ws, ['Point'], PointResponse.fromJson);
}
relative(series: Series[], interval: Interval, offset: number, count: number, next: Next<PointResponse>): void {
const request = {
ids: series.map(s => s.id),
interval: interval,
offset: offset,
count: count,
};
this.postSingle(['relative'], request, next);
}
}

View File

@ -1,22 +0,0 @@
import {getValueString, Series} from "./Series";
import {or, validateNumber} from "../common";
export class History {
readonly valueString: string;
constructor(
readonly series: Series,
readonly value: number | null,
) {
this.valueString = getValueString(value, series);
}
static fromJson(json: any): History {
return new History(
Series.fromJson(json.series),
or(json.value, validateNumber, null),
);
}
}

View File

@ -0,0 +1,8 @@
export enum Interval {
FIVE = 'FIVE',
HOUR = 'HOUR',
DAY = 'DAY',
WEEK = 'WEEK',
MONTH = 'MONTH',
YEAR = 'YEAR',
}

View File

@ -3,10 +3,6 @@ import {or, validateDate, validateEnum, validateNumber, validateString} from "..
import {SeriesType} from './SeriesType';
import {formatNumber} from '@angular/common';
export function getValueString(value: number | null, series: Series): string {
return (value === null ? '-' : formatNumber(value, "de-DE", `0.${series.decimals}-${series.decimals}`)) + ' ' + series.unit;
}
export class Series {
readonly valueString: string;
@ -21,7 +17,11 @@ export class Series {
readonly unit: string,
readonly type: SeriesType,
) {
this.valueString = getValueString(value, this);
this.valueString = this.getValueString(value);
}
getValueString(value: number | null | undefined): string {
return (value === null || value === undefined ? '-' : formatNumber(value, "de-DE", `0.${this.decimals}-${this.decimals}`)) + ' ' + this.unit;
}
static fromJson(json: any): Series {

View File

@ -1,18 +1,8 @@
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
import {ApiService, CrudService, Next, WebsocketService} from '../common';
import {Series} from './Series';
import {History} from './History';
import {DatePipe} from '@angular/common';
export enum Interval {
FIVE = 'FIVE',
HOUR = 'HOUR',
DAY = 'DAY',
WEEK = 'WEEK',
MONTH = 'MONTH',
YEAR = 'YEAR',
}
@Injectable({
providedIn: 'root'
})
@ -33,8 +23,4 @@ export class SeriesService extends CrudService<Series> {
this.getList(['findAll'], next);
}
history(series: Series, date: Date, interval: Interval, next: Next<History>) {
this.api.getSingle([...this.path, series.id, 'history', Math.floor(date.getTime() / 1000), interval], History.fromJson, next);
}
}

View File

@ -1,6 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
import {bootstrapApplication} from '@angular/platform-browser';
import {appConfig} from './app/app.config';
import {App} from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));

View File

@ -1,8 +1,8 @@
package de.ph87.data;
import de.ph87.data.topic.TopicDto;
import de.ph87.data.topic.TopicType;
import de.ph87.data.topic.TopicService;
import de.ph87.data.topic.TopicType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;

View File

@ -15,8 +15,6 @@ public class PlotDto implements IWebsocketMessage {
public final long version;
private final boolean deleted;
@NonNull
public final String name;
@ -34,10 +32,9 @@ public class PlotDto implements IWebsocketMessage {
@NonNull
public final List<AxisDto> axes;
public PlotDto(@NonNull final Plot plot, final boolean deleted) {
public PlotDto(@NonNull final Plot plot) {
this.id = plot.getId();
this.version = plot.getVersion();
this.deleted = deleted;
this.name = plot.getName();
this.interval = plot.getInterval();
this.offset = plot.getOffset();

View File

@ -8,7 +8,7 @@ import java.util.Optional;
public interface PlotRepository extends ListCrudRepository<Plot, Long> {
@Query("select new de.ph87.data.plot.PlotDto(p, false) from Plot p")
@Query("select new de.ph87.data.plot.PlotDto(p) from Plot p")
List<PlotDto> findAllDto();
@Query("select max(p.position) from Plot p")

View File

@ -87,7 +87,7 @@ public class PlotService {
} else {
log.info("Updated: plot={}", plot);
}
final PlotDto dto = new PlotDto(plot, deleted);
final PlotDto dto = new PlotDto(plot);
applicationEventPublisher.publishEvent(dto);
return dto;
}

View File

@ -14,7 +14,7 @@ public class GraphDto {
public final long version;
@NonNull
@Nullable
public final SeriesDto series;
public final double factor;
@ -53,10 +53,10 @@ public class GraphDto {
public GraphDto(@NonNull final Graph graph) {
this.id = graph.getId();
this.version = graph.getVersion();
this.series = new SeriesDto(graph.getSeries());
this.series = map(graph.getSeries(), SeriesDto::new);
this.factor = graph.getFactor();
this.operation = graph.getOperation();
this.series2 = map(graph.getSeries2(), false, SeriesDto::new);
this.series2 = map(graph.getSeries2(), SeriesDto::new);
this.factor2 = graph.getFactor2();
this.name = graph.getName();
this.visible = graph.isVisible();

View File

@ -0,0 +1,27 @@
package de.ph87.data.point;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import de.ph87.data.series.data.Interval;
import lombok.NonNull;
import java.time.ZonedDateTime;
import java.util.List;
@JsonSubTypes({
@JsonSubTypes.Type(PointRequestRelative.class),
})
public interface IPointRequest {
@NonNull
List<Long> getIds();
@NonNull
Interval getInterval();
@NonNull
ZonedDateTime getBegin();
@NonNull
ZonedDateTime getEnd();
}

View File

@ -0,0 +1,23 @@
package de.ph87.data.point;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
@RequiredArgsConstructor
@RequestMapping("Point")
public class PointController {
private final PointService pointService;
@PostMapping("relative")
public PointResponse points(@RequestBody final PointRequestRelative request) {
return pointService.points(request);
}
}

View File

@ -0,0 +1,38 @@
package de.ph87.data.point;
import de.ph87.data.series.data.Interval;
import lombok.Data;
import java.time.ZonedDateTime;
import java.util.List;
@Data
public class PointRequestRelative implements IPointRequest {
public final List<Long> ids;
public final Interval interval;
public final long offset;
public final long count;
public final ZonedDateTime begin;
public final ZonedDateTime end;
public PointRequestRelative(
final List<Long> ids,
final Interval interval,
final long offset,
final long count
) {
this.ids = ids;
this.interval = interval;
this.offset = offset;
this.count = count;
this.end = interval.align.apply(ZonedDateTime.now()).minus(interval.amount * (offset - 1), interval.unit);
this.begin = this.end.minus(interval.amount * count, interval.unit);
}
}

View File

@ -0,0 +1,17 @@
package de.ph87.data.point;
import lombok.Data;
import java.time.ZonedDateTime;
import java.util.List;
@Data
public class PointResponse {
public final ZonedDateTime begin;
public final ZonedDateTime end;
public final List<PointSeries> series;
}

View File

@ -0,0 +1,16 @@
package de.ph87.data.point;
import de.ph87.data.series.SeriesDto;
import de.ph87.data.series.data.delta.ISeriesPoint;
import lombok.Data;
import java.util.List;
@Data
public class PointSeries {
public final SeriesDto series;
public final List<? extends ISeriesPoint> points;
}

View File

@ -0,0 +1,47 @@
package de.ph87.data.point;
import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesDto;
import de.ph87.data.series.SeriesService;
import de.ph87.data.series.data.bool.BoolService;
import de.ph87.data.series.data.delta.DeltaService;
import de.ph87.data.series.data.delta.ISeriesPoint;
import de.ph87.data.series.data.varying.VaryingService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class PointService {
private final SeriesService seriesService;
private final BoolService boolService;
private final DeltaService deltaService;
private final VaryingService varyingService;
@NonNull
public PointResponse points(@NonNull final IPointRequest request) {
final List<PointSeries> series = request.getIds().stream().map(s -> points(s, request)).toList();
return new PointResponse(request.getBegin(), request.getEnd(), series);
}
@NonNull
private PointSeries points(final long id, @NonNull final IPointRequest request) {
final Series series = seriesService.getById(id);
final List<? extends ISeriesPoint> points = switch (series.getType()) {
case BOOL -> boolService.points(series, request);
case DELTA -> deltaService.points(series, request);
case VARYING -> varyingService.points(series, request);
};
return new PointSeries(new SeriesDto(series), points);
}
}

View File

@ -1,22 +0,0 @@
package de.ph87.data.series;
import de.ph87.data.series.point.AllSeriesPointResponse;
import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NonNull;
@Data
public class HistoryDto {
@NonNull
public final SeriesDto series;
@Nullable
public final Double value;
public HistoryDto(final AllSeriesPointResponse.Entry entry) {
this.series = entry.getSeries();
this.value = entry.point == null ? null : entry.point.getValue();
}
}

View File

@ -1,11 +1,5 @@
package de.ph87.data.series;
import de.ph87.data.series.data.Interval;
import de.ph87.data.series.point.AllSeriesPointRequest;
import de.ph87.data.series.point.AllSeriesPointResponse;
import de.ph87.data.series.point.OneSeriesPointsRequest;
import de.ph87.data.series.point.OneSeriesPointsResponse;
import de.ph87.data.series.point.SeriesPointService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CrossOrigin;
@ -16,9 +10,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
@CrossOrigin
@ -31,8 +22,6 @@ public class SeriesController {
private final SeriesService seriesService;
private final SeriesPointService seriesPointService;
@GetMapping("create")
public SeriesDto create() {
return seriesService.create();
@ -68,13 +57,6 @@ public class SeriesController {
return seriesService.modify(id, series -> series.setType(type));
}
@NonNull
@GetMapping("{id}/history/{epochSeconds}/{intervalName}")
public HistoryDto type(@PathVariable final long id, @PathVariable @NonNull final long epochSeconds, @PathVariable @NonNull final String intervalName) {
final Interval interval = Interval.valueOf(intervalName);
return seriesPointService.history(id, ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault()), interval);
}
@NonNull
@GetMapping("{id}")
public SeriesDto getById(@PathVariable final long id) {
@ -87,16 +69,4 @@ public class SeriesController {
return seriesRepository.findAllDto();
}
@NonNull
@PostMapping("oneSeriesPoints")
public OneSeriesPointsResponse oneSeriesPoints(@NonNull @RequestBody final OneSeriesPointsRequest request) {
return seriesPointService.oneSeriesPoints(request);
}
@NonNull
@PostMapping("allSeriesPoint")
public AllSeriesPointResponse allSeriesPoint(@NonNull @RequestBody final AllSeriesPointRequest request) {
return seriesPointService.allSeriesPoint(request);
}
}

View File

@ -14,8 +14,6 @@ public class SeriesDto implements IWebsocketMessage {
public final long version;
public final boolean deleted;
public final String name;
@NonNull
@ -38,13 +36,8 @@ public class SeriesDto implements IWebsocketMessage {
public final SeriesType type;
public SeriesDto(@NonNull final Series series) {
this(series, false);
}
public SeriesDto(@NonNull final Series series, final boolean deleted) {
this.id = series.getId();
this.version = series.getVersion();
this.deleted = deleted;
this.name = series.getName();
this.unit = series.getUnit();
this.decimals = series.getDecimals();

View File

@ -5,23 +5,17 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.ListCrudRepository;
import java.util.List;
import java.util.Optional;
public interface SeriesRepository extends ListCrudRepository<Series, Long> {
@NonNull
Optional<Series> findByName(@NonNull String seriesName);
@NonNull
@Query("select new de.ph87.data.series.SeriesDto(s, false) from Series s where s.id = :id")
@Query("select new de.ph87.data.series.SeriesDto(s) from Series s where s.id = :id")
SeriesDto getDtoById(long id);
@NonNull
@Query("select new de.ph87.data.series.SeriesDto(t, false) from Series t")
@Query("select new de.ph87.data.series.SeriesDto(t) from Series t")
List<SeriesDto> findAllDto();
Optional<Series> findFirstByOrderByNameAsc();
boolean existsByName(@NonNull String name);
}

View File

@ -32,15 +32,15 @@ public class Bool {
@Setter
@NonNull
@Column(nullable = false, name = "`end`")
private ZonedDateTime end;
private ZonedDateTime since;
@Setter
@Column(nullable = false)
private boolean terminated;
public Bool(@NonNull final DataId id, @NonNull final ZonedDateTime end, final boolean state, final boolean terminated) {
public Bool(@NonNull final DataId id, @NonNull final ZonedDateTime since, final boolean state, final boolean terminated) {
this.id = id;
this.end = end;
this.since = since;
this.state = state;
this.terminated = terminated;
}

View File

@ -27,7 +27,7 @@ public class BoolDto implements IWebsocketMessage {
public BoolDto(@NonNull final Bool bool) {
this.series = new SeriesDto(bool.getId().getSeries());
this.date = bool.getId().getDate();
this.end = bool.getEnd();
this.end = bool.getSince();
this.state = bool.isState();
this.terminated = bool.isTerminated();
}

View File

@ -1,65 +1,34 @@
package de.ph87.data.series.data.bool;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
import de.ph87.data.series.point.SeriesPoint;
import de.ph87.data.series.data.delta.ISeriesPoint;
import lombok.Data;
import lombok.NonNull;
import tools.jackson.core.JsonGenerator;
import java.time.ZonedDateTime;
import java.util.List;
@Data
public class BoolPoint implements SeriesPoint<BoolPoint> {
public class BoolPoint implements ISeriesPoint {
public final ZonedDateTime begin;
public final ZonedDateTime date;
public final ZonedDateTime end;
public final ZonedDateTime since;
public final boolean state;
public final boolean terminated;
public BoolPoint(@NonNull final Bool bool) {
this.begin = bool.getId().getDate();
this.end = bool.getEnd();
this.date = bool.getId().getDate();
this.since = bool.getSince();
this.state = bool.isState();
this.terminated = bool.isTerminated();
}
public BoolPoint(final ZonedDateTime begin, final ZonedDateTime end, final boolean state, final boolean terminated) {
this.begin = begin;
this.end = end;
this.state = state;
this.terminated = terminated;
}
@NonNull
@Override
public void toJson(final JsonGenerator jsonGenerator) {
jsonGenerator.writeNumber(begin.toEpochSecond());
jsonGenerator.writeNumber(end.toEpochSecond());
jsonGenerator.writeNumber(state ? 1 : 0);
jsonGenerator.writeNumber(terminated ? 1 : 0);
}
@Override
public BoolPoint times(final double factor) {
return this;
}
@Override
public ZonedDateTime getDate() {
return begin;
}
@Override
public double getValue() {
return state ? 1 : 0;
}
@Override
public BoolPoint combine(@NonNull final SeriesPoint<?> other, @NonNull final GraphOperation operation) throws GraphDivisionByZero {
return new BoolPoint(begin, end, operation.apply(getValue(), other.getValue()) > 0, terminated);
public List<Double> getValues() {
return List.of(state ? 1.0 : 0.0);
}
}

View File

@ -12,7 +12,7 @@ import java.util.List;
public interface BoolRepo extends CrudRepository<Bool, DataId> {
@NonNull
@Query("select new de.ph87.data.series.data.bool.BoolPoint(e) from Bool e where e.id.series = :series and e.end >= :first and e.id.date < :after")
List<BoolPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after);
@Query("select new de.ph87.data.series.data.bool.BoolPoint(e) from Bool e where e.id.series = :series and e.since >= :begin and e.id.date < :end")
List<BoolPoint> points(@NonNull Series series, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end);
}

View File

@ -1,9 +1,9 @@
package de.ph87.data.series.data.bool;
import de.ph87.data.point.IPointRequest;
import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesService;
import de.ph87.data.series.data.DataId;
import de.ph87.data.series.point.ISeriesPointRequest;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.List;
@Slf4j
@ -42,22 +43,24 @@ public class BoolService {
log.error("Differing states: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing);
return;
}
if (existing.getEnd().isAfter(end)) {
if (existing.getSince().isAfter(end)) {
log.error("End ran backwards: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing);
return;
}
if (existing.isTerminated() && (!terminated || !existing.getEnd().equals(end))) {
if (existing.isTerminated() && (!terminated || !existing.getSince().equals(end))) {
log.error("Already terminated: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing);
return;
}
existing.setEnd(end);
existing.setSince(end);
existing.setTerminated(terminated);
}).findFirst().orElseGet(() -> boolRepo.save(new Bool(id, end, state, terminated)));
}
@NonNull
public List<BoolPoint> points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
return boolRepo.points(series, request.getFirst(), request.getAfter());
@SuppressWarnings("unused")
public List<BoolPoint> points(@NonNull final Series series, @NonNull final IPointRequest request) {
log.warn("BoolService.points(...) Not implemented yet!");
return Collections.emptyList(); // TODO
}
}

View File

@ -1,16 +1,13 @@
package de.ph87.data.series.data.delta;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
import de.ph87.data.series.point.SeriesPoint;
import lombok.Data;
import lombok.NonNull;
import tools.jackson.core.JsonGenerator;
import java.time.ZonedDateTime;
import java.util.List;
@Data
public class DeltaPoint implements SeriesPoint<DeltaPoint> {
public class DeltaPoint implements ISeriesPoint {
@NonNull
public final ZonedDateTime date;
@ -22,25 +19,10 @@ public class DeltaPoint implements SeriesPoint<DeltaPoint> {
this.delta = delta;
}
@NonNull
@Override
public void toJson(final JsonGenerator jsonGenerator) {
jsonGenerator.writeNumber(date.toEpochSecond());
jsonGenerator.writeNumber(delta);
}
@Override
public DeltaPoint times(final double factor) {
return new DeltaPoint(date, delta * factor);
}
@Override
public double getValue() {
return delta;
}
@Override
public DeltaPoint combine(@NonNull final SeriesPoint<?> other, @NonNull final GraphOperation operation) throws GraphDivisionByZero {
return new DeltaPoint(date, operation.apply(getValue(), other.getValue()));
public List<Double> getValues() {
return List.of(delta);
}
}

View File

@ -13,7 +13,7 @@ import java.util.List;
public interface DeltaRepo<T extends Delta> extends CrudRepository<T, DeltaId> {
@NonNull
@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);
@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 >= :begin and e.id.date < :end group by e.id.date")
List<DeltaPoint> points(@NonNull Series series, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end);
}

View File

@ -1,11 +1,11 @@
package de.ph87.data.series.data.delta;
import de.ph87.data.point.IPointRequest;
import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesService;
import de.ph87.data.series.data.Interval;
import de.ph87.data.series.data.delta.meter.Meter;
import de.ph87.data.series.data.delta.meter.MeterRepository;
import de.ph87.data.series.point.ISeriesPointRequest;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -72,14 +72,14 @@ public class DeltaService {
}
@NonNull
public List<DeltaPoint> points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
public List<DeltaPoint> points(@NonNull final Series series, @NonNull final IPointRequest request) {
return switch (request.getInterval()) {
case FIVE -> five.points(series, request.getFirst(), request.getAfter());
case HOUR -> hour.points(series, request.getFirst(), request.getAfter());
case DAY -> day.points(series, request.getFirst(), request.getAfter());
case WEEK -> week.points(series, request.getFirst(), request.getAfter());
case MONTH -> month.points(series, request.getFirst(), request.getAfter());
case YEAR -> year.points(series, request.getFirst(), request.getAfter());
case FIVE -> five.points(series, request.getBegin(), request.getEnd());
case HOUR -> hour.points(series, request.getBegin(), request.getEnd());
case DAY -> day.points(series, request.getBegin(), request.getEnd());
case WEEK -> week.points(series, request.getBegin(), request.getEnd());
case MONTH -> month.points(series, request.getBegin(), request.getEnd());
case YEAR -> year.points(series, request.getBegin(), request.getEnd());
};
}

View File

@ -0,0 +1,18 @@
package de.ph87.data.series.data.delta;
import lombok.NonNull;
import tools.jackson.databind.annotation.JsonSerialize;
import java.time.ZonedDateTime;
import java.util.List;
@JsonSerialize(using = ISeriesPointSerializer.class)
public interface ISeriesPoint {
@NonNull
ZonedDateTime getDate();
@NonNull
List<Double> getValues();
}

View File

@ -0,0 +1,20 @@
package de.ph87.data.series.data.delta;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
public class ISeriesPointSerializer extends ValueSerializer<ISeriesPoint> {
@Override
public void serialize(final ISeriesPoint value, final JsonGenerator gen, final SerializationContext ctxt) throws JacksonException {
gen.writeStartArray();
gen.writeNumber(value.getDate().toEpochSecond());
for (double v : value.getValues()) {
gen.writeNumber(v);
}
gen.writeEndArray();
}
}

View File

@ -1,16 +1,14 @@
package de.ph87.data.series.data.varying;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
import de.ph87.data.series.point.SeriesPoint;
import de.ph87.data.series.data.delta.ISeriesPoint;
import lombok.Data;
import lombok.NonNull;
import tools.jackson.core.JsonGenerator;
import java.time.ZonedDateTime;
import java.util.List;
@Data
public class VaryingPoint implements SeriesPoint<VaryingPoint> {
public class VaryingPoint implements ISeriesPoint {
public final ZonedDateTime date;
@ -27,37 +25,10 @@ public class VaryingPoint implements SeriesPoint<VaryingPoint> {
this.avg = varying.getAvg();
}
private VaryingPoint(final ZonedDateTime date, final double min, final double avg, final double max) {
this.date = date;
this.min = min;
this.avg = avg;
this.max = max;
}
@NonNull
@Override
public void toJson(final JsonGenerator jsonGenerator) {
jsonGenerator.writeNumber(date.toEpochSecond());
jsonGenerator.writeNumber(min);
jsonGenerator.writeNumber(max);
jsonGenerator.writeNumber(avg);
}
@Override
public VaryingPoint times(final double factor) {
return new VaryingPoint(date, min * factor, avg * factor, max * factor);
}
@Override
public double getValue() {
return avg;
}
@Override
public VaryingPoint combine(@NonNull final SeriesPoint<?> other, @NonNull final GraphOperation operation) throws GraphDivisionByZero {
if (other instanceof final VaryingPoint otherVarying) {
return new VaryingPoint(date, operation.apply(min, otherVarying.min), operation.apply(avg, otherVarying.avg), operation.apply(max, otherVarying.max));
}
return new VaryingPoint(date, operation.apply(min, other.getValue()), operation.apply(avg, other.getValue()), operation.apply(max, other.getValue()));
public List<Double> getValues() {
return List.of(avg, min, max);
}
}

View File

@ -14,7 +14,7 @@ import java.util.List;
public interface VaryingRepo<T extends Varying> extends CrudRepository<T, DataId> {
@NonNull
@Query("select new de.ph87.data.series.data.varying.VaryingPoint(e) from #{#entityName} e where e.id.series = :series and e.id.date >= :first and e.id.date < :after")
List<VaryingPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after);
@Query("select new de.ph87.data.series.data.varying.VaryingPoint(e) from #{#entityName} e where e.id.series = :series and e.id.date >= :begin and e.id.date < :end")
List<VaryingPoint> points(@NonNull Series series, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end);
}

View File

@ -1,10 +1,10 @@
package de.ph87.data.series.data.varying;
import de.ph87.data.point.IPointRequest;
import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesService;
import de.ph87.data.series.data.DataId;
import de.ph87.data.series.data.Interval;
import de.ph87.data.series.point.ISeriesPointRequest;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -56,14 +56,14 @@ public class VaryingService {
}
@NonNull
public List<VaryingPoint> points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
public List<VaryingPoint> points(@NonNull final Series series, @NonNull final IPointRequest request) {
return switch (request.getInterval()) {
case FIVE -> five.points(series, request.getFirst(), request.getAfter());
case HOUR -> hour.points(series, request.getFirst(), request.getAfter());
case DAY -> day.points(series, request.getFirst(), request.getAfter());
case WEEK -> week.points(series, request.getFirst(), request.getAfter());
case MONTH -> month.points(series, request.getFirst(), request.getAfter());
case YEAR -> year.points(series, request.getFirst(), request.getAfter());
case FIVE -> five.points(series, request.getBegin(), request.getEnd());
case HOUR -> hour.points(series, request.getBegin(), request.getEnd());
case DAY -> day.points(series, request.getBegin(), request.getEnd());
case WEEK -> week.points(series, request.getBegin(), request.getEnd());
case MONTH -> month.points(series, request.getBegin(), request.getEnd());
case YEAR -> year.points(series, request.getBegin(), request.getEnd());
};
}

View File

@ -1,34 +0,0 @@
package de.ph87.data.series.point;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.ph87.data.series.data.Interval;
import lombok.Data;
import lombok.NonNull;
import java.time.ZonedDateTime;
@Data
public class AllSeriesPointRequest implements ISeriesPointRequest {
@NonNull
public final Interval interval;
public final long offset;
@NonNull
public final ZonedDateTime first;
@NonNull
public final ZonedDateTime after;
public AllSeriesPointRequest(
@JsonProperty("interval") final Interval interval,
@JsonProperty("offset") final long offset
) {
this.interval = interval;
this.offset = offset;
this.first = interval.align.apply(ZonedDateTime.now()).minus(interval.amount * offset, interval.unit);
this.after = this.first.plus(interval.amount, interval.unit);
}
}

View File

@ -1,30 +0,0 @@
package de.ph87.data.series.point;
import de.ph87.data.series.SeriesDto;
import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NonNull;
import java.util.List;
@Data
public class AllSeriesPointResponse {
@NonNull
public final AllSeriesPointRequest request;
@NonNull
public final List<Entry> seriesPoints;
@Data
public static class Entry {
@NonNull
public final SeriesDto series;
@Nullable
public final SeriesPoint<?> point;
}
}

View File

@ -1,19 +0,0 @@
package de.ph87.data.series.point;
import de.ph87.data.series.data.Interval;
import lombok.NonNull;
import java.time.ZonedDateTime;
public interface ISeriesPointRequest {
@NonNull
Interval getInterval();
@NonNull
ZonedDateTime getFirst();
@NonNull
ZonedDateTime getAfter();
}

View File

@ -1,64 +0,0 @@
package de.ph87.data.series.point;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.ph87.data.plot.axis.graph.GraphOperation;
import de.ph87.data.series.data.Interval;
import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NonNull;
import java.time.ZonedDateTime;
@Data
public class OneSeriesPointsRequest implements ISeriesPointRequest {
public final long id;
public final double factor;
public final GraphOperation operation;
@Nullable
public final Long id2;
public final double factor2;
@NonNull
public final Interval interval;
public final long offset;
public final long duration;
@NonNull
@JsonIgnore
public final ZonedDateTime first;
@NonNull
@JsonIgnore
public final ZonedDateTime after;
public OneSeriesPointsRequest(
@JsonProperty("id") final long id,
@JsonProperty("factor") final double factor,
@JsonProperty("operation") final GraphOperation operation,
@JsonProperty("id2") @Nullable final Long id2,
@JsonProperty("factor2") final double factor2,
@JsonProperty("interval") final Interval interval,
@JsonProperty("offset") final long offset,
@JsonProperty("duration") final long duration
) {
this.id = id;
this.factor = factor;
this.operation = operation;
this.id2 = id2;
this.factor2 = factor2;
this.interval = interval;
this.offset = offset;
this.duration = duration;
this.after = interval.align.apply(ZonedDateTime.now()).minus(interval.amount * (offset - 1), interval.unit);
this.first = this.after.minus(interval.amount * duration, interval.unit);
}
}

View File

@ -1,14 +0,0 @@
package de.ph87.data.series.point;
import tools.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.util.List;
@Data
@JsonSerialize(using = OneSeriesPointsResponseSerializer.class)
public class OneSeriesPointsResponse {
public final List<? extends SeriesPoint<?>> points;
}

View File

@ -1,21 +0,0 @@
package de.ph87.data.series.point;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
public class OneSeriesPointsResponseSerializer extends ValueSerializer<OneSeriesPointsResponse> {
@Override
public void serialize(final OneSeriesPointsResponse value, final JsonGenerator gen, final SerializationContext ctxt) throws JacksonException {
gen.writeStartArray();
for (final SeriesPoint<?> point : value.points) {
gen.writeStartArray();
point.toJson(gen);
gen.writeEndArray();
}
gen.writeEndArray();
}
}

View File

@ -1,66 +0,0 @@
package de.ph87.data.series.point;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
import lombok.NonNull;
import tools.jackson.core.JsonGenerator;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public interface SeriesPoint<T extends SeriesPoint<T>> {
ZonedDateTime getDate();
double getValue();
void toJson(final JsonGenerator jsonGenerator);
T times(final double factor);
T combine(@NonNull final SeriesPoint<?> other, @NonNull final GraphOperation operation) throws GraphDivisionByZero;
@NonNull
static List<SeriesPoint<?>> combine(@NonNull final List<? extends SeriesPoint<?>> points1, @NonNull final List<? extends SeriesPoint<?>> points2, @NonNull final GraphOperation operation) {
if (points1.isEmpty() || points2.isEmpty()) {
return Collections.emptyList();
}
final List<SeriesPoint<?>> as = new ArrayList<>(points1);
final List<SeriesPoint<?>> bs = new ArrayList<>(points2);
SeriesPoint<?> a = as.removeFirst();
SeriesPoint<?> b = bs.removeFirst();
final List<SeriesPoint<?>> result = new ArrayList<>(Math.min(as.size(), bs.size()));
while (true) {
final int diff = a.getDate().compareTo(b.getDate());
if (diff == 0) {
try {
result.add(a.combine(b, operation));
} catch (GraphDivisionByZero e) {
// just not add
}
a = null;
b = null;
} else if (diff < 0) {
a = null;
} else {
b = null;
}
if (a == null) {
if (as.isEmpty()) {
break;
}
a = as.removeFirst();
}
if (b == null) {
if (bs.isEmpty()) {
break;
}
b = bs.removeFirst();
}
}
return result;
}
}

View File

@ -1,100 +0,0 @@
package de.ph87.data.series.point;
import de.ph87.data.series.HistoryDto;
import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesDto;
import de.ph87.data.series.SeriesService;
import de.ph87.data.series.data.Interval;
import de.ph87.data.series.data.bool.BoolService;
import de.ph87.data.series.data.delta.DeltaService;
import de.ph87.data.series.data.varying.VaryingService;
import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.ZonedDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class SeriesPointService {
private final BoolService boolService;
private final DeltaService deltaService;
private final VaryingService varyingService;
private final SeriesService seriesService;
@NonNull
public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) {
final Series series1 = seriesService.getById(request.id);
final List<? extends SeriesPoint<?>> points1 = getSeriesPoints(series1, request, request.factor);
if (request.id2 != null) {
final Series series2 = seriesService.getById(request.id2);
final List<? extends SeriesPoint<?>> points2 = getSeriesPoints(series2, request, request.factor2);
return new OneSeriesPointsResponse(SeriesPoint.combine(points1, points2, request.operation));
}
return new OneSeriesPointsResponse(points1);
}
@NonNull
public AllSeriesPointResponse allSeriesPoint(@NonNull final AllSeriesPointRequest request) {
final List<AllSeriesPointResponse.Entry> seriesPoints = seriesService.findAll().stream().map(series -> map(series, request)).toList();
return new AllSeriesPointResponse(request, seriesPoints);
}
@NonNull
private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
final List<? extends SeriesPoint<?>> points = getSeriesPoints(series, request, null);
final SeriesDto seriesDto = new SeriesDto(series);
final SeriesPoint<?> point = points.isEmpty() ? null : points.getFirst();
return new AllSeriesPointResponse.Entry(seriesDto, point);
}
@NonNull
private List<? extends SeriesPoint<?>> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) {
final List<? extends SeriesPoint<?>> points = switch (series.getType()) {
case BOOL -> boolService.points(series, request);
case DELTA -> deltaService.points(series, request);
case VARYING -> varyingService.points(series, request);
};
if (factor == null || factor == 1) {
return points;
}
return points.stream().map(p -> p.times(factor)).toList();
}
@NonNull
public HistoryDto history(final long id, @NonNull final ZonedDateTime date, @NonNull final Interval interval) {
final Series series = seriesService.getById(id);
final AllSeriesPointResponse.Entry entry = map(series, new Request(date, interval));
return new HistoryDto(entry);
}
@Data
private static class Request implements ISeriesPointRequest {
@NonNull
public final Interval interval;
@NonNull
public final ZonedDateTime first;
@NonNull
public final ZonedDateTime after;
public Request(final @NonNull ZonedDateTime date, final @NonNull Interval interval) {
this.interval = interval;
this.first = interval.align.apply(date);
this.after = this.first.plus(interval.amount, interval.unit);
}
}
}

View File

@ -1,8 +1,8 @@
package de.ph87.data.topic;
import de.ph87.data.topic.parser.PatrixOpenDtu;
import de.ph87.data.topic.parser.ShellyPlus1PM;
import de.ph87.data.topic.parser.PatrixSmartMeter;
import de.ph87.data.topic.parser.ShellyPlus1PM;
import de.ph87.data.topic.parser.TasmotaSmartMeter;
import lombok.Getter;

View File

@ -1,6 +1,5 @@
package de.ph87.data.weather;
import tools.jackson.databind.ObjectMapper;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -9,6 +8,7 @@ import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import tools.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.HttpURLConnection;