Compare commits
2 Commits
0561940861
...
85a749f199
| Author | SHA1 | Date | |
|---|---|---|---|
| 85a749f199 | |||
| 63548dc3ff |
@ -1 +1,5 @@
|
||||
<div class="menubar">
|
||||
<div class="menuitem menuitemLeft" *ngFor="let route of menubar()" [routerLink]="[route.routerLink]" routerLinkActive="menuitemActive">{{ route.title }}</div>
|
||||
</div>
|
||||
|
||||
<router-outlet/>
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
.menubar {
|
||||
border-bottom: 1px solid black;
|
||||
background-color: #303d47;
|
||||
|
||||
.menuitem {
|
||||
padding: 0.1em 0.25em;
|
||||
}
|
||||
|
||||
.menuitemLeft {
|
||||
float: left;
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
.menuitemRight {
|
||||
float: right;
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
|
||||
.menuitemActive {
|
||||
color: white;
|
||||
background-color: #006ebc;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,12 +1,17 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||
import {menubar, ROUTING} from './app.routes';
|
||||
import {NgForOf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, RouterLink, NgForOf, RouterLinkActive],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.less'
|
||||
})
|
||||
export class AppComponent {
|
||||
|
||||
protected readonly ROUTING = ROUTING;
|
||||
|
||||
protected readonly menubar = menubar;
|
||||
}
|
||||
|
||||
@ -1,7 +1,34 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {DashboardComponent} from './dashboard/dashboard.component';
|
||||
import {ElectroComponent} from './electro/electro.component';
|
||||
import {GreenhouseComponent} from './greenhouse/greenhouse/greenhouse.component';
|
||||
|
||||
export class Path {
|
||||
|
||||
constructor(
|
||||
readonly path: string,
|
||||
readonly title: string,
|
||||
readonly menu: boolean,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
get routerLink(): string {
|
||||
return `/${this.path}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ROUTING = {
|
||||
ENERGY: new Path('Energy', 'Energie', true),
|
||||
GREENHOUSE: new Path('Greenhouse', 'Gewächshaus', true),
|
||||
}
|
||||
|
||||
export function menubar(): Path[] {
|
||||
return Object.values(ROUTING).filter(v => v.menu);
|
||||
}
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: 'Dashboard', component: DashboardComponent},
|
||||
{path: '**', redirectTo: 'Dashboard'},
|
||||
{path: ROUTING.ENERGY.path, component: ElectroComponent},
|
||||
{path: ROUTING.GREENHOUSE.path, component: GreenhouseComponent},
|
||||
{path: '**', redirectTo: ROUTING.ENERGY.path},
|
||||
];
|
||||
|
||||
91
src/main/angular/src/app/core/AbstractRepositoryService.ts
Normal file
91
src/main/angular/src/app/core/AbstractRepositoryService.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import {Subscription} from "rxjs";
|
||||
import {Next} from "./types";
|
||||
import {Series} from "../series/Series";
|
||||
import {SeriesWrapper} from "../series/SeriesWrapper";
|
||||
import {Inject, LOCALE_ID} from "@angular/core";
|
||||
import {ApiService} from "./api.service";
|
||||
|
||||
export abstract class AbstractRepositoryService {
|
||||
|
||||
private readonly clientSubscriptions: Subscription[] = [];
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
private readonly clientCallbacks: Next<Series>[] = [];
|
||||
|
||||
protected abstract get liveValues(): SeriesWrapper[];
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) readonly locale: string,
|
||||
protected readonly api: ApiService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
protected onSubscribe(subscription: Subscription): Subscription {
|
||||
this.clientSubscriptions.push(subscription);
|
||||
this.ensureApiSubscribed();
|
||||
return subscription;
|
||||
}
|
||||
|
||||
protected onUnsubscribe(subscription: Subscription): Subscription {
|
||||
this.clientSubscriptions.splice(this.clientSubscriptions.indexOf(subscription), 1);
|
||||
if (this.clientSubscriptions.length === 0) {
|
||||
this.ensureApiUnsubscribed();
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private ensureApiSubscribed() {
|
||||
if (this.subs.length !== 0) {
|
||||
return;
|
||||
}
|
||||
this.subs.push(this.api.subscribe(['Series'], j => Series.fromJson(j, this.locale), series => this.update(series)));
|
||||
this.subs.push(this.api.subscribeConnection(connected => {
|
||||
if (connected) {
|
||||
this.all();
|
||||
} else {
|
||||
this.liveValues.forEach(liveValue => liveValue.series = null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private ensureApiUnsubscribed() {
|
||||
if (this.subs.length <= 0) {
|
||||
return;
|
||||
}
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs.length = 0;
|
||||
}
|
||||
|
||||
private update(series: Series) {
|
||||
this.liveValues
|
||||
.filter(liveValue => liveValue.name === series.name)
|
||||
.forEach(liveValue => liveValue.series = series);
|
||||
this.clientCallbacks.forEach(next => next(series));
|
||||
}
|
||||
|
||||
all(next?: Next<Series[]>) {
|
||||
this.api.getList(['Series', 'all'], j => Series.fromJson(j, this.locale), list => {
|
||||
list.forEach(item => this.update(item));
|
||||
if (next) {
|
||||
next(list);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
subscribeAny(next?: Next<Series>): Subscription {
|
||||
const wrapper: Next<Series> = series => { // to let clientCallbacks only contain unique instances
|
||||
if (next) {
|
||||
next(series);
|
||||
}
|
||||
};
|
||||
this.clientCallbacks.push(wrapper);
|
||||
const subscription = new Subscription(() => {
|
||||
this.onUnsubscribe(subscription);
|
||||
this.clientCallbacks.splice(this.clientCallbacks.indexOf(wrapper), 1);
|
||||
});
|
||||
return this.onSubscribe(subscription);
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,7 +4,7 @@ import {map, Subscription} from 'rxjs';
|
||||
import {StompService} from '@stomp/ng2-stompjs';
|
||||
import {FromJson, Next} from './types';
|
||||
|
||||
const DEV_TO_PROD = false;
|
||||
const DEV_TO_PROD = true;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ElectroEnergyComponent} from "../electro/energy/electro-energy.component";
|
||||
import {ElectroPowerComponent} from "../electro/power/electro-power.component";
|
||||
import {WeatherDiagramComponent} from '../weather/weather-diagram/weather-diagram.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
imports: [
|
||||
ElectroEnergyComponent,
|
||||
ElectroPowerComponent,
|
||||
WeatherDiagramComponent
|
||||
],
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.less'
|
||||
})
|
||||
export class DashboardComponent {
|
||||
|
||||
}
|
||||
18
src/main/angular/src/app/electro/electro.component.ts
Normal file
18
src/main/angular/src/app/electro/electro.component.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ElectroEnergyComponent} from "./energy/electro-energy.component";
|
||||
import {ElectroPowerComponent} from "./power/electro-power.component";
|
||||
import {WeatherDiagramComponent} from '../weather/weather-diagram/weather-diagram.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-electro',
|
||||
imports: [
|
||||
ElectroEnergyComponent,
|
||||
ElectroPowerComponent,
|
||||
WeatherDiagramComponent
|
||||
],
|
||||
templateUrl: './electro.component.html',
|
||||
styleUrl: './electro.component.less'
|
||||
})
|
||||
export class ElectroComponent {
|
||||
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {PercentBarComponent} from "../../shared/percent-bar/percent-bar.component";
|
||||
import {Value} from '../../value/Value';
|
||||
import {SeriesService} from '../../series/series.service';
|
||||
import {Subscription} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-electro-power',
|
||||
@ -11,13 +12,23 @@ import {SeriesService} from '../../series/series.service';
|
||||
templateUrl: './electro-power.component.html',
|
||||
styleUrl: './electro-power.component.less'
|
||||
})
|
||||
export class ElectroPowerComponent {
|
||||
export class ElectroPowerComponent implements OnInit, OnDestroy {
|
||||
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
readonly seriesService: SeriesService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(this.seriesService.subscribeAny());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
get powerProduction(): Value | undefined {
|
||||
return this.seriesService.powerProduced.series?.lastValue;
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
<table class="vertical">
|
||||
<tr>
|
||||
<th>Temperatur</th>
|
||||
<td class="valueInteger">{{seriesService.greenhouseTemperature.series?.lastValue?.localeString}}</td>
|
||||
<td class="unit">{{seriesService.greenhouseTemperature.series?.lastValue?.unit?.unit}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Relative Luftfeuchte</th>
|
||||
<td class="valueInteger">{{seriesService.greenhouseHumidityRelative.series?.lastValue?.localeString}}</td>
|
||||
<td class="unit">{{seriesService.greenhouseHumidityRelative.series?.lastValue?.unit?.unit}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Absolute Luftfeuchte</th>
|
||||
<td class="valueInteger">{{seriesService.greenhouseHumidityAbsolute.series?.lastValue?.localeString}}</td>
|
||||
<td class="unit">{{seriesService.greenhouseHumidityAbsolute.series?.lastValue?.unit?.unit}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Beleuchtungsstärke</th>
|
||||
<td class="valueInteger">{{seriesService.greenhouseIlluminance.series?.lastValue?.localeString}}</td>
|
||||
<td class="unit">{{seriesService.greenhouseIlluminance.series?.lastValue?.unit?.unit}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -0,0 +1,28 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {SeriesService} from '../../series/series.service';
|
||||
import {Subscription} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-greenhouse',
|
||||
imports: [],
|
||||
templateUrl: './greenhouse.component.html',
|
||||
styleUrl: './greenhouse.component.less'
|
||||
})
|
||||
export class GreenhouseComponent implements OnInit, OnDestroy {
|
||||
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
readonly seriesService: SeriesService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(this.seriesService.subscribeAny());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
@ -2,21 +2,15 @@ import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
||||
import {ApiService} from '../core/api.service';
|
||||
import {Alignment} from './Alignment';
|
||||
import {AggregationWrapperDto} from './AggregationWrapperDto';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Series} from './Series';
|
||||
import {SeriesWrapper} from './SeriesWrapper';
|
||||
import {Next} from '../core/types';
|
||||
import {AbstractRepositoryService} from '../core/AbstractRepositoryService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SeriesService {
|
||||
|
||||
private readonly clientSubscriptions: Subscription[] = [];
|
||||
|
||||
private readonly subs: Subscription[] = [];
|
||||
|
||||
private readonly clientCallbacks: Next<Series>[] = [];
|
||||
export class SeriesService extends AbstractRepositoryService {
|
||||
|
||||
readonly powerConsumed: SeriesWrapper = new SeriesWrapper("power/consumed", this.onSubscribe, this.onUnsubscribe);
|
||||
|
||||
@ -26,84 +20,32 @@ export class SeriesService {
|
||||
|
||||
readonly powerBalance: SeriesWrapper = new SeriesWrapper("power/balance", this.onSubscribe, this.onUnsubscribe);
|
||||
|
||||
readonly liveValues: SeriesWrapper[] = [
|
||||
readonly greenhouseTemperature: SeriesWrapper = new SeriesWrapper("greenhouse/temperature", this.onSubscribe, this.onUnsubscribe);
|
||||
|
||||
readonly greenhouseHumidityRelative: SeriesWrapper = new SeriesWrapper("greenhouse/humidity/relative", this.onSubscribe, this.onUnsubscribe);
|
||||
|
||||
readonly greenhouseHumidityAbsolute: SeriesWrapper = new SeriesWrapper("greenhouse/humidity/absolute", this.onSubscribe, this.onUnsubscribe);
|
||||
|
||||
readonly greenhouseIlluminance: SeriesWrapper = new SeriesWrapper("greenhouse/illuminance", this.onSubscribe, this.onUnsubscribe);
|
||||
|
||||
protected get liveValues(): SeriesWrapper[] {
|
||||
return [
|
||||
this.powerConsumed,
|
||||
this.powerProduced,
|
||||
this.powerSelf,
|
||||
this.powerBalance,
|
||||
]
|
||||
this.greenhouseTemperature,
|
||||
this.greenhouseHumidityRelative,
|
||||
this.greenhouseHumidityAbsolute,
|
||||
this.greenhouseIlluminance,
|
||||
];
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) readonly locale: string,
|
||||
protected readonly api: ApiService,
|
||||
@Inject(LOCALE_ID) locale: string,
|
||||
api: ApiService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
private onSubscribe(subscription: Subscription): Subscription {
|
||||
this.clientSubscriptions.push(subscription);
|
||||
this.ensureApiSubscribed();
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private onUnsubscribe(subscription: Subscription): Subscription {
|
||||
this.clientSubscriptions.splice(this.clientSubscriptions.indexOf(subscription), 1);
|
||||
if (this.clientSubscriptions.length === 0) {
|
||||
this.ensureApiUnsubscribed();
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private ensureApiSubscribed() {
|
||||
if (this.subs.length !== 0) {
|
||||
return;
|
||||
}
|
||||
this.subs.push(this.api.subscribe(['Series'], j => Series.fromJson(j, this.locale), series => this.update(series)));
|
||||
this.subs.push(this.api.subscribeConnection(connected => {
|
||||
if (connected) {
|
||||
this.all();
|
||||
} else {
|
||||
this.liveValues.forEach(liveValue => liveValue.series = null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private ensureApiUnsubscribed() {
|
||||
if (this.subs.length <= 0) {
|
||||
return;
|
||||
}
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs.length = 0;
|
||||
}
|
||||
|
||||
private update(series: Series) {
|
||||
this.liveValues
|
||||
.filter(liveValue => liveValue.name === series.name)
|
||||
.forEach(liveValue => liveValue.series = series);
|
||||
this.clientCallbacks.forEach(next => next(series));
|
||||
}
|
||||
|
||||
subscribeAny(next?: Next<Series>): Subscription {
|
||||
const wrapper: Next<Series> = series => { // to let clientCallbacks only contain unique instances
|
||||
if (next) {
|
||||
next(series);
|
||||
}
|
||||
};
|
||||
this.clientCallbacks.push(wrapper);
|
||||
const subscription = new Subscription(() => {
|
||||
this.onUnsubscribe(subscription);
|
||||
this.clientCallbacks.splice(this.clientCallbacks.indexOf(wrapper), 1);
|
||||
});
|
||||
return this.onSubscribe(subscription);
|
||||
}
|
||||
|
||||
all(next?: Next<Series[]>) {
|
||||
this.api.getList(['Series', 'all'], j => Series.fromJson(j, this.locale), list => {
|
||||
list.forEach(item => this.update(item));
|
||||
if (next) {
|
||||
next(list);
|
||||
}
|
||||
});
|
||||
super(locale, api);
|
||||
}
|
||||
|
||||
aggregations(alignment: Alignment, offset: number, next: Next<AggregationWrapperDto>) {
|
||||
|
||||
@ -20,6 +20,14 @@ export class Unit {
|
||||
|
||||
static readonly PRECIPITATION_MM = new Unit('PRECIPITATION_MM', 'mm');
|
||||
|
||||
static readonly TEMPERATURE_C = new Unit('TEMPERATURE_C', '°C');
|
||||
|
||||
static readonly HUMIDITY_RELATIVE_PERCENT = new Unit('HUMIDITY_RELATIVE_PERCENT', '%');
|
||||
|
||||
static readonly HUMIDITY_ABSOLUTE_GM3 = new Unit('HUMIDITY_ABSOLUTE_GM3', 'g/m³');
|
||||
|
||||
static readonly ILLUMINANCE_LUX = new Unit('ILLUMINANCE_LUX', 'lux');
|
||||
|
||||
private constructor(
|
||||
readonly name: string,
|
||||
readonly unit: string,
|
||||
|
||||
@ -3,12 +3,20 @@ import {validateNumber} from "../core/validators";
|
||||
|
||||
export class Value {
|
||||
|
||||
readonly localeString: string;
|
||||
|
||||
readonly valueInteger: string;
|
||||
|
||||
readonly valueFraction: string;
|
||||
|
||||
constructor(
|
||||
readonly value: number,
|
||||
readonly unit: Unit,
|
||||
readonly decimals: number,
|
||||
readonly locale: string) {
|
||||
//
|
||||
this.localeString = this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals});
|
||||
this.valueInteger = this.localeString.split(/[,.]/)[0];
|
||||
this.valueFraction = this.localeString.split(/[,.]/)[1];
|
||||
}
|
||||
|
||||
static fromJson2(json: any, locale: string): Value {
|
||||
@ -41,10 +49,6 @@ export class Value {
|
||||
return `${(this.localeString)}${this.unit.unit}`;
|
||||
}
|
||||
|
||||
get localeString(): string {
|
||||
return this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals});
|
||||
}
|
||||
|
||||
negate() {
|
||||
return new Value(-this.value, this.unit, this.decimals, this.locale);
|
||||
}
|
||||
|
||||
@ -23,3 +23,36 @@ button {
|
||||
div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table.vertical {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.valueInteger {
|
||||
width: 0;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td.valueDelimiter {
|
||||
width: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.valueFraction {
|
||||
width: 0;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.unit {
|
||||
width: 0;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user