CENTRALIZED: locationService.location, seriesService.all, dateService.now
This commit is contained in:
parent
6397b74dce
commit
1e676d8e3b
19
src/main/angular/src/app/date.service.ts
Normal file
19
src/main/angular/src/app/date.service.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {timer} from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DateService {
|
||||
|
||||
private _now: Date = new Date();
|
||||
|
||||
get now(): Date {
|
||||
return this._now;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
timer(1000, 1000).subscribe(() => this._now = new Date());
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import {or, validateNumber, validateString} from '../common';
|
||||
import {Series} from '../series/Series';
|
||||
import {Value} from '../series/Value';
|
||||
|
||||
export class Location {
|
||||
|
||||
@ -8,14 +9,43 @@ export class Location {
|
||||
readonly name: string,
|
||||
readonly latitude: number,
|
||||
readonly longitude: number,
|
||||
readonly energyPurchase: Series | null,
|
||||
readonly energyDeliver: Series | null,
|
||||
readonly energyProduce: Series | null,
|
||||
readonly powerPurchase: Series | null,
|
||||
readonly powerDeliver: Series | null,
|
||||
readonly powerProduce: Series | null,
|
||||
private _energyPurchase: Series | null,
|
||||
private _energyDeliver: Series | null,
|
||||
private _energyProduce: Series | null,
|
||||
private _powerPurchase: Series | null,
|
||||
private _powerDeliver: Series | null,
|
||||
private _powerProduce: Series | null,
|
||||
private _powerConsume: Value = Value.NULL,
|
||||
) {
|
||||
//
|
||||
this.updateConsume();
|
||||
}
|
||||
|
||||
readonly updateSeries = (series: Series) => {
|
||||
if (series.equals(this._energyPurchase)) {
|
||||
this._energyPurchase = series;
|
||||
}
|
||||
if (series.equals(this._energyDeliver)) {
|
||||
this._energyDeliver = series;
|
||||
}
|
||||
if (series.equals(this._energyProduce)) {
|
||||
this._energyProduce = series;
|
||||
}
|
||||
if (series.equals(this._powerProduce)) {
|
||||
this._powerProduce = series;
|
||||
this.updateConsume();
|
||||
}
|
||||
if (series.equals(this._powerPurchase)) {
|
||||
this._powerPurchase = series;
|
||||
this.updateConsume();
|
||||
}
|
||||
if (series.equals(this._powerDeliver)) {
|
||||
this._powerDeliver = series;
|
||||
this.updateConsume();
|
||||
}
|
||||
};
|
||||
|
||||
private updateConsume() {
|
||||
this._powerConsume = Value.ZERO.plus(this._powerPurchase?.value, true).plus(this._powerProduce?.value, true).minus(this._powerDeliver?.value, true);
|
||||
}
|
||||
|
||||
static fromJson(json: any): Location {
|
||||
@ -33,4 +63,32 @@ export class Location {
|
||||
);
|
||||
}
|
||||
|
||||
get energyPurchase(): Series | null {
|
||||
return this._energyPurchase;
|
||||
}
|
||||
|
||||
get energyDeliver(): Series | null {
|
||||
return this._energyDeliver;
|
||||
}
|
||||
|
||||
get energyProduce(): Series | null {
|
||||
return this._energyProduce;
|
||||
}
|
||||
|
||||
get powerPurchase(): Series | null {
|
||||
return this._powerPurchase;
|
||||
}
|
||||
|
||||
get powerDeliver(): Series | null {
|
||||
return this._powerDeliver;
|
||||
}
|
||||
|
||||
get powerProduce(): Series | null {
|
||||
return this._powerProduce;
|
||||
}
|
||||
|
||||
get powerConsume(): Value | null {
|
||||
return this._powerConsume;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
@if (location) {
|
||||
|
||||
<app-series-history [now]="now" [location]="location" [interval]="null" heading="Aktuell"></app-series-history>
|
||||
<app-location-power [location]="location"></app-location-power>
|
||||
|
||||
<app-series-history [now]="now" [location]="location" [interval]="Interval.DAY" heading="Heute"></app-series-history>
|
||||
<app-series-history [location]="location" [interval]="Interval.DAY" heading="Heute"></app-series-history>
|
||||
|
||||
<app-series-history [now]="now" [location]="location" [interval]="Interval.DAY" [offset]="offset">
|
||||
<app-series-history [location]="location" [interval]="Interval.DAY" [offset]="offset">
|
||||
<ng-content #SeriesHistoryHeading>
|
||||
<div style="display: flex; width: 100%">
|
||||
|
||||
@ -31,17 +31,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-text [initial]="location.name" (onChange)="locationService.name(location, $event, updateLocation)"></app-text>
|
||||
<app-text [initial]="location.name" (onChange)="locationService.name(location, $event)"></app-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Section2">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Breitegrad
|
||||
Breitengrad
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-number [initial]="location.latitude" (onChange)="locationService.latitude(location, $event, updateLocation)" unit="°"></app-number>
|
||||
<app-number [initial]="location.latitude" (onChange)="locationService.latitude(location, $event)" unit="°"></app-number>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Section2">
|
||||
@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-number [initial]="location.longitude" (onChange)="locationService.longitude(location, $event, updateLocation)" unit="°"></app-number>
|
||||
<app-number [initial]="location.longitude" (onChange)="locationService.longitude(location, $event)" unit="°"></app-number>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -71,7 +71,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [now]="now" [initial]="location.energyPurchase" (onChange)="locationService.energyPurchase(location, $event, updateLocation)" [series]="filterEnergy()"></app-series-select>
|
||||
<app-series-select [initial]="location.energyPurchase" (onChange)="locationService.energyPurchase(location, $event)" [filter]="filterEnergy"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Section2">
|
||||
@ -81,7 +81,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [now]="now" [initial]="location.energyDeliver" (onChange)="locationService.energyDeliver(location, $event, updateLocation)" [series]="filterEnergy()"></app-series-select>
|
||||
<app-series-select [initial]="location.energyDeliver" (onChange)="locationService.energyDeliver(location, $event)" [filter]="filterEnergy"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Section2">
|
||||
@ -91,7 +91,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [now]="now" [initial]="location.energyProduce" (onChange)="locationService.energyProduce(location, $event, updateLocation)" [series]="filterEnergy()"></app-series-select>
|
||||
<app-series-select [initial]="location.energyProduce" (onChange)="locationService.energyProduce(location, $event)" [filter]="filterEnergy"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -111,7 +111,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [now]="now" [initial]="location.powerPurchase" (onChange)="locationService.powerPurchase(location, $event, updateLocation)" [series]="filterPower()"></app-series-select>
|
||||
<app-series-select [initial]="location.powerPurchase" (onChange)="locationService.powerPurchase(location, $event)" [filter]="filterPower"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Section2">
|
||||
@ -121,7 +121,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [now]="now" [initial]="location.powerDeliver" (onChange)="locationService.powerDeliver(location, $event, updateLocation)" [series]="filterPower()"></app-series-select>
|
||||
<app-series-select [initial]="location.powerDeliver" (onChange)="locationService.powerDeliver(location, $event)" [filter]="filterPower"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Section2">
|
||||
@ -131,7 +131,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [now]="now" [initial]="location.powerProduce" (onChange)="locationService.powerProduce(location, $event, updateLocation)" [series]="filterPower()"></app-series-select>
|
||||
<app-series-select [initial]="location.powerProduce" (onChange)="locationService.powerProduce(location, $event)" [filter]="filterPower"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,21 +5,15 @@ import {Location} from '../Location';
|
||||
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 {SeriesType} from '../../series/SeriesType';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
import {LocationElectricity} from '../electricity/location-electricity';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {LocationEnergy} from '../electricity/location-energy';
|
||||
import {Interval} from '../../series/Interval';
|
||||
import {MenuService} from '../../menu-service';
|
||||
import {DatePipe} from '@angular/common';
|
||||
import {WebsocketService} from '../../common';
|
||||
|
||||
function yesterday(now: any) {
|
||||
const yesterday = new Date(now.getTime());
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return yesterday;
|
||||
}
|
||||
import {Series} from '../../series/Series';
|
||||
import {SeriesType} from '../../series/SeriesType';
|
||||
import {DateService} from '../../date.service';
|
||||
import {LocationPower} from '../power/location-power';
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-detail',
|
||||
@ -27,13 +21,18 @@ function yesterday(now: any) {
|
||||
Text,
|
||||
Number,
|
||||
SeriesSelect,
|
||||
LocationElectricity
|
||||
LocationEnergy,
|
||||
LocationPower
|
||||
],
|
||||
templateUrl: './location-detail.html',
|
||||
styleUrl: './location-detail.less',
|
||||
})
|
||||
export class LocationDetail implements OnInit, OnDestroy {
|
||||
|
||||
protected readonly filterEnergy = (series: Series) => series.type === SeriesType.DELTA && series.unit === 'kWh';
|
||||
|
||||
protected readonly filterPower = (series: Series) => series.type === SeriesType.VARYING && series.unit === 'W';
|
||||
|
||||
protected readonly Interval = Interval;
|
||||
|
||||
protected readonly Math = Math;
|
||||
@ -42,85 +41,46 @@ export class LocationDetail implements OnInit, OnDestroy {
|
||||
|
||||
private readonly subs: Subscription [] = [];
|
||||
|
||||
private series: Series[] = [];
|
||||
|
||||
protected now: Date = new Date();
|
||||
|
||||
protected yesterday: Date = yesterday(this.now);
|
||||
|
||||
protected offset: number = 1;
|
||||
|
||||
private readonly datePipe: DatePipe;
|
||||
|
||||
constructor(
|
||||
readonly locationService: LocationService,
|
||||
readonly seriesService: SeriesService,
|
||||
readonly activatedRoute: ActivatedRoute,
|
||||
readonly menuService: MenuService,
|
||||
readonly websocketService: WebsocketService,
|
||||
readonly dateService: DateService,
|
||||
@Inject(LOCALE_ID) readonly locale: string,
|
||||
) {
|
||||
this.datePipe = new DatePipe(locale);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(this.websocketService.onChange((connected) => {
|
||||
if (connected) {
|
||||
this.seriesService.findAll(list => this.series = list);
|
||||
} else {
|
||||
this.location = null;
|
||||
this.locationService.id = null;
|
||||
this.subs.push(this.activatedRoute.params.subscribe(params => this.locationService.id = params['id'] || null));
|
||||
this.subs.push(this.locationService.location$.subscribe(this.onLocationChange));
|
||||
}
|
||||
}));
|
||||
this.subs.push(this.activatedRoute.params.subscribe(params => {
|
||||
this.locationService.getById(params['id'], location => {
|
||||
|
||||
private readonly onLocationChange = (location: Location | null): void => {
|
||||
this.location = location;
|
||||
if (this.location) {
|
||||
this.menuService.title = this.location.name;
|
||||
});
|
||||
}));
|
||||
this.subs.push(this.seriesService.subscribe(this.updateSeries));
|
||||
this.subs.push(timer(1000, 1000).subscribe(() => {
|
||||
this.now = new Date();
|
||||
this.yesterday = yesterday(this.now);
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.location = null;
|
||||
this.menuService.title = "";
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs.length = 0;
|
||||
}
|
||||
|
||||
protected readonly updateLocation = (location: Location): void => {
|
||||
if (this.location?.id === location.id) {
|
||||
this.location = location;
|
||||
}
|
||||
};
|
||||
|
||||
protected readonly updateSeries = (fresh: Series): void => {
|
||||
const index = this.series.findIndex(series => series.id === fresh.id);
|
||||
if (index >= 0) {
|
||||
this.series.splice(index, 1, fresh);
|
||||
} else {
|
||||
this.series.push(fresh);
|
||||
}
|
||||
};
|
||||
|
||||
protected readonly filterEnergy = (): Series[] => {
|
||||
return this.series.filter(series => series.type === SeriesType.DELTA && series.unit === 'kWh');
|
||||
};
|
||||
|
||||
protected readonly filterPower = (): Series[] => {
|
||||
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);
|
||||
const d = new Date(this.dateService.now);
|
||||
d.setDate(d.getDate() - this.offset);
|
||||
return this.datePipe.transform(d, 'EEEE') || '';
|
||||
}
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Location} from '../Location';
|
||||
import {Series} from '../../series/Series';
|
||||
import {Next} from '../../common';
|
||||
import {Interval} from '../../series/Interval';
|
||||
import {PointService} from '../../point/point-service';
|
||||
import {SeriesService} from '../../series/series-service';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Value} from '../../series/Value';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-history',
|
||||
imports: [],
|
||||
templateUrl: './location-electricity.html',
|
||||
styleUrl: './location-electricity.less',
|
||||
})
|
||||
export class LocationElectricity implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
protected readonly Interval = Interval;
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
protected purchase: Value = Value.NONE;
|
||||
|
||||
protected deliver: Value = Value.NONE;
|
||||
|
||||
protected produce: Value = Value.NONE;
|
||||
|
||||
protected consume: Value = Value.NONE;
|
||||
|
||||
@Input()
|
||||
heading: string = "";
|
||||
|
||||
private _o_: number = 0;
|
||||
|
||||
@Input()
|
||||
set offset(value: number) {
|
||||
this._o_ = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
get offset(): number {
|
||||
return this._o_;
|
||||
}
|
||||
|
||||
@Input()
|
||||
interval: Interval | null = null;
|
||||
|
||||
@Input()
|
||||
location!: Location;
|
||||
|
||||
@Input()
|
||||
now: Date = new Date();
|
||||
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
readonly serieService: SeriesService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(this.serieService.subscribe(this.update));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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(fresh: Series | null | undefined, series: Series | null | undefined, next: Next<Value>): void {
|
||||
const callNextAndUpdateConsume = (value: Value) => {
|
||||
next(value);
|
||||
this.consume = this.purchase.plus(this.produce).minus(this.deliver);
|
||||
};
|
||||
if (fresh !== null && fresh !== undefined) {
|
||||
if (fresh.id !== series?.id) {
|
||||
return;
|
||||
}
|
||||
series = fresh;
|
||||
}
|
||||
if (!series) {
|
||||
callNextAndUpdateConsume(Value.NONE);
|
||||
return
|
||||
}
|
||||
if (this.interval) {
|
||||
this.pointService.relative([series], this.interval, this.offset, 1, response => callNextAndUpdateConsume(Value.ofPoint(response, 0, 0, 1)));
|
||||
} else {
|
||||
callNextAndUpdateConsume(series.value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -12,7 +12,7 @@
|
||||
Bezug
|
||||
</div>
|
||||
<div class="SectionBody purchase">
|
||||
{{ purchase.toValueString(true, interval ? null : now) }}
|
||||
{{ purchase.toValueString(interval ? null : now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
Solar
|
||||
</div>
|
||||
<div class="SectionBody produce">
|
||||
{{ produce.toValueString(true, interval ? null : now) }}
|
||||
{{ produce.toValueString(interval ? null : now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
Verbrauch
|
||||
</div>
|
||||
<div class="SectionBody consume">
|
||||
{{ consume.toValueString(true, interval ? null : now) }}
|
||||
{{ consume.toValueString(interval ? null : now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
Einspeisung
|
||||
</div>
|
||||
<div class="SectionBody deliver">
|
||||
{{ deliver.toValueString(true, interval ? null : now) }}
|
||||
{{ deliver.toValueString(interval ? null : now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
107
src/main/angular/src/app/location/electricity/location-energy.ts
Normal file
107
src/main/angular/src/app/location/electricity/location-energy.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Location} from '../Location';
|
||||
import {Series} from '../../series/Series';
|
||||
import {Next} from '../../common';
|
||||
import {Interval} from '../../series/Interval';
|
||||
import {PointService} from '../../point/point-service';
|
||||
import {SeriesService} from '../../series/series-service';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Value} from '../../series/Value';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-history',
|
||||
imports: [],
|
||||
templateUrl: './location-energy.html',
|
||||
styleUrl: './location-energy.less',
|
||||
})
|
||||
export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
protected readonly Interval = Interval;
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
protected purchase: Value = Value.NULL;
|
||||
|
||||
protected deliver: Value = Value.NULL;
|
||||
|
||||
protected produce: Value = Value.NULL;
|
||||
|
||||
protected consume: Value = Value.NULL;
|
||||
|
||||
@Input()
|
||||
heading!: string;
|
||||
|
||||
private _o_: number = 0;
|
||||
|
||||
@Input()
|
||||
set offset(value: number) {
|
||||
this._o_ = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
get offset(): number {
|
||||
return this._o_;
|
||||
}
|
||||
|
||||
@Input()
|
||||
interval!: Interval;
|
||||
|
||||
@Input()
|
||||
location!: Location;
|
||||
|
||||
@Input()
|
||||
now: Date = new Date();
|
||||
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
readonly serieService: SeriesService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(this.serieService.subscribe(this.update));
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.fetch(null, this.location?.energyPurchase, history => this.purchase = history);
|
||||
this.fetch(null, this.location?.energyDeliver, history => this.deliver = history);
|
||||
this.fetch(null, this.location?.energyProduce, history => this.produce = history);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
protected readonly update = (fresh: Series): void => {
|
||||
if (this.offset > 0) {
|
||||
return;
|
||||
}
|
||||
this.fetch(fresh, this.location?.energyPurchase, value => this.purchase = value);
|
||||
this.fetch(fresh, this.location?.energyDeliver, value => this.deliver = value);
|
||||
this.fetch(fresh, this.location?.energyProduce, value => this.produce = value);
|
||||
};
|
||||
|
||||
private fetch(fresh: Series | null | undefined, series: Series | null | undefined, next: Next<Value>): void {
|
||||
const callNextAndUpdateConsume = (value: Value) => {
|
||||
next(value);
|
||||
this.consume = this.purchase.plus(this.produce, true).minus(this.deliver, true);
|
||||
};
|
||||
if (fresh !== null && fresh !== undefined) {
|
||||
if (fresh.id !== series?.id) {
|
||||
return;
|
||||
}
|
||||
series = fresh;
|
||||
}
|
||||
if (!series) {
|
||||
callNextAndUpdateConsume(Value.NULL);
|
||||
return
|
||||
}
|
||||
if (this.interval) {
|
||||
this.pointService.relative([series], this.interval, this.offset, 1, response => callNextAndUpdateConsume(Value.ofPoint(response, 0, 0, 1)));
|
||||
} else {
|
||||
callNextAndUpdateConsume(series.value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import {AfterViewInit, Component, Input} from '@angular/core';
|
||||
import {Location} from '../../Location';
|
||||
import {Interval} from '../../../series/Interval';
|
||||
import {PointService} from '../../../point/point-service';
|
||||
import {Location} from '../Location';
|
||||
import {Interval} from '../../series/Interval';
|
||||
import {PointService} from '../../point/point-service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-history-graph',
|
||||
@ -1,14 +1,44 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ApiService, CrudService, Next, WebsocketService} from '../common';
|
||||
import {Location} from './Location'
|
||||
import {SeriesService} from '../series/series-service';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LocationService extends CrudService<Location> {
|
||||
|
||||
constructor(api: ApiService, ws: WebsocketService) {
|
||||
private readonly _location = new BehaviorSubject<Location | null>(null);
|
||||
|
||||
private _id: number | null = null;
|
||||
|
||||
constructor(
|
||||
api: ApiService,
|
||||
ws: WebsocketService,
|
||||
readonly seriesService: SeriesService,
|
||||
) {
|
||||
super(api, ws, ['Location'], Location.fromJson);
|
||||
this.seriesService.subscribe(series => this._location.value?.updateSeries(series));
|
||||
this.ws.onConnect(this.fetch);
|
||||
this.ws.onDisconnect(() => this._location.next(null));
|
||||
}
|
||||
|
||||
set id(id: number | null) {
|
||||
this._id = id;
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
private readonly fetch = () => {
|
||||
if (this._id === null) {
|
||||
this._location.next(null);
|
||||
} else {
|
||||
this.getById(this._id, location => this._location.next(location));
|
||||
}
|
||||
};
|
||||
|
||||
get location$(): Observable<Location | null> {
|
||||
return this._location.asObservable();
|
||||
}
|
||||
|
||||
findAll(next: Next<Location[]>) {
|
||||
|
||||
47
src/main/angular/src/app/location/power/location-power.html
Normal file
47
src/main/angular/src/app/location/power/location-power.html
Normal file
@ -0,0 +1,47 @@
|
||||
<div class="Section3">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Aktuelle Leistung
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
|
||||
<div class="Section4">
|
||||
<div class="SectionHeadingText">
|
||||
Bezug
|
||||
</div>
|
||||
<div class="SectionBody purchase">
|
||||
{{ location.powerPurchase?.value?.toValueString(dateService.now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
<div class="SectionHeadingText">
|
||||
Solar
|
||||
</div>
|
||||
<div class="SectionBody produce">
|
||||
{{ location.powerProduce?.value?.toValueString(dateService.now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
<div class="SectionHeadingText">
|
||||
Verbrauch
|
||||
</div>
|
||||
<div class="SectionBody consume">
|
||||
{{ location.powerConsume?.toValueString(dateService.now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
<div class="SectionHeadingText">
|
||||
Einspeisung
|
||||
</div>
|
||||
<div class="SectionBody deliver">
|
||||
{{ location.powerDeliver?.value?.toValueString(dateService.now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../colors";
|
||||
22
src/main/angular/src/app/location/power/location-power.ts
Normal file
22
src/main/angular/src/app/location/power/location-power.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {Location} from '../Location';
|
||||
import {DateService} from '../../date.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-power',
|
||||
imports: [],
|
||||
templateUrl: './location-power.html',
|
||||
styleUrl: './location-power.less',
|
||||
})
|
||||
export class LocationPower {
|
||||
|
||||
@Input()
|
||||
location!: Location;
|
||||
|
||||
constructor(
|
||||
readonly dateService: DateService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
@ -33,4 +33,8 @@ export class Series {
|
||||
);
|
||||
}
|
||||
|
||||
equals(other: Series | null | undefined): boolean {
|
||||
return this.id === other?.id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,10 +4,12 @@ import {Series} from './Series';
|
||||
|
||||
export class Value {
|
||||
|
||||
static readonly NONE: Value = new Value(null, 0, 0, "", new Date());
|
||||
static readonly NULL: Value = new Value(NaN, 0, 0, "", new Date());
|
||||
|
||||
private constructor(
|
||||
readonly value: number | null,
|
||||
static readonly ZERO: Value = new Value(0, 0, Infinity, "", new Date());
|
||||
|
||||
protected constructor(
|
||||
readonly value: number,
|
||||
readonly precision: number,
|
||||
readonly seconds: number,
|
||||
readonly unit: string,
|
||||
@ -16,57 +18,43 @@ export class Value {
|
||||
//
|
||||
}
|
||||
|
||||
toValueString(zeroToDash: boolean, now_ageCheckToDash: Date | null): string {
|
||||
if (this.value === null) {
|
||||
toValueString(now: Date | null): string {
|
||||
if (isNaN(this.value)) {
|
||||
return "-";
|
||||
}
|
||||
if (this.value === 0) {
|
||||
return zeroToDash ? "-" : `0 ${this.unit}`;
|
||||
return `0 ${this.unit}`;
|
||||
}
|
||||
if (now_ageCheckToDash !== null) {
|
||||
const ageSeconds = (now_ageCheckToDash.getTime() - this.date.getTime()) / 1000;
|
||||
if (ageSeconds > this.seconds * 2.1) {
|
||||
if (now !== null && this.isOld(now)) {
|
||||
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): Value {
|
||||
return this.operateSameUnit("plus", other, (a, b) => a + b);
|
||||
plus(other: Value | null | undefined, nullToZero: boolean): Value {
|
||||
if (!nullToZero && (other === null || other === undefined)) {
|
||||
return Value.NULL;
|
||||
}
|
||||
return new BiValue(this, other || Value.ZERO, (a, b) => a + b);
|
||||
}
|
||||
|
||||
minus(other: Value): Value {
|
||||
return this.operateSameUnit("minus", other, (a, b) => a - b);
|
||||
minus(other: Value | null | undefined, nullToZero: boolean): Value {
|
||||
if (!nullToZero && (other === null || other === undefined)) {
|
||||
return Value.NULL;
|
||||
}
|
||||
|
||||
operateSameUnit(name: string, other: Value, operation: (a: number, b: number) => number): Value {
|
||||
if (this.value === null || other.value === null) {
|
||||
return Value.NONE;
|
||||
}
|
||||
if (this.unit !== other.unit) {
|
||||
throw new Error(`Operation '${name} 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);
|
||||
return new BiValue(this, other || Value.ZERO, (a, b) => a - b);
|
||||
}
|
||||
|
||||
static of(series: Series, value: number | null | undefined, date: Date | null | undefined): Value {
|
||||
value = value === undefined ? null : value;
|
||||
date = date === undefined ? null : date;
|
||||
if (value === null) {
|
||||
return this.NONE;
|
||||
return this.NULL;
|
||||
}
|
||||
if (date === null) {
|
||||
throw new Error("When 'value' is set, 'last' must be set too, but isn't!")
|
||||
@ -77,12 +65,12 @@ export class Value {
|
||||
static ofPoint(response: PointResponse, seriesIndex: number, pointIndex: number, valueIndex: number): Value {
|
||||
const series = response.series[seriesIndex];
|
||||
if (!series) {
|
||||
return this.NONE;
|
||||
return this.NULL;
|
||||
}
|
||||
|
||||
const point = series.points[pointIndex];
|
||||
if (!point) {
|
||||
return this.NONE;
|
||||
return this.NULL;
|
||||
}
|
||||
|
||||
const date = new Date(point[0] * 1000);
|
||||
@ -90,4 +78,31 @@ export class Value {
|
||||
return Value.of(series.series, value, date);
|
||||
}
|
||||
|
||||
isOld(now: Date) {
|
||||
const ageSeconds = (now.getTime() - this.date.getTime()) / 1000;
|
||||
return ageSeconds > this.seconds * 2.1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class BiValue extends Value {
|
||||
|
||||
constructor(
|
||||
readonly a: Value,
|
||||
readonly b: Value,
|
||||
readonly operation: (a: number, b: number) => number,
|
||||
) {
|
||||
if (a.unit !== "" && b.unit !== "" && a.unit !== b.unit) {
|
||||
throw new Error(`Operation needs units to be equal or empty: this=${a}, other=${b}`);
|
||||
}
|
||||
const precision = Math.max(a.precision, b.precision);
|
||||
const unit = a.unit || b.unit;
|
||||
const date = a.date.getTime() < b.date.getTime() ? a.date : b.date;
|
||||
super(operation(a.value, b.value), precision, 0, unit, date);
|
||||
}
|
||||
|
||||
override isOld(now: Date): boolean {
|
||||
return this.a.isOld(now) || this.b.isOld(now);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<option [ngValue]="null">-</option>
|
||||
@for (series of series; track series.id) {
|
||||
<option [ngValue]="series.id">
|
||||
{{ series.name }}: {{ series.value.toValueString(false, now) }}
|
||||
{{ series.name }}: {{ series.value.toValueString(dateService.now) }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
|
||||
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {NgClass} from '@angular/common';
|
||||
import {faPen} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Series} from '../Series';
|
||||
import {or} from '../../common';
|
||||
import {SeriesService} from '../series-service';
|
||||
import {map, Subscription} from 'rxjs';
|
||||
import {DateService} from '../../date.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-select',
|
||||
@ -16,21 +19,21 @@ import {or} from '../../common';
|
||||
templateUrl: './series-select.html',
|
||||
styleUrl: './series-select.less',
|
||||
})
|
||||
export class SeriesSelect {
|
||||
export class SeriesSelect implements OnInit, OnDestroy {
|
||||
|
||||
protected readonly faPen = faPen;
|
||||
|
||||
private _initial: Series | null = null;
|
||||
|
||||
@Input()
|
||||
now!: Date;
|
||||
|
||||
@Input()
|
||||
series!: Series[];
|
||||
|
||||
@Input()
|
||||
allowEmpty: boolean = true;
|
||||
|
||||
@Input()
|
||||
filter: (series: Series) => boolean = () => true;
|
||||
|
||||
@Output()
|
||||
readonly onChange = new EventEmitter<number | null>();
|
||||
|
||||
@ -40,24 +43,43 @@ export class SeriesSelect {
|
||||
|
||||
protected readonly Series = Series;
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
readonly seriesService: SeriesService,
|
||||
readonly dateService: DateService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(
|
||||
this.seriesService
|
||||
.all$
|
||||
.pipe(map(series => series.filter(this.filter)))
|
||||
.subscribe(list => this.series = list)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs.length = 0;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set initial(value: Series | null) {
|
||||
this._initial = value;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.model = or(this.initial, i => i.id, null);
|
||||
}
|
||||
|
||||
get initial(): Series | null {
|
||||
return this._initial;
|
||||
}
|
||||
private readonly reset = (): void => {
|
||||
this.model = or(this._initial, i => i.id, null);
|
||||
};
|
||||
|
||||
protected classes(): {} {
|
||||
return {
|
||||
"unchanged": this.model === this.initial,
|
||||
"changed": this.model !== or(this.initial, i => i.id, null),
|
||||
"unchanged": this.model === this._initial,
|
||||
"changed": this.model !== or(this._initial, i => i.id, null),
|
||||
"invalid": !this.allowEmpty && this.model === null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
||||
import {ApiService, CrudService, Next, WebsocketService} from '../common';
|
||||
import {ApiService, CrudService, WebsocketService} from '../common';
|
||||
import {Series} from './Series';
|
||||
import {DatePipe} from '@angular/common';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SeriesService extends CrudService<Series> {
|
||||
|
||||
private readonly datePipe: DatePipe;
|
||||
private readonly allSubject: BehaviorSubject<Series[]> = new BehaviorSubject<Series[]>([]);
|
||||
|
||||
constructor(
|
||||
api: ApiService,
|
||||
@ -16,11 +16,21 @@ export class SeriesService extends CrudService<Series> {
|
||||
@Inject(LOCALE_ID) readonly locale: string,
|
||||
) {
|
||||
super(api, ws, ['Series'], Series.fromJson);
|
||||
this.datePipe = new DatePipe(locale);
|
||||
this.subscribe(fresh => {
|
||||
const index = this.allSubject.value.findIndex(series => series.id === fresh.id);
|
||||
if (index >= 0) {
|
||||
const list = [...this.allSubject.value];
|
||||
list.splice(index, 1, fresh);
|
||||
this.allSubject.next(list);
|
||||
} else {
|
||||
this.allSubject.next([...this.allSubject.value, fresh]);
|
||||
}
|
||||
});
|
||||
this.getList(['findAll'], list => this.allSubject.next(list));
|
||||
}
|
||||
|
||||
findAll(next: Next<Series[]>) {
|
||||
this.getList(['findAll'], next);
|
||||
get all$(): Observable<Series[]> {
|
||||
return this.allSubject.asObservable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user