diff --git a/application.properties b/application.properties
index c35e625..a2f653a 100644
--- a/application.properties
+++ b/application.properties
@@ -8,4 +8,4 @@ spring.jpa.hibernate.ddl-auto=update
#-
spring.jackson.serialization.indent_output=true
#-
-de.ph87.knx.mqtt.uri=tcp://10.255.0.1:1883
+de.ph87.data.mqtt.uri=tcp://10.0.0.50:1883
diff --git a/src/main/angular/.editorconfig b/src/main/angular/.editorconfig
index f166060..be29070 100644
--- a/src/main/angular/.editorconfig
+++ b/src/main/angular/.editorconfig
@@ -9,6 +9,7 @@ insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
+# noinspection EditorConfigKeyCorrectness
quote_type = single
ij_typescript_use_double_quotes = false
diff --git a/src/main/angular/package-lock.json b/src/main/angular/package-lock.json
index 69341a0..b6eafa5 100644
--- a/src/main/angular/package-lock.json
+++ b/src/main/angular/package-lock.json
@@ -14,6 +14,9 @@
"@angular/forms": "^20.3.0",
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
+ "@fortawesome/angular-fontawesome": "^3.0.0",
+ "@fortawesome/free-regular-svg-icons": "^7.1.0",
+ "@fortawesome/free-solid-svg-icons": "^7.1.0",
"@stomp/ng2-stompjs": "^8.0.0",
"@stomp/stompjs": "^7.2.1",
"rxjs": "~7.8.0",
@@ -1344,6 +1347,64 @@
"node": ">=18"
}
},
+ "node_modules/@fortawesome/angular-fontawesome": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-3.0.0.tgz",
+ "integrity": "sha512-+8Dd6DoJnqArfrZ5NvjHyRL64IIkTigXclbOOcFdYQ8/WFERQUDaEU6SAV8Q0JBpJhMS1McED7YCOCAE6SIVyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fortawesome/fontawesome-svg-core": "^7.0.0",
+ "tslib": "^2.8.1"
+ },
+ "peerDependencies": {
+ "@angular/core": "^20.0.0"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
+ "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
+ "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-regular-svg-icons": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.1.0.tgz",
+ "integrity": "sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz",
+ "integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@inquirer/ansi": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz",
diff --git a/src/main/angular/package.json b/src/main/angular/package.json
index 402153e..f721450 100644
--- a/src/main/angular/package.json
+++ b/src/main/angular/package.json
@@ -32,7 +32,10 @@
"tslib": "^2.3.0",
"zone.js": "~0.15.0",
"@stomp/ng2-stompjs": "^8.0.0",
- "@stomp/stompjs": "^7.2.1"
+ "@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"
},
"devDependencies": {
"@angular/build": "^20.3.7",
diff --git a/src/main/angular/src/app/app.html b/src/main/angular/src/app/app.html
index 7dd570e..a969382 100644
--- a/src/main/angular/src/app/app.html
+++ b/src/main/angular/src/app/app.html
@@ -1 +1,13 @@
+
+
+
+
diff --git a/src/main/angular/src/app/app.less b/src/main/angular/src/app/app.less
index e69de29..9e44b98 100644
--- a/src/main/angular/src/app/app.less
+++ b/src/main/angular/src/app/app.less
@@ -0,0 +1,46 @@
+.MainMenu {
+ border-bottom: 1px solid #888;
+ background-color: lightsteelblue;
+ overflow: visible;
+
+ .MainMenuBar {
+ display: flex;
+ padding: 0.25em;
+
+ .MainMenuButton {
+ padding: 0.25em;
+ border: 1px solid #888;
+ border-radius: 0.25em;
+ }
+
+ .MainMenuButton:hover {
+ background-color: lightskyblue;
+ }
+ }
+
+}
+
+.MainMenuDrawer {
+ margin: 0.25em;
+ border-radius: 0.1em;
+ background-color: lightsteelblue;
+ box-shadow: 0 0 0.25em black;
+
+ .MainMenuItem {
+ padding: 0.5em;
+ }
+
+ .MainMenuItem:not(:last-child) {
+ border-bottom: 1px solid #888;
+ }
+
+ .MainMenuItem:hover {
+ background-color: lightskyblue;
+ }
+
+ .MainMenuItemActive {
+ font-weight: bold;
+ text-shadow: 0 0 0.25em white;
+ }
+
+}
diff --git a/src/main/angular/src/app/app.routes.ts b/src/main/angular/src/app/app.routes.ts
index 03015a5..ef84a51 100644
--- a/src/main/angular/src/app/app.routes.ts
+++ b/src/main/angular/src/app/app.routes.ts
@@ -1,7 +1,10 @@
import {Routes} from '@angular/router';
import {LocationList} from './location/list/location-list';
+import {LocationDetail} from './location/detail/location-detail';
export const routes: Routes = [
- {path: 'LocationList', component: LocationList},
- {path: '**', redirectTo: 'LocationList'}
+ {path: 'Location/:id', component: LocationDetail},
+ {path: 'Location', component: LocationList},
+ {path: '**', redirectTo: 'Location'},
+ {path: '**', redirectTo: 'Location'},
];
diff --git a/src/main/angular/src/app/app.ts b/src/main/angular/src/app/app.ts
index 0f4e198..bff6f61 100644
--- a/src/main/angular/src/app/app.ts
+++ b/src/main/angular/src/app/app.ts
@@ -1,11 +1,19 @@
import {Component} from '@angular/core';
-import {RouterOutlet} from '@angular/router';
+import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
+import {FaIconComponent} from '@fortawesome/angular-fontawesome';
+import {faBars, faBurger} from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'app-root',
- imports: [RouterOutlet],
+ imports: [RouterOutlet, FaIconComponent, RouterLink, RouterLinkActive],
templateUrl: './app.html',
styleUrl: './app.less'
})
export class App {
+ protected readonly faBurger = faBurger;
+
+ protected readonly faBars = faBars;
+
+ protected showDrawer: boolean = false;
+
}
diff --git a/src/main/angular/src/app/location/Location.ts b/src/main/angular/src/app/location/Location.ts
index 8df3f22..11f5e46 100644
--- a/src/main/angular/src/app/location/Location.ts
+++ b/src/main/angular/src/app/location/Location.ts
@@ -1,4 +1,5 @@
-import {validateNumber, validateString} from '../common';
+import {or, validateNumber, validateString} from '../common';
+import {Series} from '../series/Series';
export class Location {
@@ -7,6 +8,10 @@ export class Location {
readonly name: string,
readonly latitude: number,
readonly longitude: number,
+ readonly purchase: Series | null,
+ readonly delivery: Series | null,
+ readonly produce: Series | null,
+ readonly power: Series | null,
) {
//
}
@@ -17,6 +22,10 @@ export class Location {
validateString(json.name),
validateNumber(json.latitude),
validateNumber(json.longitude),
+ or(json.purchase, Series.fromJson, null),
+ or(json.delivery, Series.fromJson, null),
+ or(json.produce, Series.fromJson, null),
+ or(json.power, Series.fromJson, null),
);
}
diff --git a/src/main/angular/src/app/location/detail/location-detail.html b/src/main/angular/src/app/location/detail/location-detail.html
new file mode 100644
index 0000000..ad8c0d8
--- /dev/null
+++ b/src/main/angular/src/app/location/detail/location-detail.html
@@ -0,0 +1,9 @@
+@if (location) {
+
+
+
+
+
+
+
+}
diff --git a/src/main/angular/src/app/location/detail/location-detail.less b/src/main/angular/src/app/location/detail/location-detail.less
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/angular/src/app/location/detail/location-detail.ts b/src/main/angular/src/app/location/detail/location-detail.ts
new file mode 100644
index 0000000..b22eca3
--- /dev/null
+++ b/src/main/angular/src/app/location/detail/location-detail.ts
@@ -0,0 +1,56 @@
+import {Component, OnInit} from '@angular/core';
+import {LocationService} from '../location-service';
+import {ActivatedRoute} from '@angular/router';
+import {Location} from '../Location';
+import {Text} from '../../shared/text/text';
+import {Number} from '../../shared/number/number';
+import {SeriesSelect, SeriesType} from '../../series/select/series-select';
+import {Series} from '../../series/Series';
+import {SeriesService} from '../../series/series-service';
+
+@Component({
+ selector: 'app-location-detail',
+ imports: [
+ Text,
+ Number,
+ SeriesSelect
+ ],
+ templateUrl: './location-detail.html',
+ styleUrl: './location-detail.less',
+})
+export class LocationDetail implements OnInit {
+
+ protected location: Location | null = null;
+
+ private series: Series[] = [];
+
+ constructor(
+ readonly locationService: LocationService,
+ readonly seriesService: SeriesService,
+ readonly activatedRoute: ActivatedRoute,
+ ) {
+ //
+ }
+
+ ngOnInit(): void {
+ this.activatedRoute.params.subscribe(params => {
+ this.locationService.getById(params['id'], location => this.location = location);
+ });
+ this.seriesService.findAll(list => this.series = list);
+ }
+
+ protected readonly update = (location: Location): void => {
+ if (this.location?.id === location.id) {
+ this.location = location;
+ }
+ };
+
+ protected readonly filterEnergy = (): Series[] => {
+ return this.series.filter(series => series.type === SeriesType.DELTA && series.unit === 'kWh');
+ };
+
+ protected readonly filterPower = (): Series[] => {
+ return this.series.filter(series => series.type === SeriesType.VARYING && series.unit === 'W');
+ };
+
+}
diff --git a/src/main/angular/src/app/location/list/location-list.html b/src/main/angular/src/app/location/list/location-list.html
index d3c30ba..15405e3 100644
--- a/src/main/angular/src/app/location/list/location-list.html
+++ b/src/main/angular/src/app/location/list/location-list.html
@@ -1,6 +1,6 @@
-
+
@for (location of list; track location.id) {
-
+
{{ location.name }}
}
diff --git a/src/main/angular/src/app/location/list/location-list.ts b/src/main/angular/src/app/location/list/location-list.ts
index 784a6a0..c1dfcea 100644
--- a/src/main/angular/src/app/location/list/location-list.ts
+++ b/src/main/angular/src/app/location/list/location-list.ts
@@ -1,10 +1,13 @@
import {Component, OnInit} from '@angular/core';
import {LocationService} from '../location-service';
import {Location} from '../Location';
+import {RouterLink} from '@angular/router';
@Component({
selector: 'app-location-list',
- imports: [],
+ imports: [
+ RouterLink
+ ],
templateUrl: './location-list.html',
styleUrl: './location-list.less',
})
diff --git a/src/main/angular/src/app/location/location-service.ts b/src/main/angular/src/app/location/location-service.ts
index 95671d9..2f40049 100644
--- a/src/main/angular/src/app/location/location-service.ts
+++ b/src/main/angular/src/app/location/location-service.ts
@@ -1,6 +1,7 @@
import {Injectable} from '@angular/core';
import {ApiService, CrudService, Next, WebsocketService} from '../common';
import {Location} from './Location'
+import {Series} from '../series/Series';
@Injectable({
providedIn: 'root'
@@ -15,4 +16,36 @@ export class LocationService extends CrudService
{
this.getList(['findAll'], next);
}
+ getById(id: number, next: Next) {
+ this.getSingle([id, 'byId'], next);
+ }
+
+ name(location: Location, name: string, next?: Next) {
+ this.postSingle([location.id, 'name'], name, next);
+ }
+
+ latitude(location: Location, latitude: number, next?: Next) {
+ this.postSingle([location.id, 'latitude'], latitude, next);
+ }
+
+ longitude(location: Location, longitude: number, next?: Next) {
+ this.postSingle([location.id, 'longitude'], longitude, next);
+ }
+
+ purchase(location: Location, purchase: Series | null, next?: Next) {
+ this.postSingle([location.id, 'purchase'], purchase?.id, next);
+ }
+
+ delivery(location: Location, delivery: Series | null, next?: Next) {
+ this.postSingle([location.id, 'delivery'], delivery?.id, next);
+ }
+
+ produce(location: Location, produce: Series | null, next?: Next) {
+ this.postSingle([location.id, 'produce'], produce?.id, next);
+ }
+
+ power(location: Location, power: Series | null, next?: Next) {
+ this.postSingle([location.id, 'power'], power?.id, next);
+ }
+
}
diff --git a/src/main/angular/src/app/series/Series.ts b/src/main/angular/src/app/series/Series.ts
new file mode 100644
index 0000000..0d8c309
--- /dev/null
+++ b/src/main/angular/src/app/series/Series.ts
@@ -0,0 +1,24 @@
+import {validateEnum, validateNumber, validateString} from "../common";
+import {SeriesType} from "./select/series-select";
+
+export class Series {
+
+ constructor(
+ readonly id: number,
+ readonly name: string,
+ readonly unit: string,
+ readonly type: SeriesType,
+ ) {
+ //
+ }
+
+ static fromJson(json: any): Series {
+ return new Series(
+ validateNumber(json.id),
+ validateString(json.name),
+ validateString(json.unit),
+ validateEnum(json.type, SeriesType),
+ );
+ }
+
+}
diff --git a/src/main/angular/src/app/series/select/series-select.html b/src/main/angular/src/app/series/select/series-select.html
new file mode 100644
index 0000000..f4985d1
--- /dev/null
+++ b/src/main/angular/src/app/series/select/series-select.html
@@ -0,0 +1,27 @@
+
+
+ @if (editing) {
+
+ } @else {
+ {{ initial?.name || ' ' }}
+ }
+
+ @if (editing || showPen) {
+
+ }
+
diff --git a/src/main/angular/src/app/series/select/series-select.less b/src/main/angular/src/app/series/select/series-select.less
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/angular/src/app/series/select/series-select.ts b/src/main/angular/src/app/series/select/series-select.ts
new file mode 100644
index 0000000..61af75b
--- /dev/null
+++ b/src/main/angular/src/app/series/select/series-select.ts
@@ -0,0 +1,84 @@
+import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} 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 {Series} from '../Series';
+
+export enum SeriesType {
+ BOOL = 'BOOL',
+ DELTA = 'DELTA',
+ VARYING = 'VARYING',
+}
+
+@Component({
+ selector: 'app-series-select',
+ imports: [
+ FaIconComponent,
+ FormsModule,
+ NgClass
+ ],
+ templateUrl: './series-select.html',
+ styleUrl: './series-select.less',
+})
+export class SeriesSelect {
+
+ @ViewChild('input')
+ protected readonly input!: ElementRef;
+
+ private _initial: Series | null = null;
+
+ @Input()
+ protected allowEmpty: boolean = true;
+
+ @Input()
+ list: Series[] = [];
+
+ @Output()
+ readonly onChange = new EventEmitter();
+
+ protected showPen: boolean = false;
+
+ protected model: Series | null = null;
+
+ protected editing: boolean = false;
+
+ @Input()
+ set initial(value: Series | null) {
+ this._initial = value;
+ if (!this.editing) {
+ this.model = this.initial;
+ }
+ }
+
+ get initial(): Series | null {
+ return this._initial;
+ }
+
+ protected start() {
+ this.editing = true;
+ setTimeout(() => this.input.nativeElement.focus(), 0);
+ }
+
+ protected apply() {
+ if (this.model !== this.initial) {
+ this.onChange.emit(this.model);
+ }
+ this.editing = false;
+ }
+
+ protected cancel() {
+ this.model = this.initial;
+ this.editing = false;
+ }
+
+ protected classes(): {} {
+ return {
+ "unchanged": this.editing && this.model === this.initial,
+ "changed": this.model !== this.initial,
+ "invalid": !this.allowEmpty && this.model === null,
+ };
+ }
+
+ protected readonly faPen = faPen;
+}
diff --git a/src/main/angular/src/app/series/series-service.ts b/src/main/angular/src/app/series/series-service.ts
new file mode 100644
index 0000000..289bd74
--- /dev/null
+++ b/src/main/angular/src/app/series/series-service.ts
@@ -0,0 +1,18 @@
+import {Injectable} from '@angular/core';
+import {ApiService, CrudService, Next, WebsocketService} from '../common';
+import {Series} from './Series';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SeriesService extends CrudService {
+
+ constructor(api: ApiService, ws: WebsocketService) {
+ super(api, ws, ['Series'], Series.fromJson);
+ }
+
+ findAll(next: Next) {
+ this.getList(['findAll'], next);
+ }
+
+}
diff --git a/src/main/angular/src/app/shared/number/number.html b/src/main/angular/src/app/shared/number/number.html
new file mode 100644
index 0000000..408bc82
--- /dev/null
+++ b/src/main/angular/src/app/shared/number/number.html
@@ -0,0 +1,28 @@
+
+
+ @if (editing) {
+
+ } @else {
+ {{ initial | number:'0.0-999':locale || ' ' }}{{ unit }}
+ }
+
+ @if (!editing && showPen) {
+
+ }
+ @if (editing) {
+
+ }
+
diff --git a/src/main/angular/src/app/shared/number/number.less b/src/main/angular/src/app/shared/number/number.less
new file mode 100644
index 0000000..5caa262
--- /dev/null
+++ b/src/main/angular/src/app/shared/number/number.less
@@ -0,0 +1,29 @@
+.container {
+ display: flex;
+
+ .value {
+ flex: 1;
+ }
+
+}
+
+.container:hover {
+ background-color: #0002;
+}
+
+input {
+ all: unset;
+ width: 100%;
+}
+
+.unchanged {
+ background-color: lightgreen !important;
+}
+
+.invalid {
+ background-color: red !important;
+}
+
+.changed {
+ background-color: yellow !important;
+}
diff --git a/src/main/angular/src/app/shared/number/number.ts b/src/main/angular/src/app/shared/number/number.ts
new file mode 100644
index 0000000..38c87e0
--- /dev/null
+++ b/src/main/angular/src/app/shared/number/number.ts
@@ -0,0 +1,91 @@
+import {Component, ElementRef, EventEmitter, Inject, Input, LOCALE_ID, Output, ViewChild} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+import {FaIconComponent} from '@fortawesome/angular-fontawesome';
+import {faCancel, faPen} from '@fortawesome/free-solid-svg-icons';
+import {DecimalPipe, NgClass} from '@angular/common';
+
+@Component({
+ selector: 'app-number',
+ imports: [
+ FormsModule,
+ FaIconComponent,
+ NgClass,
+ DecimalPipe
+ ],
+ templateUrl: './number.html',
+ styleUrl: './number.less',
+})
+export class Number {
+
+ protected readonly faCancel = faCancel;
+
+ protected readonly faPen = faPen;
+
+ @ViewChild('input')
+ protected readonly input!: ElementRef;
+
+ private _initial: number = 0;
+
+ @Input()
+ set initial(value: number) {
+ this._initial = value;
+ if (!this.editing) {
+ this.model = this.initial;
+ }
+ }
+
+ get initial(): number {
+ return this._initial;
+ }
+
+ @Input()
+ unit: string = "";
+
+ @Input()
+ protected validate: (value: number) => boolean = () => true;
+
+ @Output()
+ readonly onChange = new EventEmitter();
+
+ protected showPen: boolean = false;
+
+ protected model: number = 0;
+
+ protected editing: boolean = false;
+
+ constructor(
+ @Inject(LOCALE_ID) readonly locale: string,
+ ) {
+ //
+ }
+
+ protected readonly start = () => {
+ this.editing = true;
+ setTimeout(() => this.input.nativeElement.focus(), 0);
+ };
+
+ protected readonly blur = () => {
+ setTimeout(this.apply, 0);
+ };
+
+ protected readonly apply = () => {
+ if (this.model !== this.initial) {
+ this.onChange.emit(this.model);
+ }
+ this.editing = false;
+ };
+
+ protected readonly cancel = () => {
+ this.model = this.initial;
+ this.editing = false;
+ };
+
+ protected readonly classes = (): {} => {
+ return {
+ "unchanged": this.editing && this.model === this.initial,
+ "changed": this.model !== this.initial,
+ "invalid": !this.validate(this.model),
+ };
+ };
+
+}
diff --git a/src/main/angular/src/app/shared/text/text.html b/src/main/angular/src/app/shared/text/text.html
new file mode 100644
index 0000000..7faca2c
--- /dev/null
+++ b/src/main/angular/src/app/shared/text/text.html
@@ -0,0 +1,28 @@
+
+
+ @if (editing) {
+
+ } @else {
+ {{ initial || ' ' }}
+ }
+
+ @if (!editing && showPen) {
+
+ }
+ @if (editing) {
+
+ }
+
diff --git a/src/main/angular/src/app/shared/text/text.less b/src/main/angular/src/app/shared/text/text.less
new file mode 100644
index 0000000..5caa262
--- /dev/null
+++ b/src/main/angular/src/app/shared/text/text.less
@@ -0,0 +1,29 @@
+.container {
+ display: flex;
+
+ .value {
+ flex: 1;
+ }
+
+}
+
+.container:hover {
+ background-color: #0002;
+}
+
+input {
+ all: unset;
+ width: 100%;
+}
+
+.unchanged {
+ background-color: lightgreen !important;
+}
+
+.invalid {
+ background-color: red !important;
+}
+
+.changed {
+ background-color: yellow !important;
+}
diff --git a/src/main/angular/src/app/shared/text/text.ts b/src/main/angular/src/app/shared/text/text.ts
new file mode 100644
index 0000000..3d6a1e9
--- /dev/null
+++ b/src/main/angular/src/app/shared/text/text.ts
@@ -0,0 +1,83 @@
+import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+import {FaIconComponent} from '@fortawesome/angular-fontawesome';
+import {faCancel, faPen} from '@fortawesome/free-solid-svg-icons';
+import {NgClass} from '@angular/common';
+
+@Component({
+ selector: 'app-text',
+ imports: [
+ FormsModule,
+ FaIconComponent,
+ NgClass
+ ],
+ templateUrl: './text.html',
+ styleUrl: './text.less',
+})
+export class Text {
+
+ @ViewChild('input')
+ protected readonly input!: ElementRef;
+
+ private _initial: string = "";
+
+ @Input()
+ protected allowEmpty: boolean = true;
+
+ @Input()
+ protected validate: (value: string) => boolean = () => true;
+
+ @Output()
+ readonly onChange = new EventEmitter();
+
+ protected readonly faPen = faPen;
+
+ protected showPen: boolean = false;
+
+ protected model: string = "";
+
+ protected editing: boolean = false;
+
+ @Input()
+ set initial(value: string) {
+ this._initial = value;
+ if (!this.editing) {
+ this.model = this.initial;
+ }
+ }
+
+ get initial(): string {
+ return this._initial;
+ }
+
+ protected readonly start = (): void => {
+ this.editing = true;
+ setTimeout(() => this.input.nativeElement.focus(), 0);
+ };
+
+ protected readonly blur = (): void => {
+ setTimeout(this.apply, 0);
+ };
+
+ protected readonly apply = (): void => {
+ if (this.model !== this.initial) {
+ this.onChange.emit(this.model);
+ }
+ this.editing = false;
+ };
+
+ protected readonly cancel = (): void => {
+ this.model = this.initial;
+ this.editing = false;
+ };
+
+ protected readonly classes = (): {} => {
+ return {
+ "unchanged": this.editing && this.model === this.initial,
+ "changed": this.model !== this.initial,
+ "invalid": (!this.allowEmpty && this.model === '') || !this.validate(this.model),
+ };
+ };
+
+ protected readonly faCancel = faCancel;
+}
diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less
index 90d4ee0..0b0f230 100644
--- a/src/main/angular/src/styles.less
+++ b/src/main/angular/src/styles.less
@@ -1 +1,25 @@
-/* You can add global styles to this file, and also import other style files */
+body {
+ position: relative;
+ height: 100%;
+ margin: 0;
+ font-family: sans-serif;
+ font-size: 5vw;
+}
+
+div {
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+.List {
+ overflow-y: scroll;
+
+ .ListItem {
+ padding: 0.5em;
+ border-bottom: 0.05em solid #ccc;
+ }
+}
+
+.NoUserSelect {
+ user-select: none;
+}
diff --git a/src/main/java/de/ph87/data/DemoService.java b/src/main/java/de/ph87/data/DemoService.java
index c32a415..3ac1e88 100644
--- a/src/main/java/de/ph87/data/DemoService.java
+++ b/src/main/java/de/ph87/data/DemoService.java
@@ -1,5 +1,7 @@
package de.ph87.data;
+import de.ph87.data.location.Location;
+import de.ph87.data.location.LocationRepository;
import de.ph87.data.plot.Plot;
import de.ph87.data.plot.PlotRepository;
import de.ph87.data.plot.axis.Axis;
@@ -41,15 +43,29 @@ public class DemoService {
private final DemoConfig demoConfig;
+ private final LocationRepository locationRepository;
+
@Transactional
@EventListener(ApplicationReadyEvent.class)
public void init() {
if (!demoConfig.isEnabled()) {
return;
}
+ location("Eppelborn", 49.4086, 6.9645);
+ location("Friedrichsthal", 49.3270, 7.0947);
topics();
}
+ private void location(@NonNull final String name, final double latitude, final double longitude) {
+ locationRepository.findByName(name).orElseGet(() -> {
+ final Location created = new Location();
+ created.setName(name);
+ created.setLatitude(latitude);
+ created.setLongitude(longitude);
+ return locationRepository.save(created);
+ });
+ }
+
private void topics() {
final Series fallbackRelay0 = series("fallback/relay0", "", SeriesType.BOOL, 5);
topic(
@@ -329,17 +345,17 @@ public class DemoService {
}
@NonNull
- private Series series(@NonNull final String name, @NonNull final String unit, @NonNull final SeriesType type, final int expectedEverySeconds) {
+ private Series series(@NonNull final String name, @NonNull final String unit, @NonNull final SeriesType type, final int seconds) {
return seriesRepository
.findByName(name)
.stream()
.peek(existing -> {
existing.setUnit(unit);
existing.setType(type);
- existing.setExpectedEverySeconds(expectedEverySeconds);
+ existing.setSeconds(seconds);
})
.findFirst()
- .orElseGet(() -> seriesRepository.save(new Series(name, unit, 1, expectedEverySeconds, type)));
+ .orElseGet(() -> seriesRepository.save(new Series(name, unit, 1, seconds, type)));
}
private void topic(@NonNull final String name, @NonNull final TopicQuery... queries) {
diff --git a/src/main/java/de/ph87/data/common/CrudAction.java b/src/main/java/de/ph87/data/common/CrudAction.java
new file mode 100644
index 0000000..7378729
--- /dev/null
+++ b/src/main/java/de/ph87/data/common/CrudAction.java
@@ -0,0 +1,5 @@
+package de.ph87.data.common;
+
+public enum CrudAction {
+ CREATED, MODIFIED, DELETED
+}
diff --git a/src/main/java/de/ph87/data/config/Config.java b/src/main/java/de/ph87/data/config/Config.java
deleted file mode 100644
index 3241577..0000000
--- a/src/main/java/de/ph87/data/config/Config.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package de.ph87.data.config;
-
-import de.ph87.data.series.data.Interval;
-import jakarta.annotation.Nullable;
-import jakarta.persistence.Column;
-import jakarta.persistence.ElementCollection;
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.OrderColumn;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.NonNull;
-import lombok.ToString;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Entity
-@Getter
-@ToString
-@NoArgsConstructor
-public class Config {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private long id;
-
- @Column(nullable = false)
- private boolean hidden = true;
-
- @Column(nullable = false)
- private boolean used = true;
-
- @Column(nullable = false)
- private boolean unused = true;
-
- @Column(nullable = false)
- private boolean ok = true;
-
- @Column(nullable = false)
- private boolean error = true;
-
- @Column(nullable = false)
- private boolean details = true;
-
- @Nullable
- @Column(name = "`interval`")
- @Enumerated(EnumType.STRING)
- private Interval interval = null;
-
- @Column(nullable = false, name = "`offset`")
- private long offset = 0;
-
- @NonNull
- @Column(nullable = false)
- private String search = "";
-
- @NonNull
- @OrderColumn(name = "index")
- @ElementCollection(fetch = FetchType.EAGER)
- private List seriesListOrders = new ArrayList<>(List.of(new Order("name", Direction.ASC)));
-
- public Config(@NonNull final ConfigDto dto) {
- set(dto);
- }
-
- public void set(@NonNull final ConfigDto dto) {
- this.hidden = dto.topicList.isHidden();
- this.used = dto.topicList.isUsed();
- this.unused = dto.topicList.isUnused();
- this.ok = dto.topicList.isOk();
- this.error = dto.topicList.isError();
- this.details = dto.topicList.isDetails();
- this.interval = dto.seriesList.getInterval();
- this.offset = dto.seriesList.getOffset();
- this.search = dto.seriesList.getSearch();
- this.seriesListOrders = dto.getSeriesList().orders.stream().map(Order::new).toList();
- }
-
-}
diff --git a/src/main/java/de/ph87/data/config/ConfigController.java b/src/main/java/de/ph87/data/config/ConfigController.java
deleted file mode 100644
index 8ac13a3..0000000
--- a/src/main/java/de/ph87/data/config/ConfigController.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package de.ph87.data.config;
-
-import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.CrossOrigin;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@CrossOrigin
-@RestController
-@RequestMapping("Config")
-@RequiredArgsConstructor
-public class ConfigController {
-
- private final ConfigService configService;
-
- @GetMapping("get")
- public ConfigDto get() {
- return configService.get();
- }
-
- @PostMapping("set")
- public ConfigDto set(@RequestBody @NonNull final ConfigDto inbound) {
- return configService.set(inbound);
- }
-
-}
diff --git a/src/main/java/de/ph87/data/config/ConfigDto.java b/src/main/java/de/ph87/data/config/ConfigDto.java
deleted file mode 100644
index 5ff72ea..0000000
--- a/src/main/java/de/ph87/data/config/ConfigDto.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package de.ph87.data.config;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-import lombok.NonNull;
-
-@Data
-public class ConfigDto {
-
- public final ConfigTopicListDto topicList;
-
- @NonNull
- public final ConfigSeriesListDto seriesList;
-
- public ConfigDto(
- @JsonProperty("topicList") @NonNull final ConfigTopicListDto topicList,
- @JsonProperty("seriesList") @NonNull final ConfigSeriesListDto seriesList
- ) {
- this.topicList = topicList;
- this.seriesList = seriesList;
- }
-
- ConfigDto(@NonNull final Config config) {
- this.topicList = new ConfigTopicListDto(config);
- this.seriesList = ConfigSeriesListDto.fromDB(config);
- }
-
-}
diff --git a/src/main/java/de/ph87/data/config/ConfigRepository.java b/src/main/java/de/ph87/data/config/ConfigRepository.java
deleted file mode 100644
index e401f9e..0000000
--- a/src/main/java/de/ph87/data/config/ConfigRepository.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.ph87.data.config;
-
-import org.springframework.data.repository.ListCrudRepository;
-
-import java.util.Optional;
-
-public interface ConfigRepository extends ListCrudRepository {
-
- Optional findFirstBy();
-
-}
diff --git a/src/main/java/de/ph87/data/config/ConfigSeriesListDto.java b/src/main/java/de/ph87/data/config/ConfigSeriesListDto.java
deleted file mode 100644
index ce23130..0000000
--- a/src/main/java/de/ph87/data/config/ConfigSeriesListDto.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package de.ph87.data.config;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import de.ph87.data.series.data.Interval;
-import jakarta.annotation.Nullable;
-import jakarta.persistence.Column;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import lombok.Data;
-import lombok.NonNull;
-
-import java.util.List;
-
-@Data
-public class ConfigSeriesListDto {
-
- @Column
- @Nullable
- @Enumerated(EnumType.STRING)
- public final Interval interval;
-
- @Column(nullable = false)
- public final long offset;
-
- @NonNull
- public final String search;
-
- public final List orders;
-
- public ConfigSeriesListDto(
- @JsonProperty("interval") @Nullable final Interval interval,
- @JsonProperty("offset") final long offset,
- @JsonProperty("search") @NonNull final String search,
- @JsonProperty("orders") @NonNull final List orders
- ) {
- this.interval = interval;
- this.offset = offset;
- this.search = search;
- this.orders = orders;
- }
-
- @NonNull
- public static ConfigSeriesListDto fromDB(@NonNull final Config orders) {
- return new ConfigSeriesListDto(
- orders.getInterval(),
- orders.getOffset(),
- orders.getSearch(),
- orders.getSeriesListOrders().stream().map(OrderDto::new).toList()
- );
- }
-
-}
diff --git a/src/main/java/de/ph87/data/config/ConfigService.java b/src/main/java/de/ph87/data/config/ConfigService.java
deleted file mode 100644
index f751471..0000000
--- a/src/main/java/de/ph87/data/config/ConfigService.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.ph87.data.config;
-
-import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class ConfigService {
-
- private final ConfigRepository configRepository;
-
- @Transactional
- public ConfigDto get() {
- return new ConfigDto(configRepository.findFirstBy().orElseGet(() -> configRepository.save(new Config())));
- }
-
- @Transactional
- public ConfigDto set(@NonNull final ConfigDto inbound) {
- return new ConfigDto(configRepository.findFirstBy().stream().peek(old -> old.set(inbound)).findFirst().orElseGet(() -> configRepository.save(new Config(inbound))));
- }
-
-}
diff --git a/src/main/java/de/ph87/data/config/ConfigTopicListDto.java b/src/main/java/de/ph87/data/config/ConfigTopicListDto.java
deleted file mode 100644
index 550ddd1..0000000
--- a/src/main/java/de/ph87/data/config/ConfigTopicListDto.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package de.ph87.data.config;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-import lombok.NonNull;
-
-@Data
-public class ConfigTopicListDto {
-
- public final boolean hidden;
-
- public final boolean used;
-
- public final boolean unused;
-
- public final boolean ok;
-
- public final boolean error;
-
- public final boolean details;
-
- public ConfigTopicListDto(
- @JsonProperty("hidden") final boolean hidden,
- @JsonProperty("used") final boolean used,
- @JsonProperty("unused") final boolean unused,
- @JsonProperty("ok") final boolean ok,
- @JsonProperty("error") final boolean error,
- @JsonProperty("details") final boolean details
- ) {
- this.hidden = hidden;
- this.used = used;
- this.unused = unused;
- this.ok = ok;
- this.error = error;
- this.details = details;
- }
-
- ConfigTopicListDto(@NonNull final Config config) {
- this.hidden = config.isHidden();
- this.used = config.isUsed();
- this.unused = config.isUnused();
- this.ok = config.isOk();
- this.error = config.isError();
- this.details = config.isDetails();
- }
-
-}
diff --git a/src/main/java/de/ph87/data/config/Direction.java b/src/main/java/de/ph87/data/config/Direction.java
deleted file mode 100644
index 4463204..0000000
--- a/src/main/java/de/ph87/data/config/Direction.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.ph87.data.config;
-
-public enum Direction {
- ASC, DESC
-}
diff --git a/src/main/java/de/ph87/data/config/Order.java b/src/main/java/de/ph87/data/config/Order.java
deleted file mode 100644
index c88ceb7..0000000
--- a/src/main/java/de/ph87/data/config/Order.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package de.ph87.data.config;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Embeddable;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.NonNull;
-import lombok.ToString;
-
-@Getter
-@ToString
-@Embeddable
-@NoArgsConstructor
-public class Order {
-
- @NonNull
- @Column(nullable = false)
- private String property;
-
- @NonNull
- @Column(nullable = false)
- @Enumerated(EnumType.STRING)
- private Direction direction;
-
- public Order(@NonNull final OrderDto dto) {
- this.property = dto.property;
- this.direction = dto.direction;
- }
-
- public Order(@NonNull final String property, @NonNull final Direction direction) {
- this.property = property;
- this.direction = direction;
- }
-
-}
diff --git a/src/main/java/de/ph87/data/config/OrderDto.java b/src/main/java/de/ph87/data/config/OrderDto.java
deleted file mode 100644
index f757677..0000000
--- a/src/main/java/de/ph87/data/config/OrderDto.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package de.ph87.data.config;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-import lombok.NonNull;
-
-@Data
-public class OrderDto {
-
- @NonNull
- public final String property;
-
- @NonNull
- public final Direction direction;
-
- public OrderDto(
- @JsonProperty("property") @NonNull final String property,
- @JsonProperty("direction") @NonNull final Direction direction
- ) {
- this.property = property;
- this.direction = direction;
- }
-
- public OrderDto(@NonNull final Order order) {
- this.property = order.getProperty();
- this.direction = order.getDirection();
- }
-
-}
diff --git a/src/main/java/de/ph87/data/location/Location.java b/src/main/java/de/ph87/data/location/Location.java
new file mode 100644
index 0000000..4de284f
--- /dev/null
+++ b/src/main/java/de/ph87/data/location/Location.java
@@ -0,0 +1,64 @@
+package de.ph87.data.location;
+
+import de.ph87.data.series.Series;
+import jakarta.annotation.Nullable;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Version;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.ToString;
+
+@Entity
+@Getter
+@ToString
+@NoArgsConstructor
+public class Location {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Version
+ private long version;
+
+ @Setter
+ @NonNull
+ @Column(nullable = false)
+ private String name = "";
+
+ @Setter
+ @Column(nullable = false)
+ private double latitude;
+
+ @Setter
+ @Column(nullable = false)
+ private double longitude;
+
+ @Setter
+ @Nullable
+ @ManyToOne
+ private Series purchase;
+
+ @Setter
+ @Nullable
+ @ManyToOne
+ private Series delivery;
+
+ @Setter
+ @Nullable
+ @ManyToOne
+ private Series produce;
+
+ @Setter
+ @Nullable
+ @ManyToOne
+ private Series power;
+
+}
diff --git a/src/main/java/de/ph87/data/location/LocationController.java b/src/main/java/de/ph87/data/location/LocationController.java
new file mode 100644
index 0000000..84df576
--- /dev/null
+++ b/src/main/java/de/ph87/data/location/LocationController.java
@@ -0,0 +1,80 @@
+package de.ph87.data.location;
+
+import jakarta.annotation.Nullable;
+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.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@CrossOrigin
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("Location")
+public class LocationController {
+
+ private final LocationRepository locationRepository;
+
+ private final LocationService locationService;
+
+ @GetMapping("findAll")
+ public List findAll() {
+ return locationRepository.findAllDto();
+ }
+
+ @GetMapping("create")
+ public LocationDto create() {
+ return locationService.create();
+ }
+
+ @GetMapping("{id}/byId")
+ public LocationDto byId(@PathVariable final long id) {
+ return locationRepository.dtoById(id).orElseThrow();
+ }
+
+ @GetMapping("{id}/delete")
+ public LocationDto delete(@PathVariable final long id) {
+ return locationService.delete(id);
+ }
+
+ @PostMapping("{id}/name")
+ public LocationDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String name) {
+ return locationService.name(id, name == null ? "" : name);
+ }
+
+ @PostMapping("{id}/latitude")
+ public LocationDto latitude(@PathVariable final long id, @RequestBody final double latitude) {
+ return locationService.latitude(id, latitude);
+ }
+
+ @PostMapping("{id}/longitude")
+ public LocationDto longitude(@PathVariable final long id, @RequestBody final double longitude) {
+ return locationService.longitude(id, longitude);
+ }
+
+ @PostMapping("{id}/purchase")
+ public LocationDto purchase(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) {
+ return locationService.purchase(id, seriesId);
+ }
+
+ @PostMapping("{id}/delivery")
+ public LocationDto delivery(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) {
+ return locationService.delivery(id, seriesId);
+ }
+
+ @PostMapping("{id}/produce")
+ public LocationDto produce(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) {
+ return locationService.produce(id, seriesId);
+ }
+
+ @PostMapping("{id}/power")
+ public LocationDto power(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long seriesId) {
+ return locationService.power(id, seriesId);
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/location/LocationDto.java b/src/main/java/de/ph87/data/location/LocationDto.java
new file mode 100644
index 0000000..9ca60b2
--- /dev/null
+++ b/src/main/java/de/ph87/data/location/LocationDto.java
@@ -0,0 +1,47 @@
+package de.ph87.data.location;
+
+import de.ph87.data.series.SeriesDto;
+import jakarta.annotation.Nullable;
+import lombok.Data;
+import lombok.NonNull;
+
+import static de.ph87.data.Helpers.map;
+
+@Data
+public class LocationDto {
+
+ public final long id;
+
+ public final long version;
+
+ public final String name;
+
+ public final double latitude;
+
+ public final double longitude;
+
+ @Nullable
+ public final SeriesDto purchase;
+
+ @Nullable
+ public final SeriesDto delivery;
+
+ @Nullable
+ public final SeriesDto produce;
+
+ @Nullable
+ public final SeriesDto power;
+
+ public LocationDto(@NonNull final Location location) {
+ this.id = location.getId();
+ this.version = location.getVersion();
+ this.name = location.getName();
+ this.latitude = location.getLatitude();
+ this.longitude = location.getLongitude();
+ this.purchase = map(location.getPurchase(), SeriesDto::new);
+ this.delivery = map(location.getDelivery(), SeriesDto::new);
+ this.produce = map(location.getProduce(), SeriesDto::new);
+ this.power = map(location.getPower(), SeriesDto::new);
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/location/LocationRepository.java b/src/main/java/de/ph87/data/location/LocationRepository.java
new file mode 100644
index 0000000..a29912b
--- /dev/null
+++ b/src/main/java/de/ph87/data/location/LocationRepository.java
@@ -0,0 +1,20 @@
+package de.ph87.data.location;
+
+import lombok.NonNull;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.ListCrudRepository;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface LocationRepository extends ListCrudRepository {
+
+ Optional findByName(@NonNull String name);
+
+ @Query("select new de.ph87.data.location.LocationDto(e) from Location e")
+ List findAllDto();
+
+ @Query("select new de.ph87.data.location.LocationDto(e) from Location e where e.id = :id")
+ Optional dtoById(long id);
+
+}
diff --git a/src/main/java/de/ph87/data/location/LocationService.java b/src/main/java/de/ph87/data/location/LocationService.java
new file mode 100644
index 0000000..0fb560b
--- /dev/null
+++ b/src/main/java/de/ph87/data/location/LocationService.java
@@ -0,0 +1,93 @@
+package de.ph87.data.location;
+
+import de.ph87.data.series.Series;
+import de.ph87.data.series.SeriesRepository;
+import jakarta.annotation.Nullable;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import static de.ph87.data.location.NotFoundException.notFound;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LocationService {
+
+ private final LocationRepository locationRepository;
+
+ private final SeriesRepository seriesRepository;
+
+ @NonNull
+ @Transactional
+ public LocationDto create() {
+ return new LocationDto(locationRepository.save(new Location()));
+ }
+
+ @NonNull
+ @Transactional
+ public LocationDto delete(final long id) {
+ return _set(id, locationRepository::delete);
+ }
+
+ @NonNull
+ @Transactional
+ public LocationDto name(final long id, @NonNull final String name) {
+ return _set(id, location -> location.setName(name));
+ }
+
+ @NonNull
+ @Transactional
+ public LocationDto latitude(final long id, final double latitude) {
+ return _set(id, location -> location.setLatitude(latitude));
+ }
+
+ @NonNull
+ @Transactional
+ public LocationDto longitude(final long id, final double longitude) {
+ return _set(id, location -> location.setLongitude(longitude));
+ }
+
+ @NonNull
+ @Transactional
+ public LocationDto purchase(final long id, final Long seriesId) {
+ return _setSeries(id, seriesId, Location::setPurchase);
+ }
+
+ @NonNull
+ @Transactional
+ public LocationDto delivery(final long id, final Long seriesId) {
+ return _setSeries(id, seriesId, Location::setDelivery);
+ }
+
+ @NonNull
+ @Transactional
+ public LocationDto produce(final long id, final Long seriesId) {
+ return _setSeries(id, seriesId, Location::setProduce);
+ }
+
+ @NonNull
+ @Transactional
+ public LocationDto power(final long id, final Long seriesId) {
+ return _setSeries(id, seriesId, Location::setPower);
+ }
+
+ @NonNull
+ private LocationDto _setSeries(final long id, @Nullable final Long seriesId, @NonNull final BiConsumer setter) {
+ final Series series = seriesId == null ? null : seriesRepository.findById(seriesId).orElseThrow(notFound(Series.class, "id", seriesId));
+ return _set(id, location -> setter.accept(location, series));
+ }
+
+ @NonNull
+ private LocationDto _set(final long id, @NonNull final Consumer setter) {
+ final Location location = locationRepository.findById(id).orElseThrow(notFound(Location.class, "id", id));
+ setter.accept(location);
+ return new LocationDto(location);
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/location/NotFoundException.java b/src/main/java/de/ph87/data/location/NotFoundException.java
new file mode 100644
index 0000000..e73301d
--- /dev/null
+++ b/src/main/java/de/ph87/data/location/NotFoundException.java
@@ -0,0 +1,19 @@
+package de.ph87.data.location;
+
+import jakarta.annotation.Nullable;
+import lombok.NonNull;
+
+import java.util.function.Supplier;
+
+public class NotFoundException extends RuntimeException {
+
+ public NotFoundException(@NonNull final Class> clazz, @NonNull final String key, @Nullable final Object value) {
+ super("Not found: %s(%s=%s)".formatted(clazz.getName(), key, String.valueOf(value)));
+ }
+
+ @NonNull
+ public static Supplier notFound(@NonNull final Class> clazz, @NonNull final String key, @Nullable final Object value) {
+ return () -> new NotFoundException(clazz, key, value);
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/log/AbstractEntityLog.java b/src/main/java/de/ph87/data/log/AbstractEntityLog.java
deleted file mode 100644
index d2aff23..0000000
--- a/src/main/java/de/ph87/data/log/AbstractEntityLog.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package de.ph87.data.log;
-
-import jakarta.annotation.Nullable;
-import jakarta.persistence.ElementCollection;
-import jakarta.persistence.OrderColumn;
-import lombok.Getter;
-import lombok.NonNull;
-import lombok.ToString;
-import org.slf4j.Logger;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.List;
-
-@Getter
-public abstract class AbstractEntityLog {
-
- @NonNull
- @OrderColumn
- @ToString.Exclude
- @ElementCollection
- private List log = new ArrayList<>();
-
- public void error(@NonNull final Logger logger, @NonNull final String message) {
- error(logger, message, null);
- }
-
- public void error(@NonNull final Logger logger, @NonNull final String message, @Nullable final Exception e) {
- if (e instanceof RuntimeException) {
- this.log.add(new LogMessage(LogSeverity.ERROR, message + "\n stacktrace:\n" + stacktraceToString(e)));
- logger.error(message, e);
- } else {
- this.log.add(new LogMessage(LogSeverity.ERROR, message));
- logger.error(message);
- }
- }
-
- @NonNull
- public static String stacktraceToString(@NonNull final Exception e) {
- final StringWriter sw = new StringWriter();
- e.printStackTrace(new PrintWriter(sw));
- return sw.toString();
- }
-
-}
diff --git a/src/main/java/de/ph87/data/log/LogMessage.java b/src/main/java/de/ph87/data/log/LogMessage.java
deleted file mode 100644
index aed56a1..0000000
--- a/src/main/java/de/ph87/data/log/LogMessage.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package de.ph87.data.log;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Embeddable;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.NonNull;
-import lombok.ToString;
-
-import java.time.ZonedDateTime;
-
-@Getter
-@ToString
-@Embeddable
-@NoArgsConstructor
-public class LogMessage {
-
- @NonNull
- @Column(nullable = false)
- private ZonedDateTime date;
-
- @NonNull
- @Column(nullable = false)
- @Enumerated(EnumType.STRING)
- private LogSeverity severity;
-
- @NonNull
- @Column(nullable = false)
- private String message;
-
- public LogMessage(@NonNull final LogSeverity severity, @NonNull final String message) {
- this.severity = severity;
- this.message = message;
- }
-
-}
diff --git a/src/main/java/de/ph87/data/log/LogSeverity.java b/src/main/java/de/ph87/data/log/LogSeverity.java
deleted file mode 100644
index 5ffee34..0000000
--- a/src/main/java/de/ph87/data/log/LogSeverity.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.ph87.data.log;
-
-public enum LogSeverity {
- ERROR, WARN, INFO, DEBUG
-}
diff --git a/src/main/java/de/ph87/data/mqtt/MqttInbound.java b/src/main/java/de/ph87/data/mqtt/MqttMessage.java
similarity index 60%
rename from src/main/java/de/ph87/data/mqtt/MqttInbound.java
rename to src/main/java/de/ph87/data/mqtt/MqttMessage.java
index b24daaa..c22d441 100644
--- a/src/main/java/de/ph87/data/mqtt/MqttInbound.java
+++ b/src/main/java/de/ph87/data/mqtt/MqttMessage.java
@@ -6,7 +6,7 @@ import lombok.NonNull;
import java.time.ZonedDateTime;
@Data
-public class MqttInbound {
+public class MqttMessage {
@NonNull
public final ZonedDateTime date = ZonedDateTime.now();
@@ -17,9 +17,9 @@ public class MqttInbound {
@NonNull
public final String payload;
- public MqttInbound(@NonNull final String topic, @NonNull final String payload) {
+ public MqttMessage(@NonNull final String topic, final org.eclipse.paho.client.mqttv3.MqttMessage message) {
this.topic = topic;
- this.payload = payload;
+ this.payload = new String(message.getPayload());
}
}
diff --git a/src/main/java/de/ph87/data/mqtt/MqttService.java b/src/main/java/de/ph87/data/mqtt/MqttService.java
index c7ff5e7..cda973d 100644
--- a/src/main/java/de/ph87/data/mqtt/MqttService.java
+++ b/src/main/java/de/ph87/data/mqtt/MqttService.java
@@ -1,6 +1,5 @@
package de.ph87.data.mqtt;
-import de.ph87.data.topic.TopicReceiver;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
@@ -11,6 +10,7 @@ import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.util.UUID;
@@ -20,12 +20,12 @@ import java.util.UUID;
@RequiredArgsConstructor
public class MqttService {
- private final TopicReceiver topicReceiver;
-
private final Object lock = new Object();
private final MqttConfig config;
+ private final ApplicationEventPublisher applicationEventPublisher;
+
private boolean stop = false;
@PostConstruct
@@ -56,6 +56,9 @@ public class MqttService {
private void connectOnce() throws InterruptedException {
MqttClient client = null;
try {
+ if (config.getUri() == null || config.getUri().isEmpty()) {
+ throw new RuntimeException("MQTT-Config: URI is null or empty");
+ }
final boolean cleanSession = config.getClientId() == null || config.getClientId().isEmpty();
final String clientId = cleanSession ? "Data2025-TMP-" + UUID.randomUUID() : config.getClientId();
final MqttClientPersistence persistence = cleanSession ? new MemoryPersistence() : new MqttDefaultFilePersistence();
@@ -67,7 +70,7 @@ public class MqttService {
options.setConnectionTimeout(5);
options.setKeepAliveInterval(2);
client.connect(options);
- client.subscribe(config.getTopic(), 2, (topic, message) -> topicReceiver.receive(new MqttInbound(topic, new String(message.getPayload()))));
+ client.subscribe(config.getTopic(), 2, (topic, message) -> applicationEventPublisher.publishEvent(new MqttMessage(topic, message)));
log.info("MQTT connected.");
synchronized (lock) {
while (!stop && client.isConnected()) {
diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java
index 239f0da..1a0e588 100644
--- a/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java
+++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java
@@ -53,7 +53,7 @@ public class GraphDto {
public GraphDto(@NonNull final Graph graph) {
this.id = graph.getId();
this.version = graph.getVersion();
- this.series = new SeriesDto(graph.getSeries(), false);
+ this.series = new SeriesDto(graph.getSeries());
this.factor = graph.getFactor();
this.operation = graph.getOperation();
this.series2 = map(graph.getSeries2(), false, SeriesDto::new);
diff --git a/src/main/java/de/ph87/data/series/Series.java b/src/main/java/de/ph87/data/series/Series.java
index bbdd535..ebd35a8 100644
--- a/src/main/java/de/ph87/data/series/Series.java
+++ b/src/main/java/de/ph87/data/series/Series.java
@@ -1,6 +1,5 @@
package de.ph87.data.series;
-import de.ph87.data.log.AbstractEntityLog;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -22,7 +21,7 @@ import java.time.ZonedDateTime;
@Getter
@ToString
@NoArgsConstructor
-public class Series extends AbstractEntityLog {
+public class Series {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -31,6 +30,7 @@ public class Series extends AbstractEntityLog {
@Version
private long version;
+ @Setter
@NonNull
@Column(nullable = false, unique = true)
private String name;
@@ -38,11 +38,11 @@ public class Series extends AbstractEntityLog {
@Setter
@NonNull
@Column(nullable = false)
- private String unit;
+ private String unit = "";
@Setter
@Column(nullable = false)
- private int decimals;
+ private int decimals = 1;
@Column
@Nullable
@@ -58,7 +58,7 @@ public class Series extends AbstractEntityLog {
@Setter
@Column(nullable = false)
- private int expectedEverySeconds = 5;
+ private int seconds = 5;
@Setter
@NonNull
@@ -66,14 +66,18 @@ public class Series extends AbstractEntityLog {
@Enumerated(EnumType.STRING)
private SeriesType type;
- public Series(@NonNull final String name, @NonNull final String unit, final int decimals, final int expectedEverySeconds, @NonNull final SeriesType type) {
+ public Series(@NonNull final String name, @NonNull final String unit, final int decimals, final int seconds, @NonNull final SeriesType type) {
this.name = name;
this.unit = unit;
this.decimals = decimals;
- this.expectedEverySeconds = expectedEverySeconds;
+ this.seconds = seconds;
this.type = type;
}
+ public Series(@NonNull final String name) {
+ this.name = name;
+ }
+
public void update(@NonNull final ZonedDateTime timestamp, final double value) {
if (this.last == null || this.last.isBefore(timestamp)) {
this.last = timestamp;
diff --git a/src/main/java/de/ph87/data/series/SeriesController.java b/src/main/java/de/ph87/data/series/SeriesController.java
index b2bc5e2..be0a207 100644
--- a/src/main/java/de/ph87/data/series/SeriesController.java
+++ b/src/main/java/de/ph87/data/series/SeriesController.java
@@ -1,5 +1,10 @@
package de.ph87.data.series;
+import de.ph87.data.series.point.AllSeriesPointRequest;
+import de.ph87.data.series.point.AllSeriesPointResponse;
+import de.ph87.data.series.point.OneSeriesPointsRequest;
+import de.ph87.data.series.point.OneSeriesPointsResponse;
+import de.ph87.data.series.point.SeriesPointService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CrossOrigin;
@@ -20,11 +25,43 @@ public class SeriesController {
private final SeriesRepository seriesRepository;
- private final SeriesService SeriesService;
+ private final SeriesService seriesService;
- @GetMapping("findAll")
- public List findAll() {
- return seriesRepository.findAllDto();
+ private final SeriesPointService seriesPointService;
+
+ @PostMapping("create")
+ public SeriesDto create() {
+ return seriesService.create();
+ }
+
+ @NonNull
+ @PostMapping("{id}/name")
+ public SeriesDto name(@PathVariable final long id, @RequestBody @NonNull final String name) {
+ return seriesService.modify(id, series -> series.setName(name));
+ }
+
+ @NonNull
+ @PostMapping("{id}/unit")
+ public SeriesDto unit(@PathVariable final long id, @RequestBody @NonNull final String unit) {
+ return seriesService.modify(id, series -> series.setUnit(unit));
+ }
+
+ @NonNull
+ @PostMapping("{id}/decimals")
+ public SeriesDto decimals(@PathVariable final long id, @RequestBody final int decimals) {
+ return seriesService.modify(id, series -> series.setDecimals(decimals));
+ }
+
+ @NonNull
+ @PostMapping("{id}/seconds")
+ public SeriesDto seconds(@PathVariable final long id, @RequestBody final int seconds) {
+ return seriesService.modify(id, series -> series.setSeconds(seconds));
+ }
+
+ @NonNull
+ @PostMapping("{id}/type")
+ public SeriesDto type(@PathVariable final long id, @RequestBody @NonNull final SeriesType type) {
+ return seriesService.modify(id, series -> series.setType(type));
}
@GetMapping("{id}")
@@ -32,15 +69,20 @@ public class SeriesController {
return seriesRepository.getDtoById(id);
}
+ @GetMapping("findAll")
+ public List findAll() {
+ return seriesRepository.findAllDto();
+ }
+
@NonNull
@PostMapping("oneSeriesPoints")
public OneSeriesPointsResponse oneSeriesPoints(@NonNull @RequestBody final OneSeriesPointsRequest request) {
- return SeriesService.oneSeriesPoints(request);
+ return seriesPointService.oneSeriesPoints(request);
}
@PostMapping("allSeriesPoint")
public AllSeriesPointResponse allSeriesPoint(@NonNull @RequestBody final AllSeriesPointRequest request) {
- return SeriesService.allSeriesPoint(request);
+ return seriesPointService.allSeriesPoint(request);
}
}
diff --git a/src/main/java/de/ph87/data/series/SeriesDto.java b/src/main/java/de/ph87/data/series/SeriesDto.java
index 1d2f8e8..33edc35 100644
--- a/src/main/java/de/ph87/data/series/SeriesDto.java
+++ b/src/main/java/de/ph87/data/series/SeriesDto.java
@@ -32,11 +32,15 @@ public class SeriesDto implements IWebsocketMessage {
@Nullable
public final Double value;
- public final int expectedEverySeconds;
+ public final int seconds;
@NonNull
public final SeriesType type;
+ public SeriesDto(@NonNull final Series series) {
+ this(series, false);
+ }
+
public SeriesDto(@NonNull final Series series, final boolean deleted) {
this.id = series.getId();
this.version = series.getVersion();
@@ -47,7 +51,7 @@ public class SeriesDto implements IWebsocketMessage {
this.first = series.getFirst();
this.last = series.getLast();
this.value = series.getValue();
- this.expectedEverySeconds = series.getExpectedEverySeconds();
+ this.seconds = series.getSeconds();
this.type = series.getType();
}
diff --git a/src/main/java/de/ph87/data/series/SeriesRepository.java b/src/main/java/de/ph87/data/series/SeriesRepository.java
index 5cef47c..b6c9450 100644
--- a/src/main/java/de/ph87/data/series/SeriesRepository.java
+++ b/src/main/java/de/ph87/data/series/SeriesRepository.java
@@ -22,4 +22,6 @@ public interface SeriesRepository extends ListCrudRepository {
Optional findFirstByOrderByNameAsc();
+ boolean existsByName(@NonNull String name);
+
}
diff --git a/src/main/java/de/ph87/data/series/SeriesService.java b/src/main/java/de/ph87/data/series/SeriesService.java
index 2407fdc..8d8a508 100644
--- a/src/main/java/de/ph87/data/series/SeriesService.java
+++ b/src/main/java/de/ph87/data/series/SeriesService.java
@@ -1,17 +1,15 @@
package de.ph87.data.series;
-import de.ph87.data.series.data.bool.BoolService;
-import de.ph87.data.series.data.delta.DeltaService;
-import de.ph87.data.series.data.varying.VaryingService;
-import jakarta.annotation.Nullable;
+import de.ph87.data.common.CrudAction;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
-import org.springframework.web.server.ResponseStatusException;
+import org.springframework.transaction.annotation.Transactional;
-import java.util.List;
+import java.util.function.Consumer;
+
+import static de.ph87.data.location.NotFoundException.notFound;
@Slf4j
@Service
@@ -20,49 +18,42 @@ public class SeriesService {
private final SeriesRepository seriesRepository;
- private final BoolService boolService;
-
- private final DeltaService deltaService;
-
- private final VaryingService varyingService;
+ @NonNull
+ @Transactional
+ public SeriesDto create() {
+ final String name = _generateUniqueName();
+ return publish(seriesRepository.save(new Series(name)), CrudAction.CREATED);
+ }
@NonNull
- public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) {
- final Series series1 = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- final List extends SeriesPoint>> points1 = getSeriesPoints(series1, request, request.factor);
- if (request.id2 != null) {
- final Series series2 = seriesRepository.findById(request.id2).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- final List extends SeriesPoint>> points2 = getSeriesPoints(series2, request, request.factor2);
- return new OneSeriesPointsResponse(SeriesPoint.combine(points1, points2, request.operation));
+ private String _generateUniqueName() {
+ int index = 0;
+ while (true) {
+ final String name = "series" + index;
+ if (!seriesRepository.existsByName(name)) {
+ return name;
+ }
}
- return new OneSeriesPointsResponse(points1);
}
@NonNull
- public AllSeriesPointResponse allSeriesPoint(@NonNull final AllSeriesPointRequest request) {
- final List seriesPoints = seriesRepository.findAll().stream().map(series -> map(series, request)).toList();
- return new AllSeriesPointResponse(request, seriesPoints);
+ @Transactional
+ SeriesDto modify(final long id, @NonNull Consumer modifier) {
+ final Series series = getById(id);
+ modifier.accept(series);
+ return publish(series, CrudAction.MODIFIED);
}
@NonNull
- private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
- final List extends SeriesPoint>> points = getSeriesPoints(series, request, null);
- final SeriesDto seriesDto = new SeriesDto(series, false);
- final SeriesPoint> point = points.isEmpty() ? null : points.getFirst();
- return new AllSeriesPointResponse.Entry(seriesDto, point);
+ private Series getById(final long id) {
+ return seriesRepository.findById(id).orElseThrow(notFound(Series.class, "id", id));
}
@NonNull
- private List extends SeriesPoint>> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) {
- final List extends SeriesPoint>> points = switch (series.getType()) {
- case BOOL -> boolService.points(series, request);
- case DELTA -> deltaService.points(series, request);
- case VARYING -> varyingService.points(series, request);
- };
- if (factor == null || factor == 1) {
- return points;
- }
- return points.stream().map(p -> p.times(factor)).toList();
+ private SeriesDto publish(@NonNull final Series series, @NonNull final CrudAction action) {
+ final SeriesDto dto = new SeriesDto(series);
+ log.info("{} {}: {}", Series.class.getSimpleName(), action, dto);
+ return dto;
}
}
diff --git a/src/main/java/de/ph87/data/series/data/bool/BoolDto.java b/src/main/java/de/ph87/data/series/data/bool/BoolDto.java
index 5526f7f..192a805 100644
--- a/src/main/java/de/ph87/data/series/data/bool/BoolDto.java
+++ b/src/main/java/de/ph87/data/series/data/bool/BoolDto.java
@@ -24,7 +24,7 @@ public class BoolDto implements IWebsocketMessage {
public final boolean terminated;
public BoolDto(@NonNull final Bool bool) {
- this.series = new SeriesDto(bool.getId().getSeries(), false);
+ this.series = new SeriesDto(bool.getId().getSeries());
this.date = bool.getId().getDate();
this.end = bool.getEnd();
this.state = bool.isState();
diff --git a/src/main/java/de/ph87/data/series/data/bool/BoolPoint.java b/src/main/java/de/ph87/data/series/data/bool/BoolPoint.java
index d285117..94e5df6 100644
--- a/src/main/java/de/ph87/data/series/data/bool/BoolPoint.java
+++ b/src/main/java/de/ph87/data/series/data/bool/BoolPoint.java
@@ -2,7 +2,7 @@ package de.ph87.data.series.data.bool;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
-import de.ph87.data.series.SeriesPoint;
+import de.ph87.data.series.point.SeriesPoint;
import lombok.Data;
import lombok.NonNull;
import tools.jackson.core.JsonGenerator;
diff --git a/src/main/java/de/ph87/data/series/data/bool/BoolService.java b/src/main/java/de/ph87/data/series/data/bool/BoolService.java
index afcd87b..c7885f3 100644
--- a/src/main/java/de/ph87/data/series/data/bool/BoolService.java
+++ b/src/main/java/de/ph87/data/series/data/bool/BoolService.java
@@ -1,6 +1,6 @@
package de.ph87.data.series.data.bool;
-import de.ph87.data.series.ISeriesPointRequest;
+import de.ph87.data.series.point.ISeriesPointRequest;
import de.ph87.data.series.Series;
import de.ph87.data.series.data.DataId;
import lombok.NonNull;
@@ -38,15 +38,15 @@ public class BoolService {
.peek(
existing -> {
if (existing.isState() != state) {
- id.getSeries().error(log, "Differing states: received=(begin=%s, end=%s, state=%s, terminated=%s), existing=%s".formatted(begin, end, state, terminated, existing));
+ log.error("Differing states: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing);
return;
}
if (existing.getEnd().isAfter(end)) {
- id.getSeries().error(log, "End ran backwards: received=(begin=%s, end=%s, state=%s, terminated=%s), existing=%s".formatted(begin, end, state, terminated, existing));
+ log.error("End ran backwards: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing);
return;
}
if (existing.isTerminated() && (!terminated || !existing.getEnd().equals(end))) {
- id.getSeries().error(log, "Already terminated: received=(begin=%s, end=%s, state=%s, terminated=%s), existing=%s".formatted(begin, end, state, terminated, existing));
+ log.error("Already terminated: received=(begin={}, end={}, state={}, terminated={}), existing={}", begin, end, state, terminated, existing);
return;
}
existing.setEnd(end);
diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java b/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java
index 098133b..d2cc331 100644
--- a/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java
+++ b/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java
@@ -2,7 +2,7 @@ package de.ph87.data.series.data.delta;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
-import de.ph87.data.series.SeriesPoint;
+import de.ph87.data.series.point.SeriesPoint;
import lombok.Data;
import lombok.NonNull;
import tools.jackson.core.JsonGenerator;
diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaService.java b/src/main/java/de/ph87/data/series/data/delta/DeltaService.java
index 7554f59..52d4b39 100644
--- a/src/main/java/de/ph87/data/series/data/delta/DeltaService.java
+++ b/src/main/java/de/ph87/data/series/data/delta/DeltaService.java
@@ -1,6 +1,6 @@
package de.ph87.data.series.data.delta;
-import de.ph87.data.series.ISeriesPointRequest;
+import de.ph87.data.series.point.ISeriesPointRequest;
import de.ph87.data.series.Series;
import de.ph87.data.series.data.Interval;
import de.ph87.data.series.data.delta.meter.Meter;
diff --git a/src/main/java/de/ph87/data/series/data/delta/meter/MeterDto.java b/src/main/java/de/ph87/data/series/data/delta/meter/MeterDto.java
index 6084880..bf1955c 100644
--- a/src/main/java/de/ph87/data/series/data/delta/meter/MeterDto.java
+++ b/src/main/java/de/ph87/data/series/data/delta/meter/MeterDto.java
@@ -22,7 +22,7 @@ public class MeterDto {
public MeterDto(@NonNull final Meter meter) {
this.id = meter.getId();
- this.series = new SeriesDto(meter.getSeries(), false);
+ this.series = new SeriesDto(meter.getSeries());
this.number = meter.getNumber();
this.first = meter.getFirst();
}
diff --git a/src/main/java/de/ph87/data/series/data/varying/VaryingDto.java b/src/main/java/de/ph87/data/series/data/varying/VaryingDto.java
index 4ed1bbf..1017cbf 100644
--- a/src/main/java/de/ph87/data/series/data/varying/VaryingDto.java
+++ b/src/main/java/de/ph87/data/series/data/varying/VaryingDto.java
@@ -31,7 +31,7 @@ public abstract class VaryingDto implements IWebsocketMessage {
public final Interval interval;
protected VaryingDto(@NonNull final Varying varying, @NonNull final Interval interval) {
- this.series = new SeriesDto(varying.getId().getSeries(), false);
+ this.series = new SeriesDto(varying.getId().getSeries());
this.date = varying.getId().getDate();
this.min = varying.getMin();
this.max = varying.getMax();
diff --git a/src/main/java/de/ph87/data/series/data/varying/VaryingPoint.java b/src/main/java/de/ph87/data/series/data/varying/VaryingPoint.java
index 409fe89..90978a4 100644
--- a/src/main/java/de/ph87/data/series/data/varying/VaryingPoint.java
+++ b/src/main/java/de/ph87/data/series/data/varying/VaryingPoint.java
@@ -2,7 +2,7 @@ package de.ph87.data.series.data.varying;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
-import de.ph87.data.series.SeriesPoint;
+import de.ph87.data.series.point.SeriesPoint;
import lombok.Data;
import lombok.NonNull;
import tools.jackson.core.JsonGenerator;
diff --git a/src/main/java/de/ph87/data/series/data/varying/VaryingService.java b/src/main/java/de/ph87/data/series/data/varying/VaryingService.java
index 0d92874..72ab50b 100644
--- a/src/main/java/de/ph87/data/series/data/varying/VaryingService.java
+++ b/src/main/java/de/ph87/data/series/data/varying/VaryingService.java
@@ -1,6 +1,6 @@
package de.ph87.data.series.data.varying;
-import de.ph87.data.series.ISeriesPointRequest;
+import de.ph87.data.series.point.ISeriesPointRequest;
import de.ph87.data.series.Series;
import de.ph87.data.series.data.DataId;
import de.ph87.data.series.data.Interval;
diff --git a/src/main/java/de/ph87/data/series/AllSeriesPointRequest.java b/src/main/java/de/ph87/data/series/point/AllSeriesPointRequest.java
similarity index 95%
rename from src/main/java/de/ph87/data/series/AllSeriesPointRequest.java
rename to src/main/java/de/ph87/data/series/point/AllSeriesPointRequest.java
index f6ed669..d59e7f3 100644
--- a/src/main/java/de/ph87/data/series/AllSeriesPointRequest.java
+++ b/src/main/java/de/ph87/data/series/point/AllSeriesPointRequest.java
@@ -1,4 +1,4 @@
-package de.ph87.data.series;
+package de.ph87.data.series.point;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.ph87.data.series.data.Interval;
diff --git a/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java b/src/main/java/de/ph87/data/series/point/AllSeriesPointResponse.java
similarity index 84%
rename from src/main/java/de/ph87/data/series/AllSeriesPointResponse.java
rename to src/main/java/de/ph87/data/series/point/AllSeriesPointResponse.java
index 4fd5662..72aec2b 100644
--- a/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java
+++ b/src/main/java/de/ph87/data/series/point/AllSeriesPointResponse.java
@@ -1,5 +1,6 @@
-package de.ph87.data.series;
+package de.ph87.data.series.point;
+import de.ph87.data.series.SeriesDto;
import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NonNull;
diff --git a/src/main/java/de/ph87/data/series/ISeriesPointRequest.java b/src/main/java/de/ph87/data/series/point/ISeriesPointRequest.java
similarity index 88%
rename from src/main/java/de/ph87/data/series/ISeriesPointRequest.java
rename to src/main/java/de/ph87/data/series/point/ISeriesPointRequest.java
index 5a884f0..7da4fcd 100644
--- a/src/main/java/de/ph87/data/series/ISeriesPointRequest.java
+++ b/src/main/java/de/ph87/data/series/point/ISeriesPointRequest.java
@@ -1,4 +1,4 @@
-package de.ph87.data.series;
+package de.ph87.data.series.point;
import de.ph87.data.series.data.Interval;
import lombok.NonNull;
diff --git a/src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java b/src/main/java/de/ph87/data/series/point/OneSeriesPointsRequest.java
similarity index 97%
rename from src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java
rename to src/main/java/de/ph87/data/series/point/OneSeriesPointsRequest.java
index bb009be..0c1971e 100644
--- a/src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java
+++ b/src/main/java/de/ph87/data/series/point/OneSeriesPointsRequest.java
@@ -1,4 +1,4 @@
-package de.ph87.data.series;
+package de.ph87.data.series.point;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
diff --git a/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java b/src/main/java/de/ph87/data/series/point/OneSeriesPointsResponse.java
similarity index 88%
rename from src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java
rename to src/main/java/de/ph87/data/series/point/OneSeriesPointsResponse.java
index ba1f8c7..9249188 100644
--- a/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java
+++ b/src/main/java/de/ph87/data/series/point/OneSeriesPointsResponse.java
@@ -1,4 +1,4 @@
-package de.ph87.data.series;
+package de.ph87.data.series.point;
import tools.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
diff --git a/src/main/java/de/ph87/data/series/OneSeriesPointsResponseSerializer.java b/src/main/java/de/ph87/data/series/point/OneSeriesPointsResponseSerializer.java
similarity index 94%
rename from src/main/java/de/ph87/data/series/OneSeriesPointsResponseSerializer.java
rename to src/main/java/de/ph87/data/series/point/OneSeriesPointsResponseSerializer.java
index a9917c3..e398e5f 100644
--- a/src/main/java/de/ph87/data/series/OneSeriesPointsResponseSerializer.java
+++ b/src/main/java/de/ph87/data/series/point/OneSeriesPointsResponseSerializer.java
@@ -1,4 +1,4 @@
-package de.ph87.data.series;
+package de.ph87.data.series.point;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
diff --git a/src/main/java/de/ph87/data/series/SeriesPoint.java b/src/main/java/de/ph87/data/series/point/SeriesPoint.java
similarity index 98%
rename from src/main/java/de/ph87/data/series/SeriesPoint.java
rename to src/main/java/de/ph87/data/series/point/SeriesPoint.java
index 5cfbdda..759dd10 100644
--- a/src/main/java/de/ph87/data/series/SeriesPoint.java
+++ b/src/main/java/de/ph87/data/series/point/SeriesPoint.java
@@ -1,4 +1,4 @@
-package de.ph87.data.series;
+package de.ph87.data.series.point;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
diff --git a/src/main/java/de/ph87/data/series/point/SeriesPointService.java b/src/main/java/de/ph87/data/series/point/SeriesPointService.java
new file mode 100644
index 0000000..2d3311d
--- /dev/null
+++ b/src/main/java/de/ph87/data/series/point/SeriesPointService.java
@@ -0,0 +1,71 @@
+package de.ph87.data.series.point;
+
+import de.ph87.data.series.Series;
+import de.ph87.data.series.SeriesDto;
+import de.ph87.data.series.SeriesRepository;
+import de.ph87.data.series.data.bool.BoolService;
+import de.ph87.data.series.data.delta.DeltaService;
+import de.ph87.data.series.data.varying.VaryingService;
+import jakarta.annotation.Nullable;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SeriesPointService {
+
+ private final SeriesRepository seriesRepository;
+
+ private final BoolService boolService;
+
+ private final DeltaService deltaService;
+
+ private final VaryingService varyingService;
+
+ @NonNull
+ public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) {
+ final Series series1 = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ final List extends SeriesPoint>> points1 = getSeriesPoints(series1, request, request.factor);
+ if (request.id2 != null) {
+ final Series series2 = seriesRepository.findById(request.id2).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ final List extends SeriesPoint>> points2 = getSeriesPoints(series2, request, request.factor2);
+ return new OneSeriesPointsResponse(SeriesPoint.combine(points1, points2, request.operation));
+ }
+ return new OneSeriesPointsResponse(points1);
+ }
+
+ @NonNull
+ public AllSeriesPointResponse allSeriesPoint(@NonNull final AllSeriesPointRequest request) {
+ final List seriesPoints = seriesRepository.findAll().stream().map(series -> map(series, request)).toList();
+ return new AllSeriesPointResponse(request, seriesPoints);
+ }
+
+ @NonNull
+ private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
+ final List extends SeriesPoint>> points = getSeriesPoints(series, request, null);
+ final SeriesDto seriesDto = new SeriesDto(series);
+ final SeriesPoint> point = points.isEmpty() ? null : points.getFirst();
+ return new AllSeriesPointResponse.Entry(seriesDto, point);
+ }
+
+ @NonNull
+ private List extends SeriesPoint>> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) {
+ final List extends SeriesPoint>> points = switch (series.getType()) {
+ case BOOL -> boolService.points(series, request);
+ case DELTA -> deltaService.points(series, request);
+ case VARYING -> varyingService.points(series, request);
+ };
+ if (factor == null || factor == 1) {
+ return points;
+ }
+ return points.stream().map(p -> p.times(factor)).toList();
+ }
+
+}
diff --git a/src/main/java/de/ph87/data/topic/TimestampType.java b/src/main/java/de/ph87/data/topic/TimestampType.java
deleted file mode 100644
index d2db4f7..0000000
--- a/src/main/java/de/ph87/data/topic/TimestampType.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.ph87.data.topic;
-
-public enum TimestampType {
- EPOCH_MILLISECONDS,
- EPOCH_SECONDS,
- ISO_LOCAL_DATE_TIME,
-}
diff --git a/src/main/java/de/ph87/data/topic/Topic.java b/src/main/java/de/ph87/data/topic/Topic.java
index 7cd271c..0b6bd5e 100644
--- a/src/main/java/de/ph87/data/topic/Topic.java
+++ b/src/main/java/de/ph87/data/topic/Topic.java
@@ -1,34 +1,21 @@
package de.ph87.data.topic;
-import de.ph87.data.log.AbstractEntityLog;
-import de.ph87.data.topic.query.TopicQuery;
-import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
-import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
-import jakarta.persistence.Lob;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
-import lombok.Setter;
import lombok.ToString;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.List;
-
@Entity
@Getter
@ToString
@NoArgsConstructor
-public class Topic extends AbstractEntityLog {
+public class Topic {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -41,76 +28,4 @@ public class Topic extends AbstractEntityLog {
@Column(nullable = false, unique = true)
private String name;
- @Setter
- @Column(nullable = false)
- private boolean enabled = true;
-
- @NonNull
- @Column(nullable = false)
- private ZonedDateTime first;
-
- @NonNull
- @Column(nullable = false)
- private ZonedDateTime last;
-
- @Column(nullable = false)
- private int count;
-
- @Setter
- @NonNull
- @Enumerated(EnumType.STRING)
- @Column(nullable = false)
- private TimestampType timestampType = TimestampType.EPOCH_SECONDS;
-
- @Setter
- @Column
- @Nullable
- private ZonedDateTime timestampLast = null;
-
- @Setter
- @NonNull
- @Column(nullable = false)
- private String timestampQuery = "";
-
- @Setter
- @NonNull
- @Column(nullable = false)
- private String meterNumberQuery = "";
-
- @Setter
- @NonNull
- @Column(nullable = false)
- private String meterNumberLast = "";
-
- @NonNull
- @ToString.Exclude
- @ElementCollection(fetch = FetchType.EAGER)
- private List queries = new ArrayList<>();
-
- @Lob
- @Setter
- @NonNull
- @ToString.Exclude
- @Column(nullable = false)
- private String error = "";
-
- @Lob
- @NonNull
- @ToString.Exclude
- @Column(nullable = false)
- private String payload = "";
-
- public Topic(@NonNull final String name) {
- this.name = name;
- this.first = ZonedDateTime.now();
- this.last = this.first;
- this.count = 1;
- }
-
- public void update(@NonNull final String payload) {
- this.last = ZonedDateTime.now();
- this.payload = payload;
- this.count++;
- }
-
}
diff --git a/src/main/java/de/ph87/data/topic/TopicController.java b/src/main/java/de/ph87/data/topic/TopicController.java
deleted file mode 100644
index 2fa19d7..0000000
--- a/src/main/java/de/ph87/data/topic/TopicController.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package de.ph87.data.topic;
-
-import lombok.NonNull;
-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.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.List;
-
-@CrossOrigin
-@RestController
-@RequiredArgsConstructor
-@RequestMapping("Topic")
-public class TopicController {
-
- private final TopicService topicService;
-
- private final TopicRepository topicRepository;
-
- @GetMapping("findAll")
- public List findAll() {
- return topicRepository.findAllDto();
- }
-
- @PostMapping("{id}/setEnabled")
- public TopicDto setEnabled(@PathVariable final long id, @RequestBody final boolean enabled) {
- return topicService.setEnabled(id, enabled);
- }
-
- @PostMapping("{id}/setTimestampQuery")
- public TopicDto setTimestampQuery(@PathVariable final long id, @NonNull @RequestBody final String timestampQuery) {
- return topicService.setTimestampQuery(id, timestampQuery);
- }
-
- @PostMapping("{id}/setTimestampType")
- public TopicDto setTimestampType(@PathVariable final long id, @NonNull @RequestBody final String timestampType) {
- return topicService.setTimestampType(id, TimestampType.valueOf(timestampType));
- }
-
-}
diff --git a/src/main/java/de/ph87/data/topic/TopicDto.java b/src/main/java/de/ph87/data/topic/TopicDto.java
index dc62df4..2c233cd 100644
--- a/src/main/java/de/ph87/data/topic/TopicDto.java
+++ b/src/main/java/de/ph87/data/topic/TopicDto.java
@@ -1,14 +1,9 @@
package de.ph87.data.topic;
-import de.ph87.data.topic.query.TopicQueryDto;
import de.ph87.data.websocket.IWebsocketMessage;
-import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NonNull;
-import java.time.ZonedDateTime;
-import java.util.List;
-
@Data
public class TopicDto implements IWebsocketMessage {
@@ -17,55 +12,9 @@ public class TopicDto implements IWebsocketMessage {
@NonNull
public final String name;
- @NonNull
- public final ZonedDateTime first;
-
- @NonNull
- public final ZonedDateTime last;
-
- public final long count;
-
- public final boolean enabled;
-
- @NonNull
- public final TimestampType timestampType;
-
- @NonNull
- public final String timestampQuery;
-
- @Nullable
- public final ZonedDateTime timestampLast;
-
- @NonNull
- public final String meterNumberQuery;
-
- @Nullable
- public final String meterNumberLast;
-
- @NonNull
- public final List queries;
-
- @NonNull
- public final String error;
-
- @NonNull
- public final String payload;
-
public TopicDto(@NonNull final Topic topic) {
this.id = topic.getId();
this.name = topic.getName();
- this.first = topic.getFirst();
- this.last = topic.getLast();
- this.count = topic.getCount();
- this.enabled = topic.isEnabled();
- this.timestampType = topic.getTimestampType();
- this.timestampQuery = topic.getTimestampQuery();
- this.timestampLast = topic.getTimestampLast();
- this.meterNumberQuery = topic.getMeterNumberQuery();
- this.meterNumberLast = topic.getMeterNumberLast();
- this.queries = topic.getQueries().stream().map(TopicQueryDto::new).toList();
- this.error = topic.getError();
- this.payload = topic.getPayload();
}
}
diff --git a/src/main/java/de/ph87/data/topic/TopicReceiver.java b/src/main/java/de/ph87/data/topic/TopicReceiver.java
index ca890f1..59d1f38 100644
--- a/src/main/java/de/ph87/data/topic/TopicReceiver.java
+++ b/src/main/java/de/ph87/data/topic/TopicReceiver.java
@@ -1,206 +1,20 @@
package de.ph87.data.topic;
-import com.jayway.jsonpath.DocumentContext;
-import com.jayway.jsonpath.JsonPath;
-import de.ph87.data.mqtt.MqttInbound;
-import de.ph87.data.series.Series;
-import de.ph87.data.series.SeriesDto;
-import de.ph87.data.series.SeriesType;
-import de.ph87.data.series.data.bool.BoolService;
-import de.ph87.data.series.data.delta.DeltaService;
-import de.ph87.data.series.data.varying.VaryingService;
-import de.ph87.data.topic.query.TopicQuery;
+import de.ph87.data.mqtt.MqttMessage;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class TopicReceiver {
- public static final String SML_METER_NUMBER_RAW = "sml_meter_number_raw:";
+ @EventListener(MqttMessage.class)
+ public void receive(@NonNull final MqttMessage mqttMessage) {
- private final TopicRepository topicRepository;
-
- private final BoolService boolService;
-
- private final DeltaService deltaService;
-
- private final VaryingService varyingService;
-
- private final ApplicationEventPublisher applicationEventPublisher;
-
- private final TopicService topicService;
-
- @Transactional
- public void receive(@NonNull final MqttInbound inbound) {
- final Topic topic = updateOrCreate(inbound.topic, inbound.payload);
- try {
- if (!topic.isEnabled()) {
- log.debug("Topic is not enabled: topic={}", topic);
- return;
- }
- if (topic.getTimestampQuery().isEmpty()) {
- log.debug("Topic timestampQuery is not set: topic={}", topic);
- return;
- }
- if (topic.getQueries().isEmpty()) {
- log.debug("Topic queries not set: topic={}", topic);
- return;
- }
-
- log.debug("Parsing Topic payload: topic={}", topic);
- final DocumentContext json;
- try {
- json = JsonPath.parse(inbound.payload);
- } catch (Exception e) {
- topic.error(log, "Error parsing JSON: %s\n topic=%s\n inbound=%s".formatted(e.toString(), topic, inbound), e);
- return;
- }
-
- log.debug("Executing Topic timestampQuery: topic={}", topic);
- final ZonedDateTime date;
- try {
- date = queryTimestamp(json, topic.getTimestampQuery(), topic.getTimestampType());
- } catch (Exception e) {
- topic.error(log, "Error executing Topic timestampQuery: %s\n topic=%s\n inbound=%s".formatted(e.toString(), topic, inbound), e);
- return;
- }
-
- topic.setTimestampLast(date);
- topic.getQueries().forEach(query -> query(topic, inbound, json, date, query));
- } finally {
- topicService.publish(topic);
- }
- }
-
- private void query(@NonNull final Topic topic, @NonNull final MqttInbound inbound, @NonNull final DocumentContext json, @NonNull final ZonedDateTime date, @NonNull final TopicQuery query) {
- log.debug("Executing TopicQuery: topicQuery={}", query);
- try {
- final Series series = query.getSeries();
- if (series == null) {
- log.debug("TopicQuery Series not set: topic={}", topic);
- return;
- }
- if (query.getValueQuery().isEmpty()) {
- log.debug("TopicQuery valueQuery not set: topic={}", topic);
- return;
- }
- if (series.getType() == SeriesType.BOOL) {
- if (query.getBeginQuery().isEmpty()) {
- log.debug("TopicQuery beginQuery not set: topic={}", topic);
- return;
- }
- if (query.getTerminatedQuery().isEmpty()) {
- log.debug("TopicQuery terminatedQuery not set: topic={}", topic);
- return;
- }
- }
- if (series.getType() == SeriesType.DELTA) {
- if (topic.getMeterNumberQuery().isEmpty()) {
- log.debug("TopicQuery meterNumberQuery not set: topic={}", topic);
- return;
- }
- }
-
- final Object valueRaw = json.read(query.getValueQuery());
- queryValue(valueRaw).ifPresentOrElse(v -> {
- final double value = query.getFunction().apply(v) * query.getFactor();
- series.update(date, value);
- applicationEventPublisher.publishEvent(new SeriesDto(series, false));
- switch (series.getType()) {
- case BOOL -> {
- final ZonedDateTime begin = queryTimestamp(json, query.getBeginQuery(), topic.getTimestampType());
- final boolean terminated = queryBoolean(json, query.getTerminatedQuery());
- boolService.write(series, begin, date, value > 0, terminated);
- }
- case DELTA -> {
- final String meterNumber = queryMeterNumber(topic, json);
- topic.setMeterNumberLast(meterNumber);
- deltaService.write(series, meterNumber, date, value);
- }
- case VARYING -> varyingService.write(series, date, value);
- }
- }, () -> topic.error(log, "Failed to parse value: %s".formatted(valueRaw)));
- } catch (Exception e) {
- topic.error(log, "Error executing TopicQuery: %s\n topic=%s\n query=%s\n inbound=%s".formatted(e.toString(), topic, query, inbound), e);
- }
- }
-
- @NonNull
- private static String queryMeterNumber(@NonNull final Topic topic, @NonNull final DocumentContext json) {
- final String query = topic.getMeterNumberQuery();
- if (query.startsWith(SML_METER_NUMBER_RAW)) {
- final String field = query.substring(SML_METER_NUMBER_RAW.length());
- final String raw = json.read(field, String.class);
- if (raw.isEmpty()) {
- throw new NumberFormatException("Cannot parse Meter number: No Hex-chars read.");
- }
- if (raw.length() % 2 != 0) {
- throw new NumberFormatException("Cannot parse Meter number: Hex-char count must be multiple of 2.");
- }
- final int length = Integer.parseInt(raw.substring(0, 2), 16);
- if (raw.length() != length * 2) {
- throw new NumberFormatException("Cannot parse Meter number: Invalid length");
- }
- final int type = Integer.parseInt(raw.substring(2, 4), 16);
- final String name = "" + (char) Integer.parseInt(raw.substring(4, 6), 16) + (char) Integer.parseInt(raw.substring(6, 8), 16) + (char) Integer.parseInt(raw.substring(8, 10), 16);
- final int number = Integer.parseInt(raw.substring(10), 16);
- return "%d%s%s".formatted(type, name, number);
- } else if (query.startsWith("\"") && query.endsWith("\"")) {
- return query.substring(1, query.length() - 1);
- }
- return json.read(query, String.class);
- }
-
- private static boolean queryBoolean(@NonNull final DocumentContext json, @NonNull final String terminatedQuery) {
- if ("true".equals(terminatedQuery)) {
- return true;
- }
- if ("false".equals(terminatedQuery)) {
- return false;
- }
- return json.read(terminatedQuery, Boolean.class);
- }
-
- private static Optional queryValue(final Object valueRaw) {
- if (valueRaw instanceof final Double n) {
- return Optional.of(n);
- } else if (valueRaw instanceof final Integer n) {
- return Optional.of((double) n);
- } else if (valueRaw instanceof final Long n) {
- return Optional.of((double) n);
- } else if (valueRaw instanceof final Boolean b) {
- return Optional.of(b ? 1.0 : 0.0);
- }
- return Optional.empty();
- }
-
- @NonNull
- private Topic updateOrCreate(@NonNull final String name, @NonNull final String payload) {
- return topicRepository.findByName(name).stream().peek(topic -> topic.update(payload)).findFirst().orElseGet(() -> topicRepository.save(new Topic(name)));
- }
-
- @NonNull
- private static ZonedDateTime queryTimestamp(@NonNull final DocumentContext json, @NonNull final String query, @NonNull final TimestampType type) {
- if ("now".equals(query) || "timestamp".equals(query)) {
- return ZonedDateTime.now();
- }
- return switch (type) {
- case TimestampType.EPOCH_SECONDS -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(json.read(query, Long.class)), ZoneId.systemDefault());
- case TimestampType.EPOCH_MILLISECONDS -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(json.read(query, Long.class)), ZoneId.systemDefault());
- case TimestampType.ISO_LOCAL_DATE_TIME -> ZonedDateTime.of(LocalDateTime.parse(json.read(query, String.class)), ZoneId.systemDefault());
- };
}
}
diff --git a/src/main/java/de/ph87/data/topic/TopicService.java b/src/main/java/de/ph87/data/topic/TopicService.java
deleted file mode 100644
index 8659315..0000000
--- a/src/main/java/de/ph87/data/topic/TopicService.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package de.ph87.data.topic;
-
-import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.function.Consumer;
-
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class TopicService {
-
- private final TopicRepository topicRepository;
-
- private final ApplicationEventPublisher applicationEventPublisher;
-
- @Transactional
- public TopicDto setEnabled(final long id, final boolean enabled) {
- return set(id, t -> t.setEnabled(enabled));
- }
-
- @Transactional
- public TopicDto setTimestampQuery(final long id, @NonNull final String timestampQuery) {
- return set(id, t -> t.setTimestampQuery(timestampQuery));
- }
-
- @Transactional
- public TopicDto setTimestampType(final long id, @NonNull final TimestampType timestampType) {
- return set(id, t -> t.setTimestampType(timestampType));
- }
-
- @NonNull
- private TopicDto set(final long id, @NonNull final Consumer modifier) {
- final Topic topic = topicRepository.findById(id).orElseThrow();
- modifier.accept(topic);
- log.info("Topic CHANGED: {}", topic);
- return publish(topic);
- }
-
- @NonNull
- public TopicDto publish(@NonNull final Topic topic) {
- final TopicDto dto = new TopicDto(topic);
- applicationEventPublisher.publishEvent(dto);
- return dto;
- }
-
-}
diff --git a/src/main/java/de/ph87/data/topic/query/TopicQuery.java b/src/main/java/de/ph87/data/topic/query/TopicQuery.java
deleted file mode 100644
index af72ae7..0000000
--- a/src/main/java/de/ph87/data/topic/query/TopicQuery.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package de.ph87.data.topic.query;
-
-import de.ph87.data.series.Series;
-import jakarta.annotation.Nullable;
-import jakarta.persistence.Column;
-import jakarta.persistence.Embeddable;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.ManyToOne;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.NonNull;
-import lombok.ToString;
-
-@Getter
-@ToString
-@Embeddable
-@NoArgsConstructor
-public class TopicQuery {
-
- @Nullable
- @ManyToOne
- private Series series;
-
- @NonNull
- @Column(nullable = false)
- private String valueQuery = "";
-
- @NonNull
- @Column(nullable = false)
- private String beginQuery = "";
-
- @NonNull
- @Column(nullable = false)
- private String terminatedQuery = "";
-
- @NonNull
- @Column(nullable = false)
- @Enumerated(EnumType.STRING)
- private TopicQuery.Function function = Function.NONE;
-
- @Column(nullable = false)
- private double factor;
-
- public TopicQuery(
- @Nullable final Series series,
- @NonNull final String valueQuery
- ) {
- this(series, valueQuery, "", "");
- }
-
- public TopicQuery(
- @Nullable final Series series,
- @NonNull final String valueQuery,
- final double factor
- ) {
- this(series, valueQuery, factor, Function.NONE, "", "");
- }
-
- public TopicQuery(
- @Nullable final Series series,
- @NonNull final String valueQuery,
- final double factor,
- @NonNull final TopicQuery.Function function
- ) {
- this(series, valueQuery, factor, function, "", "");
- }
-
- public TopicQuery(
- @Nullable final Series series,
- @NonNull final String valueQuery,
- @NonNull final String beginQuery,
- @NonNull final String terminatedQuery
- ) {
- this(series, valueQuery, 1, Function.NONE, beginQuery, terminatedQuery);
- }
-
- public TopicQuery(
- @Nullable final Series series,
- @NonNull final String valueQuery,
- final double factor,
- @NonNull final TopicQuery.Function function,
- @NonNull final String beginQuery,
- @NonNull final String terminatedQuery
- ) {
- this.series = series;
- this.valueQuery = valueQuery;
- this.beginQuery = beginQuery;
- this.terminatedQuery = terminatedQuery;
- this.function = function;
- this.factor = factor;
- }
-
- public enum Function {
- NONE(v -> v),
- ONLY_POSITIVE(v -> v > 0 ? v : 0),
- ONLY_NEGATIVE_BUT_NEGATE(v -> v < 0 ? -v : 0),
- ;
-
- private final java.util.function.Function function;
-
- Function(@NonNull java.util.function.Function function) {
- this.function = function;
- }
-
- public double apply(final double value) {
- return function.apply(value);
- }
-
- }
-
-}
diff --git a/src/main/java/de/ph87/data/topic/query/TopicQueryDto.java b/src/main/java/de/ph87/data/topic/query/TopicQueryDto.java
deleted file mode 100644
index d36f7e7..0000000
--- a/src/main/java/de/ph87/data/topic/query/TopicQueryDto.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package de.ph87.data.topic.query;
-
-import de.ph87.data.series.SeriesDto;
-import jakarta.annotation.Nullable;
-import lombok.Getter;
-import lombok.NonNull;
-import lombok.ToString;
-
-import static de.ph87.data.Helpers.map;
-
-@Getter
-@ToString
-public class TopicQueryDto {
-
- @Nullable
- public final SeriesDto series;
-
- @NonNull
- public final String valueQuery;
-
- @NonNull
- public final String beginQuery;
-
- @NonNull
- public final String terminatedQuery;
-
- @NonNull
- public final TopicQuery.Function function;
-
- public final double factor;
-
- public TopicQueryDto(@NonNull final TopicQuery topicQuery) {
- this.series = map(topicQuery.getSeries(), series -> new SeriesDto(series, false));
- this.valueQuery = topicQuery.getValueQuery();
- this.beginQuery = topicQuery.getBeginQuery();
- this.terminatedQuery = topicQuery.getTerminatedQuery();
- this.function = topicQuery.getFunction();
- this.factor = topicQuery.getFactor();
- }
-
-}