dark theme + percent-bar + Value.locale

This commit is contained in:
Patrick Haßel 2025-02-27 14:52:59 +01:00
parent f5cbf9cf43
commit f495ad9af1
14 changed files with 219 additions and 57 deletions

View File

@ -0,0 +1,27 @@
/* bright theme */
@foreground: gray;
@background: white;
@consumption: orange;
@purchase: orangered;
@production: dodgerblue;
@self: forestgreen;
@delivery: magenta;
@consumptionBack: @consumption;
@purchaseBack: @purchase;
@productionBack: @production;
@selfBack: @self;
@deliveryBack: @delivery;
/* dark theme */
@foreground: gray;
@background: #11171b;
@consumptionBack: #856938;
@purchaseBack: #71361d;
@productionBack: #2d4255;
@selfBack: #2b4e2b;
@deliveryBack: #753475;

View File

@ -6,7 +6,7 @@
<div class="content"> <div class="content">
<div class="entry consumption" [class.zero]="powerConsumption?.zero"> <div class="entry consumption" [class.zero]="powerConsumption?.zero">
<div class="name">Verbrauch</div> <div class="name">Bedarf</div>
<div class="percent">&nbsp;</div> <div class="percent">&nbsp;</div>
<div class="value">{{ powerConsumption?.formatted }}</div> <div class="value">{{ powerConsumption?.formatted }}</div>
</div> </div>
@ -15,13 +15,13 @@
<div class="percent">{{ powerPurchasePercent?.formatted }}</div> <div class="percent">{{ powerPurchasePercent?.formatted }}</div>
<div class="value">{{ powerPurchase?.formatted }}</div> <div class="value">{{ powerPurchase?.formatted }}</div>
</div> </div>
<div class="entry production" [class.zero]="powerProduced?.zero"> <div class="entry production" [class.zero]="powerProduction?.zero">
<div class="name">Produktion</div> <div class="name">Produktion</div>
<div class="percent">{{ powerProducedPercent?.formatted }}</div> <div class="percent">{{ powerProducedPercent?.formatted }}</div>
<div class="value">{{ powerProduced?.formatted }}</div> <div class="value">{{ powerProduction?.formatted }}</div>
</div> </div>
<div class="entry self" [class.zero]="powerSelf?.zero"> <div class="entry self" [class.zero]="powerSelf?.zero">
<div class="name">Eigenverbrauch</div> <div class="name">Eigenbedarf</div>
<div class="percent">{{ powerSelfPercent?.formatted }}</div> <div class="percent">{{ powerSelfPercent?.formatted }}</div>
<div class="value">{{ powerSelf?.formatted }}</div> <div class="value">{{ powerSelf?.formatted }}</div>
</div> </div>
@ -32,14 +32,14 @@
</div> </div>
</div> </div>
<app-percent-bar [produktion]="powerProduction" [self]="powerSelf" [purchase]="powerPurchase" [delivery]="powerDelivery"></app-percent-bar>
</div> </div>
<div class="section"> <div class="section">
<div class="title"> <div class="title">
Energie Energie
<span class="live" *ngIf="interval">Live</span>
<span class="archive" *ngIf="!interval">Archiv</span>
</div> </div>
<div class="option"> <div class="option">
@ -56,7 +56,7 @@
<div class="content"> <div class="content">
<div class="entry consumption" [class.zero]="aggregations.energyConsumed?.zero"> <div class="entry consumption" [class.zero]="aggregations.energyConsumed?.zero">
<div class="name">Verbrauch</div> <div class="name">Bedarf</div>
<div class="percent">&nbsp;</div> <div class="percent">&nbsp;</div>
<div class="value">{{ aggregations.energyConsumed?.formatted }}</div> <div class="value">{{ aggregations.energyConsumed?.formatted }}</div>
</div> </div>
@ -71,17 +71,19 @@
<div class="value">{{ aggregations.energyProduced?.delta?.formatted }}</div> <div class="value">{{ aggregations.energyProduced?.delta?.formatted }}</div>
</div> </div>
<div class="entry self" [class.zero]="aggregations.energySelf?.zero"> <div class="entry self" [class.zero]="aggregations.energySelf?.zero">
<div class="name">Eigenverbrauch</div> <div class="name">Eigenbedarf</div>
<div class="percent">{{ aggregations.energySelfPercent?.formatted }}</div> <div class="percent">{{ aggregations.energySelfPercent?.formatted }}</div>
<div class="value">{{ aggregations.energySelf?.formatted }}</div> <div class="value">{{ aggregations.energySelf?.formatted }}</div>
</div> </div>
<div class="entry delivery" [class.zero]="aggregations.energyDelivered?.delta?.zero"> <div class="entry delivery" [class.zero]="aggregations.energyDelivered?.delta?.zero">
<div class="name">Eingespeist</div> <div class="name">Einspeisung</div>
<div class="percent">{{ aggregations.energyDeliveredPercent?.formatted }}</div> <div class="percent">{{ aggregations.energyDeliveredPercent?.formatted }}</div>
<div class="value">{{ aggregations.energyDelivered?.delta?.formatted }}</div> <div class="value">{{ aggregations.energyDelivered?.delta?.formatted }}</div>
</div> </div>
</div> </div>
<app-percent-bar [produktion]="aggregations.energyProduced?.delta" [self]="aggregations.energySelf" [purchase]="aggregations.energyPurchased?.delta" [delivery]="aggregations.energyDelivered?.delta"></app-percent-bar>
</div> </div>
<router-outlet/> <router-outlet/>

