Compare commits
21 Commits
DEPLOY-BAC
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 15b34a2296 | |||
| 4e678d9c65 | |||
| 54e1487300 | |||
| 59b9a5f44f | |||
| 83d8dec332 | |||
| f5e9fd3679 | |||
| 22e5f96d1d | |||
| 2c66314941 | |||
| 9ee8060f05 | |||
| b6dfdd5686 | |||
| 6aac9b2662 | |||
| d7ee5062e4 | |||
| 5358f1b9f6 | |||
| d81034e6c4 | |||
| 49ce44eff9 | |||
| 85a749f199 | |||
| 63548dc3ff | |||
| 0561940861 | |||
| f0f68f3285 | |||
| b4a8b25ae7 | |||
| 6c49f4738a |
2
pom.xml
2
pom.xml
@ -18,7 +18,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.4.2</version>
|
<version>3.4.4</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
@foreground: gray;
|
@foreground: gray;
|
||||||
@background: white;
|
@background: white;
|
||||||
|
@FONT_SELECTABLE: white;
|
||||||
|
|
||||||
@consumption: orange;
|
@consumption: orange;
|
||||||
@purchase: orangered;
|
@purchase: orangered;
|
||||||
@production: dodgerblue;
|
@production: dodgerblue;
|
||||||
|
@cistern: #0760ff;
|
||||||
@self: forestgreen;
|
@self: forestgreen;
|
||||||
@delivery: magenta;
|
@delivery: magenta;
|
||||||
|
|
||||||
@ -46,6 +48,10 @@
|
|||||||
color: @delivery;
|
color: @delivery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cistern {
|
||||||
|
color: @cistern;
|
||||||
|
}
|
||||||
|
|
||||||
.zero {
|
.zero {
|
||||||
filter: opacity(20%);
|
filter: opacity(20%);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
@import "./colors.less";
|
@import "./colors.less";
|
||||||
|
|
||||||
.numberTable {
|
.numberTable {
|
||||||
margin-bottom: 2em;
|
|
||||||
|
|
||||||
.arrowLeft {
|
.arrowLeft {
|
||||||
float: left;
|
float: left;
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"deploy": "scp -P2222 -r dist/angular/browser/* root@10.255.0.1:/srv/Data/www/ && git tag \"DEPLOY-FRONT---$(date +'%F---%H-%M-%S')\""
|
"deploy": "ng build && scp -P2222 -r dist/angular/browser/* root@10.255.0.1:/srv/Data/www/ && git tag \"DEPLOY-FRONT---$(date +'%F---%H-%M-%S')\""
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
155
src/main/angular/src/app/View/View.ts
Normal file
155
src/main/angular/src/app/View/View.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import {Series} from '../series/Series';
|
||||||
|
import {validateNumber, validateString} from '../core/validators';
|
||||||
|
|
||||||
|
export abstract class View {
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
readonly _type_: string,
|
||||||
|
readonly uuid: string,
|
||||||
|
readonly name: string,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any, locale: string): View {
|
||||||
|
const type = validateString(json._type_);
|
||||||
|
switch (type) {
|
||||||
|
case 'literal':
|
||||||
|
return ViewLiteral.fromJson2(json);
|
||||||
|
case 'series':
|
||||||
|
return ViewSeries.fromJson2(json, locale);
|
||||||
|
case 'unary':
|
||||||
|
return ViewUnary.fromJson2(json, locale);
|
||||||
|
case 'binary':
|
||||||
|
return ViewBinary.fromJson2(json, locale);
|
||||||
|
default:
|
||||||
|
throw new Error(`View type '${type}' not implemented.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewLiteral extends View {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
_type_: string,
|
||||||
|
uuid: string,
|
||||||
|
name: string,
|
||||||
|
readonly value: number,
|
||||||
|
) {
|
||||||
|
super(_type_, uuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson2(json: any): ViewLiteral {
|
||||||
|
return new ViewLiteral(
|
||||||
|
validateString(json._type_),
|
||||||
|
validateString(json.uuid),
|
||||||
|
validateString(json.name),
|
||||||
|
validateNumber(json.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static cast(view: View): ViewLiteral {
|
||||||
|
return view as ViewLiteral;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewSeries extends View {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
_type_: string,
|
||||||
|
uuid: string,
|
||||||
|
name: string,
|
||||||
|
readonly series: Series,
|
||||||
|
) {
|
||||||
|
super(_type_, uuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson2(json: any, locale: string): ViewSeries {
|
||||||
|
return new ViewSeries(
|
||||||
|
validateString(json._type_),
|
||||||
|
validateString(json.uuid),
|
||||||
|
validateString(json.name),
|
||||||
|
Series.fromJson(json.series, locale),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static cast(view: View): ViewSeries {
|
||||||
|
return view as ViewSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ViewUnaryOperator {
|
||||||
|
NEG = "NEG",
|
||||||
|
REC = "REC",
|
||||||
|
NOT_NEG = "NOT_NEG",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewUnary extends View {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
_type_: string,
|
||||||
|
uuid: string,
|
||||||
|
name: string,
|
||||||
|
readonly operation: ViewUnaryOperator,
|
||||||
|
readonly view: View,
|
||||||
|
) {
|
||||||
|
super(_type_, uuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson2(json: any, locale: string): ViewUnary {
|
||||||
|
return new ViewUnary(
|
||||||
|
validateString(json._type_),
|
||||||
|
validateString(json.uuid),
|
||||||
|
validateString(json.name),
|
||||||
|
validateString(json.operation) as ViewUnaryOperator,
|
||||||
|
View.fromJson(json.view, locale),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static cast(view: View): ViewUnary {
|
||||||
|
return view as ViewUnary;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ViewBinaryOperator {
|
||||||
|
PLUS = 'PLUS',
|
||||||
|
MINUS = 'MINUS',
|
||||||
|
MULTIPLY = 'MULTIPLY',
|
||||||
|
DIVIDE = 'DIVIDE',
|
||||||
|
MODULO = 'MODULO',
|
||||||
|
PERCENT = 'PERCENT',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewBinary extends View {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
_type_: string,
|
||||||
|
uuid: string,
|
||||||
|
name: string,
|
||||||
|
readonly operation: ViewBinaryOperator,
|
||||||
|
readonly view0: View,
|
||||||
|
readonly view1: View,
|
||||||
|
) {
|
||||||
|
super(_type_, uuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson2(json: any, locale: string): ViewBinary {
|
||||||
|
return new ViewBinary(
|
||||||
|
validateString(json._type_),
|
||||||
|
validateString(json.uuid),
|
||||||
|
validateString(json.name),
|
||||||
|
validateString(json.operation) as ViewBinaryOperator,
|
||||||
|
View.fromJson(json.view0, locale),
|
||||||
|
View.fromJson(json.view1, locale),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static cast(view: View): ViewBinary {
|
||||||
|
return view as ViewBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
<ng-container *ngIf="view && seriesList">
|
||||||
|
|
||||||
|
<div [ngSwitch]="view._type_">
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'literal'">
|
||||||
|
<input type="number" [ngModel]="ViewLiteral.cast(view).value">
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'series'">
|
||||||
|
<select [ngModel]="ViewSeries.cast(view).series.id">
|
||||||
|
<option *ngFor="let series of seriesList" [ngValue]="series.id">{{ series.name }}</option>
|
||||||
|
</select>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'unary'">
|
||||||
|
<select [ngModel]="ViewUnary.cast(view).operation">
|
||||||
|
<option [ngValue]="ViewUnaryOperator.NEG">negiere</option>
|
||||||
|
<option [ngValue]="ViewUnaryOperator.NOT_NEG">nicht negativ</option>
|
||||||
|
<option [ngValue]="ViewUnaryOperator.REC">Kehrwert</option>
|
||||||
|
</select>
|
||||||
|
<div class="children">
|
||||||
|
<div class="child">
|
||||||
|
<app-view-body [view]="ViewUnary.cast(view).view" [seriesList]="seriesList"></app-view-body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'binary'">
|
||||||
|
<div class="children">
|
||||||
|
<div class="child">
|
||||||
|
<app-view-body [view]="ViewBinary.cast(view).view0" [seriesList]="seriesList"></app-view-body>
|
||||||
|
</div>
|
||||||
|
<select class="binary" [ngModel]="ViewBinary.cast(view).operation">
|
||||||
|
<option [ngValue]="ViewBinaryOperator.PLUS">plus</option>
|
||||||
|
<option [ngValue]="ViewBinaryOperator.MINUS">minus</option>
|
||||||
|
<option [ngValue]="ViewBinaryOperator.MULTIPLY">mal</option>
|
||||||
|
<option [ngValue]="ViewBinaryOperator.DIVIDE">geteilt</option>
|
||||||
|
<option [ngValue]="ViewBinaryOperator.MODULO">mod</option>
|
||||||
|
<option [ngValue]="ViewBinaryOperator.PERCENT">Prozent</option>
|
||||||
|
</select>
|
||||||
|
<div class="child">
|
||||||
|
<app-view-body [view]="ViewBinary.cast(view).view1" [seriesList]="seriesList"></app-view-body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
@import "../../../../colors";
|
||||||
|
|
||||||
|
.children {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
border-left: 0.1em solid green;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.child {
|
||||||
|
padding-top: 0.5em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
select.binary {
|
||||||
|
background-color: @background;
|
||||||
|
margin-left: -0.5em;
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
import {NgForOf, NgIf, NgSwitch, NgSwitchCase} from '@angular/common';
|
||||||
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {View, ViewBinary, ViewBinaryOperator, ViewLiteral, ViewSeries, ViewUnary, ViewUnaryOperator} from '../View';
|
||||||
|
import {Series} from '../../series/Series';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-view-body',
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
NgSwitchCase,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgSwitch,
|
||||||
|
FormsModule,
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './view-body.component.html',
|
||||||
|
styleUrl: './view-body.component.less'
|
||||||
|
})
|
||||||
|
export class ViewBodyComponent {
|
||||||
|
|
||||||
|
protected readonly ViewSeries = ViewSeries;
|
||||||
|
|
||||||
|
protected readonly ViewLiteral = ViewLiteral;
|
||||||
|
|
||||||
|
protected readonly ViewUnary = ViewUnary;
|
||||||
|
|
||||||
|
protected readonly ViewUnaryOperator = ViewUnaryOperator;
|
||||||
|
|
||||||
|
protected readonly ViewBinaryOperator = ViewBinaryOperator;
|
||||||
|
|
||||||
|
protected readonly ViewBinary = ViewBinary;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
view?: View;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
seriesList?: Series[] = [];
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<div class="list">
|
||||||
|
<div class="view" *ngFor="let view of rootList">
|
||||||
|
<div class="labelPair">
|
||||||
|
<div class="name">Name:</div>
|
||||||
|
<input [ngModel]="view.name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<app-view-body [view]="view" [seriesList]="seriesList"></app-view-body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
.view {
|
||||||
|
font-size: 80%;
|
||||||
|
|
||||||
|
.labelPair {
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0.5em;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {ViewService} from '../view.service';
|
||||||
|
import {View, ViewLiteral, ViewSeries, ViewUnary, ViewUnaryOperator} from '../View';
|
||||||
|
import {NgForOf} from '@angular/common';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {Series} from '../../series/Series';
|
||||||
|
import {SeriesService} from '../../series/series.service';
|
||||||
|
import {ViewBodyComponent} from '../view-body/view-body.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-view-list',
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
FormsModule,
|
||||||
|
ViewBodyComponent
|
||||||
|
],
|
||||||
|
templateUrl: './view-list.component.html',
|
||||||
|
styleUrl: './view-list.component.less'
|
||||||
|
})
|
||||||
|
export class ViewListComponent implements OnInit {
|
||||||
|
|
||||||
|
protected readonly ViewUnary = ViewUnary;
|
||||||
|
|
||||||
|
protected readonly ViewUnaryOperator = ViewUnaryOperator;
|
||||||
|
|
||||||
|
protected readonly ViewLiteral = ViewLiteral;
|
||||||
|
|
||||||
|
protected readonly ViewSeries = ViewSeries;
|
||||||
|
|
||||||
|
protected seriesList: Series[] = [];
|
||||||
|
|
||||||
|
protected rootList: View[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly viewService: ViewService,
|
||||||
|
readonly seriesService: SeriesService,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.viewService.list(list => this.rootList = list);
|
||||||
|
this.seriesService.all(list => this.seriesList = list);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
src/main/angular/src/app/View/view.service.ts
Normal file
28
src/main/angular/src/app/View/view.service.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
||||||
|
import {ApiService} from '../core/api.service';
|
||||||
|
import {FromJson, Next} from '../core/types';
|
||||||
|
import {View} from './View';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ViewService {
|
||||||
|
|
||||||
|
readonly fromJson: FromJson<View> = json => View.fromJson(json, this.locale);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly api: ApiService,
|
||||||
|
@Inject(LOCALE_ID) readonly locale: string,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
list(next: Next<View[]>) {
|
||||||
|
return this.api.getList(['View', 'rootList'], this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
byUuid(uuid: string, next: Next<View>) {
|
||||||
|
return this.api.getSingle(['View', 'byUuid', uuid], this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1 +1,33 @@
|
|||||||
|
<div class="menubar">
|
||||||
|
|
||||||
|
<div class="menuitem menuitemLeft" *ngFor="let route of menubar()" [routerLink]="[route.routerLink]" routerLinkActive="menuitemActive">{{ route.title }}</div>
|
||||||
|
|
||||||
|
<div class="menuitem electricity">
|
||||||
|
|
||||||
|
<ng-container *ngIf="isDelivering">
|
||||||
|
<div class="halfLine">
|
||||||
|
<div class="delivery">{{ powerDelivery?.formatted2 }}</div>
|
||||||
|
<div> + </div>
|
||||||
|
<div class="self">{{ powerSelf?.formatted2 }}</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!isDelivering">
|
||||||
|
<div class="halfLine">
|
||||||
|
<div class="self">{{ powerProduction?.formatted2 }}</div>
|
||||||
|
<div> + </div>
|
||||||
|
<div class="purchase">{{ powerPurchase?.formatted2 }}</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="halfLine">
|
||||||
|
<div class="delivery">{{ aggregations.energyDeliveredPercent?.formatted2 }}</div>
|
||||||
|
<div> / </div>
|
||||||
|
<div class="purchase">{{ aggregations.energyPurchasedPercent?.formatted2 }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<router-outlet/>
|
<router-outlet/>
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
.menubar {
|
||||||
|
border-bottom: 0.05em solid black;
|
||||||
|
background-color: #303d47;
|
||||||
|
|
||||||
|
.menuitem {
|
||||||
|
padding: 0.1em 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuitemLeft {
|
||||||
|
float: left;
|
||||||
|
border-right: 0.05em solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuitemRight {
|
||||||
|
float: right;
|
||||||
|
border-left: 0.05em solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuitemActive {
|
||||||
|
color: white;
|
||||||
|
background-color: #006ebc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.electricity {
|
||||||
|
float: right;
|
||||||
|
padding-right: 0.25em;
|
||||||
|
font-size: 55%;
|
||||||
|
|
||||||
|
.halfLine {
|
||||||
|
|
||||||
|
div {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,12 +1,68 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {RouterOutlet} from '@angular/router';
|
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||||
|
import {menubar} from './app.routes';
|
||||||
|
import {NgForOf, NgIf} from '@angular/common';
|
||||||
|
import {SeriesService} from './series/series.service';
|
||||||
|
import {Subscription, timer} from 'rxjs';
|
||||||
|
import {Value} from './series/value/Value';
|
||||||
|
import {AggregationWrapperDto} from './series/AggregationWrapperDto';
|
||||||
|
import {Alignment} from './series/Alignment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet],
|
imports: [RouterOutlet, RouterLink, NgForOf, RouterLinkActive, NgIf],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.less'
|
styleUrl: './app.component.less'
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly menubar = menubar;
|
||||||
|
|
||||||
|
protected aggregations: AggregationWrapperDto = AggregationWrapperDto.EMPTY;
|
||||||
|
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly seriesService: SeriesService,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.seriesService.subscribeAny());
|
||||||
|
this.subs.push(timer(0, 5000).subscribe(() => this.fetch()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDelivering(): boolean {
|
||||||
|
return (this.powerDelivery?.value || 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get powerProduction(): Value | undefined {
|
||||||
|
return this.seriesService.powerProduced.series?.lastValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get powerBalance(): Value | undefined {
|
||||||
|
return this.seriesService.powerBalance.series?.lastValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get powerSelf(): Value | undefined {
|
||||||
|
return this.powerProduction?.minus(this.powerDelivery)?.notNegative();
|
||||||
|
}
|
||||||
|
|
||||||
|
get powerPurchase(): Value | undefined {
|
||||||
|
return this.powerBalance?.notNegative();
|
||||||
|
}
|
||||||
|
|
||||||
|
get powerDelivery(): Value | undefined {
|
||||||
|
return this.powerBalance?.negate()?.notNegative();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetch() {
|
||||||
|
this.seriesService.aggregations(Alignment.DAY, 0, aggregations => this.aggregations = aggregations);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,40 @@
|
|||||||
import {Routes} from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
import {DashboardComponent} from './dashboard/dashboard.component';
|
import {LiveComponent} from './live/live.component';
|
||||||
|
import {GreenhouseComponent} from './live/greenhouse/greenhouse/greenhouse.component';
|
||||||
|
import {HistoryComponent} from './history/history.component';
|
||||||
|
import {ViewListComponent} from './View/view-list/view-list.component';
|
||||||
|
|
||||||
|
export class Path {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly path: string,
|
||||||
|
readonly title: string,
|
||||||
|
readonly menu: boolean,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
get routerLink(): string {
|
||||||
|
return `/${this.path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROUTING = {
|
||||||
|
LIVE: new Path('Live', 'Live', true),
|
||||||
|
HISTORY: new Path('History', 'History', true),
|
||||||
|
VIEW_LIST: new Path('ViewList', 'Views', true),
|
||||||
|
GREENHOUSE: new Path('Greenhouse', 'Gewächshaus', false),
|
||||||
|
}
|
||||||
|
|
||||||
|
export function menubar(): Path[] {
|
||||||
|
return Object.values(ROUTING).filter(v => v.menu);
|
||||||
|
}
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: 'Dashboard', component: DashboardComponent},
|
{path: ROUTING.LIVE.path, component: LiveComponent},
|
||||||
{path: '**', redirectTo: 'Dashboard'},
|
{path: ROUTING.HISTORY.path, component: HistoryComponent},
|
||||||
|
{path: ROUTING.GREENHOUSE.path, component: GreenhouseComponent},
|
||||||
|
{path: ROUTING.VIEW_LIST.path, component: ViewListComponent},
|
||||||
|
{path: '**', redirectTo: ROUTING.LIVE.path},
|
||||||
];
|
];
|
||||||
|
|||||||
91
src/main/angular/src/app/core/AbstractRepositoryService.ts
Normal file
91
src/main/angular/src/app/core/AbstractRepositoryService.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import {Subscription} from "rxjs";
|
||||||
|
import {Next} from "./types";
|
||||||
|
import {Series} from "../series/Series";
|
||||||
|
import {SeriesWrapper} from "../series/SeriesWrapper";
|
||||||
|
import {Inject, LOCALE_ID} from "@angular/core";
|
||||||
|
import {ApiService} from "./api.service";
|
||||||
|
|
||||||
|
export abstract class AbstractRepositoryService {
|
||||||
|
|
||||||
|
private readonly clientSubscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
private readonly clientCallbacks: Next<Series>[] = [];
|
||||||
|
|
||||||
|
protected abstract get liveValues(): SeriesWrapper[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(LOCALE_ID) readonly locale: string,
|
||||||
|
protected readonly api: ApiService,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSubscribe(subscription: Subscription): Subscription {
|
||||||
|
this.clientSubscriptions.push(subscription);
|
||||||
|
this.ensureApiSubscribed();
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onUnsubscribe(subscription: Subscription): Subscription {
|
||||||
|
this.clientSubscriptions.splice(this.clientSubscriptions.indexOf(subscription), 1);
|
||||||
|
if (this.clientSubscriptions.length === 0) {
|
||||||
|
this.ensureApiUnsubscribed();
|
||||||
|
}
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureApiSubscribed() {
|
||||||
|
if (this.subs.length !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.subs.push(this.api.subscribe(['Series'], j => Series.fromJson(j, this.locale), series => this.update(series)));
|
||||||
|
this.subs.push(this.api.subscribeConnection(connected => {
|
||||||
|
if (connected) {
|
||||||
|
this.all();
|
||||||
|
} else {
|
||||||
|
this.liveValues.forEach(liveValue => liveValue.series = null);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureApiUnsubscribed() {
|
||||||
|
if (this.subs.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
this.subs.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private update(series: Series) {
|
||||||
|
this.liveValues
|
||||||
|
.filter(liveValue => liveValue.name === series.name)
|
||||||
|
.forEach(liveValue => liveValue.series = series);
|
||||||
|
this.clientCallbacks.forEach(next => next(series));
|
||||||
|
}
|
||||||
|
|
||||||
|
all(next?: Next<Series[]>) {
|
||||||
|
this.api.getList(['Series', 'all'], j => Series.fromJson(j, this.locale), list => {
|
||||||
|
list.forEach(item => this.update(item));
|
||||||
|
if (next) {
|
||||||
|
next(list);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeAny(next?: Next<Series>): Subscription {
|
||||||
|
const wrapper: Next<Series> = series => { // to let clientCallbacks only contain unique instances
|
||||||
|
if (next) {
|
||||||
|
next(series);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.clientCallbacks.push(wrapper);
|
||||||
|
const subscription = new Subscription(() => {
|
||||||
|
this.onUnsubscribe(subscription);
|
||||||
|
this.clientCallbacks.splice(this.clientCallbacks.indexOf(wrapper), 1);
|
||||||
|
});
|
||||||
|
return this.onSubscribe(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import {ElectroEnergyComponent} from "../electro/energy/electro-energy.component";
|
|
||||||
import {ElectroPowerComponent} from "../electro/power/electro-power.component";
|
|
||||||
import {WeatherDiagramComponent} from '../weather/weather-diagram/weather-diagram.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-dashboard',
|
|
||||||
imports: [
|
|
||||||
ElectroEnergyComponent,
|
|
||||||
ElectroPowerComponent,
|
|
||||||
WeatherDiagramComponent
|
|
||||||
],
|
|
||||||
templateUrl: './dashboard.component.html',
|
|
||||||
styleUrl: './dashboard.component.less'
|
|
||||||
})
|
|
||||||
export class DashboardComponent {
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
<div class="numberTable">
|
|
||||||
|
|
||||||
<div class="title">
|
|
||||||
Energie
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="option">
|
|
||||||
<button class="arrowLeft" (click)="shiftAlignment(+1)">←</button>
|
|
||||||
{{ alignment.display }} {{ offset > 0 ? -offset : '' }}
|
|
||||||
<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">Verbraucht</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">Bezogen</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">Produziert</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">Selbst verbr.</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">Eingespeist</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 *ngIf="alignment.inner">
|
||||||
|
<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,25 @@
|
|||||||
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 aggregations: AggregationWrapperDto = AggregationWrapperDto.EMPTY;
|
protected aggregations: AggregationWrapperDto = AggregationWrapperDto.EMPTY;
|
||||||
|
|
||||||
23
src/main/angular/src/app/live/cistern/cistern.component.html
Normal file
23
src/main/angular/src/app/live/cistern/cistern.component.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<app-tile>
|
||||||
|
|
||||||
|
<div tile-head>
|
||||||
|
Zisterne
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div tile-body>
|
||||||
|
|
||||||
|
<div class="numberTable">
|
||||||
|
<div class="content">
|
||||||
|
<div class="entry cistern">
|
||||||
|
<div class="name">Volumen</div>
|
||||||
|
<div class="percent">{{ seriesService.cisternVolume.series?.lastValue?.percent(2000)?.localeString }} %</div>
|
||||||
|
<div class="value">{{ seriesService.cisternVolume.series?.lastValue?.localeString }} {{ seriesService.cisternVolume.series?.unit?.unit }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img width="100%" *ngIf="seriesService.cisternVolume.series" [src]="graph(seriesService.cisternVolume.series)" [alt]="seriesService.cisternVolume.series.title">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</app-tile>
|
||||||
39
src/main/angular/src/app/live/cistern/cistern.component.ts
Normal file
39
src/main/angular/src/app/live/cistern/cistern.component.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {SeriesService} from '../../series/series.service';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {NgIf} from '@angular/common';
|
||||||
|
import {Series} from '../../series/Series';
|
||||||
|
import {TileComponent} from '../../shared/tile/tile.component';
|
||||||
|
import {Alignment} from '../../series/Alignment';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-cistern',
|
||||||
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
TileComponent
|
||||||
|
],
|
||||||
|
templateUrl: './cistern.component.html',
|
||||||
|
styleUrl: './cistern.component.less'
|
||||||
|
})
|
||||||
|
export class CisternComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly seriesService: SeriesService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.seriesService.subscribeAny());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
graph(series: Series) {
|
||||||
|
return this.seriesService.graph(series, 600, 200, Alignment.FIVE, 0, Alignment.FIVE, 24 * 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<table class="vertical">
|
||||||
|
<tr>
|
||||||
|
<th>Temperatur</th>
|
||||||
|
<td class="valueInteger">{{seriesService.greenhouseTemperature.series?.lastValue?.localeString}}</td>
|
||||||
|
<td class="unit">{{seriesService.greenhouseTemperature.series?.lastValue?.unit?.unit}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Relative Luftfeuchte</th>
|
||||||
|
<td class="valueInteger">{{seriesService.greenhouseHumidityRelative.series?.lastValue?.localeString}}</td>
|
||||||
|
<td class="unit">{{seriesService.greenhouseHumidityRelative.series?.lastValue?.unit?.unit}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Absolute Luftfeuchte</th>
|
||||||
|
<td class="valueInteger">{{seriesService.greenhouseHumidityAbsolute.series?.lastValue?.localeString}}</td>
|
||||||
|
<td class="unit">{{seriesService.greenhouseHumidityAbsolute.series?.lastValue?.unit?.unit}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Beleuchtungsstärke</th>
|
||||||
|
<td class="valueInteger">{{seriesService.greenhouseIlluminance.series?.lastValue?.localeString}}</td>
|
||||||
|
<td class="unit">{{seriesService.greenhouseIlluminance.series?.lastValue?.unit?.unit}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {SeriesService} from '../../../series/series.service';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-greenhouse',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './greenhouse.component.html',
|
||||||
|
styleUrl: './greenhouse.component.less'
|
||||||
|
})
|
||||||
|
export class GreenhouseComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly seriesService: SeriesService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.seriesService.subscribeAny());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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>
|
||||||
0
src/main/angular/src/app/live/live.component.less
Normal file
0
src/main/angular/src/app/live/live.component.less
Normal file
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,42 @@
|
|||||||
|
<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">Verbrauch</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">Eigenverbrauch</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>
|
||||||
|
|
||||||
|
<app-percent-bar [produktion]="powerProduction" [self]="powerSelf" [purchase]="powerPurchase" [delivery]="powerDelivery"></app-percent-bar>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</app-tile>
|
||||||
@ -1,23 +1,36 @@
|
|||||||
import {Component} 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 {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'
|
||||||
})
|
})
|
||||||
export class ElectroPowerComponent {
|
export class ElectroPowerComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly seriesService: SeriesService,
|
readonly seriesService: SeriesService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.seriesService.subscribeAny());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
get powerProduction(): Value | undefined {
|
get powerProduction(): Value | undefined {
|
||||||
return this.seriesService.powerProduced.series?.lastValue;
|
return this.seriesService.powerProduced.series?.lastValue;
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
|
|
||||||
@ -8,6 +8,7 @@ export class WeatherHour {
|
|||||||
readonly clouds: Value,
|
readonly clouds: Value,
|
||||||
readonly irradiation: Value,
|
readonly irradiation: Value,
|
||||||
readonly precipitation: Value,
|
readonly precipitation: Value,
|
||||||
|
readonly temperature: Value,
|
||||||
) {
|
) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@ -18,6 +19,7 @@ export class WeatherHour {
|
|||||||
Value.fromJson2(json['clouds'], locale),
|
Value.fromJson2(json['clouds'], locale),
|
||||||
Value.fromJson2(json['irradiation'], locale),
|
Value.fromJson2(json['irradiation'], locale),
|
||||||
Value.fromJson2(json['precipitation'], locale),
|
Value.fromJson2(json['precipitation'], locale),
|
||||||
|
Value.fromJson2(json['temperature'], locale),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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>
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
.day {
|
||||||
|
display: flex;
|
||||||
|
height: 3em;
|
||||||
|
background-color: #2d4255;
|
||||||
|
|
||||||
|
.hour {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: 100%; // any width will do (flex cares about real with)
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clouds {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.irradiation {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.precipitation {
|
||||||
|
background-color: blue;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
border-top: 0.06em solid black;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureGTE30 {
|
||||||
|
border-top-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureGTE20 {
|
||||||
|
border-top-color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureGTE10 {
|
||||||
|
border-top-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureGT0 {
|
||||||
|
border-top-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureNegative {
|
||||||
|
border-top-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekdayHolder {
|
||||||
|
overflow: visible;
|
||||||
|
opacity: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.line {
|
||||||
|
padding: 0 0.25em;
|
||||||
|
font-size: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureGTE30 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureGTE20 {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureGTE10 {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureGT0 {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperatureNegative {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {NgForOf} from '@angular/common';
|
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;
|
||||||
|
|
||||||
@ -11,13 +12,19 @@ const DAY_COUNT = 7;
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-weather-diagram',
|
selector: 'app-weather-diagram',
|
||||||
imports: [
|
imports: [
|
||||||
NgForOf
|
NgForOf,
|
||||||
|
NgIf,
|
||||||
|
DatePipe,
|
||||||
|
NgClass,
|
||||||
|
TileComponent
|
||||||
],
|
],
|
||||||
templateUrl: './weather-diagram.component.html',
|
templateUrl: './weather-diagram.component.html',
|
||||||
styleUrl: './weather-diagram.component.less'
|
styleUrl: './weather-diagram.component.less'
|
||||||
})
|
})
|
||||||
export class WeatherDiagramComponent implements OnInit {
|
export class WeatherDiagramComponent implements OnInit {
|
||||||
|
|
||||||
|
protected readonly PRECIPITATION_MAX_MM = 15;
|
||||||
|
|
||||||
protected days: WeatherDay[] = [];
|
protected days: WeatherDay[] = [];
|
||||||
|
|
||||||
protected hours: WeatherHour[] = [];
|
protected hours: WeatherHour[] = [];
|
||||||
@ -44,7 +51,11 @@ export class WeatherDiagramComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
precipitation(hour: WeatherHour) {
|
precipitation(hour: WeatherHour) {
|
||||||
return (hour.precipitation.percent(15)?.value || 0) + '%';
|
return (hour.precipitation.percent(this.PRECIPITATION_MAX_MM)?.value || 0) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
temperature(hour: WeatherHour) {
|
||||||
|
return (hour.temperature.plus(10)?.percent(50)?.value || 0) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateHours() {
|
private updateHours() {
|
||||||
@ -74,4 +85,26 @@ export class WeatherDiagramComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dateFormat(date: string | null) {
|
||||||
|
if (!date) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return date.substring(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
temperatureClasses(hour: WeatherHour): {} {
|
||||||
|
const temperatureGTE30 = hour.temperature.gte(30);
|
||||||
|
const temperatureGTE20 = hour.temperature.gte(20);
|
||||||
|
const temperatureGTE10 = hour.temperature.gte(10);
|
||||||
|
const temperatureGT0 = hour.temperature.gt(0);
|
||||||
|
const temperatureNegative = hour.temperature.lte(0);
|
||||||
|
return {
|
||||||
|
"temperatureGTE30": temperatureGTE30,
|
||||||
|
"temperatureGTE20": temperatureGTE20 && !temperatureGTE30,
|
||||||
|
"temperatureGTE10": temperatureGTE10 && !temperatureGTE20,
|
||||||
|
"temperatureGT0": temperatureGT0 && !temperatureGTE10,
|
||||||
|
"temperatureNegative": temperatureNegative
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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,22 +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', Alignment.offsetTitleHour);
|
static readonly HOUR = new Alignment('HOUR', 'Stunde', 'n', Alignment.offsetTitleHour, Alignment.FIVE, 12);
|
||||||
|
|
||||||
static readonly DAY = new Alignment('DAY', 'Tag', Alignment.offsetTitleDay);
|
static readonly DAY = new Alignment('DAY', 'Tag', 'e', Alignment.offsetTitleDay, Alignment.FIVE, 24 * 12);
|
||||||
|
|
||||||
static readonly WEEK = new Alignment('WEEK', 'Woche', Alignment.offsetTitleWeek);
|
static readonly WEEK = new Alignment('WEEK', 'Woche', 'n', Alignment.offsetTitleWeek, Alignment.HOUR, 7 * 24);
|
||||||
|
|
||||||
static readonly MONTH = new Alignment('MONTH', 'Monat', Alignment.offsetTitleMonth);
|
static readonly MONTH = new Alignment('MONTH', 'Monat', 'e', Alignment.offsetTitleMonth, Alignment.HOUR, 30 * 24);
|
||||||
|
|
||||||
static readonly YEAR = new Alignment('YEAR', 'Jahr', 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 offsetTitle: (offset: number, locale: string) => string
|
readonly plural: string,
|
||||||
|
readonly offsetTitle: (offset: number, locale: string) => string,
|
||||||
|
readonly inner: Alignment | null,
|
||||||
|
readonly innerCount: number,
|
||||||
) {
|
) {
|
||||||
Alignment.values.push(this);
|
Alignment.values.push(this);
|
||||||
}
|
}
|
||||||
@ -138,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 {
|
||||||
|
|
||||||
|
|||||||
@ -2,21 +2,15 @@ import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
|||||||
import {ApiService} from '../core/api.service';
|
import {ApiService} from '../core/api.service';
|
||||||
import {Alignment} from './Alignment';
|
import {Alignment} from './Alignment';
|
||||||
import {AggregationWrapperDto} from './AggregationWrapperDto';
|
import {AggregationWrapperDto} from './AggregationWrapperDto';
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {Series} from './Series';
|
import {Series} from './Series';
|
||||||
import {SeriesWrapper} from './SeriesWrapper';
|
import {SeriesWrapper} from './SeriesWrapper';
|
||||||
import {Next} from '../core/types';
|
import {Next} from '../core/types';
|
||||||
|
import {AbstractRepositoryService} from '../core/AbstractRepositoryService';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class SeriesService {
|
export class SeriesService extends AbstractRepositoryService {
|
||||||
|
|
||||||
private readonly clientSubscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
private readonly subs: Subscription[] = [];
|
|
||||||
|
|
||||||
private readonly clientCallbacks: Next<Series>[] = [];
|
|
||||||
|
|
||||||
readonly powerConsumed: SeriesWrapper = new SeriesWrapper("power/consumed", this.onSubscribe, this.onUnsubscribe);
|
readonly powerConsumed: SeriesWrapper = new SeriesWrapper("power/consumed", this.onSubscribe, this.onUnsubscribe);
|
||||||
|
|
||||||
@ -26,88 +20,49 @@ export class SeriesService {
|
|||||||
|
|
||||||
readonly powerBalance: SeriesWrapper = new SeriesWrapper("power/balance", this.onSubscribe, this.onUnsubscribe);
|
readonly powerBalance: SeriesWrapper = new SeriesWrapper("power/balance", this.onSubscribe, this.onUnsubscribe);
|
||||||
|
|
||||||
readonly liveValues: SeriesWrapper[] = [
|
readonly greenhouseTemperature: SeriesWrapper = new SeriesWrapper("greenhouse/temperature", this.onSubscribe, this.onUnsubscribe);
|
||||||
|
|
||||||
|
readonly greenhouseHumidityRelative: SeriesWrapper = new SeriesWrapper("greenhouse/humidity/relative", this.onSubscribe, this.onUnsubscribe);
|
||||||
|
|
||||||
|
readonly greenhouseHumidityAbsolute: SeriesWrapper = new SeriesWrapper("greenhouse/humidity/absolute", this.onSubscribe, this.onUnsubscribe);
|
||||||
|
|
||||||
|
readonly greenhouseIlluminance: SeriesWrapper = new SeriesWrapper("greenhouse/illuminance", this.onSubscribe, this.onUnsubscribe);
|
||||||
|
|
||||||
|
readonly cisternHeight: SeriesWrapper = new SeriesWrapper("cistern/height", this.onSubscribe, this.onUnsubscribe);
|
||||||
|
|
||||||
|
readonly cisternVolume: SeriesWrapper = new SeriesWrapper("cistern/volume", this.onSubscribe, this.onUnsubscribe);
|
||||||
|
|
||||||
|
protected get liveValues(): SeriesWrapper[] {
|
||||||
|
return [
|
||||||
this.powerConsumed,
|
this.powerConsumed,
|
||||||
this.powerProduced,
|
this.powerProduced,
|
||||||
this.powerSelf,
|
this.powerSelf,
|
||||||
this.powerBalance,
|
this.powerBalance,
|
||||||
]
|
this.greenhouseTemperature,
|
||||||
|
this.greenhouseHumidityRelative,
|
||||||
|
this.greenhouseHumidityAbsolute,
|
||||||
|
this.greenhouseIlluminance,
|
||||||
|
this.cisternHeight,
|
||||||
|
this.cisternVolume,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LOCALE_ID) readonly locale: string,
|
@Inject(LOCALE_ID) locale: string,
|
||||||
protected readonly api: ApiService,
|
api: ApiService,
|
||||||
) {
|
) {
|
||||||
//
|
super(locale, api);
|
||||||
}
|
|
||||||
|
|
||||||
private onSubscribe(subscription: Subscription): Subscription {
|
|
||||||
this.clientSubscriptions.push(subscription);
|
|
||||||
this.ensureApiSubscribed();
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onUnsubscribe(subscription: Subscription): Subscription {
|
|
||||||
this.clientSubscriptions.splice(this.clientSubscriptions.indexOf(subscription), 1);
|
|
||||||
if (this.clientSubscriptions.length === 0) {
|
|
||||||
this.ensureApiUnsubscribed();
|
|
||||||
}
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureApiSubscribed() {
|
|
||||||
if (this.subs.length !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.subs.push(this.api.subscribe(['Series'], j => Series.fromJson(j, this.locale), series => this.update(series)));
|
|
||||||
this.subs.push(this.api.subscribeConnection(connected => {
|
|
||||||
if (connected) {
|
|
||||||
this.all();
|
|
||||||
} else {
|
|
||||||
this.liveValues.forEach(liveValue => liveValue.series = null);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureApiUnsubscribed() {
|
|
||||||
if (this.subs.length <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.subs.forEach(sub => sub.unsubscribe());
|
|
||||||
this.subs.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private update(series: Series) {
|
|
||||||
this.liveValues
|
|
||||||
.filter(liveValue => liveValue.name === series.name)
|
|
||||||
.forEach(liveValue => liveValue.series = series);
|
|
||||||
this.clientCallbacks.forEach(next => next(series));
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribeAny(next?: Next<Series>): Subscription {
|
|
||||||
const wrapper: Next<Series> = series => { // to let clientCallbacks only contain unique instances
|
|
||||||
if (next) {
|
|
||||||
next(series);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.clientCallbacks.push(wrapper);
|
|
||||||
const subscription = new Subscription(() => {
|
|
||||||
this.onUnsubscribe(subscription);
|
|
||||||
this.clientCallbacks.splice(this.clientCallbacks.indexOf(wrapper), 1);
|
|
||||||
});
|
|
||||||
return this.onSubscribe(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
all(next?: Next<Series[]>) {
|
|
||||||
this.api.getList(['Series', 'all'], j => Series.fromJson(j, this.locale), list => {
|
|
||||||
list.forEach(item => this.update(item));
|
|
||||||
if (next) {
|
|
||||||
next(list);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregations(alignment: Alignment, offset: number, next: Next<AggregationWrapperDto>) {
|
aggregations(alignment: Alignment, offset: number, next: Next<AggregationWrapperDto>) {
|
||||||
this.api.getSingle(['Series', 'agg', 'all', alignment.name, 'offset', offset], 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 {
|
||||||
|
|
||||||
@ -20,6 +20,26 @@ export class Unit {
|
|||||||
|
|
||||||
static readonly PRECIPITATION_MM = new Unit('PRECIPITATION_MM', 'mm');
|
static readonly PRECIPITATION_MM = new Unit('PRECIPITATION_MM', 'mm');
|
||||||
|
|
||||||
|
static readonly TEMPERATURE_C = new Unit('TEMPERATURE_C', '°C');
|
||||||
|
|
||||||
|
static readonly HUMIDITY_RELATIVE_PERCENT = new Unit('HUMIDITY_RELATIVE_PERCENT', '%');
|
||||||
|
|
||||||
|
static readonly HUMIDITY_ABSOLUTE_GM3 = new Unit('HUMIDITY_ABSOLUTE_GM3', 'g/m³');
|
||||||
|
|
||||||
|
static readonly ILLUMINANCE_LUX = new Unit('ILLUMINANCE_LUX', 'lux');
|
||||||
|
|
||||||
|
static readonly BOOLEAN = new Unit('BOOLEAN', '');
|
||||||
|
|
||||||
|
static readonly DOOR_BOOLEAN = new Unit('DOOR_BOOLEAN', '');
|
||||||
|
|
||||||
|
static readonly WINDOW_BOOLEAN = new Unit('WINDOW_BOOLEAN', '');
|
||||||
|
|
||||||
|
static readonly LIGHT_BOOLEAN = new Unit('LIGHT_BOOLEAN', '');
|
||||||
|
|
||||||
|
static readonly VOLUME_L = new Unit('VOLUME_L', 'l');
|
||||||
|
|
||||||
|
static readonly LENGTH_CM = new Unit('LENGTH_CM', 'cm');
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
readonly unit: string,
|
readonly unit: string,
|
||||||
119
src/main/angular/src/app/series/value/Value.ts
Normal file
119
src/main/angular/src/app/series/value/Value.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import {Unit} from "./Unit";
|
||||||
|
import {validateNumber} from "../../core/validators";
|
||||||
|
|
||||||
|
export class Value {
|
||||||
|
|
||||||
|
readonly localeString: string;
|
||||||
|
|
||||||
|
readonly valueInteger: string;
|
||||||
|
|
||||||
|
readonly valueFraction: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly value: number,
|
||||||
|
readonly unit: Unit,
|
||||||
|
readonly decimals: number,
|
||||||
|
readonly locale: string,
|
||||||
|
) {
|
||||||
|
this.localeString = this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals});
|
||||||
|
this.valueInteger = this.localeString.split(/[,.]/)[0];
|
||||||
|
this.valueFraction = this.localeString.split(/[,.]/)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson2(json: any, locale: string): Value {
|
||||||
|
return new Value(
|
||||||
|
validateNumber(json['value']),
|
||||||
|
Unit.fromJson(json['unit']),
|
||||||
|
validateNumber(json['decimals']),
|
||||||
|
locale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(value: any, unit: Unit, decimals: number, locale: string): Value {
|
||||||
|
return new Value(
|
||||||
|
validateNumber(value),
|
||||||
|
unit,
|
||||||
|
decimals,
|
||||||
|
locale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get zero(): boolean {
|
||||||
|
return this.value === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get formatted(): string {
|
||||||
|
return `${(this.localeString)} ${this.unit.unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get formatted2(): string {
|
||||||
|
return `${(this.localeString)}${this.unit.unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
negate() {
|
||||||
|
return new Value(-this.value, this.unit, this.decimals, this.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
plus(other: Value | number | undefined | null): Value | undefined {
|
||||||
|
const v = this.extractValue(other);
|
||||||
|
return v === undefined ? undefined : new Value(this.value + v, this.unit, this.decimals, this.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
minus(other: Value | number | undefined | null): Value | undefined {
|
||||||
|
const v = this.extractValue(other);
|
||||||
|
return v === undefined ? undefined : new Value(this.value - v, this.unit, this.decimals, this.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
gte(other: Value | number | undefined | null): boolean | undefined {
|
||||||
|
const v = this.extractValue(other);
|
||||||
|
return v === undefined ? undefined : this.value >= v;
|
||||||
|
}
|
||||||
|
|
||||||
|
gt(other: Value | number | undefined | null): boolean | undefined {
|
||||||
|
const v = this.extractValue(other);
|
||||||
|
return v === undefined ? undefined : this.value > v;
|
||||||
|
}
|
||||||
|
|
||||||
|
lte(other: Value | number | undefined | null): boolean | undefined {
|
||||||
|
const v = this.extractValue(other);
|
||||||
|
return v === undefined ? undefined : this.value <= v;
|
||||||
|
}
|
||||||
|
|
||||||
|
lt(other: Value | number | undefined | null): boolean | undefined {
|
||||||
|
const v = this.extractValue(other);
|
||||||
|
return v === undefined ? undefined : this.value < v;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractValue(other: Value | number | undefined | null): number | undefined {
|
||||||
|
if (other === undefined || other === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (other instanceof Value && other.unit !== this.unit) {
|
||||||
|
throw new Error(`Unit mismatch: this=${JSON.stringify(this)}, other=${JSON.stringify(other)}`);
|
||||||
|
}
|
||||||
|
return typeof other === "number" ? other : other.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
notNegative(): Value {
|
||||||
|
if (this.value < 0) {
|
||||||
|
return this.toZero();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toZero(): Value {
|
||||||
|
if (this.value === 0) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new Value(0, this.unit, this.decimals, this.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
percent(other: Value | number | undefined): Value | undefined {
|
||||||
|
const v = other instanceof Value ? other.value : typeof other === "number" ? other : 0;
|
||||||
|
if (v === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return new Value(this.value / v * 100, Unit.PERCENT, 0, this.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,18 +1,22 @@
|
|||||||
@import "../../../../colors.less";
|
@import "../../../../colors.less";
|
||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
position: relative;
|
display: flex;
|
||||||
color: white;
|
border-radius: 0.5em;
|
||||||
|
|
||||||
.part {
|
.part {
|
||||||
float: left;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 40%;
|
font-size: 40%;
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
padding-left: 0.25em;
|
padding-left: 0.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.part:first-child {
|
||||||
|
.text:first-child {
|
||||||
|
padding-left: 0.8em;
|
||||||
}
|
}
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.self {
|
.self {
|
||||||
|
|||||||
@ -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,88 +0,0 @@
|
|||||||
import {Unit} from "./Unit";
|
|
||||||
import {validateNumber} from "../core/validators";
|
|
||||||
|
|
||||||
export class Value {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly value: number,
|
|
||||||
readonly unit: Unit,
|
|
||||||
readonly decimals: number,
|
|
||||||
readonly locale: string) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJson2(json: any, locale: string): Value {
|
|
||||||
return new Value(
|
|
||||||
validateNumber(json['value']),
|
|
||||||
Unit.fromJson(json['unit']),
|
|
||||||
validateNumber(json['decimals']),
|
|
||||||
locale
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJson(value: any, unit: Unit, decimals: number, locale: string): Value {
|
|
||||||
return new Value(
|
|
||||||
validateNumber(value),
|
|
||||||
unit,
|
|
||||||
decimals,
|
|
||||||
locale
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get zero(): boolean {
|
|
||||||
return this.value === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get formatted(): string {
|
|
||||||
return `${(this.localeString)} ${this.unit.unit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get formatted2(): string {
|
|
||||||
return `${(this.localeString)}${this.unit.unit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get localeString(): string {
|
|
||||||
return this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals});
|
|
||||||
}
|
|
||||||
|
|
||||||
negate() {
|
|
||||||
return new Value(-this.value, this.unit, this.decimals, this.locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
plus(other: Value | undefined): Value | undefined {
|
|
||||||
if (!other) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return new Value(this.value + other.value, this.unit, this.decimals, this.locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
minus(other: Value | undefined): Value | undefined {
|
|
||||||
if (!other) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return new Value(this.value - other.value, this.unit, this.decimals, this.locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
notNegative(): Value {
|
|
||||||
if (this.value < 0) {
|
|
||||||
return this.toZero();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
toZero(): Value {
|
|
||||||
if (this.value === 0) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
return new Value(0, this.unit, this.decimals, this.locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
percent(other: Value | number | undefined): Value | undefined {
|
|
||||||
const v = other instanceof Value ? other.value : typeof other === "number" ? other : 0;
|
|
||||||
if (v === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return new Value(this.value / v * 100, Unit.PERCENT, 0, this.locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<div class="numberTable">
|
|
||||||
<div class="title">
|
|
||||||
Wetter
|
|
||||||
</div>
|
|
||||||
<div class="day">
|
|
||||||
<div class="hour" *ngFor="let hour of hours">
|
|
||||||
<div class="bar clouds" [style.height]="clouds(hour)"></div>
|
|
||||||
<div class="bar precipitation" [style.height]="precipitation(hour)"></div>
|
|
||||||
<div class="bar irradiation" [style.height]="irradiation(hour)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
.day {
|
|
||||||
display: flex;
|
|
||||||
height: 3em;
|
|
||||||
background-color: #2d4255;
|
|
||||||
|
|
||||||
.hour {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
width: 100%; // any width will do (flex cares about real with)
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clouds {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.irradiation {
|
|
||||||
background-color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.precipitation {
|
|
||||||
background-color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -10,6 +10,14 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: inherit;
|
||||||
|
color: @FONT_SELECTABLE;
|
||||||
|
border: 0.05em solid gray;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
all: unset;
|
all: unset;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
@ -23,3 +31,55 @@ button {
|
|||||||
div {
|
div {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.vertical {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.valueInteger {
|
||||||
|
width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.valueDelimiter {
|
||||||
|
width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.valueFraction {
|
||||||
|
width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.unit {
|
||||||
|
width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|||||||
26
src/main/java/de/ph87/data/chart/Chart.java
Normal file
26
src/main/java/de/ph87/data/chart/Chart.java
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package de.ph87.data.chart;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Chart {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private long version;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name = "";
|
||||||
|
|
||||||
|
}
|
||||||
35
src/main/java/de/ph87/data/chart/axis/Axis.java
Normal file
35
src/main/java/de/ph87/data/chart/axis/Axis.java
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package de.ph87.data.chart.axis;
|
||||||
|
|
||||||
|
import de.ph87.data.chart.Chart;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Axis {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private long version;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Chart chart;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name = "";
|
||||||
|
|
||||||
|
public Axis(@NonNull final Chart chart) {
|
||||||
|
this.chart = chart;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
src/main/java/de/ph87/data/chart/axis/graph/Graph.java
Normal file
41
src/main/java/de/ph87/data/chart/axis/graph/Graph.java
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package de.ph87.data.chart.axis.graph;
|
||||||
|
|
||||||
|
import de.ph87.data.chart.axis.Axis;
|
||||||
|
import de.ph87.data.series.Series;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Graph {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private long version;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Axis axis;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name = "";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Series series;
|
||||||
|
|
||||||
|
public Graph(@NonNull final Axis axis, @NonNull final Series series) {
|
||||||
|
this.axis = axis;
|
||||||
|
this.series = series;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,17 +1,20 @@
|
|||||||
package de.ph87.data.series.graph;
|
package de.ph87.data.graph;
|
||||||
|
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.point.Point;
|
||||||
import de.ph87.data.value.*;
|
import de.ph87.data.point.PointRequest;
|
||||||
import jakarta.annotation.*;
|
import de.ph87.data.series.SeriesDto;
|
||||||
import lombok.*;
|
import de.ph87.data.series.SeriesType;
|
||||||
|
import de.ph87.data.value.Autoscale;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.*;
|
import java.awt.geom.Line2D;
|
||||||
import java.awt.image.*;
|
import java.awt.image.BufferedImage;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.*;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import static java.lang.Math.*;
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
public class Graph {
|
public class Graph {
|
||||||
|
|
||||||
@ -23,10 +26,7 @@ public class Graph {
|
|||||||
public final SeriesDto series;
|
public final SeriesDto series;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public final Aligned begin;
|
public final PointRequest request;
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public final Aligned end;
|
|
||||||
|
|
||||||
public final int width;
|
public final int width;
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ public class Graph {
|
|||||||
|
|
||||||
public final int border;
|
public final int border;
|
||||||
|
|
||||||
public final List<Point> points;
|
public final List<java.awt.Point> points;
|
||||||
|
|
||||||
public final long minuteMin;
|
public final long minuteMin;
|
||||||
|
|
||||||
@ -60,21 +60,20 @@ public class Graph {
|
|||||||
|
|
||||||
public final int maxLabelWidth;
|
public final int maxLabelWidth;
|
||||||
|
|
||||||
private final Autoscale autoscale;
|
public final Autoscale autoscale;
|
||||||
|
|
||||||
public Graph(@NonNull final SeriesDto series, @NonNull final List<GraphPoint> points, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) {
|
public Graph(@NonNull final SeriesDto series, @NonNull final List<Point> points, @NonNull final PointRequest request, final int width, final int height, final int border) {
|
||||||
this.series = series;
|
this.series = series;
|
||||||
this.begin = begin;
|
this.request = request;
|
||||||
this.end = end;
|
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.border = border;
|
this.border = border;
|
||||||
|
|
||||||
// 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 Point point : points) {
|
||||||
vMin = Math.min(vMin, point.getValue());
|
vMin = Math.min(vMin, point.getValue());
|
||||||
vMax = max(vMax, point.getValue());
|
vMax = max(vMax, point.getValue());
|
||||||
vSum += point.getValue();
|
vSum += point.getValue();
|
||||||
@ -87,9 +86,9 @@ 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 Point point : points) {
|
||||||
__maxLabelWidth = max(__maxLabelWidth, fontMetrics.stringWidth(autoscale.format(point.getValue() * autoscale.factor)));
|
__maxLabelWidth = max(__maxLabelWidth, fontMetrics.stringWidth(autoscale.format(point.getValue() * autoscale.factor)));
|
||||||
}
|
}
|
||||||
this.maxLabelWidth = __maxLabelWidth;
|
this.maxLabelWidth = __maxLabelWidth;
|
||||||
@ -97,10 +96,10 @@ public class Graph {
|
|||||||
widthInner = width - 3 * border - this.maxLabelWidth;
|
widthInner = width - 3 * border - this.maxLabelWidth;
|
||||||
heightInner = height - 2 * border;
|
heightInner = height - 2 * border;
|
||||||
|
|
||||||
minuteMin = begin.date.toEpochSecond() / 60;
|
minuteMin = request.begin.date.toEpochSecond() / 60;
|
||||||
minuteMax = end.date.toEpochSecond() / 60;
|
minuteMax = request.end.date.toEpochSecond() / 60;
|
||||||
minuteRange = minuteMax - minuteMin;
|
minuteRange = minuteMax - minuteMin;
|
||||||
minuteScale = (double) widthInner / (minuteRange + begin.alignment.maxDuration.toMinutes());
|
minuteScale = (double) widthInner / (minuteRange + request.inner.maxDuration.toMinutes());
|
||||||
|
|
||||||
valueMin = vMin;
|
valueMin = vMin;
|
||||||
valueMax = vMax;
|
valueMax = vMax;
|
||||||
@ -115,35 +114,29 @@ 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);
|
|
||||||
yLabel(g, valueAvg, DASHED, new Color(0, 127, 0));
|
|
||||||
yLabel(g, 0, NORMAL, Color.BLACK);
|
|
||||||
yLabel(g, valueMin, DASHED, Color.blue);
|
|
||||||
|
|
||||||
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 * request.inner.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 java.awt.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);
|
java.awt.Point last = null;
|
||||||
Point last = null;
|
for (final java.awt.Point current : points) {
|
||||||
for (final Point current : points) {
|
|
||||||
if (last != null) {
|
if (last != null) {
|
||||||
g.drawLine(last.x, last.y, current.x, current.y);
|
g.drawLine(last.x, last.y, current.x, current.y);
|
||||||
}
|
}
|
||||||
@ -153,20 +146,20 @@ 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Function<GraphPoint, Point> toPoint() {
|
private Function<Point, java.awt.Point> toPoint() {
|
||||||
return point -> {
|
return point -> {
|
||||||
final long minuteEpoch = point.getDate().toEpochSecond() / 60;
|
final long minuteEpoch = point.getDate().toEpochSecond() / 60;
|
||||||
final long minuteRelative = minuteEpoch - minuteMin;
|
final long minuteRelative = minuteEpoch - minuteMin;
|
||||||
@ -177,7 +170,7 @@ public class Graph {
|
|||||||
final double valueScaled = valueRelative * valueScale;
|
final double valueScaled = valueRelative * valueScale;
|
||||||
final int y = (int) Math.round(valueScaled);
|
final int y = (int) Math.round(valueScaled);
|
||||||
|
|
||||||
return new Point(x, y);
|
return new java.awt.Point(x, y);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
49
src/main/java/de/ph87/data/graph/GraphController.java
Normal file
49
src/main/java/de/ph87/data/graph/GraphController.java
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package de.ph87.data.graph;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.point.PointRequest;
|
||||||
|
import de.ph87.data.point.PointService;
|
||||||
|
import de.ph87.data.series.Alignment;
|
||||||
|
import de.ph87.data.series.Series;
|
||||||
|
import de.ph87.data.series.SeriesDto;
|
||||||
|
import de.ph87.data.series.SeriesRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Series/Graph")
|
||||||
|
public class GraphController {
|
||||||
|
|
||||||
|
private final SeriesRepository seriesRepository;
|
||||||
|
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
|
@GetMapping(path = "{seriesId}/{width}/{height}/{outerName}/{offset}/{duration}/{innerName}", produces = "image/png")
|
||||||
|
public void graph(@PathVariable final long seriesId, final HttpServletResponse response, @PathVariable final int width, @PathVariable final int height, @PathVariable final String outerName, @PathVariable final long offset, @PathVariable final long duration, @PathVariable final String innerName) throws IOException {
|
||||||
|
final Alignment outer = Alignment.valueOf(outerName);
|
||||||
|
final Alignment inner = Alignment.valueOf(innerName);
|
||||||
|
final PointRequest request = new PointRequest(outer, offset, duration, inner);
|
||||||
|
|
||||||
|
final Series series = seriesRepository.findById(seriesId).orElseThrow();
|
||||||
|
final List<Point> points = pointService.getPoints(series, request);
|
||||||
|
final Graph graph = new Graph(new SeriesDto(series), points, request, width, height, 10);
|
||||||
|
|
||||||
|
final BufferedImage image = graph.draw();
|
||||||
|
response.setContentType("image/png");
|
||||||
|
ImageIO.write(image, "PNG", response.getOutputStream());
|
||||||
|
response.getOutputStream().flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
package de.ph87.data.message.handler;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import de.ph87.data.message.IMessageHandler;
|
||||||
|
import de.ph87.data.message.Message;
|
||||||
|
import de.ph87.data.series.meter.MeterInbound;
|
||||||
|
import de.ph87.data.series.varying.VaryingInbound;
|
||||||
|
import de.ph87.data.value.Unit;
|
||||||
|
import de.ph87.data.value.Value;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PatrixJsonHandler implements IMessageHandler {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(@NonNull final Message message) throws Exception {
|
||||||
|
if (!message.topic.endsWith("/PatrixJson")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||||
|
applicationEventPublisher.publishEvent(inbound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||||
|
@JsonSubTypes({
|
||||||
|
@JsonSubTypes.Type(name = "VARYING", value = Varying.class),
|
||||||
|
@JsonSubTypes.Type(name = "METER", value = Meter.class),
|
||||||
|
})
|
||||||
|
private interface Inbound {
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
VARYING,
|
||||||
|
METER,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public static class Varying extends VaryingInbound implements Inbound {
|
||||||
|
|
||||||
|
protected Varying(@NonNull final String name, final long date, final double value, @NonNull final Unit unit) {
|
||||||
|
super(name, ZonedDateTime.ofInstant(Instant.ofEpochSecond(date), ZoneId.systemDefault()), new Value(value, unit));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public static class Meter extends MeterInbound implements Inbound {
|
||||||
|
|
||||||
|
protected Meter(@NonNull final String name, @NonNull final String number, final long date, final double value, @NonNull final Unit unit) {
|
||||||
|
super(name, number, ZonedDateTime.ofInstant(Instant.ofEpochSecond(date), ZoneId.systemDefault()), new Value(value, unit));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,22 +1,22 @@
|
|||||||
package de.ph87.data.series.graph;
|
package de.ph87.data.point;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
import java.time.*;
|
import java.time.*;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class GraphPoint {
|
public class Point {
|
||||||
|
|
||||||
public final ZonedDateTime date;
|
public final ZonedDateTime date;
|
||||||
|
|
||||||
public final double value;
|
public final double value;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public GraphPoint plus(@NonNull final GraphPoint other) {
|
public Point plus(@NonNull final Point other) {
|
||||||
if (this.date.compareTo(other.date) != 0) {
|
if (this.date.compareTo(other.date) != 0) {
|
||||||
throw new RuntimeException("Cannot 'add' GraphPoints with different dates: this=%s, other=%s".formatted(this, other));
|
throw new RuntimeException("Cannot 'add' GraphPoints with different dates: this=%s, other=%s".formatted(this, other));
|
||||||
}
|
}
|
||||||
return new GraphPoint(date, value + other.value);
|
return new Point(date, value + other.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
26
src/main/java/de/ph87/data/point/PointController.java
Normal file
26
src/main/java/de/ph87/data/point/PointController.java
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package de.ph87.data.point;
|
||||||
|
|
||||||
|
import de.ph87.data.view.ViewPointRequest;
|
||||||
|
import de.ph87.data.view.ViewService;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("points")
|
||||||
|
public class PointController {
|
||||||
|
|
||||||
|
private final ViewService viewService;
|
||||||
|
|
||||||
|
@PostMapping("fetch")
|
||||||
|
public List<Point> fetch(@RequestBody @NonNull final ViewPointRequest request) {
|
||||||
|
return viewService.getPoints(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
src/main/java/de/ph87/data/point/PointRequest.java
Normal file
41
src/main/java/de/ph87/data/point/PointRequest.java
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package de.ph87.data.point;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import de.ph87.data.series.Aligned;
|
||||||
|
import de.ph87.data.series.Alignment;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PointRequest {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Alignment outer;
|
||||||
|
|
||||||
|
public final long outerOffset;
|
||||||
|
|
||||||
|
public final long outerCount;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Alignment inner;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@JsonIgnore
|
||||||
|
public final Aligned begin;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@JsonIgnore
|
||||||
|
public final Aligned end;
|
||||||
|
|
||||||
|
public PointRequest(@NonNull final Alignment outer, final long outerOffset, final long outerCount, @NonNull final Alignment inner) {
|
||||||
|
this.outer = outer;
|
||||||
|
this.outerOffset = outerOffset;
|
||||||
|
this.outerCount = outerCount;
|
||||||
|
this.inner = inner;
|
||||||
|
this.end = outer.align(ZonedDateTime.now()).plus(1).minus(outerOffset);
|
||||||
|
this.begin = end.minus(outerCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
src/main/java/de/ph87/data/point/PointService.java
Normal file
30
src/main/java/de/ph87/data/point/PointService.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package de.ph87.data.point;
|
||||||
|
|
||||||
|
import de.ph87.data.series.Series;
|
||||||
|
import de.ph87.data.series.meter.MeterService;
|
||||||
|
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.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PointService {
|
||||||
|
|
||||||
|
private final VaryingService varyingService;
|
||||||
|
|
||||||
|
private final MeterService meterService;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<Point> getPoints(@NonNull final Series series, @NonNull final PointRequest pointRequest) {
|
||||||
|
return switch (series.getType()) {
|
||||||
|
case METER -> meterService.getPoints(series, pointRequest);
|
||||||
|
case VARYING -> varyingService.getPoints(series, pointRequest);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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,16 +1,21 @@
|
|||||||
package de.ph87.data.series;
|
package de.ph87.data.series;
|
||||||
|
|
||||||
import de.ph87.data.value.*;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import de.ph87.data.web.*;
|
import de.ph87.data.value.Unit;
|
||||||
import lombok.*;
|
import de.ph87.data.web.IWebSocketMessage;
|
||||||
|
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
|
||||||
public class SeriesDto implements IWebSocketMessage {
|
public class SeriesDto implements IWebSocketMessage {
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
public final List<Object> websocketTopic = List.of("Series");
|
public final List<Object> websocketTopic = List.of("Series");
|
||||||
|
|
||||||
public final long id;
|
public final long id;
|
||||||
@ -25,7 +30,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 +58,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();
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
package de.ph87.data.series;
|
package de.ph87.data.series;
|
||||||
|
|
||||||
import de.ph87.data.*;
|
import de.ph87.data.Action;
|
||||||
import lombok.*;
|
import lombok.NonNull;
|
||||||
import lombok.extern.slf4j.*;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.*;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.*;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.transaction.annotation.*;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
import java.util.function.*;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -39,11 +41,6 @@ public class SeriesService {
|
|||||||
return seriesRepository.findById(id).orElseThrow();
|
return seriesRepository.findById(id).orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public SeriesDto getDtoById(final long id) {
|
|
||||||
return toDto(getById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) {
|
private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) {
|
||||||
final SeriesDto dto = toDto(series);
|
final SeriesDto dto = toDto(series);
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
package de.ph87.data.series.graph;
|
|
||||||
|
|
||||||
import de.ph87.data.series.*;
|
|
||||||
import jakarta.servlet.http.*;
|
|
||||||
import lombok.*;
|
|
||||||
import lombok.extern.slf4j.*;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import javax.imageio.*;
|
|
||||||
import java.awt.image.*;
|
|
||||||
import java.io.*;
|
|
||||||
import java.time.*;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@RequestMapping("Series/Graph")
|
|
||||||
public class GraphController {
|
|
||||||
|
|
||||||
private final GraphService graphService;
|
|
||||||
|
|
||||||
@GetMapping(path = "{seriesId}/{width}/{height}/{alignmentName}/{offset}/{duration}", 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 {
|
|
||||||
final Alignment alignment = Alignment.valueOf(alignmentName);
|
|
||||||
final Aligned end = alignment.align(ZonedDateTime.now()).minus(offset);
|
|
||||||
final Aligned begin = end.minus(duration - 1);
|
|
||||||
final Graph graph = graphService.getGraph(seriesId, begin, end, width, height, 10);
|
|
||||||
final BufferedImage image = graph.draw();
|
|
||||||
response.setContentType("image/png");
|
|
||||||
ImageIO.write(image, "PNG", response.getOutputStream());
|
|
||||||
response.getOutputStream().flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package de.ph87.data.series.graph;
|
|
||||||
|
|
||||||
import de.ph87.data.series.*;
|
|
||||||
import de.ph87.data.series.meter.*;
|
|
||||||
import de.ph87.data.series.varying.*;
|
|
||||||
import lombok.*;
|
|
||||||
import lombok.extern.slf4j.*;
|
|
||||||
import org.springframework.stereotype.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class GraphService {
|
|
||||||
|
|
||||||
private final SeriesService seriesService;
|
|
||||||
|
|
||||||
private final VaryingService varyingService;
|
|
||||||
|
|
||||||
private final MeterService meterService;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public Graph getGraph(final long seriesId, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) {
|
|
||||||
final SeriesDto series = seriesService.getDtoById(seriesId);
|
|
||||||
final List<GraphPoint> entries = switch (series.getType()) {
|
|
||||||
case METER -> meterService.getPoints(series, begin, end);
|
|
||||||
case VARYING -> varyingService.getPoints(series, begin, end);
|
|
||||||
};
|
|
||||||
return new Graph(series, entries, begin, end, width, height, border);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,22 +1,31 @@
|
|||||||
package de.ph87.data.series.meter;
|
package de.ph87.data.series.meter;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.point.PointRequest;
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.*;
|
||||||
import de.ph87.data.series.graph.*;
|
import de.ph87.data.series.meter.day.MeterDay;
|
||||||
import de.ph87.data.series.meter.day.*;
|
import de.ph87.data.series.meter.day.MeterDayRepository;
|
||||||
import de.ph87.data.series.meter.five.*;
|
import de.ph87.data.series.meter.five.MeterFive;
|
||||||
import de.ph87.data.series.meter.hour.*;
|
import de.ph87.data.series.meter.five.MeterFiveRepository;
|
||||||
import de.ph87.data.series.meter.month.*;
|
import de.ph87.data.series.meter.hour.MeterHour;
|
||||||
import de.ph87.data.series.meter.week.*;
|
import de.ph87.data.series.meter.hour.MeterHourRepository;
|
||||||
import de.ph87.data.series.meter.year.*;
|
import de.ph87.data.series.meter.month.MeterMonth;
|
||||||
import lombok.*;
|
import de.ph87.data.series.meter.month.MeterMonthRepository;
|
||||||
import lombok.extern.slf4j.*;
|
import de.ph87.data.series.meter.week.MeterWeek;
|
||||||
|
import de.ph87.data.series.meter.week.MeterWeekRepository;
|
||||||
|
import de.ph87.data.series.meter.year.MeterYear;
|
||||||
|
import de.ph87.data.series.meter.year.MeterYearRepository;
|
||||||
|
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.LinkedList;
|
||||||
import java.util.function.*;
|
import java.util.List;
|
||||||
import java.util.stream.*;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -80,13 +89,13 @@ public class MeterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<GraphPoint> getPoints(@NonNull final SeriesDto series, @NonNull final Aligned begin, @NonNull final Aligned end) {
|
public List<Point> getPoints(final @NonNull Series series, @NonNull final PointRequest pointRequest) {
|
||||||
final List<? extends MeterValue> graphPoints = findRepository(begin.alignment).findAllByIdMeterSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqualOrderByIdDate(series.id, begin.date, end.date);
|
final List<? extends MeterValue> graphPoints = findRepository(pointRequest.inner).findAllByIdMeterSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqualOrderByIdDate(series.getId(), pointRequest.begin.date, pointRequest.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<Point> points = graphPoints.stream().map(meterValue -> new Point(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) {
|
||||||
final GraphPoint first = points.remove(i);
|
final Point first = points.remove(i);
|
||||||
final GraphPoint second = points.remove(i + 1);
|
final Point second = points.remove(i + 1);
|
||||||
points.add(i, first.plus(second));
|
points.add(i, first.plus(second));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,28 @@
|
|||||||
package de.ph87.data.series.varying;
|
package de.ph87.data.series.varying;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.point.PointRequest;
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.*;
|
||||||
import de.ph87.data.series.graph.*;
|
import de.ph87.data.series.varying.day.VaryingDay;
|
||||||
import de.ph87.data.series.varying.day.*;
|
import de.ph87.data.series.varying.day.VaryingDayRepository;
|
||||||
import de.ph87.data.series.varying.five.*;
|
import de.ph87.data.series.varying.five.VaryingFive;
|
||||||
import de.ph87.data.series.varying.hour.*;
|
import de.ph87.data.series.varying.five.VaryingFiveRepository;
|
||||||
import de.ph87.data.series.varying.month.*;
|
import de.ph87.data.series.varying.hour.VaryingHour;
|
||||||
import de.ph87.data.series.varying.week.*;
|
import de.ph87.data.series.varying.hour.VaryingHourRepository;
|
||||||
import de.ph87.data.series.varying.year.*;
|
import de.ph87.data.series.varying.month.VaryingMonth;
|
||||||
import lombok.*;
|
import de.ph87.data.series.varying.month.VaryingMonthRepository;
|
||||||
import lombok.extern.slf4j.*;
|
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,11 +73,11 @@ public class VaryingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<GraphPoint> getPoints(@NonNull final SeriesDto series, @NonNull final Aligned begin, @NonNull final Aligned end) {
|
public List<Point> getPoints(final @NonNull Series series, @NonNull final PointRequest pointRequest) {
|
||||||
return findRepository(begin.alignment)
|
return findRepository(pointRequest.inner)
|
||||||
.findAllByIdSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqual(series.id, begin.date, end.date)
|
.findAllByIdSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqual(series.getId(), pointRequest.begin.date, pointRequest.end.date)
|
||||||
.stream()
|
.stream()
|
||||||
.map(v -> new GraphPoint(v.getId().getDate(), v.getAvg()))
|
.map(v -> new Point(v.getId().getDate(), v.getAvg()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package de.ph87.data.value;
|
package de.ph87.data.value;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.*;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.databind.*;
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
import lombok.*;
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public enum Unit {
|
public enum Unit {
|
||||||
TEMPERATURE_C("°C"),
|
TEMPERATURE_C("°C"),
|
||||||
@ -45,7 +47,14 @@ public enum Unit {
|
|||||||
IRRADIATION_KWH_M2("kWh/m²", 1000, IRRADIATION_WH_M2),
|
IRRADIATION_KWH_M2("kWh/m²", 1000, IRRADIATION_WH_M2),
|
||||||
|
|
||||||
PRECIPITATION_MM("mm"),
|
PRECIPITATION_MM("mm"),
|
||||||
;
|
|
||||||
|
BOOLEAN(""),
|
||||||
|
DOOR_BOOLEAN("", 1, BOOLEAN),
|
||||||
|
WINDOW_BOOLEAN("", 1, BOOLEAN),
|
||||||
|
LIGHT_BOOLEAN("", 1, BOOLEAN),
|
||||||
|
|
||||||
|
VOLUME_L("l"),
|
||||||
|
LENGTH_CM("cm");
|
||||||
|
|
||||||
public final String unit;
|
public final String unit;
|
||||||
|
|
||||||
|
|||||||
23
src/main/java/de/ph87/data/view/ViewController.java
Normal file
23
src/main/java/de/ph87/data/view/ViewController.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package de.ph87.data.view;
|
||||||
|
|
||||||
|
import de.ph87.data.view.tree.ViewDto;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("View")
|
||||||
|
public class ViewController {
|
||||||
|
|
||||||
|
private final ViewRepository viewRepository;
|
||||||
|
|
||||||
|
@GetMapping("rootList")
|
||||||
|
public List<ViewDto> rootList() {
|
||||||
|
return viewRepository.findAllDtoByNameNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
src/main/java/de/ph87/data/view/ViewPointRequest.java
Normal file
20
src/main/java/de/ph87/data/view/ViewPointRequest.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package de.ph87.data.view;
|
||||||
|
|
||||||
|
import de.ph87.data.point.PointRequest;
|
||||||
|
import de.ph87.data.series.Alignment;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class ViewPointRequest extends PointRequest {
|
||||||
|
|
||||||
|
public final String viewUuid;
|
||||||
|
|
||||||
|
public ViewPointRequest(final @NonNull Alignment outer, final int outerOffset, final int outerCount, final @NonNull Alignment inner, @NonNull final String viewUuid) {
|
||||||
|
super(outer, outerOffset, outerCount, inner);
|
||||||
|
this.viewUuid = viewUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
src/main/java/de/ph87/data/view/ViewRepository.java
Normal file
20
src/main/java/de/ph87/data/view/ViewRepository.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package de.ph87.data.view;
|
||||||
|
|
||||||
|
import de.ph87.data.view.tree.View;
|
||||||
|
import de.ph87.data.view.tree.ViewDto;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ViewRepository extends ListCrudRepository<View, String> {
|
||||||
|
|
||||||
|
boolean existsByName(@NonNull String name);
|
||||||
|
|
||||||
|
List<View> findAllByNameNot(@NonNull String name);
|
||||||
|
|
||||||
|
default List<ViewDto> findAllDtoByNameNotEmpty() {
|
||||||
|
return findAllByNameNot("").stream().map(ViewDto::map).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
src/main/java/de/ph87/data/view/ViewScope.java
Normal file
46
src/main/java/de/ph87/data/view/ViewScope.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package de.ph87.data.view;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.point.PointRequest;
|
||||||
|
import de.ph87.data.point.PointService;
|
||||||
|
import de.ph87.data.series.Series;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ViewScope {
|
||||||
|
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
|
private final PointRequest request;
|
||||||
|
|
||||||
|
private final Map<Long, List<Point>> seriesPoints = new HashMap<>();
|
||||||
|
|
||||||
|
private final Map<Double, List<Point>> literalPoints = new HashMap<>();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<Point> getPoints(@NonNull final Series series) {
|
||||||
|
return seriesPoints.computeIfAbsent(series.getId(), ignore -> pointService.getPoints(series, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<Point> getLiteral(final double value) {
|
||||||
|
return literalPoints.computeIfAbsent(value, this::_generateLiteral);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private List<Point> _generateLiteral(final double value) {
|
||||||
|
final List<Point> points = new ArrayList<>();
|
||||||
|
for (ZonedDateTime date = request.begin.date; !date.isAfter(request.end.date); date = request.inner.plus(date, 1)) {
|
||||||
|
points.add(new Point(date, value));
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
58
src/main/java/de/ph87/data/view/ViewService.java
Normal file
58
src/main/java/de/ph87/data/view/ViewService.java
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package de.ph87.data.view;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.point.PointService;
|
||||||
|
import de.ph87.data.series.SeriesRepository;
|
||||||
|
import de.ph87.data.view.tree.View;
|
||||||
|
import de.ph87.data.view.tree.ViewBinary;
|
||||||
|
import de.ph87.data.view.tree.ViewSeries;
|
||||||
|
import de.ph87.data.view.tree.ViewUnary;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ViewService {
|
||||||
|
|
||||||
|
private static final long DEMO_SERIES_POWER_BALANCE = 8L;
|
||||||
|
|
||||||
|
private static final long DEMO_SERIES_POWER_PRODUCED = 26L;
|
||||||
|
|
||||||
|
private static final String DEMO_POWER_CONSUMED = "__DEMO_POWER_CONSUMED__";
|
||||||
|
|
||||||
|
private final ViewRepository viewRepository;
|
||||||
|
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
|
private final SeriesRepository seriesRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void init() {
|
||||||
|
if (!viewRepository.existsByName(DEMO_POWER_CONSUMED)) {
|
||||||
|
final View powerBalance = new ViewSeries(seriesRepository.findById(DEMO_SERIES_POWER_BALANCE).orElseThrow());
|
||||||
|
final View powerBalance2 = new ViewBinary(ViewBinary.Operation.PLUS, powerBalance, powerBalance);
|
||||||
|
final View powerPurchased = new ViewUnary(ViewUnary.Operation.NOT_NEG, powerBalance2);
|
||||||
|
final View powerProduced = new ViewSeries(seriesRepository.findById(DEMO_SERIES_POWER_PRODUCED).orElseThrow());
|
||||||
|
final View powerConsumed = viewRepository.save(new ViewBinary(ViewBinary.Operation.PLUS, powerPurchased, powerProduced));
|
||||||
|
powerConsumed.setName(DEMO_POWER_CONSUMED);
|
||||||
|
log.warn("DEMO VIEW CREATED: {} \"{}\"", powerConsumed.getUuid(), powerConsumed.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<Point> getPoints(@NonNull final ViewPointRequest request) {
|
||||||
|
final ViewScope scope = new ViewScope(pointService, request);
|
||||||
|
final View view = viewRepository.findById(request.viewUuid).orElseThrow();
|
||||||
|
return view.getPoints(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
src/main/java/de/ph87/data/view/tree/View.java
Normal file
37
src/main/java/de/ph87/data/view/tree/View.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.view.ViewScope;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@DiscriminatorColumn(name = "_type_")
|
||||||
|
@Inheritance(strategy = InheritanceType.JOINED)
|
||||||
|
public abstract class View {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String uuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private int version;
|
||||||
|
|
||||||
|
@Column(insertable = false, updatable = false)
|
||||||
|
private String _type_;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name = "";
|
||||||
|
|
||||||
|
public abstract List<Point> getPoints(final @NonNull ViewScope scope);
|
||||||
|
|
||||||
|
}
|
||||||
84
src/main/java/de/ph87/data/view/tree/ViewBinary.java
Normal file
84
src/main/java/de/ph87/data/view/tree/ViewBinary.java
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.view.ViewScope;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@DiscriminatorValue("binary")
|
||||||
|
public class ViewBinary extends View {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private Operation operation;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@OneToOne(optional = false, orphanRemoval = true, cascade = CascadeType.ALL)
|
||||||
|
private View view0;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@OneToOne(optional = false, orphanRemoval = true, cascade = CascadeType.ALL)
|
||||||
|
private View view1;
|
||||||
|
|
||||||
|
public ViewBinary(@NonNull final Operation operation, @NonNull final View view0, @NonNull final View view1) {
|
||||||
|
this.operation = operation;
|
||||||
|
this.view0 = view0;
|
||||||
|
this.view1 = view1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Operation {
|
||||||
|
PLUS(Double::sum),
|
||||||
|
MINUS((a, b) -> a - b),
|
||||||
|
MULTIPLY((a, b) -> a * b),
|
||||||
|
DIVIDE((a, b) -> a / b),
|
||||||
|
MODULO((a, b) -> a % b),
|
||||||
|
PERCENT((a, b) -> a / b * 100),
|
||||||
|
;
|
||||||
|
|
||||||
|
public final BiFunction<Double, Double, Double> function;
|
||||||
|
|
||||||
|
Operation(final BiFunction<Double, Double, Double> function) {
|
||||||
|
this.function = function;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Point apply(@NonNull final Point a, @NonNull final Point b) {
|
||||||
|
return new Point(a.date, function.apply(a.value, b.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Point> getPoints(final @NonNull ViewScope scope) {
|
||||||
|
final List<Point> pointsA = view0.getPoints(scope);
|
||||||
|
final List<Point> pointsB = view1.getPoints(scope);
|
||||||
|
final List<Point> result = new ArrayList<>(pointsA.size() + pointsB.size());
|
||||||
|
int indexA = 0;
|
||||||
|
int indexB = 0;
|
||||||
|
while (indexA < pointsA.size() && indexB < pointsB.size()) {
|
||||||
|
final Point pointA = pointsA.get(indexA);
|
||||||
|
final Point pointB = pointsB.get(indexB);
|
||||||
|
int cmp = pointA.date.compareTo(pointB.date);
|
||||||
|
if (cmp < 0) {
|
||||||
|
indexA++;
|
||||||
|
} else if (cmp > 0) {
|
||||||
|
indexB++;
|
||||||
|
} else {
|
||||||
|
result.add(operation.apply(pointA, pointB));
|
||||||
|
indexA++;
|
||||||
|
indexB++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
src/main/java/de/ph87/data/view/tree/ViewBinaryDto.java
Normal file
27
src/main/java/de/ph87/data/view/tree/ViewBinaryDto.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class ViewBinaryDto extends ViewDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ViewBinary.Operation operation;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ViewDto view0;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ViewDto view1;
|
||||||
|
|
||||||
|
public ViewBinaryDto(@NonNull final ViewBinary viewBinary) {
|
||||||
|
super(viewBinary);
|
||||||
|
this.operation = viewBinary.getOperation();
|
||||||
|
this.view0 = ViewDto.map(viewBinary.getView0());
|
||||||
|
this.view1 = ViewDto.map(viewBinary.getView1());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
src/main/java/de/ph87/data/view/tree/ViewDto.java
Normal file
34
src/main/java/de/ph87/data/view/tree/ViewDto.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public abstract class ViewDto {
|
||||||
|
|
||||||
|
public final String _type_;
|
||||||
|
|
||||||
|
public final String uuid;
|
||||||
|
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
protected ViewDto(@NonNull final View view) {
|
||||||
|
this._type_ = view.get_type_();
|
||||||
|
this.uuid = view.getUuid();
|
||||||
|
this.name = view.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ViewDto map(@NonNull final View view) {
|
||||||
|
return switch (view) {
|
||||||
|
case final ViewLiteral viewLiteral -> new ViewLiteralDto(viewLiteral);
|
||||||
|
case final ViewSeries viewSeries -> new ViewSeriesDto(viewSeries);
|
||||||
|
case final ViewUnary viewUnary -> new ViewUnaryDto(viewUnary);
|
||||||
|
case final ViewBinary viewBinary -> new ViewBinaryDto(viewBinary);
|
||||||
|
default -> throw new RuntimeException("DTO mapping of View type not implemented: " + view.getClass());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
32
src/main/java/de/ph87/data/view/tree/ViewLiteral.java
Normal file
32
src/main/java/de/ph87/data/view/tree/ViewLiteral.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.view.ViewScope;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.DiscriminatorValue;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@DiscriminatorValue("literal")
|
||||||
|
public class ViewLiteral extends View {
|
||||||
|
|
||||||
|
@Column(name = "`value`", nullable = false)
|
||||||
|
private double value;
|
||||||
|
|
||||||
|
public ViewLiteral(final double value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Point> getPoints(final @NonNull ViewScope scope) {
|
||||||
|
return scope.getLiteral(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
src/main/java/de/ph87/data/view/tree/ViewLiteralDto.java
Normal file
18
src/main/java/de/ph87/data/view/tree/ViewLiteralDto.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class ViewLiteralDto extends ViewDto {
|
||||||
|
|
||||||
|
public final double value;
|
||||||
|
|
||||||
|
public ViewLiteralDto(@NonNull final ViewLiteral viewLiteral) {
|
||||||
|
super(viewLiteral);
|
||||||
|
this.value = viewLiteral.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
src/main/java/de/ph87/data/view/tree/ViewSeries.java
Normal file
34
src/main/java/de/ph87/data/view/tree/ViewSeries.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.series.Series;
|
||||||
|
import de.ph87.data.view.ViewScope;
|
||||||
|
import jakarta.persistence.DiscriminatorValue;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@DiscriminatorValue("series")
|
||||||
|
public class ViewSeries extends View {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Series series;
|
||||||
|
|
||||||
|
public ViewSeries(@NonNull final Series series) {
|
||||||
|
this.series = series;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Point> getPoints(final @NonNull ViewScope scope) {
|
||||||
|
return scope.getPoints(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
src/main/java/de/ph87/data/view/tree/ViewSeriesDto.java
Normal file
20
src/main/java/de/ph87/data/view/tree/ViewSeriesDto.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import de.ph87.data.series.SeriesDto;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class ViewSeriesDto extends ViewDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final SeriesDto series;
|
||||||
|
|
||||||
|
public ViewSeriesDto(@NonNull final ViewSeries viewSeries) {
|
||||||
|
super(viewSeries);
|
||||||
|
this.series = new SeriesDto(viewSeries.getSeries());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
56
src/main/java/de/ph87/data/view/tree/ViewUnary.java
Normal file
56
src/main/java/de/ph87/data/view/tree/ViewUnary.java
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import de.ph87.data.point.Point;
|
||||||
|
import de.ph87.data.view.ViewScope;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@DiscriminatorValue("unary")
|
||||||
|
public class ViewUnary extends View {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private Operation operation;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@OneToOne(optional = false, orphanRemoval = true, cascade = CascadeType.ALL)
|
||||||
|
private View view;
|
||||||
|
|
||||||
|
public ViewUnary(@NonNull final Operation operation, @NonNull final View view) {
|
||||||
|
this.operation = operation;
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Operation {
|
||||||
|
NEG(a -> -a),
|
||||||
|
REC(a -> 1 / a),
|
||||||
|
NOT_NEG(a -> a < 0 ? 0 : a),
|
||||||
|
;
|
||||||
|
|
||||||
|
public final Function<Double, Double> function;
|
||||||
|
|
||||||
|
Operation(final Function<Double, Double> function) {
|
||||||
|
this.function = function;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Point apply(@NonNull final Point point) {
|
||||||
|
return new Point(point.date, function.apply(point.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Point> getPoints(final @NonNull ViewScope scope) {
|
||||||
|
return view.getPoints(scope).stream().map(operation::apply).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
src/main/java/de/ph87/data/view/tree/ViewUnaryDto.java
Normal file
23
src/main/java/de/ph87/data/view/tree/ViewUnaryDto.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package de.ph87.data.view.tree;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class ViewUnaryDto extends ViewDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ViewUnary.Operation operation;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ViewDto view;
|
||||||
|
|
||||||
|
public ViewUnaryDto(@NonNull final ViewUnary viewUnary) {
|
||||||
|
super(viewUnary);
|
||||||
|
this.operation = viewUnary.getOperation();
|
||||||
|
this.view = ViewDto.map(viewUnary.getView());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -60,6 +60,18 @@ public class BrightSkyDto {
|
|||||||
this.timestamp = timestamp.withZoneSameInstant(TimeZone.getDefault().toZoneId());
|
this.timestamp = timestamp.withZoneSameInstant(TimeZone.getDefault().toZoneId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double getSolar() {
|
||||||
|
return solar == null ? 0.0 : solar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCloud_cover() {
|
||||||
|
return cloud_cover == null ? 0.0 : cloud_cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPrecipitation() {
|
||||||
|
return precipitation == null ? 0.0 : precipitation;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
package de.ph87.data.weather;
|
package de.ph87.data.weather;
|
||||||
|
|
||||||
|
import de.ph87.data.value.Unit;
|
||||||
import de.ph87.data.value.Value;
|
import de.ph87.data.value.Value;
|
||||||
import de.ph87.data.value.*;
|
import lombok.Data;
|
||||||
import lombok.*;
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.IOException;
|
||||||
import java.time.*;
|
import java.time.LocalDate;
|
||||||
import java.util.*;
|
import java.time.LocalTime;
|
||||||
import java.util.stream.*;
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ToString(includeFieldNames = false)
|
@ToString(includeFieldNames = false)
|
||||||
@ -67,11 +72,15 @@ public class WeatherDay {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public final Value precipitation;
|
public final Value precipitation;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Value temperature;
|
||||||
|
|
||||||
public Hour(@NonNull final BrightSkyDto.Weather dto) {
|
public Hour(@NonNull final BrightSkyDto.Weather dto) {
|
||||||
date = dto.getTimestamp();
|
date = dto.getTimestamp();
|
||||||
clouds = new Value(dto.getCloud_cover(), Unit.CLOUD_COVER_PERCENT);
|
clouds = new Value(dto.getCloud_cover(), Unit.CLOUD_COVER_PERCENT);
|
||||||
irradiation = new Value(dto.getSolar() * 1000, Unit.IRRADIATION_WH_M2);
|
irradiation = new Value(dto.getSolar() * 1000, Unit.IRRADIATION_WH_M2);
|
||||||
precipitation = new Value(dto.getPrecipitation(), Unit.PRECIPITATION_MM);
|
precipitation = new Value(dto.getPrecipitation(), Unit.PRECIPITATION_MM);
|
||||||
|
temperature = new Value((dto.getTemperature()), Unit.TEMPERATURE_C);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user