series-history current/history

This commit is contained in:
Patrick Haßel 2025-10-30 08:36:13 +01:00
parent 320729647e
commit 26baa904dd
19 changed files with 318 additions and 107 deletions

View File

@ -1,13 +1,18 @@
<div class="MainMenu NoUserSelect">
<div class="MainMenuBar">
<div class="MainMenuButton" (click)="showDrawer = !showDrawer">
<div class="MainMenuItem MainMenuButton" (click)="showDrawer = !showDrawer">
<fa-icon [icon]="faBars"></fa-icon>
</div>
<div class="MainMenuItem MainMenuTitle">
{{ menuService.title }}
</div>
</div>
</div>
<div class="MainMenuDrawer NoUserSelect" [hidden]="!showDrawer">
<div class="MainMenuItem" routerLink="Location" routerLinkActive="MainMenuItemActive">Orte</div>
@for (location of locationList; track location.id) {
<div class="MainMenuItem" routerLink="Location/{{ location.id }}" routerLinkActive="MainMenuItemActive" (click)="showDrawer = false">{{ location.name }}</div>
}
</div>
<router-outlet/>

View File

@ -7,6 +7,10 @@
display: flex;
padding: 0.25em;
.MainMenuItem {
padding: 0.25em;
}
.MainMenuButton {
padding: 0.25em;
border: 1px solid #888;
@ -16,6 +20,7 @@
.MainMenuButton:hover {
background-color: lightskyblue;
}
}
}

View File