View File

@ -1,5 +1,7 @@
@import "../../config.less";
.section { .section {
margin-bottom: 1em; margin-bottom: 2em;
.back { .back {
float: left; float: left;
@ -12,7 +14,6 @@
.title { .title {
clear: left; clear: left;
font-weight: bold; font-weight: bold;
font-size: 120%;
text-align: center; text-align: center;
} }
@ -28,7 +29,6 @@
.name { .name {
float: left; float: left;
font-weight: bold;
} }
.value { .value {
@ -39,7 +39,7 @@
float: right; float: right;
padding-top: 0.5em; padding-top: 0.5em;
font-size: 60%; font-size: 60%;
width: 3.5em; width: 4.5em;
text-align: right; text-align: right;
} }
@ -62,25 +62,25 @@
} }
.consumption { .consumption {
color: orange; color: @consumption;
} }
.purchase { .purchase {
color: orangered; color: @purchase;
} }
.production { .production {
color: dodgerblue; color: @production;
} }
.self { .self {
color: forestgreen; color: @self;
} }
.delivery { .delivery {
color: magenta; color: @delivery;
} }
.zero { .zero {
filter: opacity(30%); filter: opacity(20%);
} }

View File

@ -5,11 +5,11 @@ import {AggregationWrapperDto} from './series/AggregationWrapperDto';
import {SeriesService} from './series/series.service'; import {SeriesService} from './series/series.service';
import {Subscription} from 'rxjs'; import {Subscription} from 'rxjs';
import {Value} from './value/Value'; import {Value} from './value/Value';
import {NgIf} from '@angular/common'; import {PercentBarComponent} from './percent-bar/percent-bar.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, NgIf], imports: [RouterOutlet, PercentBarComponent],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.less' styleUrl: './app.component.less'
}) })
@ -25,7 +25,7 @@ export class AppComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = []; private readonly subs: Subscription[] = [];
get powerProduced(): Value | undefined { get powerProduction(): Value | undefined {
return this.seriesService.powerProduced.series?.lastValue; return this.seriesService.powerProduced.series?.lastValue;
} }
@ -42,19 +42,19 @@ export class AppComponent implements OnInit, OnDestroy {
} }
get powerSelf(): Value | undefined { get powerSelf(): Value | undefined {
return this.powerProduced?.minus(this.powerDelivery); return this.powerProduction?.minus(this.powerDelivery);
} }
get powerConsumption(): Value | undefined { get powerConsumption(): Value | undefined {
return this.powerBalance?.plus(this.powerProduced); return this.powerPurchase?.plus(this.powerProduction);
} }
get powerProducedPercent(): Value | undefined { get powerProducedPercent(): Value | undefined {
return this.powerProduced?.percent(this.powerConsumption); return this.powerProduction?.percent(this.powerConsumption);
} }
get powerSelfPercent(): Value | undefined { get powerSelfPercent(): Value | undefined {
return this.powerSelf?.percent(this.powerProduced); return this.powerSelf?.percent(this.powerProduction);
} }
get powerPurchasePercent(): Value | undefined { get powerPurchasePercent(): Value | undefined {
@ -62,7 +62,7 @@ export class AppComponent implements OnInit, OnDestroy {
} }
get powerDeliveryPercent(): Value | undefined { get powerDeliveryPercent(): Value | undefined {
return this.powerDelivery?.percent(this.powerProduced); return this.powerDelivery?.percent(this.powerProduction);
} }
constructor( constructor(

View File

@ -0,0 +1,20 @@
<div class="bar">
<div class="part purchase" *ngIf="barPurchasePercent" [style.width]="barPurchasePercent.value + '%'">
<div class="text">
{{ _purchase?.formatted }}<br>
{{ purchasePercent?.formatted }}
</div>
</div>
<div class="part self" *ngIf="barSelfPercent" [style.width]="barSelfPercent.value + '%'">
<div class="text">
{{ _self?.formatted }}<br>
{{ selfPercent?.formatted }}
</div>
</div>
<div class="part delivery" *ngIf="barDeliveryPercent" [style.width]="barDeliveryPercent.value + '%'">
<div class="text">
{{ _delivery?.formatted }}<br>
{{ deliveryPercent?.formatted }}
</div>
</div>
</div>

View File

@ -0,0 +1,30 @@
@import "../../../config.less";
.bar {
position: relative;
color: white;
.part {
float: left;
white-space: nowrap;
font-size: 40%;
.text {
padding-left: 0.25em;
}
border-radius: 0.5em;
}
.self {
background-color: @selfBack;
}
.purchase {
background-color: @purchaseBack;
}
.delivery {
background-color: @deliveryBack;
}
}

View File

@ -0,0 +1,78 @@
import {Component, Input} from '@angular/core';
import {Value} from '../value/Value';
import {NgIf} from '@angular/common';
@Component({
selector: 'app-percent-bar',
imports: [
NgIf
],
templateUrl: './percent-bar.component.html',
styleUrl: './percent-bar.component.less'
})
export class PercentBarComponent {
protected _produktion: Value | undefined;
@Input()
set produktion(produktion: Value | undefined) {
this._produktion = produktion;
this.update();
}
protected _self: Value | undefined;
@Input()
set self(self: Value | undefined) {
this._self = self;
this.update();
}
protected _purchase: Value | undefined;
@Input()
set purchase(purchase: Value | undefined) {
this._purchase = purchase;
this.update();
}
protected _delivery: Value | undefined;
@Input()
set delivery(delivery: Value | undefined) {
this._delivery = delivery;
this.update();
}
@Input()
percent: boolean = false;
protected consumption: Value | undefined;
protected barSum: Value | undefined;
protected selfPercent: Value | undefined;
protected purchasePercent: Value | undefined;
protected deliveryPercent: Value | undefined;
protected barSelfPercent: Value | undefined;
protected barPurchasePercent: Value | undefined;
protected barDeliveryPercent: Value | undefined;
private update() {
this.consumption = this._self?.plus(this._purchase);
this.selfPercent = this._self?.percent(this.consumption);
this.purchasePercent = this._purchase?.percent(this.consumption);
this.deliveryPercent = this._delivery?.percent(this._produktion);
this.barSum = this.consumption?.plus(this._delivery);
this.barSelfPercent = this._self?.percent(this.barSum);
this.barPurchasePercent = this._purchase?.percent(this.barSum);
this.barDeliveryPercent = this._delivery?.percent(this.barSum);
}
}

View File

@ -48,11 +48,11 @@ export class AggregationWrapperDto {
this.energySelf = this.energyProduced?.delta.minus(this.energyDelivered?.delta); this.energySelf = this.energyProduced?.delta.minus(this.energyDelivered?.delta);
} }
static fromJson(json: any): AggregationWrapperDto { static fromJson(json: any, locale: string): AggregationWrapperDto {
return new AggregationWrapperDto( return new AggregationWrapperDto(
json['alignment'] as Alignment, json['alignment'] as Alignment,
new Date(json['date']), new Date(json['date']),
(json['aggregations'] as any[]).map(a => a.hasOwnProperty('delta') ? MeterAggregate.fromJson(a) : VaryingAggregate.fromJson(a)), (json['aggregations'] as any[]).map(a => a.hasOwnProperty('delta') ? MeterAggregate.fromJson(a, locale) : VaryingAggregate.fromJson(a, locale)),
); );
} }

View File

@ -15,7 +15,7 @@ export class Series {
// //
} }
static fromJson(json: any): Series { static fromJson(json: any, locale: string): Series {
const decimals = validateNumber(json['decimals']); const decimals = validateNumber(json['decimals']);
const unit = Unit.fromJson(json['unit']); const unit = Unit.fromJson(json['unit']);
return new Series( return new Series(
@ -24,7 +24,7 @@ export class Series {
validateString(json['title']), validateString(json['title']),
decimals, decimals,
unit, unit,
Value.fromJson(json['lastValue'], json['unit'], unit, decimals), Value.fromJson(json['lastValue'], unit, decimals, locale),
); );
} }

View File

@ -11,11 +11,11 @@ export class MeterAggregate extends Aggregate {
super(series); super(series);
} }
static fromJson(json: any): MeterAggregate { static fromJson(json: any, locale: string): MeterAggregate {
const series = Series.fromJson(json['series']); const series = Series.fromJson(json['series'], locale);
return new MeterAggregate( return new MeterAggregate(
series, series,
Value.fromJson(json['delta'], series, series.unit, series.decimals), Value.fromJson(json['delta'], series.unit, series.decimals, locale),
); );
} }

View File

@ -1,4 +1,4 @@
import {Injectable} from '@angular/core'; import {Inject, Injectable, LOCALE_ID} from '@angular/core';
import {ApiService} from '../core/api.service'; import {ApiService} from '../core/api.service';
import {Alignment} from './Alignment'; import {Alignment} from './Alignment';
import {AggregationWrapperDto} from './AggregationWrapperDto'; import {AggregationWrapperDto} from './AggregationWrapperDto';
@ -34,6 +34,7 @@ export class SeriesService {
] ]
constructor( constructor(
@Inject(LOCALE_ID) readonly locale: string,
protected readonly api: ApiService, protected readonly api: ApiService,
) { ) {
// //
@ -57,7 +58,7 @@ export class SeriesService {
if (this.subs.length !== 0) { if (this.subs.length !== 0) {
return; return;
} }
this.subs.push(this.api.subscribe(['Series'], Series.fromJson, series => this.update(series))); this.subs.push(this.api.subscribe(['Series'], j => Series.fromJson(j, this.locale), series => this.update(series)));
this.subs.push(this.api.subscribeConnection(connected => { this.subs.push(this.api.subscribeConnection(connected => {
if (connected) { if (connected) {
this.all(); this.all();
@ -97,7 +98,7 @@ export class SeriesService {
} }
all(next?: Next<Series[]>) { all(next?: Next<Series[]>) {
this.api.getList(['Series', 'all'], Series.fromJson, list => { this.api.getList(['Series', 'all'], j => Series.fromJson(j, this.locale), list => {
list.forEach(item => this.update(item)); list.forEach(item => this.update(item));
if (next) { if (next) {
next(list); next(list);
@ -106,7 +107,7 @@ export class SeriesService {
} }
aggregations(alignment: Alignment, offset: number, next: Next<AggregationWrapperDto>) { aggregations(alignment: Alignment, offset: number, next: Next<AggregationWrapperDto>) {
this.api.getSingle(['Series', 'agg', 'all', alignment.name, 'offset', offset], AggregationWrapperDto.fromJson, next); this.api.getSingle(['Series', 'agg', 'all', alignment.name, 'offset', offset], j => AggregationWrapperDto.fromJson(j, this.locale), next);
} }
} }

View File

@ -12,9 +12,9 @@ export class VaryingAggregate extends Aggregate {
super(series); super(series);
} }
static fromJson(json: any): VaryingAggregate { static fromJson(json: any, locale: string): VaryingAggregate {
return new VaryingAggregate( return new VaryingAggregate(
Series.fromJson(json['series']), Series.fromJson(json['series'], locale),
json['min'] as number, json['min'] as number,
json['avg'] as number, json['avg'] as number,
json['max'] as number, json['max'] as number,

View File

@ -1,25 +1,18 @@
import {Unit} from "./Unit"; import {Unit} from "./Unit";
import {validateNumber} from "../core/validators"; import {validateNumber} from "../core/validators";
import {Series} from "../series/Series";
export class Value { export class Value {
static readonly EMPTY: Value = new Value(0, Unit._UNKNOWN_, 1);
constructor( constructor(
readonly value: number, readonly value: number,
readonly unit: Unit, readonly unit: Unit,
readonly decimals: number, readonly decimals: number,
) { readonly locale: string) {
// //
} }
static fromJson(value: any, series: Series, unit: Unit, decimals: number): Value { static fromJson(value: any, unit: Unit, decimals: number, locale: string): Value {
return new Value( return new Value(validateNumber(value), unit, decimals, locale);
validateNumber(value),
unit,
decimals,
);
} }
get zero(): boolean { get zero(): boolean {
@ -27,25 +20,25 @@ export class Value {
} }
get formatted(): string { get formatted(): string {
return `${this.value.toFixed(this.decimals)} ${this.unit.unit}`; return `${this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals})} ${this.unit.unit}`;
} }
negate() { negate() {
return new Value(-this.value, this.unit, this.decimals); return new Value(-this.value, this.unit, this.decimals, this.locale);
} }
plus(other: Value | undefined): Value | undefined { plus(other: Value | undefined): Value | undefined {
if (!other) { if (!other) {
return undefined; return undefined;
} }
return new Value(this.value + other.value, this.unit, this.decimals); return new Value(this.value + other.value, this.unit, this.decimals, this.locale);
} }
minus(other: Value | undefined): Value | undefined { minus(other: Value | undefined): Value | undefined {
if (!other) { if (!other) {
return undefined; return undefined;
} }
return new Value(this.value - other.value, this.unit, this.decimals); return new Value(this.value - other.value, this.unit, this.decimals, this.locale);
} }
notNegative(): Value { notNegative(): Value {
@ -59,14 +52,14 @@ export class Value {
if (this.value === 0) { if (this.value === 0) {
return this; return this;
} }
return new Value(0, this.unit, this.decimals); return new Value(0, this.unit, this.decimals, this.locale);
} }
percent(other: Value | undefined): Value | undefined { percent(other: Value | undefined): Value | undefined {
if (!other || other.value === 0) { if (!other || other.value === 0) {
return undefined; return undefined;
} }
return new Value(this.value / other.value * 100, Unit.PERCENT, 0); return new Value(this.value / other.value * 100, Unit.PERCENT, 0, this.locale);
} }
} }

View File

@ -1,11 +1,22 @@
@import "../config.less";
body { body {
font-family: sans-serif; font-family: sans-serif;
font-size: 6vw; font-size: 6vw;
user-select: none; user-select: none;
background-color: @background;
color: @foreground;
margin: 0;
} }
button, input, select { button {
all: unset;
font-size: inherit; font-size: inherit;
padding: 0 0.25em;
background-color: #002433;
color: #008fca;
border: unset;
border-radius: 10%;
} }
div { div {