Value.NONE not nullable + offset change

This commit is contained in:
Patrick Haßel 2025-10-30 16:33:44 +01:00
parent 49a87bb154
commit 0dff76f598
11 changed files with 112 additions and 45 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!--suppress HtmlDeprecatedAttribute -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<circle fill="#4a97b8" cx="256" cy="256" r="256"/>
<path fill="#57b153" d="M412,280l-60-64l-60,127.252L172,280L52,400v10.62C98.752,472.204,172.72,512,256,512c141.384,0,256-114.616,256-256c0-44.752-11.512-86.8-31.696-123.404L412,280z"/>
<g stroke="black" stroke-width="10px">
<path d="M52,408c-2.048,0-4.092-0.78-5.656-2.344c-3.124-3.124-3.124-8.188,0-11.312l120-120c2.48-2.48,6.28-3.064,9.388-1.42l112.6,59.36l56.432-119.688c1.124-2.4,3.368-4.08,5.984-4.492c2.624-0.424,5.268,0.496,7.084,2.428l51.892,55.356L480.74,112.64c1.86-4.012,6.612-5.748,10.62-3.896c4.008,1.856,5.752,6.612,3.896,10.62l-76,164c-1.116,2.416-3.36,4.112-5.984,4.54c-2.616,0.408-5.288-0.492-7.104-2.424l-51.948-55.424L299.24,346.66c-0.924,1.976-2.628,3.484-4.7,4.176c-2.064,0.696-4.332,0.508-6.264-0.508l-114.78-60.504L57.664,405.652C56.096,407.22,54.048,408,52,408z"/>
</g>
<g fill="#F5F5F5">
<path d="M172,264c8.84,0,16,7.16,16,16s-7.16,16-16,16s-16-7.16-16-16S163.16,264,172,264 M172,256c-13.236,0-24,10.764-24,24s10.764,24,24,24s24-10.764,24-24S185.236,256,172,256L172,256z"/>
<path d="M46.964,388c8.84,0,16,7.16,16,16s-7.16,16-16,16s-16-7.16-16-16S38.124,388,46.964,388 M46.964,380c-13.236,0-24,10.764-24,24s10.764,24,24,24s24-10.764,24-24S60.2,380,46.964,380L46.964,380z"/>
<path d="M486.96,100c8.844,0,16,7.16,16,16s-7.156,16-16,16c-8.836,0-16-7.16-16-16S478.124,100,486.96,100M486.96,92c-13.236,0-24,10.764-24,24s10.764,24,24,24c13.24,0,24-10.764,24-24S500.2,92,486.96,92L486.96,92z"/>
<path d="M352,200c8.84,0,16,7.16,16,16s-7.16,16-16,16s-16-7.16-16-16S343.16,200,352,200 M352,192c-13.236,0-24,10.764-24,24s10.764,24,24,24s24-10.764,24-24S365.236,192,352,192L352,192z"/>
</g>
<g fill="black">
<circle cx="172" cy="280" r="16"/>
<circle cx="46.964" cy="404" r="16"/>
<circle cx="486.96" cy="116" r="16"/>
<circle cx="352" cy="216" r="16"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -2,6 +2,7 @@
<div class="SectionHeading"> <div class="SectionHeading">
<div class="SectionHeadingText"> <div class="SectionHeadingText">
{{ heading }} {{ heading }}
<ng-content #SeriesHistoryHeading></ng-content>
</div> </div>
</div> </div>
<div class="SectionBody"> <div class="SectionBody">
@ -11,7 +12,7 @@
Bezug Bezug
</div> </div>
<div class="SectionBody purchase"> <div class="SectionBody purchase">
{{ purchase?.toValueString(true, interval ? null : now) }} {{ purchase.toValueString(true, interval ? null : now) }}
</div> </div>
</div> </div>
@ -20,7 +21,7 @@
Solar Solar
</div> </div>
<div class="SectionBody produce"> <div class="SectionBody produce">
{{ produce?.toValueString(true, interval ? null : now) }} {{ produce.toValueString(true, interval ? null : now) }}
</div> </div>
</div> </div>
@ -29,7 +30,7 @@
Verbrauch Verbrauch
</div> </div>
<div class="SectionBody consume"> <div class="SectionBody consume">
{{ consume?.toValueString(true, interval ? null : now) }} {{ consume.toValueString(true, interval ? null : now) }}
</div> </div>
</div> </div>
@ -38,7 +39,7 @@
Einspeisung Einspeisung
</div> </div>
<div class="SectionBody deliver"> <div class="SectionBody deliver">
{{ deliver?.toValueString(true, interval ? null : now) }} {{ deliver.toValueString(true, interval ? null : now) }}
</div> </div>
</div> </div>

View File

