Compare commits
10 Commits
26baa904dd
...
04be7692c1
| Author | SHA1 | Date | |
|---|---|---|---|
| 04be7692c1 | |||
| d962dcefa9 | |||
| cd7b755880 | |||
| eebb917a6d | |||
| 5bc68ca0c3 | |||
| 1e676d8e3b | |||
| 6397b74dce | |||
| 39a4b94ecc | |||
| 0dff76f598 | |||
| 49a87bb154 |
@ -9,3 +9,5 @@ spring.jpa.hibernate.ddl-auto=update
|
||||
spring.jackson.serialization.indent_output=true
|
||||
#-
|
||||
de.ph87.data.mqtt.uri=tcp://10.0.0.50:1883
|
||||
#
|
||||
server.port=8084
|
||||
19
deploy-backend.sh
Normal file
19
deploy-backend.sh
Normal file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
if ! ( git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ] ); then
|
||||
echo "git has changes! Aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! mvn clean install; then
|
||||
echo "mvn clean install failed! Aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="DEPLOY---BACKEND---$(date +'%Y-%m-%d---%H-%M-%S')"
|
||||
ssh root@10.255.0.1 -p 2222 "systemctl stop Data2025.service"
|
||||
rsync -e 'ssh -p2222' target/Data2025.jar root@10.255.0.1:/srv/Data2025/ --progress
|
||||
ssh root@10.255.0.1 -p 2222 "systemctl start Data2025.service"
|
||||
git tag "$TAG"
|
||||
18
deploy-frontend.sh
Normal file
18
deploy-frontend.sh
Normal file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "$0")/src/main/angular/" || exit 1
|
||||
|
||||
if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
|
||||
if ng build; then
|
||||
TAG="DEPLOY---FRONTEND---$(date +'%Y-%m-%d---%H-%M-%S')"
|
||||
git rev-parse HEAD > dist/angular/browser/git.hash
|
||||
echo "$TAG" > dist/angular/browser/git.tag
|
||||
rsync --archive --delete -e 'ssh -p2222' dist/angular/browser/ root@10.255.0.1:/srv/Data2025/www/ --progress
|
||||
git tag "$TAG"
|
||||
else
|
||||
echo "ng build failed! Aborting..."
|
||||
fi
|
||||
else
|
||||
echo "git has changes! Aborting..."
|
||||
exit 1
|
||||
fi
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
21
src/main/angular/public/favicon.svg
Normal file
21
src/main/angular/public/favicon.svg
Normal 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 |
@ -6,12 +6,15 @@
|
||||
<div class="MainMenuItem MainMenuTitle">
|
||||
{{ menuService.title }}
|
||||
</div>
|
||||
@if (!ws.connected) {
|
||||
<div class="MainMenuItem MainMenuNotConnected">NICHT VERBUNDEN</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="MainMenuDrawer NoUserSelect" [hidden]="!showDrawer">
|
||||
@for (location of locationList; track location.id) {
|
||||
<div class="MainMenuItem" routerLink="Location/{{ location.id }}" routerLinkActive="MainMenuItemActive" (click)="showDrawer = false">{{ location.name }}</div>
|
||||
<div class="MainMenuItem" (click)="navigate(`Location/${location.id}`); showDrawer = false" routerLinkActive="MainMenuItemActive">{{ location.name }}</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@ -21,6 +21,14 @@
|
||||
background-color: lightskyblue;
|
||||
}
|
||||
|
||||
.MainMenuTitle {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.MainMenuNotConnected {
|
||||
color: red;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -5,6 +5,5 @@ import {LocationDetail} from './location/detail/location-detail';
|
||||
export const routes: Routes = [
|
||||
{path: 'Location/:id', component: LocationDetail},
|
||||
{path: 'Location', component: LocationList},
|
||||
{path: '**', redirectTo: 'Location'},
|
||||
{path: '**', redirectTo: 'Location'},
|
||||
{path: '**', redirectTo: '/Location'},
|
||||
];
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||
import {Router, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||
import {faBars} from '@fortawesome/free-solid-svg-icons';
|
||||
import {MenuService} from './menu-service';
|
||||
import {Location} from './location/Location';
|
||||
import {LocationService} from './location/location-service';
|
||||
import {WebsocketService} from './common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, FaIconComponent, RouterLink, RouterLinkActive],
|
||||
imports: [RouterOutlet, FaIconComponent, RouterLinkActive],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.less'
|
||||
})
|
||||
@ -23,6 +24,8 @@ export class App implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
readonly locationService: LocationService,
|
||||
readonly menuService: MenuService,
|
||||
readonly router: Router,
|
||||
readonly ws: WebsocketService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
@ -36,4 +39,10 @@ export class App implements OnInit, OnDestroy {
|
||||
this.menuService.title = "";
|
||||
}
|
||||
|
||||
navigate(url: string): void {
|
||||
this.router.navigateByUrl('/', {skipLocationChange: true}).then(() => {
|
||||
this.router.navigate([url]);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -31,7 +31,8 @@ export function validateString(value: any): string {
|
||||
}
|
||||
|
||||
export function validateDate(value: any): Date {
|
||||
return new Date(Date.parse(validateString(value)));
|
||||
const parsed = Date.parse(validateString(value));
|
||||
return new Date(parsed);
|
||||
}
|
||||
|
||||
export function validateList<T>(value: any, fromJson: FromJson<T>): T[] {
|
||||
@ -51,7 +52,7 @@ export function or<T, R, E>(t: T | null | undefined, map: (t: T) => R, orElse: E
|
||||
}
|
||||
|
||||
export function url(protocol: string, path: any[]): string {
|
||||
return `${protocol}${location.protocol === 'https://' ? 's' : ''}://localhost:8080/${path.join('/')}`;
|
||||
return `${protocol}${location.protocol === 'https://' ? 's' : ''}://${location.hostname}:8084/${path.join('/')}`;
|
||||
}
|
||||
|
||||
export function stompServiceFactory() {
|
||||
@ -71,24 +72,28 @@ export function stompServiceFactory() {
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class WebsocketService {
|
||||
|
||||
private _connected: boolean = false;
|
||||
|
||||
get connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly stompService: StompService,
|
||||
) {
|
||||
this.websocketConnected(() => this._websocketError = false);
|
||||
this.websocketDisconnected(() => this._websocketError = true);
|
||||
this.onChange(connected => this._connected = connected);
|
||||
}
|
||||
|
||||
private _websocketError: boolean = false;
|
||||
|
||||
get websocketError(): boolean {
|
||||
return this._websocketError;
|
||||
onChange(next: Next<boolean>): Subscription {
|
||||
return this.stompService.connectionState$.pipe(map(state => state === RxStompState.OPEN)).subscribe(next);
|
||||
}
|
||||
|
||||
websocketConnected(next: Next<void>): Subscription {
|
||||
onConnect(next: Next<void>): Subscription {
|
||||
return this.stompService.connectionState$.pipe(filter(state => state === RxStompState.OPEN)).subscribe(_ => next());
|
||||
}
|
||||
|
||||
websocketDisconnected(next: Next<void>): Subscription {
|
||||
onDisconnect(next: Next<void>): Subscription {
|
||||
return this.stompService.connectionState$.pipe(filter(state => state !== RxStompState.OPEN)).subscribe(_ => next());
|
||||
}
|
||||
|
||||
|
||||
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,9 +0,0 @@
|
||||
<div class="segments">
|
||||
@for (segment of segments; track segment) {
|
||||
<div class="segment">
|
||||
<!-- @for (graph of graphs; track graph) {-->
|
||||
<!-- <div class="" [style.background-color]="graph[0]" [style.height.%]="graph[1][segment] / total(segment) * 100"></div>-->
|
||||
<!-- }-->
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -1,9 +0,0 @@
|
||||
.segments {
|
||||
display: flex;
|
||||
height: 4em;
|
||||
|
||||
.segment {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import {AfterViewInit, Component, Input} from '@angular/core';
|
||||
import {Location} from '../../../Location';
|
||||
import {Interval} from '../../../../series/Interval';
|
||||
import {PointService} from '../../../../point/point-service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-history-graph',
|
||||
imports: [],
|
||||
templateUrl: './simple-plot.component.html',
|
||||
styleUrl: './simple-plot.component.less',
|
||||
})
|
||||
export class SeriesHistoryGraph implements AfterViewInit {
|
||||
|
||||
protected segments = Array.from(Array(288).keys());
|
||||
|
||||
protected totals: number[] = [];
|
||||
|
||||
protected historyEnergyPurchase: number[] | null = null;
|
||||
|
||||
protected historyEnergyDeliver: number[] | null = null;
|
||||
|
||||
protected historyEnergyProduce: number[] | null = null;
|
||||
|
||||
protected readonly Interval = Interval;
|
||||
|
||||
@Input()
|
||||
heading!: string;
|
||||
|
||||
@Input()
|
||||
date!: Date;
|
||||
|
||||
@Input()
|
||||
interval!: Interval;
|
||||
|
||||
@Input()
|
||||
location!: Location;
|
||||
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
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<number[] | null>) {
|
||||
// if (!series || !this.interval) {
|
||||
// next(null);
|
||||
// return
|
||||
// }
|
||||
// this.pointService.points(series, this.date, this.interval, next);
|
||||
// }
|
||||
|
||||
}
|
||||
@ -1,114 +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: './series-history.html',
|
||||
styleUrl: './series-history.less',
|
||||
})
|
||||
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;
|
||||
|
||||
@Input()
|
||||
offset: number = 0;
|
||||
|
||||
@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 | 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
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,10 +1,21 @@
|
||||
@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-location-energy [location]="location" [interval]="Interval.DAY" heading="Heute"></app-location-energy>
|
||||
|
||||
<app-series-history [now]="now" [location]="location" [interval]="Interval.DAY" [offset]="1" heading="Gestern"></app-series-history>
|
||||
<app-location-energy [location]="location" [interval]="Interval.DAY" [offset]="offset">
|
||||
<ng-content #SeriesHistoryHeading>
|
||||
<div style="display: flex; width: 100%">
|
||||
|
||||
<div (click)="offset += 1">←</div>
|
||||
|
||||
<div (click)="offset = Math.max(1, offset -1)">→</div>
|
||||
|
||||
<div style="flex: 1">{{ offsetDayTitle() }}</div>
|
||||
</div>
|
||||
</ng-content>
|
||||
</app-location-energy>
|
||||
|
||||
<div class="Section">
|
||||
<div class="SectionHeading">
|
||||
@ -20,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">
|
||||
@ -40,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>
|
||||
@ -60,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">
|
||||
@ -70,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">
|
||||
@ -80,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>
|
||||
@ -100,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">
|
||||
@ -110,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">
|
||||
@ -120,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>
|
||||
|
||||
@ -1,23 +1,19 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Component, Inject, LOCALE_ID, OnDestroy, OnInit} from '@angular/core';
|
||||
import {LocationService} from '../location-service';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
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 {SeriesHistory} from './history/series-history';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {LocationEnergy} from '../energy/location-energy';
|
||||
import {Interval} from '../../series/Interval';
|
||||
import {MenuService} from '../../menu-service';
|
||||
|
||||
function yesterday(now: any) {
|
||||
const yesterday = new Date(now.getTime());
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return yesterday;
|
||||
}
|
||||
import {DatePipe} from '@angular/common';
|
||||
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',
|
||||
@ -25,75 +21,71 @@ function yesterday(now: any) {
|
||||
Text,
|
||||
Number,
|
||||
SeriesSelect,
|
||||
SeriesHistory
|
||||
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;
|
||||
|
||||
protected location: Location | null = null;
|
||||
|
||||
private readonly subs: Subscription [] = [];
|
||||
|
||||
private series: Series[] = [];
|
||||
protected offset: number = 1;
|
||||
|
||||
protected now: Date = new Date();
|
||||
|
||||
protected yesterday: Date = yesterday(this.now);
|
||||
private readonly datePipe: DatePipe;
|
||||
|
||||
constructor(
|
||||
readonly locationService: LocationService,
|
||||
readonly seriesService: SeriesService,
|
||||
readonly activatedRoute: ActivatedRoute,
|
||||
readonly menuService: MenuService,
|
||||
readonly dateService: DateService,
|
||||
@Inject(LOCALE_ID) readonly locale: string,
|
||||
) {
|
||||
//
|
||||
this.datePipe = new DatePipe(locale);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.params.subscribe(params => {
|
||||
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));
|
||||
this.subs.push(timer(1000, 1000).subscribe(() => {
|
||||
this.now = new Date();
|
||||
this.yesterday = yesterday(this.now);
|
||||
}));
|
||||
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));
|
||||
}
|
||||
|
||||
private readonly onLocationChange = (location: Location | null): void => {
|
||||
this.location = location;
|
||||
if (this.location) {
|
||||
this.menuService.title = this.location.name;
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
protected offsetDayTitle(): string {
|
||||
if (this.offset === 1) {
|
||||
return 'Gestern';
|
||||
} else {
|
||||
this.series.push(fresh);
|
||||
if (this.offset < 7) {
|
||||
const d = new Date(this.dateService.now);
|
||||
d.setDate(d.getDate() - this.offset);
|
||||
return this.datePipe.transform(d, 'EEEE') || '';
|
||||
}
|
||||
return `Vor ${this.offset} Tagen`;
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
{{ heading }}
|
||||
<ng-content #SeriesHistoryHeading></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
@ -10,8 +11,8 @@
|
||||
<div class="SectionHeadingText">
|
||||
Bezug
|
||||
</div>
|
||||
<div class="SectionBody purchase">
|
||||
{{ purchase?.toValueString(true, interval ? null : now) }}
|
||||
<div class="SectionBody COLOR_FONT_PURCHASE">
|
||||
{{ purchase.toValueString(null) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -19,8 +20,8 @@
|
||||
<div class="SectionHeadingText">
|
||||
Solar
|
||||
</div>
|
||||
<div class="SectionBody produce">
|
||||
{{ produce?.toValueString(true, interval ? null : now) }}
|
||||
<div class="SectionBody COLOR_FONT_PRODUCE">
|
||||
{{ produce.toValueString(null) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -28,8 +29,8 @@
|
||||
<div class="SectionHeadingText">
|
||||
Verbrauch
|
||||
</div>
|
||||
<div class="SectionBody consume">
|
||||
{{ consume?.toValueString(true, interval ? null : now) }}
|
||||
<div class="SectionBody COLOR_FONT_CONSUME">
|
||||
{{ consume.toValueString(null) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -37,15 +38,13 @@
|
||||
<div class="SectionHeadingText">
|
||||
Einspeisung
|
||||
</div>
|
||||
<div class="SectionBody deliver">
|
||||
{{ deliver?.toValueString(true, interval ? null : now) }}
|
||||
<div class="SectionBody COLOR_FONT_DELIVER">
|
||||
{{ deliver.toValueString(null) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@if (interval) {
|
||||
<!-- <app-series-history-graph></app-series-history-graph>-->
|
||||
}
|
||||
<app-energy-plot [location]="location" [interval]="interval" [offset]="offset"></app-energy-plot>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../colors";
|
||||
110
src/main/angular/src/app/location/energy/location-energy.ts
Normal file
110
src/main/angular/src/app/location/energy/location-energy.ts
Normal file
@ -0,0 +1,110 @@
|
||||
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';
|
||||
import {EnergyPlot} from './plot/energy-plot';
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-energy',
|
||||
imports: [
|
||||
EnergyPlot
|
||||
],
|
||||
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, this.interval, response => callNextAndUpdateConsume(Value.ofPoint(response, 0, 0, 1)));
|
||||
} else {
|
||||
callNextAndUpdateConsume(series.value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
98
src/main/angular/src/app/location/energy/plot/EnergyPoint.ts
Normal file
98
src/main/angular/src/app/location/energy/plot/EnergyPoint.ts
Normal file
@ -0,0 +1,98 @@
|
||||
export class EnergyPoint {
|
||||
|
||||
readonly epochSeconds: number;
|
||||
|
||||
private _purchase: number | null = null;
|
||||
|
||||
private _produce: number | null = null;
|
||||
|
||||
private _deliver: number | null = null;
|
||||
|
||||
private _self: number | null = null;
|
||||
|
||||
private _consume: number | null = null;
|
||||
|
||||
private _purchaseY: number | null = null;
|
||||
|
||||
private _selfY: number | null = null;
|
||||
|
||||
private _deliverY: number | null = null;
|
||||
|
||||
getPurchaseY(yFactor: number): number {
|
||||
return this.getPurchaseH(yFactor) + this.getSelfH(yFactor);
|
||||
}
|
||||
|
||||
getPurchaseH(yFactor: number): number {
|
||||
if (this._purchaseY === null) {
|
||||
this._purchaseY = (this.purchase || 0) * yFactor;
|
||||
}
|
||||
return this._purchaseY;
|
||||
}
|
||||
|
||||
getSelfY(yFactor: number): number {
|
||||
return this.getSelfH(yFactor);
|
||||
}
|
||||
|
||||
getSelfH(yFactor: number): number {
|
||||
if (this._selfY === null) {
|
||||
this._selfY = (this.self || 0) * yFactor;
|
||||
}
|
||||
return this._selfY;
|
||||
}
|
||||
|
||||
getDeliverH(yFactor: number): number {
|
||||
if (this._deliverY === null) {
|
||||
this._deliverY = (this.deliver || 0) * yFactor;
|
||||
}
|
||||
return this._deliverY;
|
||||
}
|
||||
|
||||
constructor(
|
||||
point: number[],
|
||||
) {
|
||||
this.epochSeconds = point[0];
|
||||
}
|
||||
|
||||
set deliver(value: number | null) {
|
||||
this._deliver = value;
|
||||
this.update();
|
||||
}
|
||||
|
||||
set produce(value: number | null) {
|
||||
this._produce = value;
|
||||
this.update();
|
||||
}
|
||||
|
||||
set purchase(value: number | null) {
|
||||
this._purchase = value;
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
if (this._purchase !== null && this._produce !== null && this._deliver !== null) {
|
||||
this._self = Math.max(0, this._produce - this._deliver);
|
||||
this._consume = Math.max(0, this._purchase + this._self);
|
||||
}
|
||||
}
|
||||
|
||||
get consume(): number | null {
|
||||
return this._consume;
|
||||
}
|
||||
|
||||
get self(): number | null {
|
||||
return this._self;
|
||||
}
|
||||
|
||||
get deliver(): number | null {
|
||||
return this._deliver;
|
||||
}
|
||||
|
||||
get produce(): number | null {
|
||||
return this._produce;
|
||||
}
|
||||
|
||||
get purchase(): number | null {
|
||||
return this._purchase;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<svg [attr.viewBox]="`0 0 ${widthPx} ${heightPx}`" [style.background-color]="'#eee'">
|
||||
@for (point of points; track point.epochSeconds) {
|
||||
<rect
|
||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
||||
[attr.y]="heightPx - 1 + yMinPx - point.getPurchaseY(yFactor)"
|
||||
[attr.width]="xWidthPx"
|
||||
[attr.height]="point.getPurchaseH(yFactor)"
|
||||
class="COLOR_BACK_PURCHASE"
|
||||
></rect>
|
||||
<rect
|
||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
||||
[attr.y]="heightPx - 1 + yMinPx - point.getSelfY(yFactor)"
|
||||
[attr.width]="xWidthPx"
|
||||
[attr.height]="point.getSelfH(yFactor)"
|
||||
class="COLOR_BACK_SELF"
|
||||
></rect>
|
||||
<rect
|
||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
||||
[attr.y]="heightPx + yMinPx"
|
||||
[attr.width]="xWidthPx"
|
||||
[attr.height]="point.getDeliverH(yFactor)"
|
||||
class="COLOR_BACK_DELIVER"
|
||||
></rect>
|
||||
<line
|
||||
x1="0"
|
||||
[attr.y1]="heightPx - 1 + yMinPx"
|
||||
[attr.x2]="widthPx"
|
||||
[attr.y2]="heightPx - 1 + yMinPx"
|
||||
stroke="#aaaaaa"
|
||||
stroke-width="1"
|
||||
></line>
|
||||
}
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
147
src/main/angular/src/app/location/energy/plot/energy-plot.ts
Normal file
147
src/main/angular/src/app/location/energy/plot/energy-plot.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Location} from '../../Location';
|
||||
import {Interval} from '../../../series/Interval';
|
||||
import {PointService} from '../../../point/point-service';
|
||||
import {PointResponse} from '../../../point/PointResponse';
|
||||
import {PointSeries} from '../../../point/PointSeries';
|
||||
import {EnergyPoint} from './EnergyPoint';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-energy-plot',
|
||||
imports: [],
|
||||
templateUrl: './energy-plot.html',
|
||||
styleUrl: './energy-plot.less',
|
||||
})
|
||||
export class EnergyPlot implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
readonly widthPx = 800;
|
||||
|
||||
readonly heightPx = 100;
|
||||
|
||||
private _location!: Location;
|
||||
|
||||
private _interval: Interval = Interval.FIVE;
|
||||
|
||||
private _offset: number = 0;
|
||||
|
||||
private _count: number = 1;
|
||||
|
||||
protected points: EnergyPoint[] = [];
|
||||
|
||||
protected yMin: number = 0;
|
||||
|
||||
protected yMinPx: number = 0;
|
||||
|
||||
protected yMax: number = 0;
|
||||
|
||||
protected yFactor: number = 0;
|
||||
|
||||
protected xMin: number = 0;
|
||||
|
||||
protected xMax: number = 0;
|
||||
|
||||
protected xFactor: number = 0;
|
||||
|
||||
protected xWidthPx: number = 0;
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(timer(60000, 60000).subscribe(() => this.ngAfterViewInit()));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (!this._location.energyPurchase) {
|
||||
return;
|
||||
}
|
||||
if (!this._location.energyProduce) {
|
||||
return;
|
||||
}
|
||||
if (!this._location.energyDeliver) {
|
||||
return;
|
||||
}
|
||||
const series = [this._location.energyPurchase, this._location.energyProduce, this._location.energyDeliver];
|
||||
this.pointService.relative(series, this._interval, this._offset, this._count, this._interval.inner, this.update);
|
||||
}
|
||||
|
||||
public readonly update = (response: PointResponse): void => {
|
||||
this.points.length = 0;
|
||||
this.add(response.series[0], (p, v) => p.purchase = v);
|
||||
this.add(response.series[1], (p, v) => p.produce = v);
|
||||
this.add(response.series[2], (p, v) => p.deliver = v);
|
||||
this.yMax = -Infinity;
|
||||
this.yMin = Infinity;
|
||||
for (let point of this.points) {
|
||||
this.yMax = Math.max(this.yMax, point.consume || 0);
|
||||
this.yMin = Math.min(this.yMin, -(point.deliver || 0));
|
||||
}
|
||||
this.yMinPx = this.yMin * this.yFactor;
|
||||
this.yFactor = this.heightPx / (this.yMax - this.yMin);
|
||||
this.xMin = response.begin.getTime() / 1000;
|
||||
this.xMax = response.end.getTime() / 1000;
|
||||
this.xFactor = this.widthPx / (this.xMax - this.xMin);
|
||||
this.xWidthPx = this.widthPx / response.expectedCount;
|
||||
};
|
||||
|
||||
private add(series: PointSeries, setter: (p: EnergyPoint, v: number) => void): void {
|
||||
for (const point of series.points) {
|
||||
const index = this.insert(point, setter);
|
||||
if (index >= 0) {
|
||||
const fresh = new EnergyPoint(point);
|
||||
setter(fresh, point[1])
|
||||
this.points.splice(index, 0, fresh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private insert(point: number[], setter: (p: EnergyPoint, v: number) => any): number {
|
||||
let index = 0;
|
||||
for (let old of this.points) {
|
||||
const age = old.epochSeconds - point[0];
|
||||
if (age === 0) {
|
||||
setter(old, point[1])
|
||||
return -1;
|
||||
} else if (age < 0) {
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set location(value: Location) {
|
||||
this._location = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set interval(value: Interval) {
|
||||
this._interval = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set offset(value: number) {
|
||||
this._offset = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set count(value: number) {
|
||||
this._count = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 COLOR_FONT_PURCHASE">
|
||||
{{ location.powerPurchase?.value?.toValueString(dateService.now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
<div class="SectionHeadingText">
|
||||
Solar
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_PRODUCE">
|
||||
{{ location.powerProduce?.value?.toValueString(dateService.now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
<div class="SectionHeadingText">
|
||||
Verbrauch
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_CONSUME">
|
||||
{{ location.powerConsume?.toValueString(dateService.now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
<div class="SectionHeadingText">
|
||||
Einspeisung
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_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,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {validateDate, validateList} from "../common";
|
||||
import {validateDate, validateList, validateNumber} from "../common";
|
||||
|
||||
import {PointSeries} from './PointSeries';
|
||||
|
||||
@ -8,6 +8,7 @@ export class PointResponse {
|
||||
readonly begin: Date,
|
||||
readonly end: Date,
|
||||
readonly series: PointSeries[],
|
||||
readonly expectedCount: number,
|
||||
) {
|
||||
//
|
||||
}
|
||||
@ -17,6 +18,7 @@ export class PointResponse {
|
||||
validateDate(json.begin),
|
||||
validateDate(json.end),
|
||||
validateList(json.series, PointSeries.fromJson),
|
||||
validateNumber(json.expectedCount),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -16,12 +16,13 @@ export class PointService extends CrudService<PointResponse> {
|
||||
super(api, ws, ['Point'], PointResponse.fromJson);
|
||||
}
|
||||
|
||||
relative(series: Series[], interval: Interval, offset: number, count: number, next: Next<PointResponse>): void {
|
||||
relative(series: Series[], outer: Interval, offset: number, count: number, interval: Interval, next: Next<PointResponse>): void {
|
||||
const request = {
|
||||
ids: series.map(s => s.id),
|
||||
interval: interval,
|
||||
outerInterval: outer.name,
|
||||
offset: offset,
|
||||
count: count,
|
||||
interval: interval.name,
|
||||
};
|
||||
this.postSingle(['relative'], request, next);
|
||||
}
|
||||
|
||||
@ -1,8 +1,22 @@
|
||||
export enum Interval {
|
||||
FIVE = 'FIVE',
|
||||
HOUR = 'HOUR',
|
||||
DAY = 'DAY',
|
||||
WEEK = 'WEEK',
|
||||
MONTH = 'MONTH',
|
||||
YEAR = 'YEAR',
|
||||
export class Interval {
|
||||
|
||||
static readonly FIVE = new Interval('FIVE');
|
||||
|
||||
static readonly HOUR = new Interval('HOUR', this.FIVE);
|
||||
|
||||
static readonly DAY = new Interval('DAY', this.FIVE);
|
||||
|
||||
static readonly WEEK = new Interval('WEEK', this.HOUR);
|
||||
|
||||
static readonly MONTH = new Interval('MONTH', this.HOUR);
|
||||
|
||||
static readonly YEAR = new Interval('YEAR', this.DAY);
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly inner: Interval = this,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import {Value} from './Value';
|
||||
|
||||
export class Series {
|
||||
|
||||
readonly value: Value | null = null;
|
||||
readonly value: Value;
|
||||
|
||||
constructor(
|
||||
readonly id: number,
|
||||
@ -33,4 +33,8 @@ export class Series {
|
||||
);
|
||||
}
|
||||
|
||||
equals(other: Series | null | undefined): boolean {
|
||||
return this.id === other?.id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,7 +4,11 @@ import {Series} from './Series';
|
||||
|
||||
export class Value {
|
||||
|
||||
constructor(
|
||||
static readonly NULL: Value = new Value(NaN, 0, 0, "", new Date());
|
||||
|
||||
static readonly ZERO: Value = new Value(0, 0, Infinity, "", new Date());
|
||||
|
||||
protected constructor(
|
||||
readonly value: number,
|
||||
readonly precision: number,
|
||||
readonly seconds: number,
|
||||
@ -14,71 +18,59 @@ export class Value {
|
||||
//
|
||||
}
|
||||
|
||||
toValueString(zeroToDash: boolean, now_ageCheckToDash: Date | null): string {
|
||||
if (this.value === null || this.value === undefined) {
|
||||
return "[???]";
|
||||
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) {
|
||||
return `--- ${this.unit}`
|
||||
}
|
||||
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 | null | undefined): Value | null {
|
||||
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 | null | undefined): Value | null {
|
||||
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;
|
||||
}
|
||||
return new BiValue(this, other || Value.ZERO, (a, b) => a - b);
|
||||
}
|
||||
|
||||
operateSameUnit(operationName: string, other: Value | null | undefined, operation: (a: number, b: number) => number): Value | null {
|
||||
if (!other) {
|
||||
return null;
|
||||
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.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) {
|
||||
if (date === null) {
|
||||
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 {
|
||||
static ofPoint(response: PointResponse, seriesIndex: number, pointIndex: number, valueIndex: number): Value {
|
||||
const series = response.series[seriesIndex];
|
||||
if (!series) {
|
||||
return null;
|
||||
return this.NULL;
|
||||
}
|
||||
|
||||
const point = series.points[pointIndex];
|
||||
if (!point) {
|
||||
return null;
|
||||
return this.NULL;
|
||||
}
|
||||
|
||||
const date = new Date(point[0] * 1000);
|
||||
@ -86,4 +78,38 @@ 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;
|
||||
}
|
||||
|
||||
onlyPositive(): Value {
|
||||
if (this.value < 0) {
|
||||
return Value.ZERO;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,23 +1,60 @@
|
||||
@empty: gray;
|
||||
@purchase: red;
|
||||
@deliver: magenta;
|
||||
@produce: #0095ff;
|
||||
@consume: #ff8800;
|
||||
|
||||
.purchase {
|
||||
color: @purchase;
|
||||
@COLOR_FONT_PURCHASE: red;
|
||||
@COLOR_FONT_DELIVER: magenta;
|
||||
@COLOR_FONT_PRODUCE: #0095ff;
|
||||
@COLOR_FONT_SELF: #0095ff;
|
||||
@COLOR_FONT_CONSUME: #ff8800;
|
||||
|
||||
@COLOR_BACK_PURCHASE: #ffa7a7;
|
||||
@COLOR_BACK_DELIVER: #ff00ff;
|
||||
@COLOR_BACK_PRODUCE: #5cbcff;
|
||||
@COLOR_BACK_SELF: #00ff69;
|
||||
@COLOR_BACK_CONSUME: #ffc07a;
|
||||
|
||||
.COLOR_FONT_PURCHASE {
|
||||
color: @COLOR_FONT_PURCHASE;
|
||||
}
|
||||
|
||||
.deliver {
|
||||
color: @deliver;
|
||||
.COLOR_FONT_DELIVER {
|
||||
color: @COLOR_FONT_DELIVER;
|
||||
}
|
||||
|
||||
.produce {
|
||||
color: @produce;
|
||||
.COLOR_FONT_PRODUCE {
|
||||
color: @COLOR_FONT_PRODUCE;
|
||||
}
|
||||
|
||||
.consume {
|
||||
color: @consume;
|
||||
.COLOR_FONT_SELF {
|
||||
color: @COLOR_FONT_SELF;
|
||||
}
|
||||
|
||||
.COLOR_FONT_CONSUME {
|
||||
color: @COLOR_FONT_CONSUME;
|
||||
}
|
||||
|
||||
.COLOR_BACK_PURCHASE {
|
||||
color: @COLOR_BACK_PURCHASE;
|
||||
fill: @COLOR_BACK_PURCHASE;
|
||||
}
|
||||
|
||||
.COLOR_BACK_DELIVER {
|
||||
color: @COLOR_BACK_DELIVER;
|
||||
fill: @COLOR_BACK_DELIVER;
|
||||
}
|
||||
|
||||
.COLOR_BACK_PRODUCE {
|
||||
color: @COLOR_BACK_PRODUCE;
|
||||
fill: @COLOR_BACK_PRODUCE;
|
||||
}
|
||||
|
||||
.COLOR_BACK_SELF {
|
||||
color: @COLOR_BACK_SELF;
|
||||
fill: @COLOR_BACK_SELF;
|
||||
}
|
||||
|
||||
.COLOR_BACK_CONSUME {
|
||||
color: @COLOR_BACK_CONSUME;
|
||||
fill: @COLOR_BACK_CONSUME;
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular</title>
|
||||
<title>Data2025</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="icon" type="image/svg" href="favicon.svg">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
||||
@ -4,6 +4,7 @@ body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 4vw;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
div {
|
||||
@ -46,6 +47,7 @@ div {
|
||||
|
||||
> .SectionHeading {
|
||||
color: dimgray;
|
||||
|
||||
> .SectionHeadingText {
|
||||
font-size: 70%;
|
||||
font-style: italic;
|
||||
|
||||
@ -51,7 +51,7 @@ public class Plot {
|
||||
|
||||
@Setter
|
||||
@Column(nullable = false)
|
||||
private long duration = 288;
|
||||
private long duration = 1;
|
||||
|
||||
@Setter
|
||||
@Column(nullable = false)
|
||||
|
||||
@ -15,13 +15,15 @@ public interface IPointRequest {
|
||||
@NonNull
|
||||
List<Long> getIds();
|
||||
|
||||
@NonNull
|
||||
Interval getInterval();
|
||||
|
||||
@NonNull
|
||||
ZonedDateTime getBegin();
|
||||
|
||||
@NonNull
|
||||
ZonedDateTime getEnd();
|
||||
|
||||
@NonNull
|
||||
Interval getInterval();
|
||||
|
||||
long getExpectedCount();
|
||||
|
||||
}
|
||||
|
||||
@ -5,34 +5,40 @@ import lombok.Data;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Data
|
||||
public class PointRequestRelative implements IPointRequest {
|
||||
|
||||
public final List<Long> ids;
|
||||
|
||||
public final Interval interval;
|
||||
public final Interval outerInterval;
|
||||
|
||||
public final long offset;
|
||||
|
||||
public final long count;
|
||||
|
||||
public final Interval interval;
|
||||
|
||||
public final ZonedDateTime begin;
|
||||
|
||||
public final ZonedDateTime end;
|
||||
|
||||
public PointRequestRelative(
|
||||
final List<Long> ids,
|
||||
final Interval interval,
|
||||
final long offset,
|
||||
final long count
|
||||
) {
|
||||
public final long expectedCount;
|
||||
|
||||
public PointRequestRelative(final List<Long> ids, final Interval outerInterval, final long offset, final long count, final Interval interval) {
|
||||
this.ids = ids;
|
||||
this.interval = interval;
|
||||
this.outerInterval = outerInterval;
|
||||
this.offset = offset;
|
||||
this.count = count;
|
||||
this.end = interval.align.apply(ZonedDateTime.now()).minus(interval.amount * (offset - 1), interval.unit);
|
||||
this.begin = this.end.minus(interval.amount * count, interval.unit);
|
||||
this.interval = interval;
|
||||
this.end = outerInterval.align.apply(ZonedDateTime.now()).minus(outerInterval.amount * (offset - 1), outerInterval.unit);
|
||||
this.begin = this.end.minus(outerInterval.amount * count, outerInterval.unit);
|
||||
this.expectedCount = calculateExpectedCount();
|
||||
}
|
||||
|
||||
private long calculateExpectedCount() {
|
||||
return Stream.iterate(begin, d -> d.isBefore(end), d -> d.plus(interval.amount, interval.unit)).count();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -14,4 +14,6 @@ public class PointResponse {
|
||||
|
||||
public final List<PointSeries> series;
|
||||
|
||||
public final long expectedCount;
|
||||
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ public class PointService {
|
||||
@NonNull
|
||||
public PointResponse points(@NonNull final IPointRequest request) {
|
||||
final List<PointSeries> series = request.getIds().stream().map(s -> points(s, request)).toList();
|
||||
return new PointResponse(request.getBegin(), request.getEnd(), series);
|
||||
return new PointResponse(request.getBegin(), request.getEnd(), series, request.getExpectedCount());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@ -40,9 +40,9 @@ public class SeriesController {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@PostMapping("{id}/decimals")
|
||||
public SeriesDto decimals(@PathVariable final long id, @RequestBody final int decimals) {
|
||||
return seriesService.modify(id, series -> series.setPrecision(decimals));
|
||||
@PostMapping("{id}/precision")
|
||||
public SeriesDto precision(@PathVariable final long id, @RequestBody final int precision) {
|
||||
return seriesService.modify(id, series -> series.setPrecision(precision));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
Loading…
Reference in New Issue
Block a user