Compare commits

..

No commits in common. "aa7161696166d4191d283edf58b407f5ef9e39a0" and "4feb38c14cab728b4b7fa2d49681adec037cfea0" have entirely different histories.

28 changed files with 322 additions and 514 deletions

View File

@ -19,8 +19,6 @@
"@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"
@ -425,22 +423,6 @@
}
}
},
"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",
@ -1919,13 +1901,6 @@
"@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",
@ -4223,29 +4198,6 @@
"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",
@ -4609,17 +4561,6 @@
"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",
@ -6826,12 +6767,6 @@
"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",
@ -7431,24 +7366,6 @@
"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",
@ -7987,6 +7904,7 @@
"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"
@ -8040,6 +7958,7 @@
"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"

View File

@ -35,9 +35,7 @@
"@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",
"ng2-charts": "^8.0.0",
"chartjs-adapter-date-fns": "^3.0.0"
"@fortawesome/free-solid-svg-icons": "^7.1.0"
},
"devDependencies": {
"@angular/build": "^20.3.7",

View File

@ -9,13 +9,9 @@ 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(),

View File

@ -47,16 +47,6 @@ 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);
}
@ -228,10 +218,6 @@ 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);
}

View File

@ -29,9 +29,6 @@ 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();
@ -61,6 +58,17 @@ 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),
@ -73,23 +81,9 @@ 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;
}
@ -118,16 +112,4 @@ 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;
}
}

View File

@ -148,44 +148,4 @@
</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>
}

View File

@ -33,12 +33,6 @@ 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;

View File

@ -1,3 +0,0 @@
<div>
<canvas baseChart [data]="data" [options]="options"></canvas>
</div>

View File

@ -1,3 +0,0 @@
div {
aspect-ratio: 2;
}

View File

@ -1,159 +0,0 @@
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

View File

@ -78,6 +78,6 @@
</div>
<app-energy-charts [location]="location" [interval]="interval" [offset]="offset"></app-energy-charts>
<app-energy-plot [location]="location" [interval]="interval" [offset]="offset"></app-energy-plot>
</div>

View File

@ -1,4 +1,4 @@
import {AfterViewInit, Component, Input, OnDestroy, OnInit, signal} from '@angular/core';
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
import {Location} from '../Location';
import {Series} from '../../series/Series';
import {Next} from '../../common';
@ -7,21 +7,18 @@ import {PointService} from '../../point/point-service';
import {SeriesService} from '../../series/series-service';
import {Subscription} from 'rxjs';
import {Value} from '../../series/Value';
import EnergyCharts from './charts/energy-charts';
import {EnergyPlot} from './plot/energy-plot';
@Component({
selector: 'app-location-energy',
imports: [
EnergyCharts,
EnergyCharts
EnergyPlot
],
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[] = [];

View File

@ -0,0 +1,101 @@
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;
}
}

View File

@ -0,0 +1,42 @@
<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>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,5 @@
@import "../../../../colors";
g:hover {
stroke: black;
}

View File

@ -0,0 +1,151 @@
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();
}
}

View File

@ -85,16 +85,4 @@ 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);
}
}

View File

@ -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) }}

View File

@ -1,5 +1,5 @@
import {Series} from "../series/Series";
import {maxDate, validateList, validateNumber} from "../common";
import {validateList, validateNumber} from "../common";
export class PointSeries {
@ -17,41 +17,4 @@ 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);
}
}

View File

@ -4,8 +4,6 @@ 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'
})
@ -18,9 +16,9 @@ export class PointService extends CrudService<PointResponse> {
super(api, ws, ['Point'], PointResponse.fromJson);
}
relative(series: (Series | null | undefined)[], outer: Interval, offset: number, count: number, interval: Interval, next: Next<PointResponse>): void {
relative(series: Series[], outer: Interval, offset: number, count: number, interval: Interval, next: Next<PointResponse>): void {
const request = {
ids: series.filter(notNull).map(s => s.id),
ids: series.map(s => s.id),
outerInterval: outer.name,
offset: offset,
count: count,

View File

@ -29,9 +29,6 @@ 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}`;

View File

@ -71,19 +71,4 @@ 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;
}

View File

@ -87,19 +87,4 @@ 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);
}
}

View File

@ -38,15 +38,6 @@ 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();
@ -59,9 +50,6 @@ 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);
}
}

View File

@ -1,6 +1,5 @@
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;
@ -16,7 +15,6 @@ public class PointController {
private final PointService pointService;
@NonNull
@PostMapping("relative")
public PointResponse points(@RequestBody final PointRequestRelative request) {
return pointService.points(request);

View File

@ -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 and t.enabled")
@Query("select new de.ph87.data.topic.TopicDto(t) from Topic t where t.name = :name")
Optional<TopicDto> findDtoByEnabledTrueAndName(@NonNull String name);
}

View File

@ -1,6 +1,5 @@
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;
@ -13,7 +12,6 @@ public enum TopicType {
PatrixSmartMeter(PatrixSmartMeter.class),
TasmotaSmartMeter(TasmotaSmartMeter.class),
ShellyPlus1PM(ShellyPlus1PM.class),
EspHome(EspHome.class),
;
public final Class<? extends ITopicParser> clazz;

View File

@ -1,58 +0,0 @@
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;
}
}
}