diff --git a/src/main/angular/src/app/location/detail/history/series-history.html b/src/main/angular/src/app/location/detail/history/series-history.html new file mode 100644 index 0000000..45cea1b --- /dev/null +++ b/src/main/angular/src/app/location/detail/history/series-history.html @@ -0,0 +1,33 @@ +
+
+
+ {{ heading }} +
+
+
+
+
+ Bezogen +
+
+ {{ historyEnergyPurchase?.valueString }} +
+
+
+
+ Eingespeist +
+
+ {{ historyEnergyDeliver?.valueString }} +
+
+
+
+ Erzeugt +
+
+ {{ historyEnergyProduce?.valueString }} +
+
+
+
diff --git a/src/main/angular/src/app/location/detail/history/series-history.less b/src/main/angular/src/app/location/detail/history/series-history.less new file mode 100644 index 0000000..e69de29 diff --git a/src/main/angular/src/app/location/detail/history/series-history.ts b/src/main/angular/src/app/location/detail/history/series-history.ts new file mode 100644 index 0000000..a19d3bc --- /dev/null +++ b/src/main/angular/src/app/location/detail/history/series-history.ts @@ -0,0 +1,68 @@ +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'; + +@Component({ + selector: 'app-series-history', + imports: [], + templateUrl: './series-history.html', + styleUrl: './series-history.less', +}) +export class SeriesHistory implements AfterViewInit { + + protected historyEnergyPurchase: History | null = null; + + protected historyEnergyDeliver: History | null = null; + + protected historyEnergyProduce: History | null = null; + + protected readonly Interval = Interval; + + @Input() + heading!: string; + + @Input() + date!: Date; + + @Input() + interval!: Interval; + + @Input() + location!: Location; + + constructor( + readonly seriesService: SeriesService, + ) { + // + } + + 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) { + if (!series || !this.interval) { + next(null); + return + } + this.seriesService.history(series, this.date, this.interval, next); + } + +} diff --git a/src/main/angular/src/app/location/detail/location-detail.html b/src/main/angular/src/app/location/detail/location-detail.html index 03f35e0..d3cacd8 100644 --- a/src/main/angular/src/app/location/detail/location-detail.html +++ b/src/main/angular/src/app/location/detail/location-detail.html @@ -1,5 +1,9 @@ @if (location) { + + + +
diff --git a/src/main/angular/src/app/location/detail/location-detail.ts b/src/main/angular/src/app/location/detail/location-detail.ts index 9efeb5a..66ec39c 100644 --- a/src/main/angular/src/app/location/detail/location-detail.ts +++ b/src/main/angular/src/app/location/detail/location-detail.ts @@ -1,4 +1,4 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {LocationService} from '../location-service'; import {ActivatedRoute} from '@angular/router'; import {Location} from '../Location'; @@ -6,30 +6,45 @@ 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 {SeriesService} from '../../series/series-service'; +import {Interval, SeriesService} from '../../series/series-service'; import {SeriesType} from '../../series/SeriesType'; import {Subscription, timer} from 'rxjs'; +import {SeriesHistory} from './history/series-history'; + +function yesterday(now: any) { + const yesterday = new Date(now.getTime()); + yesterday.setDate(yesterday.getDate() - 1); + return yesterday; +} @Component({ selector: 'app-location-detail', imports: [ Text, Number, - SeriesSelect + SeriesSelect, + SeriesHistory ], templateUrl: './location-detail.html', styleUrl: './location-detail.less', }) export class LocationDetail implements OnInit, OnDestroy { + @ViewChild("today") + protected today!: SeriesHistory; + + protected readonly Interval = Interval; + + protected location: Location | null = null; + private readonly subs: Subscription [] = []; private series: Series[] = []; - protected location: Location | null = null; - protected now: Date = new Date(); + protected yesterday: Date = yesterday(this.now); + constructor( readonly locationService: LocationService, readonly seriesService: SeriesService, @@ -44,7 +59,10 @@ export class LocationDetail implements OnInit, OnDestroy { }); this.seriesService.findAll(list => this.series = list); this.subs.push(this.seriesService.subscribe(this.updateSeries)); - this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date())); + this.subs.push(timer(1000, 1000).subscribe(() => { + this.now = new Date(); + this.yesterday = yesterday(this.now); + })); } ngOnDestroy(): void { @@ -64,6 +82,7 @@ export class LocationDetail implements OnInit, OnDestroy { } else { this.series.push(fresh); } + this.today.updateSeries(fresh); }; protected readonly filterEnergy = (): Series[] => { diff --git a/src/main/angular/src/app/series/History.ts b/src/main/angular/src/app/series/History.ts new file mode 100644 index 0000000..5bd46a0 --- /dev/null +++ b/src/main/angular/src/app/series/History.ts @@ -0,0 +1,22 @@ +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), + ); + } + +} diff --git a/src/main/angular/src/app/series/Series.ts b/src/main/angular/src/app/series/Series.ts index ca886e5..1020f3d 100644 --- a/src/main/angular/src/app/series/Series.ts +++ b/src/main/angular/src/app/series/Series.ts @@ -3,6 +3,10 @@ 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; @@ -17,7 +21,7 @@ export class Series { readonly unit: string, readonly type: SeriesType, ) { - this.valueString = (value === null ? '-' : formatNumber(value, "de-DE", `0.${decimals}-${decimals}`)) + ' ' + unit; + this.valueString = getValueString(value, this); } static fromJson(json: any): Series { diff --git a/src/main/angular/src/app/series/series-service.ts b/src/main/angular/src/app/series/series-service.ts index 289bd74..585acb7 100644 --- a/src/main/angular/src/app/series/series-service.ts +++ b/src/main/angular/src/app/series/series-service.ts @@ -1,18 +1,40 @@ -import {Injectable} from '@angular/core'; +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' }) export class SeriesService extends CrudService { - constructor(api: ApiService, ws: WebsocketService) { + private readonly datePipe: DatePipe; + + constructor( + api: ApiService, + ws: WebsocketService, + @Inject(LOCALE_ID) readonly locale: string, + ) { super(api, ws, ['Series'], Series.fromJson); + this.datePipe = new DatePipe(locale); } findAll(next: Next) { this.getList(['findAll'], next); } + history(series: Series, date: Date, interval: Interval, next: Next) { + this.api.getSingle([...this.path, series.id, 'history', Math.floor(date.getTime() / 1000), interval], History.fromJson, next); + } + } diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index d109b93..313a070 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -43,6 +43,7 @@ div { .Section2 { overflow: visible; + > .SectionHeading { > .SectionHeadingText { font-style: italic; @@ -55,3 +56,39 @@ div { padding-bottom: 0.5em; margin-bottom: 0.5em; } + +.Section3 { + border: 1px solid gray; + margin: 1em 0.5em 0.5em; + padding: 0.5em; + overflow: visible; + + > .SectionHeading { + display: flex; + margin-top: -1.25em; + + > .SectionHeadingText { + font-weight: bold; + background-color: white; + } + } + + > .SectionBody { + display: flex; + } + +} + +.Section4 { + flex: 1; + text-align: center; + + > .SectionHeadingText { + font-weight: bold; + background-color: white; + } + + > .SectionBody { + } + +} diff --git a/src/main/java/de/ph87/data/series/HistoryDto.java b/src/main/java/de/ph87/data/series/HistoryDto.java new file mode 100644 index 0000000..6af0a8e --- /dev/null +++ b/src/main/java/de/ph87/data/series/HistoryDto.java @@ -0,0 +1,22 @@ +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(); + } + +} diff --git a/src/main/java/de/ph87/data/series/SeriesController.java b/src/main/java/de/ph87/data/series/SeriesController.java index f4dbe3f..269324b 100644 --- a/src/main/java/de/ph87/data/series/SeriesController.java +++ b/src/main/java/de/ph87/data/series/SeriesController.java @@ -1,5 +1,6 @@ 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; @@ -15,6 +16,9 @@ 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 @@ -64,11 +68,20 @@ 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) { return seriesRepository.getDtoById(id); } + @NonNull @GetMapping("findAll") public List findAll() { return seriesRepository.findAllDto(); @@ -80,6 +93,7 @@ public class SeriesController { return seriesPointService.oneSeriesPoints(request); } + @NonNull @PostMapping("allSeriesPoint") public AllSeriesPointResponse allSeriesPoint(@NonNull @RequestBody final AllSeriesPointRequest request) { return seriesPointService.allSeriesPoint(request); diff --git a/src/main/java/de/ph87/data/series/point/SeriesPointService.java b/src/main/java/de/ph87/data/series/point/SeriesPointService.java index 3eb86c8..bc90953 100644 --- a/src/main/java/de/ph87/data/series/point/SeriesPointService.java +++ b/src/main/java/de/ph87/data/series/point/SeriesPointService.java @@ -1,17 +1,21 @@ 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 @@ -66,4 +70,31 @@ public class SeriesPointService { 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); + } + + } + }