SeriesHistory

This commit is contained in:
Patrick Haßel 2025-10-29 13:25:10 +01:00
parent 3509d8ab41
commit 03ad1615d2
12 changed files with 285 additions and 9 deletions

View File

@ -0,0 +1,33 @@
<div class="Section3">
<div class="SectionHeading">
<div class="SectionHeadingText">
{{ heading }}
</div>
</div>
<div class="SectionBody">
<div class="Section4">
<div class="SectionHeadingText">
Bezogen
</div>
<div class="SectionBody">
{{ historyEnergyPurchase?.valueString }}
</div>
</div>
<div class="Section4">
<div class="SectionHeadingText">
Eingespeist
</div>
<div class="SectionBody">
{{ historyEnergyDeliver?.valueString }}
</div>
</div>
<div class="Section4">
<div class="SectionHeadingText">
Erzeugt
</div>
<div class="SectionBody">
{{ historyEnergyProduce?.valueString }}
</div>
</div>
</div>
</div>

View File

@ -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<History | null>) {
if (!series || !this.interval) {
next(null);
return
}
this.seriesService.history(series, this.date, this.interval, next);
}
}

View File

@ -1,5 +1,9 @@
@if (location) { @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" [date]="yesterday" heading="Gestern"></app-series-history>
<div class="Section"> <div class="Section">
<div class="SectionHeading"> <div class="SectionHeading">
<div class="SectionHeadingText"> <div class="SectionHeadingText">

View File

@ -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 {LocationService} from '../location-service';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {Location} from '../Location'; import {Location} from '../Location';
@ -6,30 +6,45 @@ import {Text} from '../../shared/text/text';
import {Number} from '../../shared/number/number'; import {Number} from '../../shared/number/number';
import {SeriesSelect} from '../../series/select/series-select'; import {SeriesSelect} from '../../series/select/series-select';
import {Series} from '../../series/Series'; import {Series} from '../../series/Series';
import {SeriesService} from '../../series/series-service'; import {Interval, SeriesService} from '../../series/series-service';
import {SeriesType} from '../../series/SeriesType'; import {SeriesType} from '../../series/SeriesType';
import {Subscription, timer} from 'rxjs'; 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({ @Component({
selector: 'app-location-detail', selector: 'app-location-detail',
imports: [ imports: [
Text, Text,
Number, Number,
SeriesSelect SeriesSelect,
SeriesHistory
], ],
templateUrl: './location-detail.html', templateUrl: './location-detail.html',
styleUrl: './location-detail.less', styleUrl: './location-detail.less',
}) })
export class LocationDetail implements OnInit, OnDestroy { 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 readonly subs: Subscription [] = [];
private series: Series[] = []; private series: Series[] = [];
protected location: Location | null = null;
protected now: Date = new Date(); protected now: Date = new Date();
protected yesterday: Date = yesterday(this.now);
constructor( constructor(
readonly locationService: LocationService, readonly locationService: LocationService,
readonly seriesService: SeriesService, readonly seriesService: SeriesService,
@ -44,7 +59,10 @@ export class LocationDetail implements OnInit, OnDestroy {
}); });
this.seriesService.findAll(list => this.series = list); this.seriesService.findAll(list => this.series = list);
this.subs.push(this.seriesService.subscribe(this.updateSeries)); 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 { ngOnDestroy(): void {
@ -64,6 +82,7 @@ export class LocationDetail implements OnInit, OnDestroy {
} else { } else {
this.series.push(fresh); this.series.push(fresh);
} }
this.today.updateSeries(fresh);
}; };
protected readonly filterEnergy = (): Series[] => { protected readonly filterEnergy = (): Series[] => {

View File

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

View File

@ -3,6 +3,10 @@ import {or, validateDate, validateEnum, validateNumber, validateString} from "..
import {SeriesType} from './SeriesType'; import {SeriesType} from './SeriesType';
import {formatNumber} from '@angular/common'; 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 { export class Series {
readonly valueString: string; readonly valueString: string;
@ -17,7 +21,7 @@ export class Series {
readonly unit: string, readonly unit: string,
readonly type: SeriesType, 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 { static fromJson(json: any): Series {

View File

@ -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 {ApiService, CrudService, Next, WebsocketService} from '../common';
import {Series} from './Series'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class SeriesService extends CrudService<Series> { export class SeriesService extends CrudService<Series> {
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); super(api, ws, ['Series'], Series.fromJson);
this.datePipe = new DatePipe(locale);
} }
findAll(next: Next<Series[]>) { findAll(next: Next<Series[]>) {
this.getList(['findAll'], next); 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

@ -43,6 +43,7 @@ div {
.Section2 { .Section2 {
overflow: visible; overflow: visible;
> .SectionHeading { > .SectionHeading {
> .SectionHeadingText { > .SectionHeadingText {
font-style: italic; font-style: italic;
@ -55,3 +56,39 @@ div {
padding-bottom: 0.5em; padding-bottom: 0.5em;
margin-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 {
}
}

View File

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

View File

@ -1,5 +1,6 @@
package de.ph87.data.series; 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.AllSeriesPointRequest;
import de.ph87.data.series.point.AllSeriesPointResponse; import de.ph87.data.series.point.AllSeriesPointResponse;
import de.ph87.data.series.point.OneSeriesPointsRequest; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
@CrossOrigin @CrossOrigin
@ -64,11 +68,20 @@ public class SeriesController {
return seriesService.modify(id, series -> series.setType(type)); 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}") @GetMapping("{id}")
public SeriesDto getById(@PathVariable final long id) { public SeriesDto getById(@PathVariable final long id) {
return seriesRepository.getDtoById(id); return seriesRepository.getDtoById(id);
} }
@NonNull
@GetMapping("findAll") @GetMapping("findAll")
public List<SeriesDto> findAll() { public List<SeriesDto> findAll() {
return seriesRepository.findAllDto(); return seriesRepository.findAllDto();
@ -80,6 +93,7 @@ public class SeriesController {
return seriesPointService.oneSeriesPoints(request); return seriesPointService.oneSeriesPoints(request);
} }
@NonNull
@PostMapping("allSeriesPoint") @PostMapping("allSeriesPoint")
public AllSeriesPointResponse allSeriesPoint(@NonNull @RequestBody final AllSeriesPointRequest request) { public AllSeriesPointResponse allSeriesPoint(@NonNull @RequestBody final AllSeriesPointRequest request) {
return seriesPointService.allSeriesPoint(request); return seriesPointService.allSeriesPoint(request);

View File

@ -1,17 +1,21 @@
package de.ph87.data.series.point; package de.ph87.data.series.point;
import de.ph87.data.series.HistoryDto;
import de.ph87.data.series.Series; import de.ph87.data.series.Series;
import de.ph87.data.series.SeriesDto; import de.ph87.data.series.SeriesDto;
import de.ph87.data.series.SeriesService; 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.bool.BoolService;
import de.ph87.data.series.data.delta.DeltaService; import de.ph87.data.series.data.delta.DeltaService;
import de.ph87.data.series.data.varying.VaryingService; import de.ph87.data.series.data.varying.VaryingService;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
@Slf4j @Slf4j
@ -66,4 +70,31 @@ public class SeriesPointService {
return points.stream().map(p -> p.times(factor)).toList(); 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);
}
}
} }