Compare commits
14 Commits
aa71616961
...
15355fade4
| Author | SHA1 | Date | |
|---|---|---|---|
| 15355fade4 | |||
| e2b0476aaa | |||
| f7c30d71d2 | |||
| 9213c20116 | |||
| 2bd115f095 | |||
| b2a96e1c24 | |||
| 9f24ea08ae | |||
| dc7e57f341 | |||
| a94d80753a | |||
| 5c2f9423d3 | |||
| d1c42aa6aa | |||
| a7050cb6f1 | |||
| bed87bb0e2 | |||
| 20bb84da63 |
@ -9,12 +9,12 @@ 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 {BarController, BarElement, Chart, Filler, Legend, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip} from 'chart.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
|
||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||
|
||||
Chart.register(...registerables);
|
||||
Chart.register(TimeScale, LinearScale, BarController, BarElement, Tooltip, Legend, LineController, PointElement, LineElement, Filler);
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
|
||||
@ -1,21 +1,31 @@
|
||||
<div class="MainMenu NoUserSelect">
|
||||
<div class="MainMenuBar">
|
||||
|
||||
<div class="MainMenuItem MainMenuButton" (click)="showDrawer = !showDrawer">
|
||||
<fa-icon [icon]="faBars"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div class="MainMenuItem MainMenuTitle">
|
||||
{{ menuService.title }}
|
||||
</div>
|
||||
@if (!ws.connected) {
|
||||
<div class="MainMenuItem MainMenuNotConnected">NICHT VERBUNDEN</div>
|
||||
}
|
||||
|
||||
<div class="MainMenuItem" routerLink="/Location">
|
||||
<fa-icon [icon]="faHome"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div class="MainMenuItem" routerLink="/Settings">
|
||||
<fa-icon [icon]="faGears"></fa-icon>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="MainMenuDrawer NoUserSelect" [hidden]="!showDrawer">
|
||||
@for (location of locationList; track location.id) {
|
||||
@if (location.id !== menuService.locationId) {
|
||||
<div class="MainMenuItem" (click)="navigate(`Location/${location.id}`); showDrawer = false" routerLinkActive="MainMenuItemActive">{{ location.name }}</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<router-outlet/>
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
.MainMenuBar {
|
||||
display: flex;
|
||||
padding: 0.25em;
|
||||
font-size: 120%;
|
||||
|
||||
.MainMenuItem {
|
||||
padding: 0.25em;
|
||||
@ -25,8 +26,12 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.MainMenuNotConnected {
|
||||
color: red;
|
||||
.bookmarkInactive {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
.bookmarkActive {
|
||||
color: steelblue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {LocationList} from './location/list/location-list';
|
||||
import {LocationDetail} from './location/detail/location-detail';
|
||||
import {SettingsComponent} from './settings/settings-component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: 'Location/:id', component: LocationDetail},
|
||||
{path: 'Location', component: LocationList},
|
||||
{path: 'Location', component: LocationDetail},
|
||||
{path: 'Settings', component: SettingsComponent},
|
||||
{path: '**', redirectTo: '/Location'},
|
||||
];
|
||||
|
||||
@ -1,19 +1,26 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Router, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||
import {faBars} from '@fortawesome/free-solid-svg-icons';
|
||||
import {faBars, faGears} from '@fortawesome/free-solid-svg-icons';
|
||||
import {MenuService} from './menu-service';
|
||||
import {Location} from './location/Location';
|
||||
import {LocationService} from './location/location-service';
|
||||
import {WebsocketService} from './common';
|
||||
import {faHome} from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, FaIconComponent, RouterLinkActive],
|
||||
imports: [RouterOutlet, FaIconComponent, RouterLinkActive, RouterLink],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.less'
|
||||
})
|
||||
export class App implements OnInit, OnDestroy {
|
||||
export class App implements OnInit {
|
||||
|
||||
protected readonly faHome = faHome;
|
||||
|
||||
protected readonly location = location;
|
||||
|
||||
protected readonly faGears = faGears;
|
||||
|
||||
protected readonly faBars = faBars;
|
||||
|
||||
@ -32,11 +39,6 @@ export class App implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.locationService.findAll(list => this.locationList = list);
|
||||
this.menuService.title = "Orte";
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.menuService.title = "";
|
||||
}
|
||||
|
||||
navigate(url: string): void {
|
||||
|
||||
103
src/main/angular/src/app/config.service.ts
Normal file
103
src/main/angular/src/app/config.service.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ConfigService {
|
||||
|
||||
private readonly LOCATION_ID_KEY = "locationId";
|
||||
|
||||
private readonly ENERGY_PERCENT_KEY = "energyPercent";
|
||||
|
||||
private readonly ENERGY_PERCENT_FALLBACK = false;
|
||||
|
||||
private readonly LOCATION_CONFIG_KEY = "locationConfig";
|
||||
|
||||
private readonly LOCATION_CONFIG_FALLBACK = false;
|
||||
|
||||
constructor() {
|
||||
this._locationId = this.readNumberOrNull(this.LOCATION_ID_KEY);
|
||||
this._energyPercent = this.readBoolean(this.ENERGY_PERCENT_KEY, this.ENERGY_PERCENT_FALLBACK);
|
||||
this._locationConfig = this.readBoolean(this.LOCATION_CONFIG_KEY, this.LOCATION_CONFIG_FALLBACK);
|
||||
}
|
||||
|
||||
private readNumberOrNull(key: string): number | null {
|
||||
const value = this.read(key);
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
const number = parseInt(value);
|
||||
return isNaN(number) ? null : number;
|
||||
}
|
||||
|
||||
private readBoolean(key: string, fallback: boolean): boolean {
|
||||
const value = this.read(key);
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private read(key: string): string | null {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
private writeBoolean(key: string, value: boolean | null): void {
|
||||
if (value !== null && value !== undefined) {
|
||||
localStorage.setItem(key, value + "");
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
// locationId -----------------------------------------------------------------------------------
|
||||
|
||||
private _locationId: number | null = null;
|
||||
|
||||
get locationId(): number | null {
|
||||
return this._locationId;
|
||||
}
|
||||
|
||||
set locationId(value: number | null) {
|
||||
this._locationId = value;
|
||||
if (value !== null && value !== undefined) {
|
||||
localStorage.setItem(this.LOCATION_ID_KEY, value + "");
|
||||
} else {
|
||||
localStorage.removeItem(this.LOCATION_ID_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// energyPercent -----------------------------------------------------------------------------------
|
||||
|
||||
private _energyPercent: boolean = this.ENERGY_PERCENT_FALLBACK;
|
||||
|
||||
get energyPercent(): boolean {
|
||||
return this._energyPercent;
|
||||
}
|
||||
|
||||
set energyPercent(value: boolean) {
|
||||
this._energyPercent = value;
|
||||
this.writeBoolean(this.ENERGY_PERCENT_KEY, value);
|
||||
}
|
||||
|
||||
// locationConfig -----------------------------------------------------------------------------------
|
||||
|
||||
private _locationConfig: boolean = this.LOCATION_CONFIG_FALLBACK;
|
||||
|
||||
get locationConfig(): boolean {
|
||||
return this._locationConfig;
|
||||
}
|
||||
|
||||
set locationConfig(value: boolean) {
|
||||
this._locationConfig = value;
|
||||
if (value !== null && value !== undefined) {
|
||||
localStorage.setItem(this.LOCATION_CONFIG_KEY, value + "");
|
||||
} else {
|
||||
localStorage.removeItem(this.LOCATION_CONFIG_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,66 +1,26 @@
|
||||
import {or, validateNumber, validateString} from '../common';
|
||||
import {Series} from '../series/Series';
|
||||
import {Value} from '../series/Value';
|
||||
|
||||
export class Location {
|
||||
|
||||
powerSelf: Value = Value.NULL;
|
||||
|
||||
powerPurchasePercentConsume: Value = Value.NULL;
|
||||
|
||||
powerProducePercentConsume: Value = Value.NULL;
|
||||
|
||||
powerDeliveryPercentConsume: Value = Value.NULL;
|
||||
|
||||
powerDeliveryPercentProduce: Value = Value.NULL;
|
||||
|
||||
powerSelfPercentConsume: Value = Value.NULL;
|
||||
|
||||
powerSelfPercentProduce: Value = Value.NULL;
|
||||
|
||||
constructor(
|
||||
readonly id: number,
|
||||
readonly name: string,
|
||||
readonly latitude: number,
|
||||
readonly longitude: number,
|
||||
private _energyPurchase: Series | null,
|
||||
private _energyDeliver: Series | null,
|
||||
private _energyProduce: Series | null,
|
||||
private _powerPurchase: Series | null,
|
||||
private _powerDeliver: Series | null,
|
||||
private _powerProduce: Series | null,
|
||||
private _outsideTemperature: Series | null,
|
||||
private _outsideHumidityRelative: Series | null,
|
||||
private _outsideHumidityAbsolute: Series | null,
|
||||
private _powerConsume: Value = Value.NULL,
|
||||
readonly energyPurchase: Series | null,
|
||||
readonly energyDeliver: Series | null,
|
||||
readonly energyProduce: Series | null,
|
||||
readonly powerPurchase: Series | null,
|
||||
readonly powerDeliver: Series | null,
|
||||
readonly powerProduce: Series | null,
|
||||
readonly outsideTemperature: Series | null,
|
||||
readonly outsideHumidityRelative: Series | null,
|
||||
readonly outsideHumidityAbsolute: Series | null,
|
||||
) {
|
||||
this.updateConsume();
|
||||
//
|
||||
}
|
||||
|
||||
readonly updateSeries = (series: Series) => {
|
||||
if (series.equals(this._energyPurchase)) {
|
||||
this._energyPurchase = series;
|
||||
}
|
||||
if (series.equals(this._energyDeliver)) {
|
||||
this._energyDeliver = series;
|
||||
}
|
||||
if (series.equals(this._energyProduce)) {
|
||||
this._energyProduce = series;
|
||||
}
|
||||
if (series.equals(this._powerProduce)) {
|
||||
this._powerProduce = series;
|
||||
this.updateConsume();
|
||||
}
|
||||
if (series.equals(this._powerPurchase)) {
|
||||
this._powerPurchase = series;
|
||||
this.updateConsume();
|
||||
}
|
||||
if (series.equals(this._powerDeliver)) {
|
||||
this._powerDeliver = series;
|
||||
this.updateConsume();
|
||||
}
|
||||
};
|
||||
|
||||
static fromJson(json: any): Location {
|
||||
return new Location(
|
||||
validateNumber(json.id),
|
||||
@ -79,55 +39,18 @@ 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);
|
||||
}
|
||||
|
||||
get energyPurchase(): Series | null {
|
||||
return this._energyPurchase;
|
||||
}
|
||||
|
||||
get energyDeliver(): Series | null {
|
||||
return this._energyDeliver;
|
||||
}
|
||||
|
||||
get energyProduce(): Series | null {
|
||||
return this._energyProduce;
|
||||
}
|
||||
|
||||
get powerPurchase(): Series | null {
|
||||
return this._powerPurchase;
|
||||
}
|
||||
|
||||
get powerDeliver(): Series | null {
|
||||
return this._powerDeliver;
|
||||
}
|
||||
|
||||
get powerProduce(): Series | null {
|
||||
return this._powerProduce;
|
||||
}
|
||||
|
||||
get powerConsume(): Value | null {
|
||||
return this._powerConsume;
|
||||
}
|
||||
|
||||
get outsideTemperature(): Series | null {
|
||||
return this._outsideTemperature;
|
||||
}
|
||||
|
||||
get outsideHumidityRelative(): Series | null {
|
||||
return this._outsideHumidityRelative;
|
||||
}
|
||||
|
||||
get outsideHumidityAbsolute(): Series | null {
|
||||
return this._outsideHumidityAbsolute;
|
||||
getSeries(): Series[] {
|
||||
return [
|
||||
this.energyPurchase,
|
||||
this.energyDeliver,
|
||||
this.energyProduce,
|
||||
this.powerPurchase,
|
||||
this.powerDeliver,
|
||||
this.powerProduce,
|
||||
this.outsideTemperature,
|
||||
this.outsideHumidityRelative,
|
||||
this.outsideHumidityAbsolute,
|
||||
].filter(s => s) as Series[];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,32 +2,32 @@
|
||||
|
||||
<app-location-power [location]="location"></app-location-power>
|
||||
|
||||
<app-location-energy [location]="location" [interval]="Interval.DAY" [offset]="offsetDay">
|
||||
<ng-content #SeriesHistoryHeading>
|
||||
<div style="display: flex; width: 100%">
|
||||
|
||||
<div (click)="offsetDay += 1">←</div>
|
||||
|
||||
<div (click)="offsetDay = Math.max(0, offsetDay -1)">→</div>
|
||||
|
||||
<div style="flex: 1">{{ offsetDayTitle() }}</div>
|
||||
<div class="Section3">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Wettervorhersage
|
||||
</div>
|
||||
</div>
|
||||
<app-weather-component [location]="location"></app-weather-component>
|
||||
</div>
|
||||
|
||||
<app-location-energy [location]="location" [interval]="Interval.DAY" [offset]="offsetDay" unit="⌀W" [factor]="12 * 1000" [maxY]="850" [minY]="-850">
|
||||
<div style="display: flex; width: 100%; gap: 0.25em;">
|
||||
<div (click)="offsetDayAdd(+1)">←</div>
|
||||
<div (click)="offsetDayAdd(-1)">→</div>
|
||||
<div>{{ offsetDayTitle() }}</div>
|
||||
</div>
|
||||
</ng-content>
|
||||
</app-location-energy>
|
||||
|
||||
<app-location-energy [location]="location" [interval]="Interval.MONTH" [offset]="offsetMonth">
|
||||
<ng-content #SeriesHistoryHeading>
|
||||
<div style="display: flex; width: 100%">
|
||||
|
||||
<div (click)="offsetMonth += 1">←</div>
|
||||
|
||||
<div (click)="offsetMonth = Math.max(0, offsetMonth -1)">→</div>
|
||||
|
||||
<div style="flex: 1">{{ offsetMonthTitle() }}</div>
|
||||
<div style="display: flex; width: 100%; gap: 0.25em;">
|
||||
<div (click)="offsetMonthAdd(+1)">←</div>
|
||||
<div (click)="offsetMonthAdd(-1)">→</div>
|
||||
<div>{{ offsetMonthTitle() }}</div>
|
||||
</div>
|
||||
</ng-content>
|
||||
</app-location-energy>
|
||||
|
||||
@if (configService.locationConfig) {
|
||||
<div class="Section">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
@ -188,4 +188,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {Component, Inject, LOCALE_ID, OnDestroy, OnInit} from '@angular/core';
|
||||
import {LocationService} from '../location-service';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {ActivatedRoute, Params, Router} from '@angular/router';
|
||||
import {Location} from '../Location';
|
||||
import {Text} from '../../shared/text/text';
|
||||
import {Number} from '../../shared/number/number';
|
||||
@ -14,6 +14,20 @@ import {Series} from '../../series/Series';
|
||||
import {SeriesType} from '../../series/SeriesType';
|
||||
import {DateService} from '../../date.service';
|
||||
import {LocationPower} from '../power/location-power';
|
||||
import {ConfigService} from '../../config.service';
|
||||
import {WeatherComponent} from '../../weather/plot/weather-component';
|
||||
|
||||
export function paramNumberOrNull(params: Params, key: string): number | null {
|
||||
const param = params[key];
|
||||
if (param === null || param === undefined) {
|
||||
return null;
|
||||
}
|
||||
const value = parseInt(param);
|
||||
if (isNaN(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-detail',
|
||||
@ -22,7 +36,8 @@ import {LocationPower} from '../power/location-power';
|
||||
Number,
|
||||
SeriesSelect,
|
||||
LocationEnergy,
|
||||
LocationPower
|
||||
LocationPower,
|
||||
WeatherComponent
|
||||
],
|
||||
templateUrl: './location-detail.html',
|
||||
styleUrl: './location-detail.less',
|
||||
@ -43,6 +58,8 @@ export class LocationDetail implements OnInit, OnDestroy {
|
||||
|
||||
protected readonly Math = Math;
|
||||
|
||||
private locationId: number | null = null;
|
||||
|
||||
protected location: Location | null = null;
|
||||
|
||||
private readonly subs: Subscription [] = [];
|
||||
@ -58,27 +75,36 @@ export class LocationDetail implements OnInit, OnDestroy {
|
||||
readonly activatedRoute: ActivatedRoute,
|
||||
readonly menuService: MenuService,
|
||||
readonly dateService: DateService,
|
||||
readonly configService: ConfigService,
|
||||
readonly router: Router,
|
||||
@Inject(LOCALE_ID) readonly locale: string,
|
||||
) {
|
||||
this.datePipe = new DatePipe(locale);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.locationService.id = null;
|
||||
this.subs.push(this.activatedRoute.params.subscribe(params => this.locationService.id = params['id'] || null));
|
||||
this.subs.push(this.locationService.location$.subscribe(this.onLocationChange));
|
||||
this.subs.push(this.activatedRoute.params.subscribe(params => {
|
||||
const id = paramNumberOrNull(params, "id");
|
||||
if (id === null && this.configService.locationId !== null) {
|
||||
this.router.navigate(["Location/" + this.configService.locationId]);
|
||||
return;
|
||||
}
|
||||
this.locationId = id;
|
||||
if (this.locationId) {
|
||||
this.locationService.getById(this.locationId, this.onLocationChange);
|
||||
}
|
||||
}));
|
||||
this.subs.push(this.locationService.subscribe(this.onLocationChange));
|
||||
}
|
||||
|
||||
private readonly onLocationChange = (location: Location | null): void => {
|
||||
if (this.locationId === location?.id) {
|
||||
this.location = location;
|
||||
if (this.location) {
|
||||
this.menuService.title = this.location.name;
|
||||
this.menuService.setLocation(location);
|
||||
}
|
||||
};
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.location = null;
|
||||
this.menuService.title = "";
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs.length = 0;
|
||||
}
|
||||
@ -101,4 +127,12 @@ export class LocationDetail implements OnInit, OnDestroy {
|
||||
return this.datePipe.transform(d, 'yyyy MMMM') || '';
|
||||
}
|
||||
|
||||
protected offsetDayAdd(delta: number) {
|
||||
this.offsetDay = Math.max(0, this.offsetDay + delta);
|
||||
}
|
||||
|
||||
protected offsetMonthAdd(delta: number) {
|
||||
this.offsetMonth = Math.max(0, this.offsetMonth + delta);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {Component, Input, ViewChild} from '@angular/core';
|
||||
import {Component, Inject, Input, LOCALE_ID, OnChanges, ViewChild} from '@angular/core';
|
||||
import {Interval} from '../../../series/Interval';
|
||||
import {PointService} from '../../../point/point-service';
|
||||
import {Location} from '../../Location';
|
||||
@ -10,8 +10,10 @@ import {formatNumber} from '@angular/common';
|
||||
|
||||
const COLOR_BACK_PURCHASE = "#ffb9b9";
|
||||
const COLOR_BACK_DELIVER = "#ff59ff";
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
const COLOR_BACK_PRODUCE = "#5cbcff";
|
||||
const COLOR_BACK_SELF = "#60ff8c";
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
const COLOR_BACK_CONSUME = "#ffc07a";
|
||||
|
||||
@Component({
|
||||
@ -22,83 +24,31 @@ const COLOR_BACK_CONSUME = "#ffc07a";
|
||||
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();
|
||||
}
|
||||
export class EnergyCharts implements OnChanges {
|
||||
|
||||
@ViewChild(BaseChartDirective)
|
||||
chart?: BaseChartDirective;
|
||||
|
||||
private _interval!: Interval | null;
|
||||
@Input()
|
||||
unit: string = "";
|
||||
|
||||
@Input()
|
||||
set interval(value: Interval | null) {
|
||||
this._interval = value;
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
private _location!: Location | null;
|
||||
factor: number = 1;
|
||||
|
||||
@Input()
|
||||
set location(value: Location | null) {
|
||||
this._location = value;
|
||||
this.fetch();
|
||||
}
|
||||
maxY: number | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
@Input()
|
||||
minY: number | undefined = undefined;
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
@Input()
|
||||
interval!: Interval;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
@Input()
|
||||
location!: Location;
|
||||
|
||||
@Input()
|
||||
offset: number = 0;
|
||||
|
||||
protected data: ChartConfiguration['data'] = {
|
||||
datasets: [],
|
||||
@ -124,10 +74,7 @@ class EnergyCharts {
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
suggestedMax: 0.5,
|
||||
suggestedMin: -0.1,
|
||||
}
|
||||
y: {},
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
@ -142,18 +89,89 @@ class EnergyCharts {
|
||||
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;
|
||||
title: (items) => {
|
||||
const date = new Date(items[0].parsed.x || 0);
|
||||
const d = date.toLocaleString(this.locale, {
|
||||
dateStyle: 'long',
|
||||
});
|
||||
const t = date.toLocaleString(this.locale, {
|
||||
timeStyle: 'long',
|
||||
});
|
||||
return `${d}\n${t}`;
|
||||
},
|
||||
label: ((ctx: any) => {
|
||||
const groups = /^.*Energie (?<name>.+)\[(?<unit>.+)]$/.exec(ctx.dataset.label || '')?.groups;
|
||||
if (groups) {
|
||||
const value = ctx.parsed.y === null ? '-' : formatNumber(Math.abs(ctx.parsed.y), this.locale, '0.2-2');
|
||||
return `${value} ${groups['unit']} ${groups['name']}`;
|
||||
}
|
||||
return ctx.dataset.label;
|
||||
}) as any,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
private refreshTimeout: number | undefined;
|
||||
|
||||
export default EnergyCharts
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
@Inject(LOCALE_ID) readonly locale: string,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
clearTimeout(this.refreshTimeout);
|
||||
this.refreshTimeout = setTimeout(() => this.ngOnChanges(), 60 * 1000);
|
||||
|
||||
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 => {
|
||||
this.data.datasets.length = 0;
|
||||
const xScale = this.chart?.chart?.options?.scales?.['x'];
|
||||
if (xScale) {
|
||||
xScale.min = result.begin.getTime();
|
||||
xScale.max = result.end.getTime() - (this.interval?.inner?.millis || 0);
|
||||
}
|
||||
const yScale = this.chart?.chart?.options?.scales?.['y'];
|
||||
if (yScale) {
|
||||
yScale.max = this.maxY;
|
||||
yScale.min = this.minY;
|
||||
}
|
||||
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.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?.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: 0.95,
|
||||
data: pointSeries.points.map(p => {
|
||||
return {x: p[0] * 1000, y: p[1] * factor * this.factor};
|
||||
}),
|
||||
label: `${pointSeries.series.name} [${this.unit || pointSeries.series.unit}]`,
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
stack: stack ? stack : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
{{ heading }}
|
||||
<ng-content #SeriesHistoryHeading></ng-content>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
@ -14,10 +14,12 @@
|
||||
<div class="SectionBody COLOR_FONT_PURCHASE">
|
||||
{{ purchase.toValueString(null) }}
|
||||
</div>
|
||||
@if (configService.energyPercent) {
|
||||
<div class="SectionBody COLOR_FONT_PURCHASE percent">
|
||||
{{ purchasePercentConsume.toValueString(null) }}
|
||||
<sub class="subscript">Verbrauch</sub>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
@ -27,10 +29,12 @@
|
||||
<div class="SectionBody COLOR_FONT_PRODUCE">
|
||||
{{ produce.toValueString(null) }}
|
||||
</div>
|
||||
@if (configService.energyPercent) {
|
||||
<div class="SectionBody COLOR_FONT_PRODUCE percent">
|
||||
{{ producePercentConsume.toValueString(null) }}
|
||||
<sub class="subscript">Verbrauch</sub>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
@ -40,6 +44,7 @@
|
||||
<div class="SectionBody COLOR_FONT_SELF">
|
||||
{{ self.toValueString(null) }}
|
||||
</div>
|
||||
@if (configService.energyPercent) {
|
||||
<div class="SectionBody COLOR_FONT_SELF percent">
|
||||
{{ selfPercentConsume.toValueString(null) }}
|
||||
<sub class="subscript">Verbrauch</sub>
|
||||
@ -48,6 +53,7 @@
|
||||
{{ selfPercentProduce.toValueString(null) }}
|
||||
<sub class="subscript">Produktion</sub>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
@ -66,6 +72,7 @@
|
||||
<div class="SectionBody COLOR_FONT_DELIVER">
|
||||
{{ deliver.toValueString(null) }}
|
||||
</div>
|
||||
@if (configService.energyPercent) {
|
||||
<div class="SectionBody COLOR_FONT_DELIVER percent">
|
||||
{{ deliveryPercentConsume.toValueString(null) }}
|
||||
<sub class="subscript">Verbrauch</sub>
|
||||
@ -74,10 +81,11 @@
|
||||
{{ deliveryPercentProduce.toValueString(null) }}
|
||||
<sub class="subscript">Produktion</sub>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<app-energy-charts [location]="location" [interval]="interval" [offset]="offset"></app-energy-charts>
|
||||
<app-energy-charts #chart [location]="location" [interval]="interval" [offset]="offset" [unit]="unit" [factor]="factor" [maxY]="maxY" [minY]="minY"></app-energy-charts>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit, signal} from '@angular/core';
|
||||
import {Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges} from '@angular/core';
|
||||
import {Location} from '../Location';
|
||||
import {Series} from '../../series/Series';
|
||||
import {Next} from '../../common';
|
||||
@ -7,20 +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 {ConfigService} from '../../config.service';
|
||||
import {EnergyCharts} from './charts/energy-charts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-energy',
|
||||
imports: [
|
||||
EnergyCharts,
|
||||
EnergyCharts
|
||||
],
|
||||
templateUrl: './location-energy.html',
|
||||
styleUrl: './location-energy.less',
|
||||
})
|
||||
export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
protected readonly signal = signal;
|
||||
export class LocationEnergy implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
protected readonly Interval = Interval;
|
||||
|
||||
@ -51,17 +49,20 @@ export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||
@Input()
|
||||
heading!: string;
|
||||
|
||||
private _o_: number = 0;
|
||||
@Input()
|
||||
offset: number = 0;
|
||||
|
||||
@Input()
|
||||
set offset(value: number) {
|
||||
this._o_ = value;
|
||||
this.ngAfterViewInit();
|
||||
}
|
||||
unit: string = "";
|
||||
|
||||
get offset(): number {
|
||||
return this._o_;
|
||||
}
|
||||
@Input()
|
||||
factor: number = 1;
|
||||
|
||||
@Input()
|
||||
maxY: number | undefined = undefined;
|
||||
|
||||
@Input()
|
||||
minY: number | undefined = undefined;
|
||||
|
||||
@Input()
|
||||
interval!: Interval;
|
||||
@ -75,6 +76,7 @@ export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||
constructor(
|
||||
readonly pointService: PointService,
|
||||
readonly serieService: SeriesService,
|
||||
readonly configService: ConfigService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
@ -83,7 +85,7 @@ export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.subs.push(this.serieService.subscribe(this.update));
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.fetch(null, this.location?.energyPurchase, history => this.purchase = history);
|
||||
this.fetch(null, this.location?.energyDeliver, history => this.deliver = history);
|
||||
this.fetch(null, this.location?.energyProduce, history => this.produce = history);
|
||||
@ -91,6 +93,7 @@ export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs.length = 0;
|
||||
}
|
||||
|
||||
protected readonly update = (fresh: Series): void => {
|
||||
@ -107,12 +110,12 @@ export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||
next(value);
|
||||
this.consume = this.purchase.plus(this.produce, true).minus(this.deliver, true);
|
||||
this.self = this.produce.minus(this.deliver, true);
|
||||
this.purchasePercentConsume = this.purchase.percent(this.consume, "%", 0);
|
||||
this.producePercentConsume = this.produce.percent(this.consume, "%", 0);
|
||||
this.deliveryPercentConsume = this.deliver.percent(this.consume, "%", 0);
|
||||
this.deliveryPercentProduce = this.deliver.percent(this.produce, "%", 0);
|
||||
this.selfPercentConsume = this.self.percent(this.consume, "%", 0);
|
||||
this.selfPercentProduce = this.self.percent(this.produce, "%", 0);
|
||||
this.purchasePercentConsume = this.purchase.percent(this.consume);
|
||||
this.producePercentConsume = this.produce.percent(this.consume);
|
||||
this.deliveryPercentConsume = this.deliver.percent(this.consume);
|
||||
this.deliveryPercentProduce = this.deliver.percent(this.produce);
|
||||
this.selfPercentConsume = this.self.percent(this.consume);
|
||||
this.selfPercentProduce = this.self.percent(this.produce);
|
||||
};
|
||||
if (fresh !== null && fresh !== undefined) {
|
||||
if (fresh.id !== series?.id) {
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
<div class="List LocationList NoUserSelect">
|
||||
@for (location of list; track location.id) {
|
||||
<div class="ListItem Location" routerLink="/Location/{{location.id}}">
|
||||
{{ location.name }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -1,35 +0,0 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {LocationService} from '../location-service';
|
||||
import {Location} from '../Location';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {MenuService} from '../../menu-service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-list',
|
||||
imports: [
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './location-list.html',
|
||||
styleUrl: './location-list.less',
|
||||
})
|
||||
export class LocationList implements OnInit, OnDestroy {
|
||||
|
||||
protected list: Location[] = [];
|
||||
|
||||
constructor(
|
||||
readonly locationService: LocationService,
|
||||
readonly menuService: MenuService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.locationService.findAll(list => this.list = list);
|
||||
this.menuService.title = "Orte";
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.menuService.title = "";
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,44 +1,17 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ApiService, CrudService, Next, WebsocketService} from '../common';
|
||||
import {Location} from './Location'
|
||||
import {SeriesService} from '../series/series-service';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LocationService extends CrudService<Location> {
|
||||
|
||||
private readonly _location = new BehaviorSubject<Location | null>(null);
|
||||
|
||||
private _id: number | null = null;
|
||||
|
||||
constructor(
|
||||
api: ApiService,
|
||||
ws: WebsocketService,
|
||||
readonly seriesService: SeriesService,
|
||||
) {
|
||||
super(api, ws, ['Location'], Location.fromJson);
|
||||
this.seriesService.subscribe(series => this._location.value?.updateSeries(series));
|
||||
this.ws.onConnect(this.fetch);
|
||||
this.ws.onDisconnect(() => this._location.next(null));
|
||||
}
|
||||
|
||||
set id(id: number | null) {
|
||||
this._id = id;
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
private readonly fetch = () => {
|
||||
if (this._id === null) {
|
||||
this._location.next(null);
|
||||
} else {
|
||||
this.getById(this._id, location => this._location.next(location));
|
||||
}
|
||||
};
|
||||
|
||||
get location$(): Observable<Location | null> {
|
||||
return this._location.asObservable();
|
||||
}
|
||||
|
||||
findAll(next: Next<Location[]>) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div class="Section3">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Aktuelle Leistung
|
||||
Aktuell
|
||||
</div>
|
||||
</div>
|
||||
<div class="SectionBody">
|
||||
@ -11,12 +11,14 @@
|
||||
Bezug
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_PURCHASE">
|
||||
{{ location.powerPurchase?.value?.toValueString(dateService.now) }}
|
||||
{{ powerPurchase.toValueString(dateService.now) }}
|
||||
</div>
|
||||
@if (configService.energyPercent) {
|
||||
<div class="SectionBody COLOR_FONT_PURCHASE percent">
|
||||
{{ location.powerPurchasePercentConsume.toValueString(dateService.now) }}
|
||||
{{ powerPurchasePercentConsume.toValueString(dateService.now) }}
|
||||
<sub class="subscript">Verbrauch</sub>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
@ -24,12 +26,14 @@
|
||||
Solar
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_PRODUCE">
|
||||
{{ location.powerProduce?.value?.toValueString(dateService.now) }}
|
||||
{{ powerProduce.toValueString(dateService.now) }}
|
||||
</div>
|
||||
@if (configService.energyPercent) {
|
||||
<div class="SectionBody COLOR_FONT_PRODUCE percent">
|
||||
{{ location.powerProducePercentConsume.toValueString(dateService.now) }}
|
||||
{{ powerProducePercentConsume.toValueString(dateService.now) }}
|
||||
<sub class="subscript">Verbrauch</sub>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
@ -37,16 +41,18 @@
|
||||
Selbst
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_SELF">
|
||||
{{ location.powerSelf.toValueString(dateService.now) }}
|
||||
{{ powerSelf.toValueString(dateService.now) }}
|
||||
</div>
|
||||
@if (configService.energyPercent) {
|
||||
<div class="SectionBody COLOR_FONT_SELF percent">
|
||||
{{ location.powerSelfPercentConsume.toValueString(dateService.now) }}
|
||||
{{ powerSelfPercentConsume.toValueString(dateService.now) }}
|
||||
<sub class="subscript">Verbrauch</sub>
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_SELF percent">
|
||||
{{ location.powerSelfPercentProduce.toValueString(dateService.now) }}
|
||||
{{ powerSelfPercentProduce.toValueString(dateService.now) }}
|
||||
<sub class="subscript">Produktion</sub>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="Section4">
|
||||
@ -54,7 +60,7 @@
|
||||
Verbrauch
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_CONSUME">
|
||||
{{ location.powerConsume?.toValueString(dateService.now) }}
|
||||
{{ powerConsume.toValueString(dateService.now) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -63,16 +69,18 @@
|
||||
Einspeisung
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_DELIVER">
|
||||
{{ location.powerDeliver?.value?.toValueString(dateService.now) }}
|
||||
{{ powerDeliver.toValueString(dateService.now) }}
|
||||
</div>
|
||||
@if (configService.energyPercent) {
|
||||
<div class="SectionBody COLOR_FONT_DELIVER percent">
|
||||
{{ location.powerDeliveryPercentConsume.toValueString(dateService.now) }}
|
||||
{{ powerDeliverPercentConsume.toValueString(dateService.now) }}
|
||||
<sub class="subscript">Verbrauch</sub>
|
||||
</div>
|
||||
<div class="SectionBody COLOR_FONT_DELIVER percent">
|
||||
{{ location.powerDeliveryPercentProduce.toValueString(dateService.now) }}
|
||||
{{ powerDeliverPercentProduce.toValueString(dateService.now) }}
|
||||
<sub class="subscript">Produktion</sub>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {Component, Input, OnChanges, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Location} from '../Location';
|
||||
import {DateService} from '../../date.service';
|
||||
import {ConfigService} from '../../config.service';
|
||||
import {Value} from '../../series/Value';
|
||||
import {SeriesService} from '../../series/series-service';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Series} from '../../series/Series';
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-power',
|
||||
@ -8,15 +13,86 @@ import {DateService} from '../../date.service';
|
||||
templateUrl: './location-power.html',
|
||||
styleUrl: './location-power.less',
|
||||
})
|
||||
export class LocationPower {
|
||||
export class LocationPower implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input()
|
||||
location!: Location;
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
protected energyPurchase: Value = Value.NULL;
|
||||
|
||||
protected energyDeliver: Value = Value.NULL;
|
||||
|
||||
protected energyProduce: Value = Value.NULL;
|
||||
|
||||
protected powerPurchase: Value = Value.NULL;
|
||||
|
||||
protected powerDeliver: Value = Value.NULL;
|
||||
|
||||
protected powerProduce: Value = Value.NULL;
|
||||
|
||||
protected outsideTemperature: Value = Value.NULL;
|
||||
|
||||
protected outsideHumidityRelative: Value = Value.NULL;
|
||||
|
||||
protected outsideHumidityAbsolute: Value = Value.NULL;
|
||||
|
||||
protected powerConsume: Value = Value.NULL;
|
||||
|
||||
protected powerSelf: Value = Value.NULL;
|
||||
|
||||
protected powerPurchasePercentConsume: Value = Value.NULL;
|
||||
|
||||
protected powerProducePercentConsume: Value = Value.NULL;
|
||||
|
||||
protected powerDeliverPercentConsume: Value = Value.NULL;
|
||||
|
||||
protected powerDeliverPercentProduce: Value = Value.NULL;
|
||||
|
||||
protected powerSelfPercentConsume: Value = Value.NULL;
|
||||
|
||||
protected powerSelfPercentProduce: Value = Value.NULL;
|
||||
|
||||
constructor(
|
||||
readonly dateService: DateService,
|
||||
readonly configService: ConfigService,
|
||||
readonly seriesService: SeriesService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(this.seriesService.subscribe(this.seriesUpdate));
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.location.getSeries().forEach(this.seriesUpdate);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs.length = 0;
|
||||
}
|
||||
|
||||
private readonly seriesUpdate = (series: Series): void => {
|
||||
if (series.id === this.location.energyPurchase?.id) this.energyPurchase = series.value;
|
||||
if (series.id === this.location.energyDeliver?.id) this.energyDeliver = series.value;
|
||||
if (series.id === this.location.energyProduce?.id) this.energyProduce = series.value;
|
||||
if (series.id === this.location.powerPurchase?.id) this.powerPurchase = series.value;
|
||||
if (series.id === this.location.powerDeliver?.id) this.powerDeliver = series.value;
|
||||
if (series.id === this.location.powerProduce?.id) this.powerProduce = series.value;
|
||||
if (series.id === this.location.outsideTemperature?.id) this.outsideTemperature = series.value;
|
||||
if (series.id === this.location.outsideHumidityRelative?.id) this.outsideHumidityRelative = series.value;
|
||||
if (series.id === this.location.outsideHumidityAbsolute?.id) this.outsideHumidityAbsolute = series.value;
|
||||
this.powerSelf = Value.ZERO.plus(this.powerProduce).minus(this.powerDeliver);
|
||||
this.powerConsume = Value.ZERO.plus(this.powerPurchase).plus(this.powerSelf);
|
||||
this.powerPurchasePercentConsume = this.powerPurchase.percent(this.powerConsume);
|
||||
this.powerProducePercentConsume = this.powerProduce.percent(this.powerConsume);
|
||||
this.powerDeliverPercentConsume = this.powerDeliver.percent(this.powerConsume)
|
||||
this.powerDeliverPercentProduce = this.powerDeliver.percent(this.powerProduce)
|
||||
this.powerSelfPercentConsume = this.powerSelf.percent(this.powerConsume);
|
||||
this.powerSelfPercentProduce = this.powerSelf.percent(this.powerProduce);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
<div
|
||||
class="container NoUserSelect"
|
||||
[ngClass]="classes()"
|
||||
(mouseenter)="showPen = true"
|
||||
(mouseleave)="showPen = false"
|
||||
>
|
||||
<select [(ngModel)]="model" (ngModelChange)="changed($event)">
|
||||
<option [ngValue]="null">-</option>
|
||||
@for (location of locations; track location.id) {
|
||||
<option [ngValue]="location.id">
|
||||
{{ location.name }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
@if (showPen) {
|
||||
<fa-icon [icon]="faPen"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
@ -0,0 +1,25 @@
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
background-color: #0002;
|
||||
}
|
||||
|
||||
select {
|
||||
all: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
background-color: red !important;
|
||||
}
|
||||
|
||||
.changed {
|
||||
background-color: yellow !important;
|
||||
}
|
||||
80
src/main/angular/src/app/location/select/location-select.ts
Normal file
80
src/main/angular/src/app/location/select/location-select.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {NgClass} from '@angular/common';
|
||||
import {faPen} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Location} from '../Location';
|
||||
import {LocationService} from '../location-service';
|
||||
import {DateService} from '../../date.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-location-select',
|
||||
imports: [
|
||||
FaIconComponent,
|
||||
FormsModule,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './location-select.html',
|
||||
styleUrl: './location-select.less',
|
||||
})
|
||||
export class LocationSelect implements OnInit {
|
||||
|
||||
protected readonly faPen = faPen;
|
||||
|
||||
private _initial: number | null = null;
|
||||
|
||||
@Input()
|
||||
locations!: Location[];
|
||||
|
||||
@Input()
|
||||
allowEmpty: boolean = true;
|
||||
|
||||
@Input()
|
||||
filter: (location: Location) => boolean = () => true;
|
||||
|
||||
@Output()
|
||||
readonly onChange = new EventEmitter<number | null>();
|
||||
|
||||
protected showPen: boolean = false;
|
||||
|
||||
protected model: number | null = null;
|
||||
|
||||
protected readonly Location = Location;
|
||||
|
||||
constructor(
|
||||
readonly locationService: LocationService,
|
||||
readonly dateService: DateService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.locationService.findAll(list => this.locations = list);
|
||||
}
|
||||
|
||||
@Input()
|
||||
set initial(value: number | null) {
|
||||
this._initial = value;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private readonly reset = (): void => {
|
||||
this.model = this._initial;
|
||||
};
|
||||
|
||||
protected classes(): {} {
|
||||
return {
|
||||
"changed": this.model !== this._initial,
|
||||
"invalid": !this.allowEmpty && this.model === null,
|
||||
};
|
||||
}
|
||||
|
||||
protected changed(id: number | null) {
|
||||
if (this.allowEmpty || id !== null) {
|
||||
this.onChange.emit(id);
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,10 +1,31 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Location} from './location/Location';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MenuService {
|
||||
|
||||
title: string = "";
|
||||
private _locationId: number | null = null;
|
||||
|
||||
private _title: string = "";
|
||||
|
||||
get locationId(): number | null {
|
||||
return this._locationId;
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this._title;
|
||||
}
|
||||
|
||||
setLocation(location: Location): void {
|
||||
this._locationId = location.id;
|
||||
this._title = location.name;
|
||||
}
|
||||
|
||||
setNonLocation(title: string) {
|
||||
this._title = title;
|
||||
this._locationId = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
export class Interval {
|
||||
|
||||
static readonly FIVE = new Interval('FIVE');
|
||||
static readonly FIVE = new Interval('FIVE', 5 * 60 * 1000);
|
||||
|
||||
static readonly HOUR = new Interval('HOUR', this.FIVE);
|
||||
static readonly HOUR = new Interval('HOUR', 60 * 60 * 1000, this.FIVE);
|
||||
|
||||
static readonly DAY = new Interval('DAY', this.FIVE);
|
||||
static readonly DAY = new Interval('DAY', 24 * 60 * 60 * 1000, this.FIVE);
|
||||
|
||||
static readonly WEEK = new Interval('WEEK', this.HOUR);
|
||||
static readonly WEEK = new Interval('WEEK', 7 * 24 * 60 * 60 * 1000, this.HOUR);
|
||||
|
||||
static readonly MONTH = new Interval('MONTH', this.DAY);
|
||||
static readonly MONTH = new Interval('MONTH', 30 * 24 * 60 * 60 * 1000, this.DAY);
|
||||
|
||||
static readonly YEAR = new Interval('YEAR', this.DAY);
|
||||
static readonly YEAR = new Interval('YEAR', 365 * 24 * 60 * 60 * 1000, this.DAY);
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly millis: number,
|
||||
readonly inner: Interval = this,
|
||||
) {
|
||||
//
|
||||
|
||||
22
src/main/angular/src/app/series/SeriesListResponse.ts
Normal file
22
src/main/angular/src/app/series/SeriesListResponse.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {Series} from "./Series";
|
||||
import {validateList} from "../common";
|
||||
|
||||
export class SeriesListResponse {
|
||||
|
||||
constructor(
|
||||
readonly series: Series[]
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
static fromJson(json: any): SeriesListResponse {
|
||||
return new SeriesListResponse(
|
||||
validateList(json, Series.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
findSeries(series: Series | null): Series | null {
|
||||
return series ? this.series.filter(s => s.id === series.id)[0] || null : null;
|
||||
}
|
||||
|
||||
}
|
||||
@ -29,7 +29,7 @@ export class Value {
|
||||
return `--- ${this.unit}`
|
||||
}
|
||||
const scale = Math.floor(Math.log10(this.value));
|
||||
if(isNaN(scale)) {
|
||||
if (isNaN(scale)) {
|
||||
return '0';
|
||||
}
|
||||
const rest = scale - this.precision + 1;
|
||||
@ -39,14 +39,14 @@ export class Value {
|
||||
return formatNumber(this.value, "de-DE", `0.${-rest}-${-rest}`) + ' ' + this.unit;
|
||||
}
|
||||
|
||||
plus(other: Value | null | undefined, nullToZero: boolean): Value {
|
||||
plus(other: Value | null | undefined, nullToZero: boolean = true): Value {
|
||||
if (!nullToZero && (other === null || other === undefined)) {
|
||||
return Value.NULL;
|
||||
}
|
||||
return new BiValue(this, other || Value.ZERO, (a, b) => a + b);
|
||||
}
|
||||
|
||||
minus(other: Value | null | undefined, nullToZero: boolean): Value {
|
||||
minus(other: Value | null | undefined, nullToZero: boolean = true): Value {
|
||||
if (!nullToZero && (other === null || other === undefined)) {
|
||||
return Value.NULL;
|
||||
}
|
||||
@ -86,11 +86,11 @@ export class Value {
|
||||
return ageSeconds > this.seconds * 2.1;
|
||||
}
|
||||
|
||||
percent(other: Value | null | undefined, unit: string | null = null, precision: number | null = null): Value {
|
||||
percent(other: Value | null | undefined): Value {
|
||||
if (other === null || other === undefined) {
|
||||
return Value.NULL;
|
||||
}
|
||||
return new BiValue(this, other, (a, b) => a / b * 100, unit, precision);
|
||||
return new BiValue(this, other, (a, b) => a / b * 100, '%', 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,10 +16,6 @@ select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.unchanged {
|
||||
background-color: lightgreen !important;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
background-color: red !important;
|
||||
}
|
||||
|
||||
@ -78,7 +78,6 @@ export class SeriesSelect implements OnInit, OnDestroy {
|
||||
|
||||
protected classes(): {} {
|
||||
return {
|
||||
"unchanged": this.model === this._initial,
|
||||
"changed": this.model !== or(this._initial, i => i.id, null),
|
||||
"invalid": !this.allowEmpty && this.model === null,
|
||||
};
|
||||
|
||||
22
src/main/angular/src/app/settings/settings-component.html
Normal file
22
src/main/angular/src/app/settings/settings-component.html
Normal file
@ -0,0 +1,22 @@
|
||||
<div class="Section3">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Favorit
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<app-location-select [initial]="configService.locationId" (onChange)="configService.locationId = $event"></app-location-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Section3">
|
||||
<div class="SectionHeading">
|
||||
<div class="SectionHeadingText">
|
||||
Anzeige
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<app-checkbox [initial]="configService.energyPercent" (onChange)="configService.energyPercent = $event" label="Energie Prozentsätze"></app-checkbox>
|
||||
<app-checkbox [initial]="configService.locationConfig" (onChange)="configService.locationConfig = $event" label="Ort Konfiguration"></app-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
34
src/main/angular/src/app/settings/settings-component.ts
Normal file
34
src/main/angular/src/app/settings/settings-component.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {LocationSelect} from '../location/select/location-select';
|
||||
import {Location} from '../location/Location'
|
||||
import {ConfigService} from '../config.service';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Checkbox} from '../shared/checkbox/checkbox';
|
||||
import {MenuService} from '../menu-service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings-component',
|
||||
imports: [
|
||||
LocationSelect,
|
||||
FormsModule,
|
||||
Checkbox
|
||||
],
|
||||
templateUrl: './settings-component.html',
|
||||
styleUrl: './settings-component.less',
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
|
||||
protected location: Location | null = null;
|
||||
|
||||
constructor(
|
||||
readonly configService: ConfigService,
|
||||
readonly menuService: MenuService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.menuService.setNonLocation("Einstellungen");
|
||||
}
|
||||
|
||||
}
|
||||
12
src/main/angular/src/app/shared/checkbox/checkbox.html
Normal file
12
src/main/angular/src/app/shared/checkbox/checkbox.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="all" (click)="onChange.emit(!initial)">
|
||||
<div class="box">
|
||||
@if (initial) {
|
||||
<div class="inside">
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="label">
|
||||
{{ label }}
|
||||
</div>
|
||||
</div>
|
||||
21
src/main/angular/src/app/shared/checkbox/checkbox.less
Normal file
21
src/main/angular/src/app/shared/checkbox/checkbox.less
Normal file
@ -0,0 +1,21 @@
|
||||
.all {
|
||||
display: flex;
|
||||
|
||||
.box {
|
||||
height: 1em;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid lightgray;
|
||||
padding: 0.1em;
|
||||
margin: 0.2em 0.2em 0.2em 0;
|
||||
|
||||
.inside {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: steelblue;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
20
src/main/angular/src/app/shared/checkbox/checkbox.ts
Normal file
20
src/main/angular/src/app/shared/checkbox/checkbox.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-checkbox',
|
||||
imports: [],
|
||||
templateUrl: './checkbox.html',
|
||||
styleUrl: './checkbox.less',
|
||||
})
|
||||
export class Checkbox {
|
||||
|
||||
@Input()
|
||||
initial: boolean = false;
|
||||
|
||||
@Input()
|
||||
label: string = "";
|
||||
|
||||
@Output()
|
||||
readonly onChange: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
}
|
||||
25
src/main/angular/src/app/weather/WeatherHour.ts
Normal file
25
src/main/angular/src/app/weather/WeatherHour.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {validateDate, validateNumber} from '../common';
|
||||
|
||||
export class WeatherHour {
|
||||
|
||||
constructor(
|
||||
readonly date: Date,
|
||||
readonly clouds: number,
|
||||
readonly irradiation: number,
|
||||
readonly precipitation: number,
|
||||
readonly temperature: number,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
static fromJson(json: any): WeatherHour {
|
||||
return new WeatherHour(
|
||||
validateDate(json.date),
|
||||
validateNumber(json.clouds),
|
||||
validateNumber(json.irradiation),
|
||||
validateNumber(json.precipitation),
|
||||
validateNumber(json.temperature),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<div #container class="container">
|
||||
<canvas #chartCanvas></canvas>
|
||||
</div>
|
||||
@ -0,0 +1,8 @@
|
||||
.container {
|
||||
aspect-ratio: 4;
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
238
src/main/angular/src/app/weather/plot/weather-component.ts
Normal file
238
src/main/angular/src/app/weather/plot/weather-component.ts
Normal file
@ -0,0 +1,238 @@
|
||||
import {AfterViewInit, Component, ElementRef, Inject, Input, LOCALE_ID, ViewChild} from '@angular/core';
|
||||
import {WeatherService} from '../weather-service';
|
||||
import {WeatherHour} from '../WeatherHour';
|
||||
import {Chart} from 'chart.js';
|
||||
|
||||
import {de} from 'date-fns/locale';
|
||||
import {format} from 'date-fns';
|
||||
import {Location} from '../../location/Location';
|
||||
import {formatNumber} from '@angular/common';
|
||||
|
||||
export function toPoint(f: (hour: WeatherHour) => number): (hour: WeatherHour) => { x: number, y: number } {
|
||||
return (hour: WeatherHour) => {
|
||||
return {
|
||||
x: hour.date.getTime(),
|
||||
y: f(hour),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function temperatureColor(value: number | null | undefined) {
|
||||
if (!value) {
|
||||
return "black";
|
||||
} else if (value < 0) {
|
||||
return 'blue';
|
||||
} else if (value < 20) {
|
||||
return '#c1b100';
|
||||
} else if (value < 30) {
|
||||
return '#ff8100';
|
||||
}
|
||||
return TEMPERATURE_HIGH_COLOR;
|
||||
}
|
||||
|
||||
const TEMPERATURE_HIGH_COLOR = '#FF0000';
|
||||
const PRECIPITATION_COLOR = '#0000FF';
|
||||
const CLOUDS_COLOR = '#cccccc';
|
||||
const SUN_COLOR = '#ffc400';
|
||||
|
||||
@Component({
|
||||
selector: 'app-weather-component',
|
||||
imports: [],
|
||||
templateUrl: './weather-component.html',
|
||||
styleUrl: './weather-component.less'
|
||||
})
|
||||
export class WeatherComponent implements AfterViewInit {
|
||||
|
||||
@ViewChild('chartCanvas')
|
||||
protected canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@ViewChild('container')
|
||||
protected chartContainer!: ElementRef<HTMLDivElement>;
|
||||
|
||||
private chart!: Chart;
|
||||
|
||||
@Input()
|
||||
location!: Location;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) readonly locale: string,
|
||||
readonly weatherService: WeatherService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.chart = new Chart(this.canvasRef.nativeElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
itemSort: (a, b) => b.datasetIndex - a.datasetIndex,
|
||||
usePointStyle: true,
|
||||
callbacks: {
|
||||
title: function (items) {
|
||||
const date = items[0].parsed.x as unknown as Date;
|
||||
return format(date, 'EE dd.MM. HH:mm', {locale: de});
|
||||
},
|
||||
label: ((ctx: any) => {
|
||||
const groups = /^(?<name>.+)\[(?<unit>.+)]$/.exec(ctx.dataset.label || '')?.groups;
|
||||
if (groups) {
|
||||
const value = ctx.parsed.y === null ? '-' : formatNumber(Math.abs(ctx.parsed.y), this.locale, '0.2-2');
|
||||
return `${value} ${groups['unit']} ${groups['name']}`;
|
||||
}
|
||||
return ctx.dataset.label;
|
||||
}) as any,
|
||||
},
|
||||
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Wetter",
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: "day",
|
||||
displayFormats: {
|
||||
day: "EE dd.MM"
|
||||
}
|
||||
},
|
||||
adapters: {
|
||||
date: {
|
||||
locale: de,
|
||||
}
|
||||
},
|
||||
},
|
||||
y_temperature: {
|
||||
display: true,
|
||||
position: "right",
|
||||
title: {
|
||||
display: false,
|
||||
text: "Temperatur [°C]",
|
||||
color: TEMPERATURE_HIGH_COLOR,
|
||||
},
|
||||
ticks: {
|
||||
color: context => temperatureColor(context.tick.value),
|
||||
},
|
||||
min: -10,
|
||||
max: 35,
|
||||
},
|
||||
y_precipitation: {
|
||||
display: false,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
text: "Niederschlag [mm]",
|
||||
color: PRECIPITATION_COLOR,
|
||||
},
|
||||
ticks: {
|
||||
color: PRECIPITATION_COLOR,
|
||||
},
|
||||
min: 0,
|
||||
max: 15,
|
||||
},
|
||||
y_sun: {
|
||||
display: false,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
position: "right",
|
||||
title: {
|
||||
display: false,
|
||||
text: "Sonne [kWh/m²]",
|
||||
color: SUN_COLOR,
|
||||
},
|
||||
ticks: {
|
||||
color: SUN_COLOR,
|
||||
},
|
||||
min: 0,
|
||||
max: 1000,
|
||||
},
|
||||
y_clouds: {
|
||||
display: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Bewölkung [%]",
|
||||
color: CLOUDS_COLOR,
|
||||
},
|
||||
ticks: {
|
||||
color: CLOUDS_COLOR,
|
||||
},
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
this.weatherService.forLocation(this.location, hours => {
|
||||
const now = Date.now();
|
||||
const filtered = hours.filter(h => h.date.getTime() >= now);
|
||||
this.chart.data.datasets.push({
|
||||
label: "Niederschlag [mm]",
|
||||
categoryPercentage: 1.0,
|
||||
barPercentage: 1.0,
|
||||
type: "bar",
|
||||
yAxisID: "y_precipitation",
|
||||
data: filtered.map(toPoint(h => h.precipitation)),
|
||||
borderColor: PRECIPITATION_COLOR,
|
||||
backgroundColor: PRECIPITATION_COLOR + '66',
|
||||
borderWidth: 0,
|
||||
pointStyle: "rect",
|
||||
});
|
||||
this.chart.data.datasets.push({
|
||||
label: "Sonne [W/m²]",
|
||||
type: "line",
|
||||
fill: "origin",
|
||||
yAxisID: "y_sun",
|
||||
data: filtered.map(toPoint(h => h.irradiation)),
|
||||
borderColor: SUN_COLOR,
|
||||
backgroundColor: SUN_COLOR + '88',
|
||||
borderWidth: 0,
|
||||
pointRadius: 0,
|
||||
});
|
||||
this.chart.data.datasets.push({
|
||||
label: "Temperatur [°C]",
|
||||
type: "line",
|
||||
yAxisID: "y_temperature",
|
||||
data: filtered.map(toPoint(h => h.temperature)),
|
||||
backgroundColor: TEMPERATURE_HIGH_COLOR + '66',
|
||||
borderWidth: 0,
|
||||
pointRadius: 1,
|
||||
pointBackgroundColor: context => temperatureColor(context.parsed.y),
|
||||
pointStyle: "point",
|
||||
});
|
||||
this.chart.data.datasets.push({
|
||||
label: "Bewölkung [%]",
|
||||
type: "line",
|
||||
fill: "origin",
|
||||
yAxisID: "y_clouds",
|
||||
data: filtered.map(toPoint(h => h.clouds)),
|
||||
borderColor: CLOUDS_COLOR,
|
||||
backgroundColor: CLOUDS_COLOR + '88',
|
||||
borderWidth: 0,
|
||||
pointRadius: 0,
|
||||
});
|
||||
this.chart.update();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
22
src/main/angular/src/app/weather/weather-service.ts
Normal file
22
src/main/angular/src/app/weather/weather-service.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ApiService, CrudService, Next, WebsocketService} from '../common';
|
||||
import {WeatherHour} from './WeatherHour';
|
||||
import {Location} from '../location/Location';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class WeatherService extends CrudService<WeatherHour> {
|
||||
|
||||
constructor(
|
||||
api: ApiService,
|
||||
ws: WebsocketService,
|
||||
) {
|
||||
super(api, ws, ['Weather'], WeatherHour.fromJson);
|
||||
}
|
||||
|
||||
forLocation(location: Location, next: Next<WeatherHour[]>) {
|
||||
this.getList(['forLocation', location.id], next);
|
||||
}
|
||||
|
||||
}
|
||||
@ -62,7 +62,8 @@ div {
|
||||
}
|
||||
|
||||
.Section3 {
|
||||
border: 1px solid gray;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 0.25em;
|
||||
margin: 1em 0.5em 0.5em;
|
||||
padding: 0.5em;
|
||||
overflow: visible;
|
||||
@ -70,6 +71,7 @@ div {
|
||||
> .SectionHeading {
|
||||
display: flex;
|
||||
color: dimgray;
|
||||
font-size: 80%;
|
||||
margin-top: -1.25em;
|
||||
|
||||
> .SectionHeadingText {
|
||||
|
||||
@ -11,10 +11,6 @@ public class WeatherConfig {
|
||||
|
||||
private String urlPattern = "https://api.brightsky.dev/weather?date={date}&lat={latitude}&lon={longitude}&units=dwd";
|
||||
|
||||
private double latitude = 49.320789191091194;
|
||||
|
||||
private double longitude = 7.102111982262271;
|
||||
|
||||
private int pastDays = 9;
|
||||
|
||||
private int futureDays = 9;
|
||||
|
||||
@ -3,6 +3,7 @@ package de.ph87.data.weather;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
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;
|
||||
|
||||
@ -16,9 +17,9 @@ public class WeatherController {
|
||||
|
||||
private final WeatherService weatherService;
|
||||
|
||||
@GetMapping("all")
|
||||
public List<WeatherHour> all() {
|
||||
return weatherService.all();
|
||||
@GetMapping("forLocation/{id}")
|
||||
public List<WeatherHour> forLocation(@PathVariable final long id) {
|
||||
return weatherService.forLocation(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
package de.ph87.data.weather;
|
||||
|
||||
import de.ph87.data.location.LocationDto;
|
||||
import de.ph87.data.location.LocationRepository;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -20,7 +24,9 @@ import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -28,43 +34,53 @@ import java.util.List;
|
||||
@RequiredArgsConstructor
|
||||
public class WeatherService {
|
||||
|
||||
private List<WeatherHour> hours = new ArrayList<>();
|
||||
private final LocationRepository locationRepository;
|
||||
|
||||
private final Map<Long, List<WeatherHour>> locationHours = new HashMap<>();
|
||||
|
||||
private final WeatherConfig weatherConfig;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@NonNull
|
||||
public List<WeatherHour> all() {
|
||||
public List<WeatherHour> forLocation(final long id) {
|
||||
final List<WeatherHour> hours = this.locationHours.get(id);
|
||||
if (hours == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return new ArrayList<>(hours);
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 * * * *")
|
||||
@EventListener(ApplicationStartedEvent.class)
|
||||
public void update() {
|
||||
public void updateAll() {
|
||||
locationRepository.findAllDto().forEach(this::update);
|
||||
}
|
||||
|
||||
private void update(@NonNull final LocationDto location) {
|
||||
try {
|
||||
final LocalDate today = LocalDate.now();
|
||||
final LocalDate first = today.minusDays(weatherConfig.getPastDays());
|
||||
final LocalDate end = today.plusDays(weatherConfig.getFutureDays());
|
||||
final List<WeatherHour> hours = new ArrayList<>();
|
||||
log.debug("Updating Weather...");
|
||||
log.debug("Updating Weather for Location: {}", location.name);
|
||||
for (LocalDate day = first; !day.isAfter(end); day = day.plusDays(1)) {
|
||||
fetchDay(day).getWeather().stream().map(WeatherHour::new).forEach(hours::add);
|
||||
fetchDay(location, day).getWeather().stream().map(WeatherHour::new).forEach(hours::add);
|
||||
}
|
||||
this.hours = hours;
|
||||
log.info("Weather update complete");
|
||||
this.locationHours.put(location.id, hours);
|
||||
log.info("Weather update complete for Location: {}", location.name);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed fetching Weather data: {}", e.toString());
|
||||
log.error("Failed fetching Weather data for Location: {}: {}", location.name, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public BrightSkyDto fetchDay(@NonNull final LocalDate day) throws IOException {
|
||||
public BrightSkyDto fetchDay(@NonNull final LocationDto location, @NonNull final LocalDate day) throws IOException {
|
||||
final String url = weatherConfig
|
||||
.getUrlPattern()
|
||||
.replace("{date}", ZonedDateTime.of(day, LocalTime.MIDNIGHT, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
|
||||
.replace("{latitude}", weatherConfig.getLatitude() + "")
|
||||
.replace("{longitude}", weatherConfig.getLongitude() + "");
|
||||
.replace("{latitude}", location.getLatitude() + "")
|
||||
.replace("{longitude}", location.getLongitude() + "");
|
||||
final HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection();
|
||||
final int responseCode = connection.getResponseCode();
|
||||
final byte[] bytes = connection.getInputStream().readAllBytes();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user