@ -1,7 +1,10 @@
import {Component} from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faBars, faBurger} from '@fortawesome/free-solid-svg-icons';
import {faBars} from '@fortawesome/free-solid-svg-icons';
import {MenuService} from './menu-service';
import {Location} from './location/Location';
import {LocationService} from './location/location-service';
@Component({
selector: 'app-root',
@ -9,11 +12,28 @@ import {faBars, faBurger} from '@fortawesome/free-solid-svg-icons';
templateUrl: './app.html',
styleUrl: './app.less'
})
export class App {
protected readonly faBurger = faBurger;
export class App implements OnInit, OnDestroy {
protected readonly faBars = faBars;
protected showDrawer: boolean = false;
protected locationList: Location[] = [];
constructor(
readonly locationService: LocationService,
readonly menuService: MenuService,
) {
//
}
ngOnInit(): void {
this.locationService.findAll(list => this.locationList = list);
this.menuService.title = "Orte";
}
ngOnDestroy(): void {
this.menuService.title = "";
}
}

View File

@ -5,30 +5,47 @@
</div>
</div>
<div class="SectionBody">
<div class="Section4">
<div class="SectionHeadingText">
Bezogen
Bezug
</div>
<div class="SectionBody">
{{ historyEnergyPurchase?.valueString }}
<div class="SectionBody purchase">
{{ purchase?.toValueString(true, interval ? null : now) }}
</div>
</div>
<div class="Section4">
<div class="SectionHeadingText">
Eingespeist
Solar
</div>
<div class="SectionBody">
{{ historyEnergyDeliver?.valueString }}
<div class="SectionBody produce">
{{ produce?.toValueString(true, interval ? null : now) }}
</div>
</div>
<div class="Section4">
<div class="SectionHeadingText">
Erzeugt
Verbrauch
</div>
<div class="SectionBody">
{{ historyEnergyProduce?.valueString }}
<div class="SectionBody consume">
{{ consume?.toValueString(true, interval ? null : now) }}
</div>
</div>
<div class="Section4">
<div class="SectionHeadingText">
Einspeisung
</div>
<app-series-history-graph></app-series-history-graph>
<div class="SectionBody deliver">
{{ deliver?.toValueString(true, interval ? null : now) }}
</div>
</div>
</div>
@if (interval) {
<!-- <app-series-history-graph></app-series-history-graph>-->
}
</div>

View File

@ -0,0 +1 @@
@import "../../../../colors";

View File

@ -1,30 +1,33 @@
import {AfterViewInit, Component, Input} from '@angular/core';
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
import {Location} from '../../Location';
import {Series} from '../../../series/Series';
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';
import {SeriesService} from '../../../series/series-service';
import {Subscription} from 'rxjs';
import {Value} from '../../../series/Value';
@Component({
selector: 'app-series-history',
imports: [
SeriesHistoryGraph
],
imports: [],
templateUrl: './series-history.html',
styleUrl: './series-history.less',
})
export class SeriesHistory implements AfterViewInit {
protected historyEnergyPurchase: PointSeries | null = null;
protected historyEnergyDeliver: PointSeries | null = null;
protected historyEnergyProduce: PointSeries | null = null;
export class SeriesHistory implements OnInit, AfterViewInit, OnDestroy {
protected readonly Interval = Interval;
private readonly subs: Subscription[] = [];
protected purchase: Value | null = null;
protected deliver: Value | null = null;
protected produce: Value | null = null;
protected consume: Value | null = null;
@Input()
heading!: string;
@ -32,41 +35,80 @@ export class SeriesHistory implements AfterViewInit {
offset: number = 0;
@Input()
interval!: Interval;
interval: Interval | null = null;
@Input()
location!: Location;
@Input()
now: Date = new Date();
constructor(
readonly pointService: PointService,
readonly serieService: 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);
ngOnInit(): void {
this.subs.push(this.serieService.subscribe(this.update));
}
public readonly updateSeries = (fresh: Series): void => {
if (fresh.id === this.location?.energyPurchase?.id) {
this.history(this.location?.energyPurchase, history => this.historyEnergyPurchase = history);
ngAfterViewInit(): void {
if (this.interval) {
this.history(null, this.location?.energyPurchase, history => this.purchase = history);
this.history(null, this.location?.energyDeliver, history => this.deliver = history);
this.history(null, this.location?.energyProduce, history => this.produce = history);
} else {
this.history(null, this.location?.powerPurchase, history => this.purchase = history);
this.history(null, this.location?.powerDeliver, history => this.deliver = history);
this.history(null, this.location?.powerProduce, history => this.produce = 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);
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
protected readonly update = (fresh: Series): void => {
if (this.interval) {
if (this.offset > 0) {
return;
}
this.history(fresh, this.location?.energyPurchase, value => this.purchase = value);
this.history(fresh, this.location?.energyDeliver, value => this.deliver = value);
this.history(fresh, this.location?.energyProduce, value => this.produce = value);
} else {
this.history(fresh, this.location?.powerPurchase, value => this.purchase = value);
this.history(fresh, this.location?.powerDeliver, value => this.deliver = value);
this.history(fresh, this.location?.powerProduce, value => this.produce = value);
}
};
private history(series: Series | null | undefined, next: Next<PointSeries | null>) {
if (!series || !this.interval) {
next(null);
private history(fresh: Series | null | undefined, series: Series | null | undefined, next: Next<Value | null>) {
const n = (value: Value | null) => {
next(value);
this.consume = this.purchase?.plus(this.produce)?.minus(this.deliver) || null;
}
if (fresh !== null && fresh !== undefined) {
if (fresh.id !== series?.id) {
return;
}
series = fresh;
}
if (!series) {
n(null);
return
}
this.pointService.relative([series], this.interval, this.offset, 1, response => next(response.series[0]));
if (this.interval) {
this.pointService.relative([series], this.interval, this.offset, 1, response => n(Value.ofPoint(response, 0, 0, 1)));
} else {
n(series.value);
}
}
protected nullOrZero(value: Value | null | undefined): boolean {
return value === null || value === undefined || value.value === 0;
}
}

View File

@ -1,8 +1,10 @@
@if (location) {
<app-series-history [location]="location" [interval]="Interval.DAY" [offset]="0" heading="Heute" #today></app-series-history>
<app-series-history [now]="now" [location]="location" [interval]="null" heading="Aktuell"></app-series-history>
<app-series-history [location]="location" [interval]="Interval.DAY" [offset]="1" heading="Gestern"></app-series-history>
<app-series-history [now]="now" [location]="location" [interval]="Interval.DAY" heading="Heute"></app-series-history>
<app-series-history [now]="now" [location]="location" [interval]="Interval.DAY" [offset]="1" heading="Gestern"></app-series-history>
<div class="Section">
<div class="SectionHeading">
@ -14,7 +16,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Name:
Name
</div>
</div>
<div class="SectionBody">
@ -24,7 +26,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Breitegrad:
Breitegrad
</div>
</div>
<div class="SectionBody">
@ -34,7 +36,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Längengrad:
Längengrad
</div>
</div>
<div class="SectionBody">
@ -54,7 +56,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Bezug:
Bezug
</div>
</div>
<div class="SectionBody">
@ -64,7 +66,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Einspeisung:
Einspeisung
</div>
</div>
<div class="SectionBody">
@ -74,7 +76,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Erzeugung:
Erzeugung
</div>
</div>
<div class="SectionBody">
@ -94,7 +96,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Bezug:
Bezug
</div>
</div>
<div class="SectionBody">
@ -104,7 +106,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Einspeisung:
Einspeisung
</div>
</div>
<div class="SectionBody">
@ -114,7 +116,7 @@
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Erzeugung:
Erzeugung
</div>
</div>
<div class="SectionBody">

View File

@ -1,4 +1,4 @@
import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {LocationService} from '../location-service';
import {ActivatedRoute} from '@angular/router';
import {Location} from '../Location';
@ -11,6 +11,7 @@ import {SeriesType} from '../../series/SeriesType';
import {Subscription, timer} from 'rxjs';
import {SeriesHistory} from './history/series-history';
import {Interval} from '../../series/Interval';
import {MenuService} from '../../menu-service';
function yesterday(now: any) {
const yesterday = new Date(now.getTime());
@ -31,9 +32,6 @@ function yesterday(now: any) {
})
export class LocationDetail implements OnInit, OnDestroy {
@ViewChild("today")
protected today!: SeriesHistory;
protected readonly Interval = Interval;
protected location: Location | null = null;
@ -50,13 +48,17 @@ export class LocationDetail implements OnInit, OnDestroy {
readonly locationService: LocationService,
readonly seriesService: SeriesService,
readonly activatedRoute: ActivatedRoute,
readonly menuService: MenuService,
) {
//
}
ngOnInit(): void {
this.activatedRoute.params.subscribe(params => {
this.locationService.getById(params['id'], location => this.location = location);
this.locationService.getById(params['id'], location => {
this.location = location;
this.menuService.title = this.location.name;
});
});
this.seriesService.findAll(list => this.series = list);
this.subs.push(this.seriesService.subscribe(this.updateSeries));
@ -67,6 +69,7 @@ export class LocationDetail implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
this.menuService.title = "";
this.subs.forEach(sub => sub.unsubscribe());
}
@ -83,7 +86,6 @@ export class LocationDetail implements OnInit, OnDestroy {
} else {
this.series.push(fresh);
}
this.today.updateSeries(fresh);
};
protected readonly filterEnergy = (): Series[] => {

View File

@ -1,7 +1,8 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {LocationService} from '../location-service';
import {Location} from '../Location';
import {RouterLink} from '@angular/router';
import {MenuService} from '../../menu-service';
@Component({
selector: 'app-location-list',
@ -11,18 +12,24 @@ import {RouterLink} from '@angular/router';
templateUrl: './location-list.html',
styleUrl: './location-list.less',
})
export class LocationList implements OnInit {
export class LocationList implements OnInit, OnDestroy {
protected list: Location[] = [];
constructor(
readonly locationService: LocationService,
readonly menuService: MenuService,
) {
//
}
ngOnInit(): void {
this.locationService.findAll(list => this.list = list);
this.menuService.title = "Orte";
}
ngOnDestroy(): void {
this.menuService.title = "";
}
}

View File

@ -0,0 +1,10 @@
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MenuService {
title: string = "";
}

View File

@ -3,13 +3,11 @@ 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 {

View File

@ -1,48 +1,36 @@
import {or, validateDate, validateEnum, validateNumber, validateString} from "../common";
import {SeriesType} from './SeriesType';
import {formatNumber} from '@angular/common';
import {Value} from './Value';
export class Series {
readonly valueString: string;
readonly value: Value | null = null;
constructor(
readonly id: number,
readonly name: string,
readonly decimals: number,
readonly precision: number,
readonly seconds: number,
readonly value: number | null,
readonly last: Date | null,
readonly unit: string,
readonly type: SeriesType,
value: number | null,
readonly unit: string,
readonly last: Date | null,
) {
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;
this.value = Value.of(this, value, last);
}
static fromJson(json: any): Series {
return new Series(
validateNumber(json.id),
validateString(json.name),
validateNumber(json.decimals),
validateNumber(json.precision),
validateNumber(json.seconds),
or(json.value, validateNumber, null),
or(json.last, validateDate, null),
validateString(json.unit),
validateEnum(json.type, SeriesType),
or(json.value, validateNumber, null),
validateString(json.unit),
or(json.last, validateDate, null),
);
}
isOld(now: Date): boolean {
if (this.last === null) {
return true;
}
const ageSeconds = (now.getTime() - this.last.getTime()) / 1000;
return ageSeconds > this.seconds * 2.1;
}
}

View File

@ -0,0 +1,89 @@
import {formatNumber} from '@angular/common';
import {PointResponse} from '../point/PointResponse';
import {Series} from './Series';
export class Value {
constructor(
readonly value: number,
readonly precision: number,
readonly seconds: number,
readonly unit: string,
readonly date: Date,
) {
//
}
toValueString(zeroToDash: boolean, now_ageCheckToDash: Date | null): string {
if (this.value === null || this.value === undefined) {
return "[???]";
}
if (this.value === 0) {
return zeroToDash ? "-" : `0 ${this.unit}`;
}
if (now_ageCheckToDash !== null) {
const ageSeconds = (now_ageCheckToDash.getTime() - this.date.getTime()) / 1000;
if (ageSeconds > this.seconds * 2.1) {
return `--- ${this.unit}`
}
}
const scale = Math.floor(Math.log10(this.value));
const rest = scale - this.precision + 1;
if (isNaN(rest)) {
console.log(this);
}
if (rest >= 0) {
return `${Math.round(this.value)} ${this.unit}`;
}
return formatNumber(this.value, "de-DE", `0.${-rest}-${-rest}`) + ' ' + this.unit;
}
plus(other: Value | null | undefined): Value | null {
return this.operateSameUnit("plus", other, (a, b) => a + b);
}
minus(other: Value | null | undefined): Value | null {
return this.operateSameUnit("minus", other, (a, b) => a - b);
}
operateSameUnit(operationName: string, other: Value | null | undefined, operation: (a: number, b: number) => number): Value | null {
if (!other) {
return null;
}
if (this.unit !== other.unit) {
throw new Error(`Operation '${operationName} needs units to be the same: this=${this}, other=${other}`);
}
const decimals = Math.max(this.precision, other.precision);
const seconds = Math.max(this.seconds, other.seconds);
const date = this.date.getTime() < other.date.getTime() ? this.date : other.date;
return new Value(operation(this.value, other.value), decimals, seconds, this.unit, date);
}
static of(series: Series, value: number | null | undefined, date: Date | null | undefined): Value | null {
if (value === null || value === undefined) {
return null;
}
if (date === null || date === undefined) {
throw new Error("When 'value' is set, 'last' must be set too, but isn't!")
}
return new Value(value, series.precision, series.seconds, series.unit, date);
}
static ofPoint(response: PointResponse, seriesIndex: number, pointIndex: number, valueIndex: number): Value | null {
const series = response.series[seriesIndex];
if (!series) {
return null;
}
const point = series.points[pointIndex];
if (!point) {
return null;
}
const date = new Date(point[0] * 1000);
const value = point[valueIndex];
return Value.of(series.series, value, date);
}
}

View File

@ -8,12 +8,7 @@
<option [ngValue]="null">-</option>
@for (series of series; track series.id) {
<option [ngValue]="series.id">
{{ series.name }}:
@if (series.isOld(now)) {
--- {{ series.unit }}
} @else {
{{ series.valueString }}
}
{{ series.name }}: {{ series.value?.toValueString(false, now) }}
</option>
}
</select>

View File

@ -0,0 +1,25 @@
@empty: gray;
@purchase: red;
@deliver: magenta;
@produce: #0095ff;
@consume: #ff8800;
.purchase {
color: @purchase;
}
.deliver {
color: @deliver;
}
.produce {
color: @produce;
}
.consume {
color: @consume;
}
.empty {
color: @empty !important;
}

View File

@ -32,10 +32,10 @@ div {
> .SectionHeading {
display: flex;
color: dimgray;
margin-top: -1.25em;
> .SectionHeadingText {
font-weight: bold;
background-color: white;
}
}
@ -45,7 +45,9 @@ div {
overflow: visible;
> .SectionHeading {
color: dimgray;
> .SectionHeadingText {
font-size: 70%;
font-style: italic;
}
}
@ -65,10 +67,10 @@ div {
> .SectionHeading {
display: flex;
color: dimgray;
margin-top: -1.25em;
> .SectionHeadingText {
font-weight: bold;
background-color: white;
}
}
@ -81,14 +83,17 @@ div {
.Section4 {
flex: 1;
text-align: center;
> .SectionHeadingText {
font-weight: bold;
text-align: right;
font-size: 70%;
color: dimgray;
font-style: italic;
background-color: white;
}
> .SectionBody {
text-align: right;
}
}

View File

@ -42,7 +42,7 @@ public class Series {
@Setter
@Column(nullable = false)
private int decimals = 1;
private int precision = 2;
@Column
@Nullable
@ -66,10 +66,10 @@ public class Series {
@Enumerated(EnumType.STRING)
private SeriesType type = SeriesType.VARYING;
public Series(@NonNull final String name, @NonNull final String unit, final int decimals, final int seconds, @NonNull final SeriesType type) {
public Series(@NonNull final String name, @NonNull final String unit, final int precision, final int seconds, @NonNull final SeriesType type) {
this.name = name;
this.unit = unit;
this.decimals = decimals;
this.precision = precision;
this.seconds = seconds;
this.type = type;
}

View File

@ -42,7 +42,7 @@ public class SeriesController {
@NonNull
@PostMapping("{id}/decimals")
public SeriesDto decimals(@PathVariable final long id, @RequestBody final int decimals) {
return seriesService.modify(id, series -> series.setDecimals(decimals));
return seriesService.modify(id, series -> series.setPrecision(decimals));
}
@NonNull

View File

@ -19,7 +19,7 @@ public class SeriesDto implements IWebsocketMessage {
@NonNull
public final String unit;
public final int decimals;
public final int precision;
@Nullable
public final ZonedDateTime first;
@ -40,7 +40,7 @@ public class SeriesDto implements IWebsocketMessage {
this.version = series.getVersion();
this.name = series.getName();
this.unit = series.getUnit();
this.decimals = series.getDecimals();
this.precision = series.getPrecision();
this.first = series.getFirst();
this.last = series.getLast();
this.value = series.getValue();