Compare commits
5 Commits
4feb38c14c
...
aa71616961
| Author | SHA1 | Date | |
|---|---|---|---|
| aa71616961 | |||
| 5ef3f6b0da | |||
| a3d2a92302 | |||
| a96cf9d62c | |||
| 98c08201f3 |
85
src/main/angular/package-lock.json
generated
85
src/main/angular/package-lock.json
generated
@ -19,6 +19,8 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@stomp/ng2-stompjs": "^8.0.0",
|
||||
"@stomp/stompjs": "^7.2.1",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
@ -423,6 +425,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cdk": {
|
||||
"version": "20.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.13.tgz",
|
||||
"integrity": "sha512-h1jTkCmJ/rEQQMkxgKFMCBOrMfjZEnppgdekNmSTerwdVp4vdosTDTzFH/kwiOGFeRClffmvqQ2XLG8mQOKOtA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"parse5": "^8.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^20.0.0 || ^21.0.0",
|
||||
"@angular/core": "^20.0.0 || ^21.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cli": {
|
||||
"version": "20.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.7.tgz",
|
||||
@ -1901,6 +1919,13 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@listr2/prompt-adapter-inquirer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz",
|
||||
@ -4198,6 +4223,29 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-adapter-date-fns": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">=2.8.0",
|
||||
"date-fns": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
@ -4561,6 +4609,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-format": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
||||
@ -6767,6 +6826,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||
@ -7366,6 +7431,24 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ng2-charts": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-8.0.0.tgz",
|
||||
"integrity": "sha512-nofsNHI2Zt+EAwT+BJBVg0kgOhNo9ukO4CxULlaIi7VwZSr7I1km38kWSoU41Oq6os6qqIh5srnL+CcV+RFPFA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash-es": "^4.17.15",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": ">=19.0.0",
|
||||
"@angular/common": ">=19.0.0",
|
||||
"@angular/core": ">=19.0.0",
|
||||
"@angular/platform-browser": ">=19.0.0",
|
||||
"chart.js": "^3.4.0 || ^4.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||
@ -7904,7 +7987,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
@ -7958,7 +8040,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
|
||||
@ -35,7 +35,9 @@
|
||||
"@stomp/stompjs": "^7.2.1",
|
||||
"@fortawesome/angular-fontawesome": "^3.0.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0"
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^20.3.7",
|
||||
|
||||
@ -9,9 +9,13 @@ import {registerLocaleData} from '@angular/common';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import {stompServiceFactory} from './common';
|
||||
import {Chart, registerables} from 'chart.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
|
||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
|
||||
@ -47,6 +47,16 @@ export function validateEnum<T extends Record<string, string>>(value: any, enumT
|
||||
throw new Error(`Invalid enum value: ${str}`);
|
||||
}
|
||||
|
||||
export function maxDate(a: Date | null | undefined, b: Date | null | undefined): Date | null {
|
||||
if (!a || !b) {
|
||||
return null;
|
||||
}
|
||||
if (a.getTime() < b.getTime()) {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
export function or<T, R, E>(t: T | null | undefined, map: (t: T) => R, orElse: E): R | E {
|
||||
return t === null || t === undefined ? orElse : map(t);
|
||||
}
|
||||
@ -218,6 +228,10 @@ export abstract class CrudService<T> {
|
||||
this.api.postSingle<T>([...this.path, ...path], data, this.fromJson, next);
|
||||
}
|
||||
|
||||
protected postSingle2<T>(path: any[], data: any, fromJson: FromJson<T>, next?: Next<T>): void {
|
||||
this.api.postSingle<T>([...this.path, ...path], data, fromJson, next);
|
||||
}
|
||||
|
||||
protected postList(path: any[], data: any, next?: Next<T[]>): void {
|
||||
this.api.postList<T>([...this.path, ...path], data, this.fromJson, next);
|
||||
}
|
||||
|
||||
@ -29,6 +29,9 @@ export class Location {
|
||||
private _powerPurchase: Series | null,
|
||||
private _powerDeliver: Series | null,
|
||||
private _powerProduce: Series | null,
|
||||
private _outsideTemperature: Series | null,
|
||||
private _outsideHumidityRelative: Series | null,
|
||||
private _outsideHumidityAbsolute: Series | null,
|
||||
private _powerConsume: Value = Value.NULL,
|
||||
) {
|
||||
this.updateConsume();
|
||||
@ -58,17 +61,6 @@ export class Location {
|
||||
}
|
||||
};
|
||||
|
||||
private updateConsume() {
|
||||
this._powerConsume = Value.ZERO.plus(this._powerPurchase?.value, true).plus(this._powerProduce?.value, true).minus(this._powerDeliver?.value, true);
|
||||
this.powerSelf = Value.ZERO.plus(this.powerProduce?.value.minus(this.powerDeliver?.value, true), true);
|
||||
this.powerPurchasePercentConsume = Value.ZERO.plus(this.powerPurchase?.value.percent(this.powerConsume, "%", 0), true);
|
||||
this.powerProducePercentConsume = Value.ZERO.plus(this.powerProduce?.value.percent(this.powerConsume, "%", 0), true);
|
||||
this.powerDeliveryPercentConsume = Value.ZERO.plus(this.powerDeliver?.value.percent(this.powerConsume, "%", 0), true);
|
||||
this.powerDeliveryPercentProduce = Value.ZERO.plus(this.powerDeliver?.value.percent(this.powerProduce?.value, "%", 0), true);
|
||||
this.powerSelfPercentConsume = Value.ZERO.plus(this.powerSelf.percent(this.powerConsume, "%", 0), true);
|
||||
this.powerSelfPercentProduce = Value.ZERO.plus(this.powerSelf.percent(this.powerProduce?.value, "%", 0), true);
|
||||
}
|
||||
|
||||
static fromJson(json: any): Location {
|
||||
return new Location(
|
||||
validateNumber(json.id),
|
||||
@ -81,9 +73,23 @@ export class Location {
|
||||
or(json.powerPurchase, Series.fromJson, null),
|
||||
or(json.powerDeliver, Series.fromJson, null),
|
||||
or(json.powerProduce, Series.fromJson, null),
|
||||
or(json.outsideTemperature, Series.fromJson, null),
|
||||
or(json.outsideHumidityRelative, Series.fromJson, null),
|
||||
or(json.outsideHumidityAbsolute, Series.fromJson, null),
|
||||
);
|
||||
}
|
||||
|
||||
private updateConsume() {
|
||||
this._powerConsume = Value.ZERO.plus(this._powerPurchase?.value, true).plus(this._powerProduce?.value, true).minus(this._powerDeliver?.value, true);
|
||||
this.powerSelf = Value.ZERO.plus(this.powerProduce?.value.minus(this.powerDeliver?.value, true), true);
|
||||
this.powerPurchasePercentConsume = Value.ZERO.plus(this.powerPurchase?.value.percent(this.powerConsume, "%", 0), true);
|
||||
this.powerProducePercentConsume = Value.ZERO.plus(this.powerProduce?.value.percent(this.powerConsume, "%", 0), true);
|
||||
this.powerDeliveryPercentConsume = Value.ZERO.plus(this.powerDeliver?.value.percent(this.powerConsume, "%", 0), true);
|
||||
this.powerDeliveryPercentProduce = Value.ZERO.plus(this.powerDeliver?.value.percent(this.powerProduce?.value, "%", 0), true);
|
||||
this.powerSelfPercentConsume = Value.ZERO.plus(this.powerSelf.percent(this.powerConsume, "%", 0), true);
|
||||
this.powerSelfPercentProduce = Value.ZERO.plus(this.powerSelf.percent(this.powerProduce?.value, "%", 0), true);
|
||||
}
|
||||
|
||||
get energyPurchase(): Series | null {
|
||||
return this._energyPurchase;
|
||||
}
|
||||
@ -112,4 +118,16 @@ export class Location {
|
||||
return this._powerConsume;
|
||||
}
|
||||
|
||||
get outsideTemperature(): Series | null {
|
||||
return this._outsideTemperature;
|
||||
}
|
||||
|
||||
get outsideHumidityRelative(): Series | null {
|
||||
return this._outsideHumidityRelative;
|
||||
}
|
||||
|
||||
get outsideHumidityAbsolute(): Series | null {
|
||||
return this._outsideHumidityAbsolute;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -148,4 +148,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Section">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Außen
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<div class="Section2">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Temperatur
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [initial]="location.outsideTemperature" (onChange)="locationService.outsideTemperature(location, $event)" [filter]="filterTemperature"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Section2">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Relative Luftfeuchte
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [initial]="location.outsideHumidityRelative" (onChange)="locationService.outsideHumidityRelative(location, $event)" [filter]="filterHumidityRelative"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Section2">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Absolute Luftfeuchte
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
<app-series-select [initial]="location.outsideHumidityAbsolute" (onChange)="locationService.outsideHumidityAbsolute(location, $event)" [filter]="filterHumidityAbsolute"></app-series-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
@ -33,6 +33,12 @@ export class LocationDetail implements OnInit, OnDestroy {
|
||||
|
||||
protected readonly filterPower = (series: Series) => series.type === SeriesType.VARYING && series.unit === 'W';
|
||||
|
||||
protected readonly filterTemperature = (series: Series) => series.type === SeriesType.VARYING && series.unit === '°C';
|
||||
|
||||
protected readonly filterHumidityRelative = (series: Series) => series.type === SeriesType.VARYING && series.unit === '%';
|
||||
|
||||
protected readonly filterHumidityAbsolute = (series: Series) => series.type === SeriesType.VARYING && series.unit === 'g/m³';
|
||||
|
||||
protected readonly Interval = Interval;
|
||||
|
||||
protected readonly Math = Math;
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<canvas baseChart [data]="data" [options]="options"></canvas>
|
||||
</div>
|
||||
@ -0,0 +1,3 @@
|
||||
div {
|
||||
aspect-ratio: 2;
|
||||
}
|
||||
159
src/main/angular/src/app/location/energy/charts/energy-charts.ts
Normal file
159
src/main/angular/src/app/location/energy/charts/energy-charts.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import {Component, Input, ViewChild} from '@angular/core';
|
||||
import {Interval} from '../../../series/Interval';
|
||||
import {PointService} from '../../../point/point-service';
|
||||
import {Location} from '../../Location';
|
||||
import {BaseChartDirective} from 'ng2-charts';
|
||||
import {ChartConfiguration} from 'chart.js';
|
||||
import {de} from 'date-fns/locale';
|
||||
import {PointSeries} from '../../../point/PointSeries';
|
||||
import {formatNumber} from '@angular/common';
|
||||
|
||||
const COLOR_BACK_PURCHASE = "#ffb9b9";
|
||||
const COLOR_BACK_DELIVER = "#ff59ff";
|
||||
const COLOR_BACK_PRODUCE = "#5cbcff";
|
||||
const COLOR_BACK_SELF = "#60ff8c";
|
||||
const COLOR_BACK_CONSUME = "#ffc07a";
|
||||
|
||||
@Component({
|
||||
selector: 'app-energy-charts',
|
||||
imports: [
|
||||
BaseChartDirective
|
||||
],
|
||||
templateUrl: './energy-charts.html',
|
||||
styleUrl: './energy-charts.less',
|
||||
})
|
||||
class EnergyCharts {
|
||||
|
||||
private _offset!: number | null;
|
||||
|
||||
@Input()
|
||||
set offset(value: number | null) {
|
||||
this._offset = value;
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
@ViewChild(BaseChartDirective)
|
||||
chart?: BaseChartDirective;
|
||||
|
||||
private _interval!: Interval | null;
|
||||
|
||||
@Input()
|
||||
set interval(value: Interval | null) {
|
||||
this._interval = value;
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
private _location!: Location | null;
|
||||
|
||||
@Input()
|
||||
set location(value: Location | null) {
|
||||
this._location = value;
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
fetch(): void {
|
||||
if (!this._location || !this._interval || this._offset == null) {
|
||||
return;
|
||||
}
|
||||
const series = [
|
||||
this._location.energyPurchase,
|
||||
this._location.energyDeliver,
|
||||
this._location.energyProduce,
|
||||
];
|
||||
const location = this._location;
|
||||
const interval = this._interval;
|
||||
const offset = this._offset;
|
||||
this.pointService.relative(series, interval, offset, 1, interval.inner, result => {
|
||||
const energyPurchase = result.series.filter(s => s.series.id === location.energyPurchase?.id)[0] || null;
|
||||
const energyDeliver = result.series.filter(s => s.series.id === location.energyDeliver?.id)[0] || null;
|
||||
const energyProduce = result.series.filter(s => s.series.id === location.energyProduce?.id)[0] || null;
|
||||
const energySelf = energyProduce?.merge(energyDeliver, "Energie Selbst", (a, b) => a - b) || null;
|
||||
this.data.datasets.length = 0;
|
||||
this.add(energyDeliver, COLOR_BACK_DELIVER, -1, "a");
|
||||
this.add(energySelf, COLOR_BACK_SELF, 1, "a");
|
||||
this.add(energyPurchase, COLOR_BACK_PURCHASE, 1, "a");
|
||||
this.chart?.update();
|
||||
});
|
||||
}
|
||||
|
||||
private add(pointSeries: PointSeries | null, color: string, factor: number, stack: string) {
|
||||
if (!pointSeries) {
|
||||
return;
|
||||
}
|
||||
this.data.datasets.push({
|
||||
type: 'bar',
|
||||
categoryPercentage: 1.0,
|
||||
barPercentage: 1.0,
|
||||
data: pointSeries.points.map(p => {
|
||||
return {x: p[0] * 1000, y: p[1] * factor};
|
||||
}),
|
||||
label: `${pointSeries.series.name} [${pointSeries.series.unit}]`,
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
stack: stack ? stack : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
protected data: ChartConfiguration['data'] = {
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
protected options: ChartConfiguration['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
displayFormats: {
|
||||
minute: "HH:mm",
|
||||
hour: "HH:mm",
|
||||
day: "dd.MM"
|
||||
},
|
||||
},
|
||||
adapters: {
|
||||
date: {
|
||||
locale: de
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
suggestedMax: 0.5,
|
||||
suggestedMin: -0.1,
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
itemSort: (a, b) => b.datasetIndex - a.datasetIndex,
|
||||
callbacks: {
|
||||
label: (ctx) => {
|
||||
const groups = /^Energie (?<name>.+)\[(?<unit>.+)]$/.exec(ctx.dataset.label || '')?.groups;
|
||||
if (groups) {
|
||||
return `${groups['name']}: ${ctx.parsed.y === null ? '-' : formatNumber(ctx.parsed.y, 'de-DE', '0.2-2')} ${groups['unit']}`;
|
||||
}
|
||||
return ctx.label;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default EnergyCharts
|
||||
@ -78,6 +78,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<app-energy-plot [location]="location" [interval]="interval" [offset]="offset"></app-energy-plot>
|
||||
<app-energy-charts [location]="location" [interval]="interval" [offset]="offset"></app-energy-charts>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit, signal} from '@angular/core';
|
||||
import {Location} from '../Location';
|
||||
import {Series} from '../../series/Series';
|
||||
import {Next} from '../../common';
|
||||
@ -7,18 +7,21 @@ import {PointService} from '../../point/point-service';
|
||||
import {SeriesService} from '../../series/series-service';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Value} from '../../series/Value';
|
||||
import {EnergyPlot} from './plot/energy-plot';
|
||||
import EnergyCharts from './charts/energy-charts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-energy',
|
||||
imports: [
|
||||
EnergyPlot
|
||||
EnergyCharts,
|
||||
EnergyCharts
|
||||
],
|
||||
templateUrl: './location-energy.html',
|
||||
styleUrl: './location-energy.less',
|
||||
})
|
||||
export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
protected readonly signal = signal;
|
||||
|
||||
protected readonly Interval = Interval;
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
@ -1,101 +0,0 @@
|
||||
export class EnergyPoint {
|
||||
|
||||
readonly epochSeconds: number;
|
||||
|
||||
readonly date: Date;
|
||||
|
||||
private _purchase: number | null = null;
|
||||
|
||||
private _produce: number | null = null;
|
||||
|
||||
private _deliver: number | null = null;
|
||||
|
||||
private _self: number | null = null;
|
||||
|
||||
private _consume: number | null = null;
|
||||
|
||||
private _purchaseY: number | null = null;
|
||||
|
||||
private _selfY: number | null = null;
|
||||
|
||||
private _deliverY: number | null = null;
|
||||
|
||||
getPurchaseY(yFactor: number): number {
|
||||
return this.getPurchaseH(yFactor) + this.getSelfH(yFactor);
|
||||
}
|
||||
|
||||
getPurchaseH(yFactor: number): number {
|
||||
if (this._purchaseY === null) {
|
||||
this._purchaseY = (this.purchase || 0) * yFactor;
|
||||
}
|
||||
return this._purchaseY;
|
||||
}
|
||||
|
||||
getSelfY(yFactor: number): number {
|
||||
return this.getSelfH(yFactor);
|
||||
}
|
||||
|
||||
getSelfH(yFactor: number): number {
|
||||
if (this._selfY === null) {
|
||||
this._selfY = (this.self || 0) * yFactor;
|
||||
}
|
||||
return this._selfY;
|
||||
}
|
||||
|
||||
getDeliverH(yFactor: number): number {
|
||||
if (this._deliverY === null) {
|
||||
this._deliverY = (this.deliver || 0) * yFactor;
|
||||
}
|
||||
return this._deliverY;
|
||||
}
|
||||
|
||||
constructor(
|
||||
point: number[],
|
||||
) {
|
||||
this.epochSeconds = point[0];
|
||||
this.date = new Date(this.epochSeconds * 1000);
|
||||
}
|
||||
|
||||
set deliver(value: number | null) {
|
||||
this._deliver = value;
|
||||
this.update();
|
||||
}
|
||||
|
||||
set produce(value: number | null) {
|
||||
this._produce = value;
|
||||
this.update();
|
||||
}
|
||||
|
||||
set purchase(value: number | null) {
|
||||
this._purchase = value;
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
if (this._purchase !== null && this._produce !== null && this._deliver !== null) {
|
||||
this._self = Math.max(0, this._produce - this._deliver);
|
||||
this._consume = Math.max(0, this._purchase + this._self);
|
||||
}
|
||||
}
|
||||
|
||||
get consume(): number | null {
|
||||
return this._consume;
|
||||
}
|
||||
|
||||
get self(): number | null {
|
||||
return this._self;
|
||||
}
|
||||
|
||||
get deliver(): number | null {
|
||||
return this._deliver;
|
||||
}
|
||||
|
||||
get produce(): number | null {
|
||||
return this._produce;
|
||||
}
|
||||
|
||||
get purchase(): number | null {
|
||||
return this._purchase;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<svg [attr.viewBox]="`0 0 ${widthPx} ${heightPx+1}`" [style.background-color]="'#eee'">
|
||||
@for (point of points; track point.epochSeconds) {
|
||||
<g>
|
||||
<title>
|
||||
Bezug: {{ location.energyPurchase?.toValue(point.purchase, point.date)?.toValueString(null) }}
|
||||
Solar: {{ location.energyProduce?.toValue(point.produce, point.date)?.toValueString(null) }}
|
||||
Selbst: {{ location.energyPurchase?.toValue(point.self, point.date)?.toValueString(null) }}
|
||||
Einsp.: {{ location.energyDeliver?.toValue(point.deliver, point.date)?.toValueString(null) }}
|
||||
Verbrauch: {{ location.energyPurchase?.toValue(point.consume, point.date)?.toValueString(null) }}
|
||||
</title>
|
||||
<rect
|
||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
||||
[attr.y]="heightPx + yMinPx - point.getPurchaseY(yFactor)"
|
||||
[attr.width]="xWidthPx"
|
||||
[attr.height]="point.getPurchaseH(yFactor)"
|
||||
class="COLOR_BACK_PURCHASE"
|
||||
></rect>
|
||||
<rect
|
||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
||||
[attr.y]="heightPx + yMinPx - point.getSelfY(yFactor)"
|
||||
[attr.width]="xWidthPx"
|
||||
[attr.height]="point.getSelfH(yFactor)"
|
||||
class="COLOR_BACK_SELF"
|
||||
></rect>
|
||||
<rect
|
||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
||||
[attr.y]="heightPx+1 + yMinPx"
|
||||
[attr.width]="xWidthPx"
|
||||
[attr.height]="point.getDeliverH(yFactor)"
|
||||
class="COLOR_BACK_DELIVER"
|
||||
></rect>
|
||||
</g>
|
||||
}
|
||||
<line
|
||||
x1="0"
|
||||
[attr.y1]="heightPx +0.5 + yMinPx"
|
||||
[attr.x2]="widthPx"
|
||||
[attr.y2]="heightPx +0.5 + yMinPx"
|
||||
stroke="#aaaaaa"
|
||||
stroke-width="1"
|
||||
></line>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,5 +0,0 @@
|
||||
@import "../../../../colors";
|
||||
|
||||
g:hover {
|
||||
stroke: black;
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Location} from '../../Location';
|
||||
import {Interval} from '../../../series/Interval';
|
||||
import {PointService} from '../../../point/point-service';
|
||||
import {PointResponse} from '../../../point/PointResponse';
|
||||
import {PointSeries} from '../../../point/PointSeries';
|
||||
import {EnergyPoint} from './EnergyPoint';
|
||||
import {Subscription, timer} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-energy-plot',
|
||||
imports: [],
|
||||
templateUrl: './energy-plot.html',
|
||||
styleUrl: './energy-plot.less',
|
||||
})
|
||||
export class EnergyPlot implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
readonly widthPx = 800;
|
||||
|
||||
readonly heightPx = 100;
|
||||
|
||||
private _location!: Location;
|
||||
|
||||
private _interval: Interval = Interval.FIVE;
|
||||
|
||||
private _offset: number = 0;
|
||||
|
||||
private _count: number = 1;
|
||||
|
||||
protected points: EnergyPoint[] = [];
|
||||
|
||||
protected yMin: number = 0;
|
||||
|
||||
protected yMinPx: number = 0;
|
||||
|
||||
protected yMax: number = 0;
|
||||
|
||||
protected yFactor: number = 0;
|
||||
|
||||
protected xMin: number = 0;
|
||||
|
||||
protected xMax: number = 0;
|
||||
|
||||
protected xFactor: number = 0;
|
||||
|
||||
protected xWidthPx: number = 0;
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(timer(60000, 60000).subscribe(() => this.ngAfterViewInit()));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (!this._location.energyPurchase) {
|
||||
return;
|
||||
}
|
||||
if (!this._location.energyProduce) {
|
||||
return;
|
||||
}
|
||||
if (!this._location.energyDeliver) {
|
||||
return;
|
||||
}
|
||||
const series = [this._location.energyPurchase, this._location.energyProduce, this._location.energyDeliver];
|
||||
this.pointService.relative(series, this._interval, this._offset, this._count, this._interval.inner, this.update);
|
||||
}
|
||||
|
||||
public readonly update = (response: PointResponse): void => {
|
||||
this.points.length = 0;
|
||||
this.add(response.series[0], (p, v) => p.purchase = v);
|
||||
this.add(response.series[1], (p, v) => p.produce = v);
|
||||
this.add(response.series[2], (p, v) => p.deliver = v);
|
||||
this.yMax = -Infinity;
|
||||
this.yMin = Infinity;
|
||||
for (let point of this.points) {
|
||||
this.yMax = Math.max(this.yMax, point.consume || 0);
|
||||
this.yMin = Math.min(this.yMin, -(point.deliver || 0));
|
||||
}
|
||||
this.yMinPx = this.yMin * this.yFactor;
|
||||
this.yFactor = this.heightPx / (this.yMax - this.yMin);
|
||||
this.xMin = response.begin.getTime() / 1000;
|
||||
this.xMax = response.end.getTime() / 1000;
|
||||
this.xFactor = this.widthPx / (this.xMax - this.xMin);
|
||||
this.xWidthPx = this.widthPx / response.expectedCount;
|
||||
};
|
||||
|
||||
private add(series: PointSeries, setter: (p: EnergyPoint, v: number) => void): void {
|
||||
for (const point of series.points) {
|
||||
const index = this.insert(point, setter);
|
||||
if (index >= 0) {
|
||||
const fresh = new EnergyPoint(point);
|
||||
setter(fresh, point[1])
|
||||
this.points.splice(index, 0, fresh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private insert(point: number[], setter: (p: EnergyPoint, v: number) => any): number {
|
||||
let index = 0;
|
||||
for (let old of this.points) {
|
||||
const age = old.epochSeconds - point[0];
|
||||
if (age === 0) {
|
||||
setter(old, point[1])
|
||||
return -1;
|
||||
} else if (age < 0) {
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set location(value: Location) {
|
||||
this._location = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
get location(): Location {
|
||||
return this._location;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set interval(value: Interval) {
|
||||
this._interval = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set offset(value: number) {
|
||||
this._offset = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set count(value: number) {
|
||||
this._count = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
|
||||
}
|
||||
@ -85,4 +85,16 @@ export class LocationService extends CrudService<Location> {
|
||||
this.postSingle([location.id, 'powerProduce'], seriesId, next);
|
||||
}
|
||||
|
||||
outsideTemperature(location: Location, seriesId: number | null, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'outsideTemperature'], seriesId, next);
|
||||
}
|
||||
|
||||
outsideHumidityRelative(location: Location, seriesId: number | null, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'outsideHumidityRelative'], seriesId, next);
|
||||
}
|
||||
|
||||
outsideHumidityAbsolute(location: Location, seriesId: number | null, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'outsideHumidityAbsolute'], seriesId, next);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
Selbst
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_SELF">
|
||||
{{ location.powerSelf?.toValueString(dateService.now) }}
|
||||
{{ location.powerSelf.toValueString(dateService.now) }}
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_SELF percent">
|
||||
{{ location.powerSelfPercentConsume.toValueString(dateService.now) }}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {Series} from "../series/Series";
|
||||
import {validateList, validateNumber} from "../common";
|
||||
import {maxDate, validateList, validateNumber} from "../common";
|
||||
|
||||
export class PointSeries {
|
||||
|
||||
@ -17,4 +17,41 @@ export class PointSeries {
|
||||
);
|
||||
}
|
||||
|
||||
merge(that: PointSeries | null, name: string, fun: (a: number, b: number) => number): PointSeries | null {
|
||||
if (!that) {
|
||||
return null;
|
||||
}
|
||||
if (this.series.type !== that.series.type) {
|
||||
throw new Error(`Cannot combine PointSeries of different Series.type: this=${this.series.name}/${this.series.type}, that=${that.series.name}/${that.series.type}`);
|
||||
}
|
||||
const a = [...this.points];
|
||||
const b = [...that.points];
|
||||
let ai = 0;
|
||||
let bi = 0;
|
||||
const result: number[][] = [];
|
||||
while (ai < a.length && ai < b.length) {
|
||||
const av = a[ai];
|
||||
const bv = b[bi];
|
||||
const at = av[0];
|
||||
const bt = bv[0];
|
||||
if (at < bt) {
|
||||
ai++;
|
||||
} else if (bt < at) {
|
||||
bi++;
|
||||
} else {
|
||||
const r = [at];
|
||||
for (let i = 1; i < bv.length; i++) {
|
||||
r.push(fun(av[i], bv[i]));
|
||||
}
|
||||
result.push(r)
|
||||
ai++;
|
||||
bi++;
|
||||
}
|
||||
}
|
||||
const value = fun(this.series.value.value, that.series.value.value);
|
||||
const last = maxDate(this.series.last, that.series.last);
|
||||
const series = new Series(-1, name, this.series.precision, this.series.seconds, this.series.type, value, this.series.unit, last);
|
||||
return new PointSeries(series, result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import {PointResponse} from './PointResponse';
|
||||
import {Series} from '../series/Series';
|
||||
import {Interval} from '../series/Interval';
|
||||
|
||||
const notNull = <T>(v: T | null | undefined): v is T => v != null;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@ -16,9 +18,9 @@ export class PointService extends CrudService<PointResponse> {
|
||||
super(api, ws, ['Point'], PointResponse.fromJson);
|
||||
}
|
||||
|
||||
relative(series: Series[], outer: Interval, offset: number, count: number, interval: Interval, next: Next<PointResponse>): void {
|
||||
relative(series: (Series | null | undefined)[], outer: Interval, offset: number, count: number, interval: Interval, next: Next<PointResponse>): void {
|
||||
const request = {
|
||||
ids: series.map(s => s.id),
|
||||
ids: series.filter(notNull).map(s => s.id),
|
||||
outerInterval: outer.name,
|
||||
offset: offset,
|
||||
count: count,
|
||||
|
||||
@ -29,6 +29,9 @@ export class Value {
|
||||
return `--- ${this.unit}`
|
||||
}
|
||||
const scale = Math.floor(Math.log10(this.value));
|
||||
if(isNaN(scale)) {
|
||||
return '0';
|
||||
}
|
||||
const rest = scale - this.precision + 1;
|
||||
if (rest >= 0) {
|
||||
return `${Math.round(this.value)} ${this.unit}`;
|
||||
|
||||
@ -71,4 +71,19 @@ public class Location {
|
||||
@ManyToOne
|
||||
private Series powerProduce;
|
||||
|
||||
@Setter
|
||||
@Nullable
|
||||
@ManyToOne
|
||||
private Series outsideTemperature;
|
||||
|
||||
@Setter
|
||||
@Nullable
|
||||
@ManyToOne
|
||||
private Series outsideHumidityRelative;
|
||||
|
||||
@Setter
|
||||
@Nullable
|
||||
@ManyToOne
|
||||
private Series outsideHumidityAbsolute;
|
||||
|
||||
}
|
||||
|
||||
@ -87,4 +87,19 @@ public class LocationController {
|
||||
return locationService.setSeries(id, seriesId, Location::setPowerProduce);
|
||||
}
|
||||
|
||||
@PostMapping("{id}/outsideTemperature")
|
||||
public LocationDto outsideTemperature(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) {
|
||||
return locationService.setSeries(id, seriesId, Location::setOutsideTemperature);
|
||||
}
|
||||
|
||||
@PostMapping("{id}/outsideHumidityRelative")
|
||||
public LocationDto outsideHumidityRelative(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) {
|
||||
return locationService.setSeries(id, seriesId, Location::setOutsideHumidityRelative);
|
||||
}
|
||||
|
||||
@PostMapping("{id}/outsideHumidityAbsolute")
|
||||
public LocationDto outsideHumidityAbsolute(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) {
|
||||
return locationService.setSeries(id, seriesId, Location::setOutsideHumidityAbsolute);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -38,6 +38,15 @@ public class LocationDto {
|
||||
@Nullable
|
||||
public final SeriesDto powerProduce;
|
||||
|
||||
@Nullable
|
||||
public final SeriesDto outsideTemperature;
|
||||
|
||||
@Nullable
|
||||
public final SeriesDto outsideHumidityRelative;
|
||||
|
||||
@Nullable
|
||||
public final SeriesDto outsideHumidityAbsolute;
|
||||
|
||||
public LocationDto(@NonNull final Location location) {
|
||||
this.id = location.getId();
|
||||
this.version = location.getVersion();
|
||||
@ -50,6 +59,9 @@ public class LocationDto {
|
||||
this.powerPurchase = map(location.getPowerPurchase(), SeriesDto::new);
|
||||
this.powerDeliver = map(location.getPowerDeliver(), SeriesDto::new);
|
||||
this.powerProduce = map(location.getPowerProduce(), SeriesDto::new);
|
||||
this.outsideTemperature = map(location.getOutsideTemperature(), SeriesDto::new);
|
||||
this.outsideHumidityRelative = map(location.getOutsideHumidityRelative(), SeriesDto::new);
|
||||
this.outsideHumidityAbsolute = map(location.getOutsideHumidityAbsolute(), SeriesDto::new);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package de.ph87.data.point;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@ -15,6 +16,7 @@ public class PointController {
|
||||
|
||||
private final PointService pointService;
|
||||
|
||||
@NonNull
|
||||
@PostMapping("relative")
|
||||
public PointResponse points(@RequestBody final PointRequestRelative request) {
|
||||
return pointService.points(request);
|
||||
|
||||
@ -10,7 +10,7 @@ public interface TopicRepository extends ListCrudRepository<Topic, Long> {
|
||||
|
||||
boolean existsByName(@NonNull String name);
|
||||
|
||||
@Query("select new de.ph87.data.topic.TopicDto(t) from Topic t where t.name = :name")
|
||||
@Query("select new de.ph87.data.topic.TopicDto(t) from Topic t where t.name = :name and t.enabled")
|
||||
Optional<TopicDto> findDtoByEnabledTrueAndName(@NonNull String name);
|
||||
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
import de.ph87.data.topic.parser.EspHome;
|
||||
import de.ph87.data.topic.parser.PatrixOpenDtu;
|
||||
import de.ph87.data.topic.parser.PatrixSmartMeter;
|
||||
import de.ph87.data.topic.parser.ShellyPlus1PM;
|
||||
@ -12,6 +13,7 @@ public enum TopicType {
|
||||
PatrixSmartMeter(PatrixSmartMeter.class),
|
||||
TasmotaSmartMeter(TasmotaSmartMeter.class),
|
||||
ShellyPlus1PM(ShellyPlus1PM.class),
|
||||
EspHome(EspHome.class),
|
||||
;
|
||||
|
||||
public final Class<? extends ITopicParser> clazz;
|
||||
|
||||
58
src/main/java/de/ph87/data/topic/parser/EspHome.java
Normal file
58
src/main/java/de/ph87/data/topic/parser/EspHome.java
Normal file
@ -0,0 +1,58 @@
|
||||
package de.ph87.data.topic.parser;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import de.ph87.data.series.data.delta.DeltaService;
|
||||
import de.ph87.data.series.data.varying.VaryingService;
|
||||
import de.ph87.data.topic.TopicDto;
|
||||
import de.ph87.data.topic.TopicParserAbstract;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class EspHome extends TopicParserAbstract<EspHome.Dto> {
|
||||
|
||||
public EspHome(@NonNull final ObjectMapper mapper, @NonNull final DeltaService deltaService, @NonNull final VaryingService varyingService) {
|
||||
super(Dto.class, mapper, deltaService, varyingService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle2(@NonNull final TopicDto topic, @NonNull final Dto dto) {
|
||||
if (topic.series0 != null) {
|
||||
if (topic.series0.unit.equals(dto.unit)) {
|
||||
varying(topic.series0, dto.date, dto.value);
|
||||
} else {
|
||||
log.warn("Unit mismatch: dto={}, series={}", dto, topic.series0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Dto {
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime date;
|
||||
|
||||
public final double value;
|
||||
|
||||
@NonNull
|
||||
public final String unit;
|
||||
|
||||
public Dto(
|
||||
@JsonProperty(value = "timestamp", required = true) @NonNull final long epochSeconds,
|
||||
@JsonProperty(value = "value", required = true) final double value,
|
||||
@JsonProperty(value = "unit", required = true) final String unit
|
||||
) {
|
||||
this.date = ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault());
|
||||
this.value = value;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user