big cleanup + refactor: live+history, enhanced Graph
This commit is contained in:
parent
9ee8060f05
commit
2c66314941
@ -5,7 +5,7 @@ spring.datasource.driverClassName=org.h2.Driver
|
|||||||
spring.datasource.username=sa
|
spring.datasource.username=sa
|
||||||
spring.datasource.password=password
|
spring.datasource.password=password
|
||||||
#-
|
#-
|
||||||
spring.jpa.hibernate.ddl-auto=create
|
#spring.jpa.hibernate.ddl-auto=create
|
||||||
#-
|
#-
|
||||||
de.ph87.data.message.receive.mqtt.host=10.0.0.50
|
de.ph87.data.message.receive.mqtt.host=10.0.0.50
|
||||||
de.ph87.data.message.receive.mqtt.topic=#
|
de.ph87.data.message.receive.mqtt.topic=#
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
@import "./colors.less";
|
@import "./colors.less";
|
||||||
|
|
||||||
.numberTable {
|
.numberTable {
|
||||||
margin-bottom: 2em;
|
|
||||||
|
|
||||||
.arrowLeft {
|
.arrowLeft {
|
||||||
float: left;
|
float: left;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||||
import {menubar, ROUTING} from './app.routes';
|
import {menubar} from './app.routes';
|
||||||
import {NgForOf} from '@angular/common';
|
import {NgForOf} from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -11,7 +11,5 @@ import {NgForOf} from '@angular/common';
|
|||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
|
||||||
protected readonly ROUTING = ROUTING;
|
|
||||||
|
|
||||||
protected readonly menubar = menubar;
|
protected readonly menubar = menubar;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {Routes} from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
import {ElectroComponent} from './electro/electro.component';
|
import {LiveComponent} from './live/live.component';
|
||||||
import {GreenhouseComponent} from './greenhouse/greenhouse/greenhouse.component';
|
import {GreenhouseComponent} from './live/greenhouse/greenhouse/greenhouse.component';
|
||||||
import {CisternComponent} from './cistern/cistern.component';
|
import {HistoryComponent} from './history/history.component';
|
||||||
|
|
||||||
export class Path {
|
export class Path {
|
||||||
|
|
||||||
@ -20,8 +20,8 @@ export class Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ROUTING = {
|
export const ROUTING = {
|
||||||
ENERGY: new Path('Energy', 'Energie', true),
|
LIVE: new Path('Live', 'Live', true),
|
||||||
CISTERN: new Path('Cistern', 'Zisterne', true),
|
HISTORY: new Path('History', 'Historie', true),
|
||||||
GREENHOUSE: new Path('Greenhouse', 'Gewächshaus', false),
|
GREENHOUSE: new Path('Greenhouse', 'Gewächshaus', false),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,8 +30,8 @@ export function menubar(): Path[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: ROUTING.ENERGY.path, component: ElectroComponent},
|
{path: ROUTING.LIVE.path, component: LiveComponent},
|
||||||
{path: ROUTING.CISTERN.path, component: CisternComponent},
|
{path: ROUTING.HISTORY.path, component: HistoryComponent},
|
||||||
{path: ROUTING.GREENHOUSE.path, component: GreenhouseComponent},
|
{path: ROUTING.GREENHOUSE.path, component: GreenhouseComponent},
|
||||||
{path: '**', redirectTo: ROUTING.ENERGY.path},
|
{path: '**', redirectTo: ROUTING.LIVE.path},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
<table class="vertical">
|
|
||||||
<tr>
|
|
||||||
<th>Füllgrad</th>
|
|
||||||
<td class="valueInteger">{{ seriesService.cisternHeight.series?.lastValue?.percent(93.5)?.localeString }}</td>
|
|
||||||
<td class="unit">%</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Füllhöhe</th>
|
|
||||||
<td class="valueInteger">{{ seriesService.cisternHeight.series?.lastValue?.localeString }}</td>
|
|
||||||
<td class="unit">{{ seriesService.cisternHeight.series?.lastValue?.unit?.unit }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Volumen</th>
|
|
||||||
<td class="valueInteger">{{ seriesService.cisternVolume.series?.lastValue?.localeString }}</td>
|
|
||||||
<td class="unit">{{ seriesService.cisternVolume.series?.lastValue?.unit?.unit }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<img width="100%" *ngIf="seriesService.cisternVolume.series" [src]="graph(seriesService.cisternVolume.series)" [alt]="seriesService.cisternVolume.series.title">
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import {Component} from '@angular/core';
|
|
||||||
import {ElectroEnergyComponent} from "./energy/electro-energy.component";
|
|
||||||
import {ElectroPowerComponent} from "./power/electro-power.component";
|
|
||||||
import {WeatherDiagramComponent} from '../weather/weather-diagram/weather-diagram.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-electro',
|
|
||||||
imports: [
|
|
||||||
ElectroEnergyComponent,
|
|
||||||
ElectroPowerComponent,
|
|
||||||
WeatherDiagramComponent
|
|
||||||
],
|
|
||||||
templateUrl: './electro.component.html',
|
|
||||||
styleUrl: './electro.component.less'
|
|
||||||
})
|
|
||||||
export class ElectroComponent {
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
<div class="numberTable">
|
|
||||||
|
|
||||||
<div class="title">
|
|
||||||
Energie
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="option">
|
|
||||||
<button class="arrowLeft" (click)="shiftAlignment(+1)">←</button>
|
|
||||||
{{ offset > 0 ? -offset : '' }}{{ alignment === Alignment.FIVE && offset > 0 ? 'x' : '' }} {{ alignment.display }}{{ offset > 1 ? alignment.plural : '' }}
|
|
||||||
<button class="arrowRight" (click)="shiftAlignment(-1)">→</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="option">
|
|
||||||
<button class="arrowLeft" (click)="shiftOffset(+1)">←</button>
|
|
||||||
{{ alignment.offsetTitle(offset, locale) }}
|
|
||||||
<button class="arrowRight" (click)="shiftOffset(-1)" [disabled]="offset === 0">→</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<div class="entry consumption" [class.zero]="aggregations.energyConsumed?.zero">
|
|
||||||
<div class="name">Bedarf</div>
|
|
||||||
<div class="percent"> </div>
|
|
||||||
<div class="value">{{ aggregations.energyConsumed?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="entry purchase" [class.zero]="aggregations.energyPurchased?.delta?.zero">
|
|
||||||
<div class="name">Bezug</div>
|
|
||||||
<div class="percent">{{ aggregations.energyPurchasedPercent?.formatted }}</div>
|
|
||||||
<div class="value">{{ aggregations.energyPurchased?.delta?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="entry production" [class.zero]="aggregations.energyProduced?.delta?.zero">
|
|
||||||
<div class="name">Produktion</div>
|
|
||||||
<div class="percent">{{ aggregations.energyProducedPercent?.formatted }}</div>
|
|
||||||
<div class="value">{{ aggregations.energyProduced?.delta?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="entry self" [class.zero]="aggregations.energySelf?.zero">
|
|
||||||
<div class="name">Eigenbedarf</div>
|
|
||||||
<div class="percent">{{ aggregations.energySelfPercent?.formatted }}</div>
|
|
||||||
<div class="value">{{ aggregations.energySelf?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="entry delivery" [class.zero]="aggregations.energyDelivered?.delta?.zero">
|
|
||||||
<div class="name">Einspeisung</div>
|
|
||||||
<div class="percent">{{ aggregations.energyDeliveredPercent?.formatted }}</div>
|
|
||||||
<div class="value">{{ aggregations.energyDelivered?.delta?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<app-percent-bar [produktion]="aggregations.energyProduced?.delta" [self]="aggregations.energySelf" [purchase]="aggregations.energyPurchased?.delta" [delivery]="aggregations.energyDelivered?.delta"></app-percent-bar>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
@import "../../../../colors.less";
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
<div class="numberTable">
|
|
||||||
|
|
||||||
<div class="title">
|
|
||||||
Leistung
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<div class="entry consumption" [class.zero]="powerConsumption?.zero">
|
|
||||||
<div class="name">Bedarf</div>
|
|
||||||
<div class="percent"> </div>
|
|
||||||
<div class="value">{{ powerConsumption?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="entry purchase" [class.zero]="powerPurchase?.zero">
|
|
||||||
<div class="name">Bezug</div>
|
|
||||||
<div class="percent">{{ powerPurchasePercent?.formatted }}</div>
|
|
||||||
<div class="value">{{ powerPurchase?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="entry production" [class.zero]="powerProduction?.zero">
|
|
||||||
<div class="name">Produktion</div>
|
|
||||||
<div class="percent">{{ powerProducedPercent?.formatted }}</div>
|
|
||||||
<div class="value">{{ powerProduction?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="entry self" [class.zero]="powerSelf?.zero">
|
|
||||||
<div class="name">Eigenbedarf</div>
|
|
||||||
<div class="percent">{{ powerSelfPercent?.formatted }}</div>
|
|
||||||
<div class="value">{{ powerSelf?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="entry delivery" [class.zero]="powerDelivery?.zero">
|
|
||||||
<div class="name">Einspeisung</div>
|
|
||||||
<div class="percent">{{ powerDeliveryPercent?.formatted }}</div>
|
|
||||||
<div class="value">{{ powerDelivery?.formatted }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<app-percent-bar [produktion]="powerProduction" [self]="powerSelf" [purchase]="powerPurchase" [delivery]="powerDelivery"></app-percent-bar>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
64
src/main/angular/src/app/history/history.component.html
Normal file
64
src/main/angular/src/app/history/history.component.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<app-tile>
|
||||||
|
<div tile-body>
|
||||||
|
<div class="numberTable">
|
||||||
|
<div class="option">
|
||||||
|
<button class="arrowLeft" (click)="shiftAlignment(+1)">←</button>
|
||||||
|
{{ offset > 0 ? -offset : '' }}{{ alignment === Alignment.FIVE && offset > 0 ? 'x' : '' }} {{ alignment.display }}{{ offset > 1 ? alignment.plural : '' }}
|
||||||
|
<button class="arrowRight" (click)="shiftAlignment(-1)">→</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option">
|
||||||
|
<button class="arrowLeft" (click)="shiftOffset(+1)">←</button>
|
||||||
|
{{ alignment.offsetTitle(offset, locale) }}
|
||||||
|
<button class="arrowRight" (click)="shiftOffset(-1)" [disabled]="offset === 0">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-tile>
|
||||||
|
|
||||||
|
<app-tile>
|
||||||
|
<div tile-head>
|
||||||
|
Energie
|
||||||
|
</div>
|
||||||
|
<div tile-body>
|
||||||
|
<div class="numberTable">
|
||||||
|
<div class="content">
|
||||||
|
<div class="entry consumption" [class.zero]="aggregations.energyConsumed?.zero">
|
||||||
|
<div class="name">Bedarf</div>
|
||||||
|
<div class="percent"> </div>
|
||||||
|
<div class="value">{{ aggregations.energyConsumed?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry purchase" [class.zero]="aggregations.energyPurchased?.delta?.zero">
|
||||||
|
<div class="name">Bezug</div>
|
||||||
|
<div class="percent">{{ aggregations.energyPurchasedPercent?.formatted }}</div>
|
||||||
|
<div class="value">{{ aggregations.energyPurchased?.delta?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry production" [class.zero]="aggregations.energyProduced?.delta?.zero">
|
||||||
|
<div class="name">Produktion</div>
|
||||||
|
<div class="percent">{{ aggregations.energyProducedPercent?.formatted }}</div>
|
||||||
|
<div class="value">{{ aggregations.energyProduced?.delta?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry self" [class.zero]="aggregations.energySelf?.zero">
|
||||||
|
<div class="name">Eigenbedarf</div>
|
||||||
|
<div class="percent">{{ aggregations.energySelfPercent?.formatted }}</div>
|
||||||
|
<div class="value">{{ aggregations.energySelf?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry delivery" [class.zero]="aggregations.energyDelivered?.delta?.zero">
|
||||||
|
<div class="name">Einspeisung</div>
|
||||||
|
<div class="percent">{{ aggregations.energyDeliveredPercent?.formatted }}</div>
|
||||||
|
<div class="value">{{ aggregations.energyDelivered?.delta?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-percent-bar [produktion]="aggregations.energyProduced?.delta" [self]="aggregations.energySelf" [purchase]="aggregations.energyPurchased?.delta" [delivery]="aggregations.energyDelivered?.delta"></app-percent-bar>
|
||||||
|
</div>
|
||||||
|
</app-tile>
|
||||||
|
|
||||||
|
<app-tile>
|
||||||
|
<div tile-head>
|
||||||
|
Zisterne
|
||||||
|
</div>
|
||||||
|
<div tile-body>
|
||||||
|
<img width="100%" *ngIf="seriesService.cisternVolume.series" [src]="seriesService.graph(seriesService.cisternVolume.series, 600, 200, alignment, offset, alignment.inner,1)" [alt]="seriesService.cisternVolume.series.title">
|
||||||
|
</div>
|
||||||
|
</app-tile>
|
||||||
1
src/main/angular/src/app/history/history.component.less
Normal file
1
src/main/angular/src/app/history/history.component.less
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import "../../../colors.less";
|
||||||
@ -1,19 +1,23 @@
|
|||||||
import {Component, Inject, LOCALE_ID, OnDestroy, OnInit} from '@angular/core';
|
import {Component, Inject, LOCALE_ID, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {PercentBarComponent} from '../../shared/percent-bar/percent-bar.component';
|
import {PercentBarComponent} from '../shared/percent-bar/percent-bar.component';
|
||||||
import {AggregationWrapperDto} from '../../series/AggregationWrapperDto';
|
import {AggregationWrapperDto} from '../series/AggregationWrapperDto';
|
||||||
import {Alignment} from '../../series/Alignment';
|
import {Alignment} from '../series/Alignment';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {SeriesService} from '../../series/series.service';
|
import {SeriesService} from '../series/series.service';
|
||||||
|
import {NgIf} from "@angular/common";
|
||||||
|
import {TileComponent} from '../shared/tile/tile.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-electro-energy',
|
selector: 'app-history',
|
||||||
imports: [
|
imports: [
|
||||||
PercentBarComponent
|
PercentBarComponent,
|
||||||
|
NgIf,
|
||||||
|
TileComponent
|
||||||
],
|
],
|
||||||
templateUrl: './electro-energy.component.html',
|
templateUrl: './history.component.html',
|
||||||
styleUrl: './electro-energy.component.less'
|
styleUrl: './history.component.less'
|
||||||
})
|
})
|
||||||
export class ElectroEnergyComponent implements OnInit, OnDestroy {
|
export class HistoryComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected readonly Alignment = Alignment;
|
protected readonly Alignment = Alignment;
|
||||||
|
|
||||||
31
src/main/angular/src/app/live/cistern/cistern.component.html
Normal file
31
src/main/angular/src/app/live/cistern/cistern.component.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<app-tile>
|
||||||
|
|
||||||
|
<div tile-head>
|
||||||
|
Zisterne
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div tile-body>
|
||||||
|
|
||||||
|
<table class="vertical">
|
||||||
|
<tr>
|
||||||
|
<th>Füllgrad</th>
|
||||||
|
<td class="valueInteger">{{ seriesService.cisternHeight.series?.lastValue?.percent(93.5)?.localeString }}</td>
|
||||||
|
<td class="unit">%</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Füllhöhe</th>
|
||||||
|
<td class="valueInteger">{{ seriesService.cisternHeight.series?.lastValue?.localeString }}</td>
|
||||||
|
<td class="unit">{{ seriesService.cisternHeight.series?.lastValue?.unit?.unit }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Volumen</th>
|
||||||
|
<td class="valueInteger">{{ seriesService.cisternVolume.series?.lastValue?.localeString }}</td>
|
||||||
|
<td class="unit">{{ seriesService.cisternVolume.series?.lastValue?.unit?.unit }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<img width="100%" *ngIf="seriesService.cisternVolume.series" [src]="graph(seriesService.cisternVolume.series)" [alt]="seriesService.cisternVolume.series.title">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</app-tile>
|
||||||
@ -1,14 +1,16 @@
|
|||||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {SeriesService} from '../series/series.service';
|
import {SeriesService} from '../../series/series.service';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {NgIf} from '@angular/common';
|
import {NgIf} from '@angular/common';
|
||||||
import {Series} from '../series/Series';
|
import {Series} from '../../series/Series';
|
||||||
import {ApiService} from '../core/api.service';
|
import {TileComponent} from '../../shared/tile/tile.component';
|
||||||
|
import {Alignment} from '../../series/Alignment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-cistern',
|
selector: 'app-cistern',
|
||||||
imports: [
|
imports: [
|
||||||
NgIf
|
NgIf,
|
||||||
|
TileComponent
|
||||||
],
|
],
|
||||||
templateUrl: './cistern.component.html',
|
templateUrl: './cistern.component.html',
|
||||||
styleUrl: './cistern.component.less'
|
styleUrl: './cistern.component.less'
|
||||||
@ -31,7 +33,7 @@ export class CisternComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
graph(series: Series) {
|
graph(series: Series) {
|
||||||
return ApiService.url('http', ['Series', 'Graph', series.id, '400', '100', 'FIVE', '0', '288']);
|
return this.seriesService.graph(series, 600, 200, Alignment.FIVE, 0, Alignment.FIVE, 24 * 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {SeriesService} from '../../series/series.service';
|
import {SeriesService} from '../../../series/series.service';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
<app-electro-power></app-electro-power>
|
<app-electro-power></app-electro-power>
|
||||||
|
|
||||||
<app-electro-energy></app-electro-energy>
|
<app-cistern></app-cistern>
|
||||||
18
src/main/angular/src/app/live/live.component.ts
Normal file
18
src/main/angular/src/app/live/live.component.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {ElectroPowerComponent} from "./power/electro-power.component";
|
||||||
|
import {WeatherDiagramComponent} from './weather/weather-diagram/weather-diagram.component';
|
||||||
|
import {CisternComponent} from './cistern/cistern.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-live',
|
||||||
|
imports: [
|
||||||
|
ElectroPowerComponent,
|
||||||
|
WeatherDiagramComponent,
|
||||||
|
CisternComponent
|
||||||
|
],
|
||||||
|
templateUrl: './live.component.html',
|
||||||
|
styleUrl: './live.component.less'
|
||||||
|
})
|
||||||
|
export class LiveComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<app-tile>
|
||||||
|
|
||||||
|
<div tile-head>
|
||||||
|
Leistung
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div tile-body>
|
||||||
|
<div class="numberTable">
|
||||||
|
<div class="content">
|
||||||
|
<div class="entry consumption" [class.zero]="powerConsumption?.zero">
|
||||||
|
<div class="name">Bedarf</div>
|
||||||
|
<div class="percent"> </div>
|
||||||
|
<div class="value">{{ powerConsumption?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry purchase" [class.zero]="powerPurchase?.zero">
|
||||||
|
<div class="name">Bezug</div>
|
||||||
|
<div class="percent">{{ powerPurchasePercent?.formatted }}</div>
|
||||||
|
<div class="value">{{ powerPurchase?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry production" [class.zero]="powerProduction?.zero">
|
||||||
|
<div class="name">Produktion</div>
|
||||||
|
<div class="percent">{{ powerProducedPercent?.formatted }}</div>
|
||||||
|
<div class="value">{{ powerProduction?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry self" [class.zero]="powerSelf?.zero">
|
||||||
|
<div class="name">Eigenbedarf</div>
|
||||||
|
<div class="percent">{{ powerSelfPercent?.formatted }}</div>
|
||||||
|
<div class="value">{{ powerSelf?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry delivery" [class.zero]="powerDelivery?.zero">
|
||||||
|
<div class="name">Einspeisung</div>
|
||||||
|
<div class="percent">{{ powerDeliveryPercent?.formatted }}</div>
|
||||||
|
<div class="value">{{ powerDelivery?.formatted }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-percent-bar [produktion]="powerProduction" [self]="powerSelf" [purchase]="powerPurchase" [delivery]="powerDelivery"></app-percent-bar>
|
||||||
|
|
||||||
|
</app-tile>
|
||||||
@ -1,13 +1,15 @@
|
|||||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {PercentBarComponent} from "../../shared/percent-bar/percent-bar.component";
|
import {PercentBarComponent} from "../../shared/percent-bar/percent-bar.component";
|
||||||
import {Value} from '../../value/Value';
|
import {Value} from '../../series/value/Value';
|
||||||
import {SeriesService} from '../../series/series.service';
|
import {SeriesService} from '../../series/series.service';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
|
import {TileComponent} from '../../shared/tile/tile.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-electro-power',
|
selector: 'app-electro-power',
|
||||||
imports: [
|
imports: [
|
||||||
PercentBarComponent
|
PercentBarComponent,
|
||||||
|
TileComponent
|
||||||
],
|
],
|
||||||
templateUrl: './electro-power.component.html',
|
templateUrl: './electro-power.component.html',
|
||||||
styleUrl: './electro-power.component.less'
|
styleUrl: './electro-power.component.less'
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {Value} from "../../value/Value";
|
import {Value} from "../../../series/value/Value";
|
||||||
import {validateDate, validateList} from "../../core/validators";
|
import {validateDate, validateList} from "../../../core/validators";
|
||||||
import {WeatherHour} from "./WeatherHour";
|
import {WeatherHour} from "./WeatherHour";
|
||||||
|
|
||||||
export class WeatherDay {
|
export class WeatherDay {
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {Value} from '../../value/Value';
|
import {Value} from '../../../series/value/Value';
|
||||||
import {validateDate} from '../../core/validators';
|
import {validateDate} from '../../../core/validators';
|
||||||
|
|
||||||
export class WeatherHour {
|
export class WeatherHour {
|
||||||
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<app-tile [padding]="false">
|
||||||
|
|
||||||
|
<div tile-body>
|
||||||
|
<div class="day">
|
||||||
|
<div class="hour" *ngFor="let hour of hours">
|
||||||
|
<div class="bar weekdayHolder" *ngIf="hour.date.getHours() === 8">
|
||||||
|
{{ dateFormat(hour.date | date:'E') }}
|
||||||
|
</div>
|
||||||
|
<div class="bar clouds" [style.height]="clouds(hour)"></div>
|
||||||
|
<div class="bar irradiation" [style.height]="irradiation(hour)"></div>
|
||||||
|
<div class="bar precipitation" [style.height]="precipitation(hour)"></div>
|
||||||
|
<div class="bar temperature" [style.height]="temperature(hour)" [ngClass]="temperatureClasses(hour)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="legend">
|
||||||
|
<span class="line temperatureGTE30">≥30°C</span>
|
||||||
|
<span class="line temperatureGTE20">≥20°C</span>
|
||||||
|
<span class="line temperatureGTE10">≥10°C</span>
|
||||||
|
<span class="line temperatureGT0">>0°C</span>
|
||||||
|
<span class="line temperatureNegative">≤0°C</span>
|
||||||
|
<span class="line"> </span>
|
||||||
|
<span class="line">Niederschlag 100% = {{ PRECIPITATION_MAX_MM }}mm</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</app-tile>
|
||||||
@ -3,6 +3,7 @@ import {DatePipe, NgClass, NgForOf, NgIf} from '@angular/common';
|
|||||||
import {WeatherHour} from './WeatherHour';
|
import {WeatherHour} from './WeatherHour';
|
||||||
import {WeatherService} from './weather.service';
|
import {WeatherService} from './weather.service';
|
||||||
import {WeatherDay} from './WeatherDay';
|
import {WeatherDay} from './WeatherDay';
|
||||||
|
import {TileComponent} from '../../../shared/tile/tile.component';
|
||||||
|
|
||||||
const PAST_HOURS_COUNT = 0;
|
const PAST_HOURS_COUNT = 0;
|
||||||
|
|
||||||
@ -14,7 +15,8 @@ const DAY_COUNT = 7;
|
|||||||
NgForOf,
|
NgForOf,
|
||||||
NgIf,
|
NgIf,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
NgClass
|
NgClass,
|
||||||
|
TileComponent
|
||||||
],
|
],
|
||||||
templateUrl: './weather-diagram.component.html',
|
templateUrl: './weather-diagram.component.html',
|
||||||
styleUrl: './weather-diagram.component.less'
|
styleUrl: './weather-diagram.component.less'
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
||||||
import {ApiService} from '../../core/api.service';
|
import {ApiService} from '../../../core/api.service';
|
||||||
import {Next} from '../../core/types';
|
import {Next} from '../../../core/types';
|
||||||
import {WeatherDay} from './WeatherDay';
|
import {WeatherDay} from './WeatherDay';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -4,7 +4,7 @@ import {MeterAggregate} from './meter/MeterAggregate';
|
|||||||
import {Aggregate} from './Aggregate';
|
import {Aggregate} from './Aggregate';
|
||||||
import {VaryingAggregate} from './varying/VaryingAggregate';
|
import {VaryingAggregate} from './varying/VaryingAggregate';
|
||||||
|
|
||||||
import {Value} from '../value/Value';
|
import {Value} from './value/Value';
|
||||||
|
|
||||||
export class AggregationWrapperDto {
|
export class AggregationWrapperDto {
|
||||||
|
|
||||||
|
|||||||
@ -4,23 +4,25 @@ export class Alignment {
|
|||||||
|
|
||||||
private static readonly values: Alignment[] = [];
|
private static readonly values: Alignment[] = [];
|
||||||
|
|
||||||
static readonly FIVE = new Alignment('FIVE', '5 Minuten', '', Alignment.offsetTitleFive);
|
static readonly FIVE = new Alignment('FIVE', '5 Minuten', '', Alignment.offsetTitleFive, null, 0);
|
||||||
|
|
||||||
static readonly HOUR = new Alignment('HOUR', 'Stunde', 'n', Alignment.offsetTitleHour);
|
static readonly HOUR = new Alignment('HOUR', 'Stunde', 'n', Alignment.offsetTitleHour, Alignment.FIVE, 12);
|
||||||
|
|
||||||
static readonly DAY = new Alignment('DAY', 'Tag', 'e', Alignment.offsetTitleDay);
|
static readonly DAY = new Alignment('DAY', 'Tag', 'e', Alignment.offsetTitleDay, Alignment.FIVE, 24 * 12);
|
||||||
|
|
||||||
static readonly WEEK = new Alignment('WEEK', 'Woche', 'n', Alignment.offsetTitleWeek);
|
static readonly WEEK = new Alignment('WEEK', 'Woche', 'n', Alignment.offsetTitleWeek, Alignment.HOUR, 7 * 24);
|
||||||
|
|
||||||
static readonly MONTH = new Alignment('MONTH', 'Monat', 'e', Alignment.offsetTitleMonth);
|
static readonly MONTH = new Alignment('MONTH', 'Monat', 'e', Alignment.offsetTitleMonth, Alignment.HOUR, 30 * 24);
|
||||||
|
|
||||||
static readonly YEAR = new Alignment('YEAR', 'Jahr', 'e', Alignment.offsetTitleYear);
|
static readonly YEAR = new Alignment('YEAR', 'Jahr', 'e', Alignment.offsetTitleYear, Alignment.DAY, 365);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
readonly display: string,
|
readonly display: string,
|
||||||
readonly plural: string,
|
readonly plural: string,
|
||||||
readonly offsetTitle: (offset: number, locale: string) => string
|
readonly offsetTitle: (offset: number, locale: string) => string,
|
||||||
|
readonly inner: Alignment | null,
|
||||||
|
readonly innerCount: number,
|
||||||
) {
|
) {
|
||||||
Alignment.values.push(this);
|
Alignment.values.push(this);
|
||||||
}
|
}
|
||||||
@ -139,4 +141,8 @@ export class Alignment {
|
|||||||
return `${formatDate(date, "yyyy", locale)}`;
|
return `${formatDate(date, "yyyy", locale)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {Unit} from '../value/Unit';
|
import {Unit} from './value/Unit';
|
||||||
import {validateNumber, validateString} from '../core/validators';
|
import {validateNumber, validateString} from '../core/validators';
|
||||||
import {Value} from '../value/Value';
|
import {Value} from './value/Value';
|
||||||
|
|
||||||
export class Series {
|
export class Series {
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {Series} from '../Series';
|
import {Series} from '../Series';
|
||||||
import {Aggregate} from '../Aggregate';
|
import {Aggregate} from '../Aggregate';
|
||||||
import {Value} from '../../value/Value';
|
import {Value} from '../value/Value';
|
||||||
|
|
||||||
export class MeterAggregate extends Aggregate {
|
export class MeterAggregate extends Aggregate {
|
||||||
|
|
||||||
|
|||||||
@ -58,4 +58,11 @@ export class SeriesService extends AbstractRepositoryService {
|
|||||||
this.api.getSingle(['Series', 'agg', 'all', alignment.name, 'offset', offset], j => AggregationWrapperDto.fromJson(j, this.locale), next);
|
this.api.getSingle(['Series', 'agg', 'all', alignment.name, 'offset', offset], j => AggregationWrapperDto.fromJson(j, this.locale), next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
graph(series: Series, width: number, height: number, alignment: Alignment, offset: number, innerAlignment: Alignment | null, count: number): any {
|
||||||
|
if (innerAlignment === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ApiService.url('http', ['Series', 'Graph', series.id, width, height, alignment, offset, count, innerAlignment]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {validateString} from '../core/validators';
|
import {validateString} from '../../core/validators';
|
||||||
|
|
||||||
export class Unit {
|
export class Unit {
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {Unit} from "./Unit";
|
import {Unit} from "./Unit";
|
||||||
import {validateNumber} from "../core/validators";
|
import {validateNumber} from "../../core/validators";
|
||||||
|
|
||||||
export class Value {
|
export class Value {
|
||||||
|
|
||||||
@ -13,7 +13,8 @@ export class Value {
|
|||||||
readonly value: number,
|
readonly value: number,
|
||||||
readonly unit: Unit,
|
readonly unit: Unit,
|
||||||
readonly decimals: number,
|
readonly decimals: number,
|
||||||
readonly locale: string) {
|
readonly locale: string,
|
||||||
|
) {
|
||||||
this.localeString = this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals});
|
this.localeString = this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals});
|
||||||
this.valueInteger = this.localeString.split(/[,.]/)[0];
|
this.valueInteger = this.localeString.split(/[,.]/)[0];
|
||||||
this.valueFraction = this.localeString.split(/[,.]/)[1];
|
this.valueFraction = this.localeString.split(/[,.]/)[1];
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
import {Value} from '../../value/Value';
|
import {Value} from '../../series/value/Value';
|
||||||
import {NgIf} from '@angular/common';
|
import {NgIf} from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
10
src/main/angular/src/app/shared/tile/tile.component.html
Normal file
10
src/main/angular/src/app/shared/tile/tile.component.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<div class="tile">
|
||||||
|
<div class="tileInner" [class.tilePadding]="padding">
|
||||||
|
<div class="tileHead">
|
||||||
|
<ng-content select="[tile-head]"></ng-content>
|
||||||
|
</div>
|
||||||
|
<div class="tileBody">
|
||||||
|
<ng-content select="[tile-body]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
16
src/main/angular/src/app/shared/tile/tile.component.less
Normal file
16
src/main/angular/src/app/shared/tile/tile.component.less
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.tile {
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tilePadding {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileHead {
|
||||||
|
font-size: 70%;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: underline;
|
||||||
|
margin-bottom: 0.1em;
|
||||||
|
}
|
||||||
14
src/main/angular/src/app/shared/tile/tile.component.ts
Normal file
14
src/main/angular/src/app/shared/tile/tile.component.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tile',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './tile.component.html',
|
||||||
|
styleUrl: './tile.component.less'
|
||||||
|
})
|
||||||
|
export class TileComponent {
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
padding: boolean = true;
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,25 +0,0 @@
|
|||||||
<div class="numberTable">
|
|
||||||
<div class="title">
|
|
||||||
Wetter
|
|
||||||
</div>
|
|
||||||
<div class="day">
|
|
||||||
<div class="hour" *ngFor="let hour of hours">
|
|
||||||
<div class="bar weekdayHolder" *ngIf="hour.date.getHours() === 8">
|
|
||||||
{{ dateFormat(hour.date | date:'E') }}
|
|
||||||
</div>
|
|
||||||
<div class="bar clouds" [style.height]="clouds(hour)"></div>
|
|
||||||
<div class="bar irradiation" [style.height]="irradiation(hour)"></div>
|
|
||||||
<div class="bar precipitation" [style.height]="precipitation(hour)"></div>
|
|
||||||
<div class="bar temperature" [style.height]="temperature(hour)" [ngClass]="temperatureClasses(hour)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="legend">
|
|
||||||
<span class="line temperatureGTE30">≥30°C</span>
|
|
||||||
<span class="line temperatureGTE20">≥20°C</span>
|
|
||||||
<span class="line temperatureGTE10">≥10°C</span>
|
|
||||||
<span class="line temperatureGT0">>0°C</span>
|
|
||||||
<span class="line temperatureNegative">≤0°C</span>
|
|
||||||
<span class="line"> </span>
|
|
||||||
<span class="line">Niederschlag 100% = {{ PRECIPITATION_MAX_MM }}mm</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Data</title>
|
<title>Data</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
<meta name="viewport" content="width=device-width, user-scalable=yes">
|
||||||
<!--suppress HtmlUnknownTarget -->
|
<!--suppress HtmlUnknownTarget -->
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.svg">
|
<link rel="icon" type="image/x-icon" href="favicon.svg">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -56,3 +56,22 @@ table.vertical {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@tile-width: 400px;
|
||||||
|
@font-base: 6vw;
|
||||||
|
|
||||||
|
.generate-tiles(@i, @max) when (@i =< @max) {
|
||||||
|
@media (min-width: @tile-width * @i) {
|
||||||
|
body {
|
||||||
|
font-size: calc(@font-base / @i);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
width: calc(100% / @i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-tiles(@i + 1, @max);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-tiles(1, 10);
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
package de.ph87.data.series;
|
package de.ph87.data.series;
|
||||||
|
|
||||||
import de.ph87.data.value.*;
|
import de.ph87.data.value.Unit;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
import java.time.*;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
@ -39,8 +40,13 @@ public class Series {
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private SeriesType type;
|
private SeriesType type;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column
|
||||||
private boolean graphZero = false;
|
@Nullable
|
||||||
|
public final Double yMin = null;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
public final Double yMax = null;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean autoscale = false;
|
private boolean autoscale = false;
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
package de.ph87.data.series;
|
package de.ph87.data.series;
|
||||||
|
|
||||||
import de.ph87.data.value.*;
|
import de.ph87.data.value.Unit;
|
||||||
import de.ph87.data.web.*;
|
import de.ph87.data.web.IWebSocketMessage;
|
||||||
import lombok.*;
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
import java.time.*;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString
|
@ToString
|
||||||
@ -25,7 +28,11 @@ public class SeriesDto implements IWebSocketMessage {
|
|||||||
|
|
||||||
public final SeriesType type;
|
public final SeriesType type;
|
||||||
|
|
||||||
public final boolean graphZero;
|
@Nullable
|
||||||
|
public final Double yMin;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final Double yMax;
|
||||||
|
|
||||||
public final boolean autoscale;
|
public final boolean autoscale;
|
||||||
|
|
||||||
@ -49,7 +56,8 @@ public class SeriesDto implements IWebSocketMessage {
|
|||||||
this.unit = series.getUnit();
|
this.unit = series.getUnit();
|
||||||
this.decimals = series.getDecimals();
|
this.decimals = series.getDecimals();
|
||||||
this.type = series.getType();
|
this.type = series.getType();
|
||||||
this.graphZero = series.isGraphZero();
|
this.yMin = series.getYMin();
|
||||||
|
this.yMax = series.getYMax();
|
||||||
this.autoscale = series.isAutoscale();
|
this.autoscale = series.isAutoscale();
|
||||||
this.min = series.getMin();
|
this.min = series.getMin();
|
||||||
this.max = series.getMax();
|
this.max = series.getMax();
|
||||||
|
|||||||
@ -74,8 +74,8 @@ public class Graph {
|
|||||||
|
|
||||||
// find bounds
|
// find bounds
|
||||||
double vSum = 0;
|
double vSum = 0;
|
||||||
double vMin = series.isGraphZero() ? 0.0 : Double.MAX_VALUE;
|
double vMin = series.getYMin() == null || Double.isNaN(series.getYMin()) ? Double.MIN_VALUE : series.getYMin();
|
||||||
double vMax = series.isGraphZero() ? 0.0 : Double.MIN_VALUE;
|
double vMax = series.getYMax() == null || Double.isNaN(series.getYMax()) ? Double.MAX_VALUE : series.getYMax();
|
||||||
for (final GraphPoint point : points) {
|
for (final GraphPoint point : points) {
|
||||||
vMin = Math.min(vMin, point.getValue());
|
vMin = Math.min(vMin, point.getValue());
|
||||||
vMax = max(vMax, point.getValue());
|
vMax = max(vMax, point.getValue());
|
||||||
@ -89,7 +89,7 @@ public class Graph {
|
|||||||
vSum *= autoscale.factor;
|
vSum *= autoscale.factor;
|
||||||
|
|
||||||
// find max label width
|
// find max label width
|
||||||
int __maxLabelWidth = 80;
|
int __maxLabelWidth = 0;
|
||||||
final FontMetrics fontMetrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics().getFontMetrics();
|
final FontMetrics fontMetrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics().getFontMetrics();
|
||||||
for (final GraphPoint point : points) {
|
for (final GraphPoint point : points) {
|
||||||
__maxLabelWidth = max(__maxLabelWidth, fontMetrics.stringWidth(autoscale.format(point.getValue() * autoscale.factor)));
|
__maxLabelWidth = max(__maxLabelWidth, fontMetrics.stringWidth(autoscale.format(point.getValue() * autoscale.factor)));
|
||||||
@ -117,33 +117,27 @@ public class Graph {
|
|||||||
public BufferedImage draw() {
|
public BufferedImage draw() {
|
||||||
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
final Graphics2D g = (Graphics2D) image.getGraphics();
|
final Graphics2D g = (Graphics2D) image.getGraphics();
|
||||||
final int fontH3_4 = (int) Math.round(g.getFontMetrics().getHeight() * 0.75);
|
|
||||||
|
|
||||||
// g.setColor(Color.gray);
|
yLabel(g, valueMax, Color.red);
|
||||||
// final String string = "%s [%s]".formatted(series.getTitle(), autoscale.unit);
|
yLabel(g, valueAvg, new Color(0, 255, 0));
|
||||||
// g.drawString(string, border, border + fontH3_4);
|
yLabel(g, valueMin, new Color(64, 128, 255));
|
||||||
|
|
||||||
yLabel(g, valueMax, DASHED, Color.red.darker().darker());
|
|
||||||
// yLabel(g, valueAvg, DASHED, new Color(0, 127, 0));
|
|
||||||
yLabel(g, 0, NORMAL, Color.BLACK);
|
|
||||||
yLabel(g, valueMin, DASHED, new Color(64, 128, 255).darker().darker());
|
|
||||||
|
|
||||||
g.translate(border, height - border);
|
g.translate(border, height - border);
|
||||||
g.scale(1, -1);
|
g.scale(1, -1);
|
||||||
|
|
||||||
|
// y-axis
|
||||||
g.setStroke(NORMAL);
|
g.setStroke(NORMAL);
|
||||||
g.setColor(Color.BLACK);
|
g.setColor(Color.GRAY);
|
||||||
g.drawLine(widthInner, 0, widthInner, heightInner); // y-axis
|
g.drawLine(widthInner, 0, widthInner, heightInner);
|
||||||
|
|
||||||
|
g.setColor(Color.WHITE);
|
||||||
if (series.type == SeriesType.METER) {
|
if (series.type == SeriesType.METER) {
|
||||||
g.setColor(Color.PINK);
|
|
||||||
final int space = (int) (minuteScale * begin.alignment.maxDuration.toMinutes());
|
final int space = (int) (minuteScale * begin.alignment.maxDuration.toMinutes());
|
||||||
final int width = (int) (space * 0.95);
|
final int width = (int) (space * 0.95);
|
||||||
for (final Point point : points) {
|
for (final Point point : points) {
|
||||||
g.fillRect(point.x + (space - width), 0, width, point.y);
|
g.fillRect(point.x + (space - width), 0, width, point.y);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g.setColor(Color.RED);
|
|
||||||
Point last = null;
|
Point last = null;
|
||||||
for (final Point current : points) {
|
for (final Point current : points) {
|
||||||
if (last != null) {
|
if (last != null) {
|
||||||
@ -155,14 +149,14 @@ public class Graph {
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void yLabel(@NonNull final Graphics2D g, final double value, @Nullable final Stroke stroke, @Nullable final Color color) {
|
private void yLabel(@NonNull final Graphics2D g, final double value, @Nullable final Color color) {
|
||||||
final String string = autoscale.format(value);
|
final String string = autoscale.format(value);
|
||||||
final int offset = maxLabelWidth - g.getFontMetrics().stringWidth(string);
|
final int offset = maxLabelWidth - g.getFontMetrics().stringWidth(string);
|
||||||
final int y = height - ((int) Math.round((value - valueMin) * valueScale) + border);
|
final int y = height - ((int) Math.round((value - valueMin) * valueScale) + border);
|
||||||
g.setColor(color);
|
g.setColor(color);
|
||||||
g.drawString(string, widthInner + 2 * border + offset, y + (int) Math.round(g.getFontMetrics().getHeight() * 0.25));
|
g.drawString(string, widthInner + 2 * border + offset, y + (int) Math.round(g.getFontMetrics().getHeight() * 0.25));
|
||||||
if (stroke != null && color != null) {
|
if (color != null) {
|
||||||
g.setStroke(stroke);
|
g.setStroke(Graph.DASHED);
|
||||||
g.draw(new Line2D.Double(border, y, width - maxLabelWidth - border * 1.5, y));
|
g.draw(new Line2D.Double(border, y, width - maxLabelWidth - border * 1.5, y));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
package de.ph87.data.series.graph;
|
package de.ph87.data.series.graph;
|
||||||
|
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.Aligned;
|
||||||
import jakarta.servlet.http.*;
|
import de.ph87.data.series.Alignment;
|
||||||
import lombok.*;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.*;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import javax.imageio.*;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.*;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.IOException;
|
||||||
import java.time.*;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@ -19,12 +23,13 @@ public class GraphController {
|
|||||||
|
|
||||||
private final GraphService graphService;
|
private final GraphService graphService;
|
||||||
|
|
||||||
@GetMapping(path = "{seriesId}/{width}/{height}/{alignmentName}/{offset}/{duration}", produces = "image/png")
|
@GetMapping(path = "{seriesId}/{width}/{height}/{outerAlignmentName}/{offset}/{duration}/{innerAlignmentName}", produces = "image/png")
|
||||||
public void graph(@PathVariable final long seriesId, final HttpServletResponse response, @PathVariable final int width, @PathVariable final int height, @PathVariable final String alignmentName, @PathVariable final long offset, @PathVariable final long duration) throws IOException {
|
public void graph(@PathVariable final long seriesId, final HttpServletResponse response, @PathVariable final int width, @PathVariable final int height, @PathVariable final String outerAlignmentName, @PathVariable final long offset, @PathVariable final long duration, @PathVariable final String innerAlignmentName) throws IOException {
|
||||||
final Alignment alignment = Alignment.valueOf(alignmentName);
|
final Alignment outerAlignment = Alignment.valueOf(outerAlignmentName);
|
||||||
final Aligned end = alignment.align(ZonedDateTime.now()).minus(offset);
|
final Alignment innerAlignment = Alignment.valueOf(innerAlignmentName);
|
||||||
final Aligned begin = end.minus(duration - 1);
|
final Aligned end = outerAlignment.align(ZonedDateTime.now()).plus(1).minus(offset);
|
||||||
final Graph graph = graphService.getGraph(seriesId, begin, end, width, height, 10);
|
final Aligned begin = end.minus(duration);
|
||||||
|
final Graph graph = graphService.getGraph(seriesId, innerAlignment, begin, end, width, height, 10);
|
||||||
final BufferedImage image = graph.draw();
|
final BufferedImage image = graph.draw();
|
||||||
response.setContentType("image/png");
|
response.setContentType("image/png");
|
||||||
ImageIO.write(image, "PNG", response.getOutputStream());
|
ImageIO.write(image, "PNG", response.getOutputStream());
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
package de.ph87.data.series.graph;
|
package de.ph87.data.series.graph;
|
||||||
|
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.Aligned;
|
||||||
import de.ph87.data.series.meter.*;
|
import de.ph87.data.series.Alignment;
|
||||||
import de.ph87.data.series.varying.*;
|
import de.ph87.data.series.SeriesDto;
|
||||||
import lombok.*;
|
import de.ph87.data.series.SeriesService;
|
||||||
import lombok.extern.slf4j.*;
|
import de.ph87.data.series.meter.MeterService;
|
||||||
import org.springframework.stereotype.*;
|
import de.ph87.data.series.varying.VaryingService;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -21,11 +25,11 @@ public class GraphService {
|
|||||||
private final MeterService meterService;
|
private final MeterService meterService;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public Graph getGraph(final long seriesId, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) {
|
public Graph getGraph(final long seriesId, @NonNull final Alignment innerAlignment, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) {
|
||||||
final SeriesDto series = seriesService.getDtoById(seriesId);
|
final SeriesDto series = seriesService.getDtoById(seriesId);
|
||||||
final List<GraphPoint> entries = switch (series.getType()) {
|
final List<GraphPoint> entries = switch (series.getType()) {
|
||||||
case METER -> meterService.getPoints(series, begin, end);
|
case METER -> meterService.getPoints(series, innerAlignment, begin, end);
|
||||||
case VARYING -> varyingService.getPoints(series, begin, end);
|
case VARYING -> varyingService.getPoints(series, innerAlignment, begin, end);
|
||||||
};
|
};
|
||||||
return new Graph(series, entries, begin, end, width, height, border);
|
return new Graph(series, entries, begin, end, width, height, border);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,8 +80,8 @@ public class MeterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<GraphPoint> getPoints(@NonNull final SeriesDto series, @NonNull final Aligned begin, @NonNull final Aligned end) {
|
public List<GraphPoint> getPoints(@NonNull final SeriesDto series, @NonNull final Alignment innerAlignment, @NonNull final Aligned begin, @NonNull final Aligned end) {
|
||||||
final List<? extends MeterValue> graphPoints = findRepository(begin.alignment).findAllByIdMeterSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqualOrderByIdDate(series.id, begin.date, end.date);
|
final List<? extends MeterValue> graphPoints = findRepository(innerAlignment).findAllByIdMeterSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqualOrderByIdDate(series.id, begin.date, end.date);
|
||||||
final List<GraphPoint> points = graphPoints.stream().map(meterValue -> new GraphPoint(meterValue.getId().getDate(), meterValue.getMax() - meterValue.getMin())).collect(Collectors.toCollection(LinkedList::new));
|
final List<GraphPoint> points = graphPoints.stream().map(meterValue -> new GraphPoint(meterValue.getId().getDate(), meterValue.getMax() - meterValue.getMin())).collect(Collectors.toCollection(LinkedList::new));
|
||||||
for (int i = 0; i < points.size() - 1; i++) {
|
for (int i = 0; i < points.size() - 1; i++) {
|
||||||
if (points.get(i).date.compareTo(points.get(i + 1).date) == 0) {
|
if (points.get(i).date.compareTo(points.get(i + 1).date) == 0) {
|
||||||
|
|||||||
@ -1,20 +1,27 @@
|
|||||||
package de.ph87.data.series.varying;
|
package de.ph87.data.series.varying;
|
||||||
|
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.*;
|
||||||
import de.ph87.data.series.graph.*;
|
import de.ph87.data.series.graph.GraphPoint;
|
||||||
import de.ph87.data.series.varying.day.*;
|
import de.ph87.data.series.varying.day.VaryingDay;
|
||||||
import de.ph87.data.series.varying.five.*;
|
import de.ph87.data.series.varying.day.VaryingDayRepository;
|
||||||
import de.ph87.data.series.varying.hour.*;
|
import de.ph87.data.series.varying.five.VaryingFive;
|
||||||
import de.ph87.data.series.varying.month.*;
|
import de.ph87.data.series.varying.five.VaryingFiveRepository;
|
||||||
import de.ph87.data.series.varying.week.*;
|
import de.ph87.data.series.varying.hour.VaryingHour;
|
||||||
import de.ph87.data.series.varying.year.*;
|
import de.ph87.data.series.varying.hour.VaryingHourRepository;
|
||||||
import lombok.*;
|
import de.ph87.data.series.varying.month.VaryingMonth;
|
||||||
import lombok.extern.slf4j.*;
|
import de.ph87.data.series.varying.month.VaryingMonthRepository;
|
||||||
|
import de.ph87.data.series.varying.week.VaryingWeek;
|
||||||
|
import de.ph87.data.series.varying.week.VaryingWeekRepository;
|
||||||
|
import de.ph87.data.series.varying.year.VaryingYear;
|
||||||
|
import de.ph87.data.series.varying.year.VaryingYearRepository;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.*;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.*;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -65,8 +72,8 @@ public class VaryingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<GraphPoint> getPoints(@NonNull final SeriesDto series, @NonNull final Aligned begin, @NonNull final Aligned end) {
|
public List<GraphPoint> getPoints(@NonNull final SeriesDto series, @NonNull final Alignment innerAlignment, @NonNull final Aligned begin, @NonNull final Aligned end) {
|
||||||
return findRepository(begin.alignment)
|
return findRepository(innerAlignment)
|
||||||
.findAllByIdSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqual(series.id, begin.date, end.date)
|
.findAllByIdSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqual(series.id, begin.date, end.date)
|
||||||
.stream()
|
.stream()
|
||||||
.map(v -> new GraphPoint(v.getId().getDate(), v.getAvg()))
|
.map(v -> new GraphPoint(v.getId().getDate(), v.getAvg()))
|
||||||
|
|||||||
@ -1,19 +1,26 @@
|
|||||||
package de.ph87.data.weather;
|
package de.ph87.data.weather;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.*;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.*;
|
import lombok.NonNull;
|
||||||
import lombok.extern.slf4j.*;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.boot.context.event.*;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.scheduling.annotation.*;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.stereotype.*;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.IOException;
|
||||||
import java.net.*;
|
import java.net.HttpURLConnection;
|
||||||
import java.nio.charset.*;
|
import java.net.URI;
|
||||||
import java.time.*;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.format.*;
|
import java.time.LocalDate;
|
||||||
import java.util.*;
|
import java.time.LocalTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -53,8 +60,8 @@ public class WeatherService {
|
|||||||
}
|
}
|
||||||
days = newDays;
|
days = newDays;
|
||||||
log.info("Weather update complete");
|
log.info("Weather update complete");
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
log.error(e.toString());
|
log.error("Failed fetching Weather data!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user