back to the roots: removed config, removed log, reduced Topic
This commit is contained in:
parent
479b5ff76e
commit
278fe60906
@ -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
|
||||
|
||||
@ -9,6 +9,7 @@ insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
|
||||
61
src/main/angular/package-lock.json
generated
61
src/main/angular/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1 +1,13 @@
|
||||
<div class="MainMenu NoUserSelect">
|
||||
<div class="MainMenuBar">
|
||||
<div class="MainMenuButton" (click)="showDrawer = !showDrawer">
|
||||
<fa-icon [icon]="faBars"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="MainMenuDrawer NoUserSelect" [hidden]="!showDrawer">
|
||||
<div class="MainMenuItem" routerLink="Location" routerLinkActive="MainMenuItemActive">Orte</div>
|
||||
</div>
|
||||
|
||||
<router-outlet/>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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'},
|
||||
];
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
@if (location) {
|
||||
<app-text [initial]="location.name" (onChange)="locationService.name(location, $event, update)"></app-text>
|
||||
<app-number [initial]="location.latitude" (onChange)="locationService.latitude(location, $event, update)" unit="°"></app-number>
|
||||
<app-number [initial]="location.longitude" (onChange)="locationService.longitude(location, $event, update)" unit="°"></app-number>
|
||||
<app-series-select [initial]="location.purchase" (onChange)="locationService.purchase(location, $event, update)" [list]="filterEnergy()"></app-series-select>
|
||||
<app-series-select [initial]="location.delivery" (onChange)="locationService.delivery(location, $event, update)" [list]="filterEnergy()"></app-series-select>
|
||||
<app-series-select [initial]="location.produce" (onChange)="locationService.produce(location, $event, update)" [list]="filterEnergy()"></app-series-select>
|
||||
<app-series-select [initial]="location.power" (onChange)="locationService.power(location, $event, update)" [list]="filterPower()"></app-series-select>
|
||||
}
|
||||
56
src/main/angular/src/app/location/detail/location-detail.ts
Normal file
56
src/main/angular/src/app/location/detail/location-detail.ts
Normal file
@ -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');
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="LocationList">
|
||||
<div class="List LocationList NoUserSelect">
|
||||
@for (location of list; track location.id) {
|
||||
<div class="Location">
|
||||
<div class="ListItem Location" routerLink="/Location/{{location.id}}">
|
||||
{{ location.name }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -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',
|
||||
})
|
||||
|
||||
@ -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<Location> {
|
||||
this.getList(['findAll'], next);
|
||||
}
|
||||
|
||||
getById(id: number, next: Next<Location>) {
|
||||
this.getSingle([id, 'byId'], next);
|
||||
}
|
||||
|
||||
name(location: Location, name: string, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'name'], name, next);
|
||||
}
|
||||
|
||||
latitude(location: Location, latitude: number, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'latitude'], latitude, next);
|
||||
}
|
||||
|
||||
longitude(location: Location, longitude: number, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'longitude'], longitude, next);
|
||||
}
|
||||
|
||||
purchase(location: Location, purchase: Series | null, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'purchase'], purchase?.id, next);
|
||||
}
|
||||
|
||||
delivery(location: Location, delivery: Series | null, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'delivery'], delivery?.id, next);
|
||||
}
|
||||
|
||||
produce(location: Location, produce: Series | null, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'produce'], produce?.id, next);
|
||||
}
|
||||
|
||||
power(location: Location, power: Series | null, next?: Next<Location>) {
|
||||
this.postSingle([location.id, 'power'], power?.id, next);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
src/main/angular/src/app/series/Series.ts
Normal file
24
src/main/angular/src/app/series/Series.ts
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
27
src/main/angular/src/app/series/select/series-select.html
Normal file
27
src/main/angular/src/app/series/select/series-select.html
Normal file
@ -0,0 +1,27 @@
|
||||
<div
|
||||
class="container NoUserSelect"
|
||||
[ngClass]="classes()"
|
||||
(mouseenter)="showPen = true"
|
||||
(mouseleave)="showPen = false"
|
||||
>
|
||||
<div class="value" (click)="start()">
|
||||
@if (editing) {
|
||||
<select
|
||||
#input
|
||||
[(ngModel)]="model"
|
||||
(blur)="apply()"
|
||||
(keydown.enter)="apply()"
|
||||
(keydown.escape)="cancel()"
|
||||
>
|
||||
@for (series of list; track series.id) {
|
||||
<option [ngValue]="series">{{ series.name }}</option>
|
||||
}
|
||||
</select>
|
||||
} @else {
|
||||
{{ initial?.name || ' ' }}
|
||||
}
|
||||
</div>
|
||||
@if (editing || showPen) {
|
||||
<fa-icon [icon]="faPen"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
84
src/main/angular/src/app/series/select/series-select.ts
Normal file
84
src/main/angular/src/app/series/select/series-select.ts
Normal file
@ -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<Series | null>();
|
||||
|
||||
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;
|
||||
}
|
||||
18
src/main/angular/src/app/series/series-service.ts
Normal file
18
src/main/angular/src/app/series/series-service.ts
Normal file
@ -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<Series> {
|
||||
|
||||
constructor(api: ApiService, ws: WebsocketService) {
|
||||
super(api, ws, ['Series'], Series.fromJson);
|
||||
}
|
||||
|
||||
findAll(next: Next<Series[]>) {
|
||||
this.getList(['findAll'], next);
|
||||
}
|
||||
|
||||
}
|
||||
28
src/main/angular/src/app/shared/number/number.html
Normal file
28
src/main/angular/src/app/shared/number/number.html
Normal file
@ -0,0 +1,28 @@
|
||||
<div
|
||||
(click)="start()"
|
||||
class="container NoUserSelect"
|
||||
[ngClass]="classes()"
|
||||
(mouseenter)="showPen = true"
|
||||
(mouseleave)="showPen = false"
|
||||
>
|
||||
<div class="value">
|
||||
@if (editing) {
|
||||
<input
|
||||
#input
|
||||
type="number"
|
||||
[(ngModel)]="model"
|
||||
(blur)="blur()"
|
||||
(keydown.enter)="apply()"
|
||||
(keydown.escape)="cancel()"
|
||||
>
|
||||
} @else {
|
||||
{{ initial | number:'0.0-999':locale || ' ' }}{{ unit }}
|
||||
}
|
||||
</div>
|
||||
@if (!editing && showPen) {
|
||||
<fa-icon [icon]="faPen"></fa-icon>
|
||||
}
|
||||
@if (editing) {
|
||||
<fa-icon [icon]="faCancel" (mousedown)="cancel()"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
29
src/main/angular/src/app/shared/number/number.less
Normal file
29
src/main/angular/src/app/shared/number/number.less
Normal file
@ -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;
|
||||
}
|
||||
91
src/main/angular/src/app/shared/number/number.ts
Normal file
91
src/main/angular/src/app/shared/number/number.ts
Normal file
@ -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<number>();
|
||||
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
28
src/main/angular/src/app/shared/text/text.html
Normal file
28
src/main/angular/src/app/shared/text/text.html
Normal file
@ -0,0 +1,28 @@
|
||||
<div
|
||||
(click)="start()"
|
||||
class="container NoUserSelect"
|
||||
[ngClass]="classes()"
|
||||
(mouseenter)="showPen = true"
|
||||
(mouseleave)="showPen = false"
|
||||
>
|
||||
<div class="value">
|
||||
@if (editing) {
|
||||
<input
|
||||
#input
|
||||
type="text"
|
||||
[(ngModel)]="model"
|
||||
(blur)="blur()"
|
||||
(keydown.enter)="apply()"
|
||||
(keydown.escape)="cancel()"
|
||||
>
|
||||
} @else {
|
||||
{{ initial || ' ' }}
|
||||
}
|
||||
</div>
|
||||
@if (!editing && showPen) {
|
||||
<fa-icon [icon]="faPen"></fa-icon>
|
||||
}
|
||||
@if (editing) {
|
||||
<fa-icon [icon]="faCancel" (mousedown)="cancel()"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
29
src/main/angular/src/app/shared/text/text.less
Normal file
29
src/main/angular/src/app/shared/text/text.less
Normal file
@ -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;
|
||||
}
|
||||
83
src/main/angular/src/app/shared/text/text.ts
Normal file
83
src/main/angular/src/app/shared/text/text.ts
Normal file
@ -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<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
5
src/main/java/de/ph87/data/common/CrudAction.java
Normal file
5
src/main/java/de/ph87/data/common/CrudAction.java
Normal file
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.common;
|
||||
|
||||
public enum CrudAction {
|
||||
CREATED, MODIFIED, DELETED
|
||||
}
|
||||
@ -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<Order> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package de.ph87.data.config;
|
||||
|
||||
import org.springframework.data.repository.ListCrudRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ConfigRepository extends ListCrudRepository<Config, Long> {
|
||||
|
||||
Optional<Config> findFirstBy();
|
||||
|
||||
}
|
||||
@ -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<OrderDto> 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<OrderDto> 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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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))));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package de.ph87.data.config;
|
||||
|
||||
public enum Direction {
|
||||
ASC, DESC
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
64
src/main/java/de/ph87/data/location/Location.java
Normal file
64
src/main/java/de/ph87/data/location/Location.java
Normal file
@ -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;
|
||||
|
||||
}
|
||||
80
src/main/java/de/ph87/data/location/LocationController.java
Normal file
80
src/main/java/de/ph87/data/location/LocationController.java
Normal file
@ -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<LocationDto> 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);
|
||||
}
|
||||
|
||||
}
|
||||
47
src/main/java/de/ph87/data/location/LocationDto.java
Normal file
47
src/main/java/de/ph87/data/location/LocationDto.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
20
src/main/java/de/ph87/data/location/LocationRepository.java
Normal file
20
src/main/java/de/ph87/data/location/LocationRepository.java
Normal file
@ -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<Location, Long> {
|
||||
|
||||
Optional<Location> findByName(@NonNull String name);
|
||||
|
||||
@Query("select new de.ph87.data.location.LocationDto(e) from Location e")
|
||||
List<LocationDto> findAllDto();
|
||||
|
||||
@Query("select new de.ph87.data.location.LocationDto(e) from Location e where e.id = :id")
|
||||
Optional<LocationDto> dtoById(long id);
|
||||
|
||||
}
|
||||
93
src/main/java/de/ph87/data/location/LocationService.java
Normal file
93
src/main/java/de/ph87/data/location/LocationService.java
Normal file
@ -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<Location, Series> 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<Location> setter) {
|
||||
final Location location = locationRepository.findById(id).orElseThrow(notFound(Location.class, "id", id));
|
||||
setter.accept(location);
|
||||
return new LocationDto(location);
|
||||
}
|
||||
|
||||
}
|
||||
19
src/main/java/de/ph87/data/location/NotFoundException.java
Normal file
19
src/main/java/de/ph87/data/location/NotFoundException.java
Normal file
@ -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<NotFoundException> notFound(@NonNull final Class<?> clazz, @NonNull final String key, @Nullable final Object value) {
|
||||
return () -> new NotFoundException(clazz, key, value);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<LogMessage> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package de.ph87.data.log;
|
||||
|
||||
public enum LogSeverity {
|
||||
ERROR, WARN, INFO, DEBUG
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<SeriesDto> 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<SeriesDto> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -22,4 +22,6 @@ public interface SeriesRepository extends ListCrudRepository<Series, Long> {
|
||||
|
||||
Optional<Series> findFirstByOrderByNameAsc();
|
||||
|
||||
boolean existsByName(@NonNull String name);
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) {
|
||||
final Series series1 = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
final List<? extends SeriesPoint<?>> points1 = getSeriesPoints(series1, request, request.factor);
|
||||
if (request.id2 != null) {
|
||||
final Series series2 = seriesRepository.findById(request.id2).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
final List<? extends SeriesPoint<?>> points2 = getSeriesPoints(series2, request, request.factor2);
|
||||
return new OneSeriesPointsResponse(SeriesPoint.combine(points1, points2, request.operation));
|
||||
}
|
||||
return new OneSeriesPointsResponse(points1);
|
||||
@Transactional
|
||||
public SeriesDto create() {
|
||||
final String name = _generateUniqueName();
|
||||
return publish(seriesRepository.save(new Series(name)), CrudAction.CREATED);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AllSeriesPointResponse allSeriesPoint(@NonNull final AllSeriesPointRequest request) {
|
||||
final List<AllSeriesPointResponse.Entry> seriesPoints = seriesRepository.findAll().stream().map(series -> map(series, request)).toList();
|
||||
return new AllSeriesPointResponse(request, seriesPoints);
|
||||
private String _generateUniqueName() {
|
||||
int index = 0;
|
||||
while (true) {
|
||||
final String name = "series" + index;
|
||||
if (!seriesRepository.existsByName(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
|
||||
final List<? extends SeriesPoint<?>> points = getSeriesPoints(series, request, null);
|
||||
final SeriesDto seriesDto = new SeriesDto(series, false);
|
||||
final SeriesPoint<?> point = points.isEmpty() ? null : points.getFirst();
|
||||
return new AllSeriesPointResponse.Entry(seriesDto, point);
|
||||
@Transactional
|
||||
SeriesDto modify(final long id, @NonNull Consumer<Series> modifier) {
|
||||
final Series series = getById(id);
|
||||
modifier.accept(series);
|
||||
return publish(series, CrudAction.MODIFIED);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<? extends SeriesPoint<?>> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) {
|
||||
final List<? extends SeriesPoint<?>> points = switch (series.getType()) {
|
||||
case BOOL -> boolService.points(series, request);
|
||||
case DELTA -> deltaService.points(series, request);
|
||||
case VARYING -> varyingService.points(series, request);
|
||||
};
|
||||
if (factor == null || factor == 1) {
|
||||
return points;
|
||||
private Series getById(final long id) {
|
||||
return seriesRepository.findById(id).orElseThrow(notFound(Series.class, "id", id));
|
||||
}
|
||||
return points.stream().map(p -> p.times(factor)).toList();
|
||||
|
||||
@NonNull
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,4 +1,4 @@
|
||||
package de.ph87.data.series;
|
||||
package de.ph87.data.series.point;
|
||||
|
||||
import tools.jackson.databind.annotation.JsonSerialize;
|
||||
import lombok.Data;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -0,0 +1,71 @@
|
||||
package de.ph87.data.series.point;
|
||||
|
||||
import de.ph87.data.series.Series;
|
||||
import de.ph87.data.series.SeriesDto;
|
||||
import de.ph87.data.series.SeriesRepository;
|
||||
import de.ph87.data.series.data.bool.BoolService;
|
||||
import de.ph87.data.series.data.delta.DeltaService;
|
||||
import de.ph87.data.series.data.varying.VaryingService;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SeriesPointService {
|
||||
|
||||
private final SeriesRepository seriesRepository;
|
||||
|
||||
private final BoolService boolService;
|
||||
|
||||
private final DeltaService deltaService;
|
||||
|
||||
private final VaryingService varyingService;
|
||||
|
||||
@NonNull
|
||||
public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) {
|
||||
final Series series1 = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
final List<? extends SeriesPoint<?>> points1 = getSeriesPoints(series1, request, request.factor);
|
||||
if (request.id2 != null) {
|
||||
final Series series2 = seriesRepository.findById(request.id2).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
final List<? extends SeriesPoint<?>> points2 = getSeriesPoints(series2, request, request.factor2);
|
||||
return new OneSeriesPointsResponse(SeriesPoint.combine(points1, points2, request.operation));
|
||||
}
|
||||
return new OneSeriesPointsResponse(points1);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AllSeriesPointResponse allSeriesPoint(@NonNull final AllSeriesPointRequest request) {
|
||||
final List<AllSeriesPointResponse.Entry> seriesPoints = seriesRepository.findAll().stream().map(series -> map(series, request)).toList();
|
||||
return new AllSeriesPointResponse(request, seriesPoints);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
|
||||
final List<? extends SeriesPoint<?>> points = getSeriesPoints(series, request, null);
|
||||
final SeriesDto seriesDto = new SeriesDto(series);
|
||||
final SeriesPoint<?> point = points.isEmpty() ? null : points.getFirst();
|
||||
return new AllSeriesPointResponse.Entry(seriesDto, point);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<? extends SeriesPoint<?>> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) {
|
||||
final List<? extends SeriesPoint<?>> points = switch (series.getType()) {
|
||||
case BOOL -> boolService.points(series, request);
|
||||
case DELTA -> deltaService.points(series, request);
|
||||
case VARYING -> varyingService.points(series, request);
|
||||
};
|
||||
if (factor == null || factor == 1) {
|
||||
return points;
|
||||
}
|
||||
return points.stream().map(p -> p.times(factor)).toList();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package de.ph87.data.topic;
|
||||
|
||||
public enum TimestampType {
|
||||
EPOCH_MILLISECONDS,
|
||||
EPOCH_SECONDS,
|
||||
ISO_LOCAL_DATE_TIME,
|
||||
}
|
||||
@ -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<TopicQuery> 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++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<TopicDto> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<TopicQueryDto> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<Double> 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());
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<Topic> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<Double, Double> function;
|
||||
|
||||
Function(@NonNull java.util.function.Function<Double, Double> function) {
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
public double apply(final double value) {
|
||||
return function.apply(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user