@ -20,19 +20,28 @@ export class SeriesHistory implements OnInit, AfterViewInit, OnDestroy {
private readonly subs: Subscription[] = []; private readonly subs: Subscription[] = [];
protected purchase: Value | null = null; protected purchase: Value = Value.NONE;
protected deliver: Value | null = null; protected deliver: Value = Value.NONE;
protected produce: Value | null = null; protected produce: Value = Value.NONE;
protected consume: Value | null = null; protected consume: Value = Value.NONE;
@Input() @Input()
heading!: string; heading: string = "";
private _o_: number = 0;
@Input() @Input()
offset: number = 0; set offset(value: number) {
this._o_ = value;
this.ngAfterViewInit();
}
get offset(): number {
return this._o_;
}
@Input() @Input()
interval: Interval | null = null; interval: Interval | null = null;
@ -85,11 +94,11 @@ export class SeriesHistory implements OnInit, AfterViewInit, OnDestroy {
} }
}; };
private history(fresh: Series | null | undefined, series: Series | null | undefined, next: Next<Value | null>) { private history(fresh: Series | null | undefined, series: Series | null | undefined, next: Next<Value>): void {
const n = (value: Value | null) => { const callNextAndUpdateConsume = (value: Value) => {
next(value); next(value);
this.consume = this.purchase?.plus(this.produce)?.minus(this.deliver) || null; this.consume = this.purchase.plus(this.produce).minus(this.deliver);
} };
if (fresh !== null && fresh !== undefined) { if (fresh !== null && fresh !== undefined) {
if (fresh.id !== series?.id) { if (fresh.id !== series?.id) {
return; return;
@ -97,18 +106,14 @@ export class SeriesHistory implements OnInit, AfterViewInit, OnDestroy {
series = fresh; series = fresh;
} }
if (!series) { if (!series) {
n(null); callNextAndUpdateConsume(Value.NONE);
return return
} }
if (this.interval) { if (this.interval) {
this.pointService.relative([series], this.interval, this.offset, 1, response => n(Value.ofPoint(response, 0, 0, 1))); this.pointService.relative([series], this.interval, this.offset, 1, response => callNextAndUpdateConsume(Value.ofPoint(response, 0, 0, 1)));
} else { } else {
n(series.value); callNextAndUpdateConsume(series.value);
} }
} }
protected nullOrZero(value: Value | null | undefined): boolean {
return value === null || value === undefined || value.value === 0;
}
} }

View File

@ -4,7 +4,18 @@
<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" heading="Heute"></app-series-history>
<app-series-history [now]="now" [location]="location" [interval]="Interval.DAY" [offset]="1" heading="Gestern"></app-series-history> <app-series-history [now]="now" [location]="location" [interval]="Interval.DAY" [offset]="offset">
<ng-content #SeriesHistoryHeading>
<div style="display: flex; width: 100%">
&nbsp;
<div (click)="offset += 1">&larr;</div>
&nbsp;
<div (click)="offset = Math.max(1, offset -1)">&rarr;</div>
&nbsp;
<div style="flex: 1">{{ offsetDayTitle() }}</div>
</div>
</ng-content>
</app-series-history>
<div class="Section"> <div class="Section">
<div class="SectionHeading"> <div class="SectionHeading">

View File

@ -1,4 +1,4 @@
import {Component, OnDestroy, OnInit} from '@angular/core'; import {Component, Inject, LOCALE_ID, OnDestroy, OnInit} 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';
@ -12,6 +12,7 @@ import {Subscription, timer} from 'rxjs';
import {SeriesHistory} from './history/series-history'; import {SeriesHistory} from './history/series-history';
import {Interval} from '../../series/Interval'; import {Interval} from '../../series/Interval';
import {MenuService} from '../../menu-service'; import {MenuService} from '../../menu-service';
import {DatePipe} from '@angular/common';
function yesterday(now: any) { function yesterday(now: any) {
const yesterday = new Date(now.getTime()); const yesterday = new Date(now.getTime());
@ -34,6 +35,8 @@ export class LocationDetail implements OnInit, OnDestroy {
protected readonly Interval = Interval; protected readonly Interval = Interval;
protected readonly Math = Math;
protected location: Location | null = null; protected location: Location | null = null;
private readonly subs: Subscription [] = []; private readonly subs: Subscription [] = [];
@ -44,13 +47,18 @@ export class LocationDetail implements OnInit, OnDestroy {
protected yesterday: Date = yesterday(this.now); protected yesterday: Date = yesterday(this.now);
protected offset: number = 1;
private readonly datePipe: DatePipe;
constructor( constructor(
readonly locationService: LocationService, readonly locationService: LocationService,
readonly seriesService: SeriesService, readonly seriesService: SeriesService,
readonly activatedRoute: ActivatedRoute, readonly activatedRoute: ActivatedRoute,
readonly menuService: MenuService, readonly menuService: MenuService,
@Inject(LOCALE_ID) readonly locale: string,
) { ) {
// this.datePipe = new DatePipe(locale);
} }
ngOnInit(): void { ngOnInit(): void {
@ -96,4 +104,19 @@ export class LocationDetail implements OnInit, OnDestroy {
return this.series.filter(series => series.type === SeriesType.VARYING && series.unit === 'W'); return this.series.filter(series => series.type === SeriesType.VARYING && series.unit === 'W');
}; };
protected offsetDayTitle(): string {
if (this.offset === 1) {
return 'Gestern';
} else if (this.offset === 2) {
return 'Vorgestern';
} else {
if (this.offset < 7) {
const d = new Date(this.now);
d.setDate(d.getDate() - this.offset);
return this.datePipe.transform(d, 'EEEE') || '';
}
return `Vor ${this.offset} Tagen`;
}
}
} }

View File

@ -5,7 +5,7 @@ import {Value} from './Value';
export class Series { export class Series {
readonly value: Value | null = null; readonly value: Value;
constructor( constructor(
readonly id: number, readonly id: number,

View File

@ -4,8 +4,10 @@ import {Series} from './Series';
export class Value { export class Value {
constructor( static readonly NONE: Value = new Value(null, 0, 0, "", new Date());
readonly value: number,
private constructor(
readonly value: number | null,
readonly precision: number, readonly precision: number,
readonly seconds: number, readonly seconds: number,
readonly unit: string, readonly unit: string,
@ -15,8 +17,8 @@ export class Value {
} }
toValueString(zeroToDash: boolean, now_ageCheckToDash: Date | null): string { toValueString(zeroToDash: boolean, now_ageCheckToDash: Date | null): string {
if (this.value === null || this.value === undefined) { if (this.value === null) {
return "[???]"; return "-";
} }
if (this.value === 0) { if (this.value === 0) {
return zeroToDash ? "-" : `0 ${this.unit}`; return zeroToDash ? "-" : `0 ${this.unit}`;
@ -39,20 +41,20 @@ export class Value {
return formatNumber(this.value, "de-DE", `0.${-rest}-${-rest}`) + ' ' + this.unit; return formatNumber(this.value, "de-DE", `0.${-rest}-${-rest}`) + ' ' + this.unit;
} }
plus(other: Value | null | undefined): Value | null { plus(other: Value): Value {
return this.operateSameUnit("plus", other, (a, b) => a + b); return this.operateSameUnit("plus", other, (a, b) => a + b);
} }
minus(other: Value | null | undefined): Value | null { minus(other: Value): Value {
return this.operateSameUnit("minus", other, (a, b) => a - b); return this.operateSameUnit("minus", other, (a, b) => a - b);
} }
operateSameUnit(operationName: string, other: Value | null | undefined, operation: (a: number, b: number) => number): Value | null { operateSameUnit(name: string, other: Value, operation: (a: number, b: number) => number): Value {
if (!other) { if (this.value === null || other.value === null) {
return null; return Value.NONE;
} }
if (this.unit !== other.unit) { if (this.unit !== other.unit) {
throw new Error(`Operation '${operationName} needs units to be the same: this=${this}, other=${other}`); throw new Error(`Operation '${name} needs units to be the same: this=${this}, other=${other}`);
} }
const decimals = Math.max(this.precision, other.precision); const decimals = Math.max(this.precision, other.precision);
const seconds = Math.max(this.seconds, other.seconds); const seconds = Math.max(this.seconds, other.seconds);
@ -60,25 +62,27 @@ export class Value {
return new Value(operation(this.value, other.value), decimals, seconds, this.unit, 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 { static of(series: Series, value: number | null | undefined, date: Date | null | undefined): Value {
if (value === null || value === undefined) { value = value === undefined ? null : value;
return null; date = date === undefined ? null : date;
if (value === null) {
return this.NONE;
} }
if (date === null || date === undefined) { if (date === null) {
throw new Error("When 'value' is set, 'last' must be set too, but isn't!") 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); return new Value(value, series.precision, series.seconds, series.unit, date);
} }
static ofPoint(response: PointResponse, seriesIndex: number, pointIndex: number, valueIndex: number): Value | null { static ofPoint(response: PointResponse, seriesIndex: number, pointIndex: number, valueIndex: number): Value {
const series = response.series[seriesIndex]; const series = response.series[seriesIndex];
if (!series) { if (!series) {
return null; return this.NONE;
} }
const point = series.points[pointIndex]; const point = series.points[pointIndex];
if (!point) { if (!point) {
return null; return this.NONE;
} }
const date = new Date(point[0] * 1000); const date = new Date(point[0] * 1000);

View File

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

View File

@ -2,11 +2,11 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Angular</title> <title>Data2025</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!--suppress HtmlUnknownTarget --> <!--suppress HtmlUnknownTarget -->
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/svg" href="favicon.svg">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@ -4,6 +4,7 @@ body {
margin: 0; margin: 0;
font-family: sans-serif; font-family: sans-serif;
font-size: 4vw; font-size: 4vw;
user-select: none;
} }
div { div {
@ -46,6 +47,7 @@ div {
> .SectionHeading { > .SectionHeading {
color: dimgray; color: dimgray;
> .SectionHeadingText { > .SectionHeadingText {
font-size: 70%; font-size: 70%;
font-style: italic; font-style: italic;