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> 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> 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> 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> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) { - final List> 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> 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> 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> 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> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) { + final List> 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(); - } - -}