angular ui + websocket
This commit is contained in:
parent
297e4b4029
commit
21956c0d28
68
pom.xml
68
pom.xml
@ -35,16 +35,6 @@
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-java8time</artifactId>
|
||||
<version>3.0.4.RELEASE</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
@ -75,11 +65,45 @@
|
||||
<build>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
|
||||
<execution>
|
||||
<id>npm-install</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<workingDirectory>${project.basedir}/src/main/angular</workingDirectory>
|
||||
<executable>npm</executable>
|
||||
<arguments>
|
||||
<argument>install</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
<execution>
|
||||
<id>ng-build</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<workingDirectory>${project.basedir}/src/main/angular</workingDirectory>
|
||||
<executable>ng</executable>
|
||||
<arguments>
|
||||
<argument>build</argument>
|
||||
<argument>--base-href</argument>
|
||||
<argument>/Data/</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
<execution>
|
||||
<id>scp</id>
|
||||
<phase>package</phase>
|
||||
@ -96,8 +120,32 @@
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
</executions>
|
||||
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>target/classes/resources/</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/angular/dist/angular/browser/</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
13
src/main/angular/.editorconfig
Normal file
13
src/main/angular/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
42
src/main/angular/.gitignore
vendored
Normal file
42
src/main/angular/.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
27
src/main/angular/README.md
Normal file
27
src/main/angular/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Angular
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.3.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
130
src/main/angular/angular.json
Normal file
130
src/main/angular/angular.json
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "less",
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:class": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"skipTests": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/angular",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "less",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.less"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kB",
|
||||
"maximumError": "4kB"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "less",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.less"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14611
src/main/angular/package-lock.json
generated
Normal file
14611
src/main/angular/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
src/main/angular/package.json
Normal file
39
src/main/angular/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.2.0",
|
||||
"@angular/common": "^18.2.0",
|
||||
"@angular/compiler": "^18.2.0",
|
||||
"@angular/core": "^18.2.0",
|
||||
"@angular/forms": "^18.2.0",
|
||||
"@angular/platform-browser": "^18.2.0",
|
||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||
"@angular/router": "^18.2.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.10",
|
||||
"@stomp/ng2-stompjs": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^18.2.3",
|
||||
"@angular/cli": "^18.2.3",
|
||||
"@angular/compiler-cli": "^18.2.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.2.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.5.2"
|
||||
}
|
||||
}
|
||||
20
src/main/angular/public/favicon.svg
Normal file
20
src/main/angular/public/favicon.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800px" height="800px" viewBox="0 0 64 64">
|
||||
<path fill="#394240" d="M60,0H4C1.789,0,0,1.789,0,4v56c0,2.211,1.789,4,4,4h56c2.211,0,4-1.789,4-4V4C64,1.789,62.211,0,60,0z
|
||||
M4,2h56c1.104,0,2,0.896,2,2v21.428l-11.409,6.713C49.49,30.832,47.844,30,46,30c-1.8,0-3.41,0.796-4.51,2.051l-15.93-9.803
|
||||
C25.842,21.553,26,20.796,26,20c0-3.314-2.686-6-6-6s-6,2.686-6,6c0,1.296,0.414,2.492,1.112,3.473L2,36.586V4C2,2.896,2.896,2,4,2
|
||||
z M46,32c2.209,0,4,1.791,4,4s-1.791,4-4,4s-4-1.791-4-4S43.791,32,46,32z M16,20c0-2.209,1.791-4,4-4s4,1.791,4,4s-1.791,4-4,4
|
||||
S16,22.209,16,20z M60,62H4c-1.104,0-2-0.896-2-2V39.414l14.526-14.527C17.507,25.586,18.704,26,20,26c1.8,0,3.41-0.795,4.51-2.051
|
||||
l15.93,9.804C40.158,34.447,40,35.205,40,36c0,3.314,2.686,6,6,6s6-2.686,6-6c0-0.752-0.145-1.471-0.397-2.135L62,27.748V60
|
||||
C62,61.104,61.104,62,60,62z"/>
|
||||
<circle fill="#F76D57" cx="46" cy="36" r="4"/>
|
||||
<circle fill="#45AAB8" cx="20" cy="20" r="4"/>
|
||||
<g>
|
||||
<path fill="#F9EBB2" d="M60,2H4C2.896,2,2,2.896,2,4v32.586l13.112-13.113C14.414,22.492,14,21.296,14,20c0-3.314,2.686-6,6-6
|
||||
s6,2.686,6,6c0,0.796-0.158,1.553-0.439,2.248l15.93,9.803C42.59,30.796,44.2,30,46,30c1.844,0,3.49,0.832,4.591,2.141L62,25.428
|
||||
V4C62,2.896,61.104,2,60,2z"/>
|
||||
</g>
|
||||
<path fill="#B4CCB9" d="M52,36c0,3.314-2.686,6-6,6s-6-2.686-6-6c0-0.795,0.158-1.553,0.439-2.247l-15.93-9.804
|
||||
C23.41,25.205,21.8,26,20,26c-1.296,0-2.493-0.414-3.474-1.113L2,39.414V60c0,1.104,0.896,2,2,2h56c1.104,0,2-0.896,2-2V27.748
|
||||
l-10.397,6.117C51.855,34.529,52,35.248,52,36z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
13
src/main/angular/src/app/api/Order.ts
Normal file
13
src/main/angular/src/app/api/Order.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {CrudDirection} from "./crud/CrudDirection";
|
||||
|
||||
export class Order<T> {
|
||||
|
||||
constructor(
|
||||
readonly property: string,
|
||||
readonly compare: (a: T, b: T) => number,
|
||||
public direction: CrudDirection,
|
||||
) {
|
||||
// -
|
||||
}
|
||||
|
||||
}
|
||||
28
src/main/angular/src/app/api/Page.ts
Normal file
28
src/main/angular/src/app/api/Page.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {validateList, validateNumber} from "./validators";
|
||||
import {FromJson} from "./types";
|
||||
|
||||
export class Page<T> {
|
||||
|
||||
static readonly EMPTY: Page<any> = new Page(0, 0, 0, 0, []);
|
||||
|
||||
constructor(
|
||||
readonly size: number,
|
||||
readonly number: number,
|
||||
readonly totalPages: number,
|
||||
readonly totalElements: number,
|
||||
readonly content: T[],
|
||||
) {
|
||||
// -
|
||||
}
|
||||
|
||||
static fromJson<T>(fromJson: FromJson<T>): FromJson<Page<T>> {
|
||||
return (json: any) => new Page<T>(
|
||||
validateNumber(json.page.size),
|
||||
validateNumber(json.page.number),
|
||||
validateNumber(json.page.totalPages),
|
||||
validateNumber(json.page.totalElements),
|
||||
validateList(json.content, fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
65
src/main/angular/src/app/api/Sort.ts
Normal file
65
src/main/angular/src/app/api/Sort.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import {CrudDirection} from "./crud/CrudDirection";
|
||||
|
||||
import {Order} from "./Order";
|
||||
|
||||
export class Sort<T> {
|
||||
|
||||
private list: Order<T>[] = [];
|
||||
|
||||
toggle(key: string, compare: (a: T, b: T) => number) {
|
||||
const index: number = this.indexOf(key);
|
||||
if (index >= 0) {
|
||||
const existing: Order<T> = this.list[index];
|
||||
if (existing.direction === CrudDirection.ASC) {
|
||||
existing.direction = CrudDirection.DESC;
|
||||
} else {
|
||||
this.list.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
this.list.push(new Order(key, compare, CrudDirection.ASC));
|
||||
}
|
||||
}
|
||||
|
||||
compare(a: T, b: T): number {
|
||||
for (const order of this.list) {
|
||||
const result = order.compare(a, b);
|
||||
if (result != 0) {
|
||||
return order.direction === CrudDirection.DESC ? -result : result;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.list.length === 0;
|
||||
}
|
||||
|
||||
indexOf(key: string): number {
|
||||
return this.list.findIndex(o => o.property === key);
|
||||
}
|
||||
|
||||
isActive(key: string): boolean {
|
||||
return this.indexOf(key) >= 0;
|
||||
}
|
||||
|
||||
isAscending(key: string): boolean {
|
||||
return this.isDirection(key, CrudDirection.ASC);
|
||||
}
|
||||
|
||||
isDescending(key: string): boolean {
|
||||
return this.isDirection(key, CrudDirection.DESC);
|
||||
}
|
||||
|
||||
isDirection(key: string, direction: CrudDirection): boolean {
|
||||
const index: number = this.indexOf(key);
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.list[index].direction === direction;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
}
|
||||
75
src/main/angular/src/app/api/api.service.ts
Normal file
75
src/main/angular/src/app/api/api.service.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {map, Subscription} from "rxjs";
|
||||
import {FromJson, getUrl, Next} from "./types";
|
||||
import {Page} from "./Page";
|
||||
import {StompService} from "@stomp/ng2-stompjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
private _websocketError: boolean = false;
|
||||
|
||||
get websocketError(): boolean {
|
||||
return this._websocketError;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly http: HttpClient,
|
||||
private readonly stompService: StompService,
|
||||
) {
|
||||
stompService.connected$.subscribe(() => this._websocketError = false);
|
||||
stompService.webSocketErrors$.subscribe(() => this._websocketError = true);
|
||||
}
|
||||
|
||||
connected(next: Next<void>): Subscription {
|
||||
return this.stompService.connected$.subscribe(_ => next());
|
||||
}
|
||||
|
||||
subscribe<T>(topic: any[], fromJson: FromJson<T>, next: Next<T>): Subscription {
|
||||
console.info("WEBSOCKET SUBSCRIBE", topic)
|
||||
return this.stompService
|
||||
.subscribe(topic.join("/"))
|
||||
.pipe(
|
||||
map(message => message.body),
|
||||
map(b => JSON.parse(b)),
|
||||
map(j => fromJson(j)),
|
||||
)
|
||||
.subscribe(next);
|
||||
}
|
||||
|
||||
getNone<T>(path: any[], next: Next<void> | undefined = undefined): Subscription {
|
||||
return this.http.get<void>(getUrl('http', path)).subscribe(next);
|
||||
}
|
||||
|
||||
getString(path: any[], next: Next<string>): Subscription {
|
||||
return this.http.get(getUrl('http', path), {responseType: "text"}).subscribe(next);
|
||||
}
|
||||
|
||||
getSingle<T>(path: any[], fromJson: FromJson<T>, next: Next<T> | undefined = undefined): Subscription {
|
||||
return this.http.get<any>(getUrl('http', path)).pipe(map(fromJson)).subscribe(next);
|
||||
}
|
||||
|
||||
getList<T>(path: any[], fromJson: FromJson<T>, next: Next<T[]> | undefined = undefined): Subscription {
|
||||
return this.http.get<any[]>(getUrl('http', path)).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||
}
|
||||
|
||||
postNone(path: any[], data: any, next: Next<void> | undefined = undefined): Subscription {
|
||||
return this.http.post<void>(getUrl('http', path), data).subscribe(next);
|
||||
}
|
||||
|
||||
postSingle<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T> | undefined = undefined): Subscription {
|
||||
return this.http.post<any>(getUrl('http', path), data).pipe(map(fromJson)).subscribe(next);
|
||||
}
|
||||
|
||||
postPage<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<Page<T>> | undefined = undefined): Subscription {
|
||||
return this.http.post<any>(getUrl('http', path), data).pipe(map(Page.fromJson(fromJson))).subscribe(next);
|
||||
}
|
||||
|
||||
postList<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T[]> | undefined = undefined): Subscription {
|
||||
return this.http.post<any[]>(getUrl('http', path), data).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||
}
|
||||
|
||||
}
|
||||
21
src/main/angular/src/app/api/series/IValue.ts
Normal file
21
src/main/angular/src/app/api/series/IValue.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export class Display {
|
||||
|
||||
constructor(
|
||||
readonly title: string,
|
||||
readonly color: string,
|
||||
readonly iValue: IValue | null,
|
||||
) {
|
||||
// -
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface IValue {
|
||||
|
||||
readonly date: Date | null;
|
||||
|
||||
readonly value: number | null;
|
||||
|
||||
readonly unit: string | null;
|
||||
|
||||
}
|
||||
40
src/main/angular/src/app/api/series/Series.ts
Normal file
40
src/main/angular/src/app/api/series/Series.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import {Period} from "./consumption/period/Period";
|
||||
import {validateDate, validateNumber, validateString} from "../validators";
|
||||
|
||||
import {IValue} from "./IValue";
|
||||
|
||||
export class Series implements IValue {
|
||||
|
||||
constructor(
|
||||
readonly id: number,
|
||||
readonly name: string,
|
||||
readonly mode: string,
|
||||
readonly unit: string,
|
||||
readonly period: Period | null,
|
||||
readonly lastDate: Date,
|
||||
readonly lastValue: number,
|
||||
) {
|
||||
// -
|
||||
}
|
||||
|
||||
get date(): Date {
|
||||
return this.lastDate;
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this.lastValue;
|
||||
}
|
||||
|
||||
static fromJson(json: any): Series {
|
||||
return new Series(
|
||||
validateNumber(json['id']),
|
||||
validateString(json['name']),
|
||||
validateString(json['mode']),
|
||||
validateString(json['unit']),
|
||||
Period.fromJsonOrNull(json['period']),
|
||||
validateDate(json['lastDate']),
|
||||
validateNumber(json['lastValue']),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
68
src/main/angular/src/app/api/series/Value.ts
Normal file
68
src/main/angular/src/app/api/series/Value.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import {IValue} from "./IValue";
|
||||
|
||||
type GetValue = (v: number) => number;
|
||||
|
||||
type CombineValue = (a: number, b: number) => number;
|
||||
|
||||
function dateMin(a: Date, b: Date): Date {
|
||||
if (a.getTime() < b.getTime()) {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
export class Value implements IValue {
|
||||
|
||||
constructor(
|
||||
readonly date: Date | null,
|
||||
readonly value: number | null,
|
||||
readonly unit: string | null,
|
||||
) {
|
||||
// -
|
||||
}
|
||||
|
||||
static positiveOnly(iValue: IValue | null): Value | null {
|
||||
return this.map(iValue, v => v > 0 ? v : 0);
|
||||
}
|
||||
|
||||
static negativeOnlyToPositive(iValue: IValue | null): Value | null {
|
||||
return this.map(iValue, v => v < 0 ? -v : 0);
|
||||
}
|
||||
|
||||
static map(iValue: IValue | null, mapping: GetValue): Value | null {
|
||||
if (!iValue || !iValue.value) {
|
||||
return null;
|
||||
}
|
||||
const value = mapping(iValue.value);
|
||||
return new Value(iValue.date, value, iValue.unit);
|
||||
}
|
||||
|
||||
static plus(a: IValue | null, b: IValue | null): Value | null {
|
||||
return this.bi(a, b, (a, b) => a + b);
|
||||
}
|
||||
|
||||
static minus(a: IValue | null, b: IValue | null): Value | null {
|
||||
return this.bi(a, b, (a, b) => a - b);
|
||||
}
|
||||
|
||||
static mul(a: IValue | null, b: IValue | null): Value | null {
|
||||
return this.bi(a, b, (a, b) => a * b);
|
||||
}
|
||||
|
||||
static div(a: IValue | null, b: IValue | null): Value | null {
|
||||
return this.bi(a, b, (a, b) => a / b);
|
||||
}
|
||||
|
||||
static bi(a: IValue | null, b: IValue | null, combineValue: CombineValue) {
|
||||
if (!a) {
|
||||
return new Value(null, null, b?.unit || null);
|
||||
}
|
||||
if (!b) {
|
||||
return new Value(null, null, a?.unit || null);
|
||||
}
|
||||
const oldestDate = !a.date || !b.date ? null : dateMin(a.date, b.date);
|
||||
const difference = !a.value || !b.value ? null : combineValue(a.value, b.value);
|
||||
return new Value(oldestDate, difference, a.unit);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import {validateDate, validateNumber, validateString} from "../../../validators";
|
||||
|
||||
export class Period {
|
||||
|
||||
readonly deltaMillis: number;
|
||||
|
||||
readonly deltaValue: number;
|
||||
|
||||
constructor(
|
||||
readonly id: number,
|
||||
readonly name: string,
|
||||
readonly firstDate: Date,
|
||||
readonly firstValue: number,
|
||||
readonly lastDate: Date,
|
||||
readonly lastValue: number,
|
||||
) {
|
||||
this.deltaMillis = lastDate.getTime() - firstDate.getTime();
|
||||
this.deltaValue = lastValue - firstValue;
|
||||
}
|
||||
|
||||
static fromJson(json: any): Period {
|
||||
return new Period(
|
||||
validateNumber(json['id']),
|
||||
validateString(json['name']),
|
||||
validateDate(json['firstDate']),
|
||||
validateNumber(json['firstValue']),
|
||||
validateDate(json['lastDate']),
|
||||
validateNumber(json['lastValue']),
|
||||
);
|
||||
}
|
||||
|
||||
static fromJsonOrNull(json: any): Period | null {
|
||||
if (!json) {
|
||||
return null;
|
||||
}
|
||||
return this.fromJson(json);
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/angular/src/app/api/series/series.service.ts
Normal file
26
src/main/angular/src/app/api/series/series.service.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Series} from "./Series";
|
||||
import {ApiService} from "../api.service";
|
||||
|
||||
type Next<T> = (t: T) => any;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SeriesService {
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
) {
|
||||
// -
|
||||
}
|
||||
|
||||
subscribe(next: Next<Series>) {
|
||||
this.api.subscribe(['Series'], Series.fromJson, next);
|
||||
}
|
||||
|
||||
findAll(next: Next<Series[]>): void {
|
||||
this.api.getList(['Series', 'findAll'], Series.fromJson, next);
|
||||
}
|
||||
|
||||
}
|
||||
9
src/main/angular/src/app/api/types.ts
Normal file
9
src/main/angular/src/app/api/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {environment} from '../../environments/environment';
|
||||
|
||||
export type FromJson<T> = (json: any) => T;
|
||||
|
||||
export type Next<T> = (item: T) => void;
|
||||
|
||||
export function getUrl(protocol: string, path: any[]): string {
|
||||
return protocol + (environment.secure ? 's' : '') + '://' + environment.host + ':' + environment.port + '/' + environment.base + path.join('/');
|
||||
}
|
||||
69
src/main/angular/src/app/api/validators.ts
Normal file
69
src/main/angular/src/app/api/validators.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import {FromJson} from "./types";
|
||||
|
||||
export function validateNumber(json: any): number {
|
||||
if (!(typeof json == "number")) {
|
||||
throw new Error("Not a number: " + json + " (" + typeof json + "): " + JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
export function validateNumberOrNull(json: any): number | null {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return validateNumber(json);
|
||||
}
|
||||
|
||||
export function validateBoolean(json: any): boolean {
|
||||
if (!(typeof json == "boolean")) {
|
||||
throw new Error("Not a boolean: " + json + " (" + typeof json + "): " + JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
export function validateBooleanOrNull(json: any): boolean | null {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return validateBoolean(json);
|
||||
}
|
||||
|
||||
export function validateString(json: any): string {
|
||||
if (!(typeof json == "string")) {
|
||||
throw new Error("Not a string: " + json + " (" + typeof json + "): " + JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
export function validateStringOrNull(json: any): string | null {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return validateString(json);
|
||||
}
|
||||
|
||||
export function validateDate(json: any): Date {
|
||||
return new Date(Date.parse(validateString(json)));
|
||||
}
|
||||
|
||||
export function validateDateOrNull(json: any): Date | null {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return validateDate(json);
|
||||
}
|
||||
|
||||
export function validateList<T>(json: any, fromJson: FromJson<T>): T[] {
|
||||
return json.map(fromJson);
|
||||
}
|
||||
|
||||
export function validateLocalDateOrNull(json: any): Date | null {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return validateLocalDate(json);
|
||||
}
|
||||
|
||||
export function validateLocalDate(json: any): Date {
|
||||
return new Date(validateString(json));
|
||||
}
|
||||
17
src/main/angular/src/app/api/ws.ts
Normal file
17
src/main/angular/src/app/api/ws.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {StompService} from "@stomp/ng2-stompjs";
|
||||
import {getUrl} from "./types";
|
||||
|
||||
export function stompServiceFactory() {
|
||||
const stomp = new StompService({
|
||||
url: getUrl('ws', ['ws']),
|
||||
debug: false,
|
||||
heartbeat_in: 2000,
|
||||
heartbeat_out: 2000,
|
||||
reconnect_delay: 2000,
|
||||
headers: {},
|
||||
});
|
||||
stomp.connected$.subscribe(_ => console.info("WEBSOCKET CONNECTED"));
|
||||
stomp.webSocketErrors$.subscribe(_ => console.info("WEBSOCKET DISCONNECTED"));
|
||||
stomp.activate();
|
||||
return stomp;
|
||||
}
|
||||
1
src/main/angular/src/app/app.component.html
Normal file
1
src/main/angular/src/app/app.component.html
Normal file
@ -0,0 +1 @@
|
||||
<router-outlet/>
|
||||
1
src/main/angular/src/app/app.component.less
Normal file
1
src/main/angular/src/app/app.component.less
Normal file
@ -0,0 +1 @@
|
||||
@import "../common";
|
||||
13
src/main/angular/src/app/app.component.ts
Normal file
13
src/main/angular/src/app/app.component.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.less'
|
||||
})
|
||||
export class AppComponent {
|
||||
|
||||
}
|
||||
23
src/main/angular/src/app/app.config.ts
Normal file
23
src/main/angular/src/app/app.config.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {ApplicationConfig, LOCALE_ID, provideZoneChangeDetection} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
import {routes} from './app.routes';
|
||||
import {provideHttpClient} from "@angular/common/http";
|
||||
import {stompServiceFactory} from "./api/ws";
|
||||
import {StompService} from "@stomp/ng2-stompjs";
|
||||
import {registerLocaleData} from "@angular/common";
|
||||
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
|
||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({eventCoalescing: true}),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(),
|
||||
{provide: LOCALE_ID, useValue: 'de-DE'},
|
||||
{provide: StompService, useFactory: stompServiceFactory},
|
||||
],
|
||||
};
|
||||
7
src/main/angular/src/app/app.routes.ts
Normal file
7
src/main/angular/src/app/app.routes.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {DashboardComponent} from "./pages/dashboard/dashboard.component";
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '', component: DashboardComponent},
|
||||
{path: '**', redirectTo: '/'},
|
||||
];
|
||||
@ -0,0 +1,3 @@
|
||||
<div class="width100">
|
||||
<app-values-tile title="Elektrizität" [seriesList]="getElectricitySeries()"></app-values-tile>
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../common";
|
||||
@ -0,0 +1,92 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {JsonPipe, NgForOf} from "@angular/common";
|
||||
import {Series} from "../../api/series/Series";
|
||||
import {SeriesService} from "../../api/series/series.service";
|
||||
import {ValuesTileComponent} from "../../shared/values-tile/values-tile.component";
|
||||
import {Value} from "../../api/series/Value";
|
||||
import {Display} from "../../api/series/IValue";
|
||||
|
||||
const PURCHASING_MUCH = 200;
|
||||
|
||||
const PRODUCED_UNTIL_METER_CHANGE = 287.995;
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
ValuesTileComponent,
|
||||
JsonPipe
|
||||
],
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.less'
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
protected gridPurchased: Series | null = null;
|
||||
|
||||
protected gridDelivered: Series | null = null;
|
||||
|
||||
protected photovoltaicProduced: Series | null = null;
|
||||
|
||||
protected photovoltaicPower: Series | null = null;
|
||||
|
||||
protected gridPower: Series | null = null;
|
||||
|
||||
constructor(
|
||||
readonly seriesService: SeriesService,
|
||||
) {
|
||||
// -
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seriesService.findAll(list => list.forEach(s => this.seriesUpdate(s)));
|
||||
this.seriesService.subscribe(series => this.seriesUpdate(series));
|
||||
}
|
||||
|
||||
private seriesUpdate(series: Series) {
|
||||
this.gridPurchased = this.seriesUpdate2(series, this.gridPurchased, 'electricity.grid.purchase.energy');
|
||||
this.gridDelivered = this.seriesUpdate2(series, this.gridDelivered, 'electricity.grid.delivery.energy');
|
||||
this.gridPower = this.seriesUpdate2(series, this.gridPower, 'electricity.grid.power');
|
||||
|
||||
this.photovoltaicProduced = this.seriesUpdate2(series, this.photovoltaicProduced, 'electricity.photovoltaic.energy');
|
||||
this.photovoltaicPower = this.seriesUpdate2(series, this.photovoltaicPower, 'electricity.photovoltaic.power');
|
||||
}
|
||||
|
||||
private seriesUpdate2(fresh: Series, old: Series | null, name: string): Series | null {
|
||||
if (fresh.name !== name) {
|
||||
return old;
|
||||
}
|
||||
if (old == null || old.lastDate.getTime() <= fresh.lastDate.getTime()) {
|
||||
return fresh;
|
||||
}
|
||||
return old;
|
||||
}
|
||||
|
||||
getElectricitySeries(): Display[] {
|
||||
const consumptionPower = Value.positiveOnly(Value.plus(this.photovoltaicPower, this.gridPower));
|
||||
const selfConsumed = Value.map(this.photovoltaicProduced, producedTotal => {
|
||||
const producedAfterChange = producedTotal - PRODUCED_UNTIL_METER_CHANGE;
|
||||
const deliveredAfterChange = this.gridDelivered?.value || 0;
|
||||
const selfAfterChange = producedAfterChange - deliveredAfterChange;
|
||||
const selfRatio = selfAfterChange / producedAfterChange;
|
||||
return selfRatio * producedTotal;
|
||||
});
|
||||
|
||||
const purchasingAny = (this.gridPower?.value || 0) > 0;
|
||||
const purchasingMuch = (this.gridPower?.value || 0) > PURCHASING_MUCH;
|
||||
const color = purchasingMuch ? 'red' : (purchasingAny ? 'orange' : 'green');
|
||||
|
||||
return [
|
||||
new Display('Bezug', '', this.gridPurchased),
|
||||
new Display('Produktion', '', this.photovoltaicProduced),
|
||||
new Display('Einspeisung', '', this.gridDelivered),
|
||||
new Display('Eigenverbrauch', '', selfConsumed),
|
||||
|
||||
new Display('Produktion', color, this.photovoltaicPower),
|
||||
new Display('Netz', color, this.gridPower),
|
||||
new Display('Verbrauch', color, consumptionPower),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<div class="meter" [class.invalid]="!valid">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="number">{{ number() }}</div>
|
||||
<table class="values">
|
||||
<tr class="rate" *ngFor="let display of seriesList" [style.color]="display.color">
|
||||
<th>{{ display.title }}</th>
|
||||
<td class="v">{{ display?.iValue?.value | number:'0.0-0' }}</td>
|
||||
<td class="u">{{ display?.iValue?.unit }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -0,0 +1,38 @@
|
||||
@import "../../../common";
|
||||
|
||||
.meter {
|
||||
position: relative;
|
||||
padding: @space;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: @space;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.values {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.v {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.u {
|
||||
text-align: left;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Series} from "../../api/series/Series";
|
||||
import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
||||
import {Subscription, timer} from "rxjs";
|
||||
import {Display} from "../../api/series/IValue";
|
||||
|
||||
export const TOO_OLD_MILLIS = 10000;
|
||||
|
||||
@Component({
|
||||
selector: 'app-values-tile',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIf,
|
||||
DecimalPipe,
|
||||
NgForOf
|
||||
],
|
||||
templateUrl: './values-tile.component.html',
|
||||
styleUrl: './values-tile.component.less'
|
||||
})
|
||||
export class ValuesTileComponent implements OnInit, OnDestroy {
|
||||
|
||||
private now: Date = new Date();
|
||||
|
||||
private timer?: Subscription;
|
||||
|
||||
protected valid: boolean = false;
|
||||
|
||||
@Input()
|
||||
title: string = '';
|
||||
|
||||
@Input()
|
||||
protected seriesList_: Display[] = [];
|
||||
|
||||
@Input()
|
||||
set seriesList(seriesList: Display[]) {
|
||||
this.seriesList_ = seriesList;
|
||||
this.displayUpdate();
|
||||
}
|
||||
|
||||
get seriesList(): Display[] {
|
||||
return this.seriesList_;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set purchase(purchase: Series | null) {
|
||||
this.purchase_ = purchase;
|
||||
this.displayUpdate();
|
||||
}
|
||||
|
||||
get purchase(): Series | null {
|
||||
return this.purchase_;
|
||||
}
|
||||
|
||||
private purchase_: Series | null = null;
|
||||
|
||||
@Input()
|
||||
set delivery(delivery: Series | null) {
|
||||
this.delivery_ = delivery;
|
||||
this.displayUpdate();
|
||||
}
|
||||
|
||||
get delivery(): Series | null {
|
||||
return this.delivery_;
|
||||
}
|
||||
|
||||
private delivery_: Series | null = null;
|
||||
|
||||
@Input()
|
||||
set rate(rate: Series | null) {
|
||||
this.rate_ = rate;
|
||||
this.displayUpdate();
|
||||
}
|
||||
|
||||
get rate(): Series | null {
|
||||
return this.rate_;
|
||||
}
|
||||
|
||||
private rate_: Series | null = null;
|
||||
|
||||
number(): string {
|
||||
if (this.purchase?.period) {
|
||||
return this.purchase?.period.name;
|
||||
} else if (this.delivery?.period) {
|
||||
return this.delivery?.period.name;
|
||||
} else if (this.rate?.period) {
|
||||
return this.rate?.period.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.timer = timer(0, 1000).subscribe(() => this.displayUpdate());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.timer?.unsubscribe();
|
||||
}
|
||||
|
||||
private displayUpdate() {
|
||||
this.now = new Date();
|
||||
this.valid = this.seriesList.some(d => !!d && !!d.iValue && !!d.iValue.date && (this.now.getTime() - d.iValue.date.getTime()) <= TOO_OLD_MILLIS)
|
||||
}
|
||||
|
||||
}
|
||||
2
src/main/angular/src/common.less
Normal file
2
src/main/angular/src/common.less
Normal file
@ -0,0 +1,2 @@
|
||||
@import "states";
|
||||
@import "widths";
|
||||
4
src/main/angular/src/config.less
Normal file
4
src/main/angular/src/config.less
Normal file
@ -0,0 +1,4 @@
|
||||
@font: 5vw;
|
||||
@fontDesktop: 18px;
|
||||
|
||||
@space: 0.5em;
|
||||
7
src/main/angular/src/environments/environment.prod.ts
Normal file
7
src/main/angular/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
host: window.location.host.split(":")[0],
|
||||
port: window.location.port,
|
||||
base: 'Data/',
|
||||
secure: window.location.protocol === "https:",
|
||||
};
|
||||
8
src/main/angular/src/environments/environment.ts
Normal file
8
src/main/angular/src/environments/environment.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
host: window.location.host.split(":")[0],
|
||||
port: 8080,
|
||||
base: '',
|
||||
secure: window.location.protocol === "https:",
|
||||
};
|
||||
|
||||
14
src/main/angular/src/index.html
Normal file
14
src/main/angular/src/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Data</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<link rel="icon" type="image/svg" href="favicon.svg">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
6
src/main/angular/src/main.ts
Normal file
6
src/main/angular/src/main.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
import {appConfig} from './app/app.config';
|
||||
import {AppComponent} from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
6
src/main/angular/src/states.less
Normal file
6
src/main/angular/src/states.less
Normal file
@ -0,0 +1,6 @@
|
||||
@import "config";
|
||||
|
||||
.invalid, .invalid * {
|
||||
color: lightgray;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
21
src/main/angular/src/styles.less
Normal file
21
src/main/angular/src/styles.less
Normal file
@ -0,0 +1,21 @@
|
||||
@import "config";
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: @font;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
|
||||
body {
|
||||
font-size: @fontDesktop;
|
||||
}
|
||||
|
||||
}
|
||||
29
src/main/angular/src/widths.less
Normal file
29
src/main/angular/src/widths.less
Normal file
@ -0,0 +1,29 @@
|
||||
@import "config";
|
||||
|
||||
.width100 {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: @space;
|
||||
}
|
||||
|
||||
.width50 {
|
||||
float: left;
|
||||
width: 50%;
|
||||
padding: @space;
|
||||
}
|
||||
|
||||
.width50:nth-child(2n) {
|
||||
padding-left: calc(@space / 2);
|
||||
}
|
||||
|
||||
.width50:nth-child(2n+1) {
|
||||
padding-right: calc(@space / 2);
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
|
||||
.width100 {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
}
|
||||
15
src/main/angular/tsconfig.app.json
Normal file
15
src/main/angular/tsconfig.app.json
Normal file
@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
33
src/main/angular/tsconfig.json
Normal file
33
src/main/angular/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
15
src/main/angular/tsconfig.spec.json
Normal file
15
src/main/angular/tsconfig.spec.json
Normal file
@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@ -2,8 +2,8 @@ package de.ph87.data.electricity;
|
||||
|
||||
public class ElectricityHelpers {
|
||||
|
||||
public static double energyToKWh(final double energy, final String energyUnit) {
|
||||
return switch (energyUnit) {
|
||||
public static double energyToKWh(final double energy, final String isUnit) {
|
||||
return switch (isUnit) {
|
||||
case "mWh" -> energy / 1000000;
|
||||
case "Wh" -> energy / 1000;
|
||||
case "kWh" -> energy;
|
||||
@ -14,8 +14,8 @@ public class ElectricityHelpers {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static double powerToW(final double power, final String powerUnit) {
|
||||
return switch (powerUnit) {
|
||||
public static double powerToW(final double power, final String isUnit) {
|
||||
return switch (isUnit) {
|
||||
case "mW" -> power / 1000;
|
||||
case "W" -> power;
|
||||
case "kW" -> power * 1000;
|
||||
|
||||
@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.ph87.data.mqtt.MqttEvent;
|
||||
import de.ph87.data.series.consumption.ConsumptionEvent;
|
||||
import de.ph87.data.series.measure.MeasureEvent;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -12,15 +13,18 @@ import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import static de.ph87.data.electricity.ElectricityHelpers.energyToKWh;
|
||||
import static de.ph87.data.electricity.ElectricityHelpers.powerToW;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GridReceiver {
|
||||
|
||||
public static final String GRID_PURCHASE_SERIES_NAME = "electricity.grid.purchase";
|
||||
public static final String GRID_PURCHASE = "electricity.grid.purchase.energy";
|
||||
|
||||
public static final String GRID_DELIVERY_SERIES_NAME = "electricity.grid.delivery";
|
||||
public static final String GRID_DELIVERY = "electricity.grid.delivery.energy";
|
||||
|
||||
public static final String GRID_POWER = "electricity.grid.power";
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@ -40,11 +44,14 @@ public class GridReceiver {
|
||||
return;
|
||||
}
|
||||
|
||||
final double purchaseKWh = energyToKWh(inbound.purchaseWh, "Wh");
|
||||
applicationEventPublisher.publishEvent(new ConsumptionEvent(GRID_PURCHASE_SERIES_NAME, "kWh", true, inbound.meter, inbound.date, purchaseKWh));
|
||||
final double purchase = energyToKWh(inbound.purchaseWh, "Wh");
|
||||
applicationEventPublisher.publishEvent(new ConsumptionEvent(GRID_PURCHASE, "kWh", true, inbound.meter, inbound.date, purchase));
|
||||
|
||||
final double deliveryKWh = energyToKWh(inbound.deliveryWh, "Wh");
|
||||
applicationEventPublisher.publishEvent(new ConsumptionEvent(GRID_DELIVERY_SERIES_NAME, "kWh", true, inbound.meter, inbound.date, deliveryKWh));
|
||||
final double delivery = energyToKWh(inbound.deliveryWh, "Wh");
|
||||
applicationEventPublisher.publishEvent(new ConsumptionEvent(GRID_DELIVERY, "kWh", true, inbound.meter, inbound.date, delivery));
|
||||
|
||||
final double power = powerToW(inbound.powerW, "W");
|
||||
applicationEventPublisher.publishEvent(new MeasureEvent(GRID_POWER, "W", inbound.date, power));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package de.ph87.data.electricity.photovoltaic;
|
||||
|
||||
import de.ph87.data.mqtt.MqttEvent;
|
||||
import de.ph87.data.series.consumption.ConsumptionEvent;
|
||||
import de.ph87.data.series.measure.MeasureEvent;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -15,15 +16,18 @@ import java.util.regex.Pattern;
|
||||
|
||||
import static de.ph87.data.common.DateTimeHelpers.ZDT;
|
||||
import static de.ph87.data.electricity.ElectricityHelpers.energyToKWh;
|
||||
import static de.ph87.data.electricity.ElectricityHelpers.powerToW;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PhotovoltaicReceiver {
|
||||
|
||||
public static final String PHOTOVOLTAIC_ENERGY_SERIES_NAME = "electricity.photovoltaic.energy";
|
||||
public static final String PHOTOVOLTAIC_ENERGY = "electricity.photovoltaic.energy";
|
||||
|
||||
private static final Pattern REGEX = Pattern.compile("^(?<serial>\\S+) (?<epochSeconds>\\d+) (?<powerValue>\\d+(:?\\.\\d+)?)(?<powerUnit>\\S+) (?<producedValue>\\d+(:?\\.\\d+)?)(?<producedUnit>\\S+)$");
|
||||
public static final String PHOTOVOLTAIC_POWER = "electricity.photovoltaic.power";
|
||||
|
||||
private static final Pattern REGEX = Pattern.compile("^(?<serial>\\S+) (?<epochSeconds>\\d+) (?<powerValue>\\d+(:?\\.\\d+)?)(?<powerUnit>\\S+) (?<energyValue>\\d+(:?\\.\\d+)?)(?<energyUnit>\\S+)$");
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@ -40,10 +44,16 @@ public class PhotovoltaicReceiver {
|
||||
|
||||
final String serial = matcher.group("serial");
|
||||
final ZonedDateTime date = ZDT(Long.parseLong(matcher.group("epochSeconds")));
|
||||
final double producedValue = Double.parseDouble(matcher.group("producedValue"));
|
||||
final String producedUnit = matcher.group("producedUnit");
|
||||
final double producedKWh = energyToKWh(producedValue, producedUnit);
|
||||
applicationEventPublisher.publishEvent(new ConsumptionEvent(PHOTOVOLTAIC_ENERGY_SERIES_NAME, "kWh", true, serial, date, producedKWh));
|
||||
|
||||
final double energyValue = Double.parseDouble(matcher.group("energyValue"));
|
||||
final String energyUnit = matcher.group("energyUnit");
|
||||
final double energy = energyToKWh(energyValue, energyUnit);
|
||||
applicationEventPublisher.publishEvent(new ConsumptionEvent(PHOTOVOLTAIC_ENERGY, "kWh", true, serial, date, energy));
|
||||
|
||||
final double powerValue = Double.parseDouble(matcher.group("powerValue"));
|
||||
final String powerUnit = matcher.group("powerUnit");
|
||||
final double power = powerToW(powerValue, powerUnit);
|
||||
applicationEventPublisher.publishEvent(new MeasureEvent(PHOTOVOLTAIC_POWER, "W", date, power));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -37,12 +37,10 @@ public class Series {
|
||||
@ToString.Exclude
|
||||
private Period period;
|
||||
|
||||
@Setter
|
||||
@NonNull
|
||||
@Column(nullable = false)
|
||||
private ZonedDateTime lastDate;
|
||||
|
||||
@Setter
|
||||
@Column(nullable = false)
|
||||
private double lastValue;
|
||||
|
||||
@ -58,4 +56,9 @@ public class Series {
|
||||
this.lastValue = value;
|
||||
}
|
||||
|
||||
public void update(@NonNull final ZonedDateTime date, final double value) {
|
||||
lastDate = date;
|
||||
lastValue = mode.update(lastValue, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,48 +1,26 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@Transactional
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("Series")
|
||||
public class SeriesController {
|
||||
|
||||
private final SeriesRepository seriesRepository;
|
||||
private final SeriesService seriesService;
|
||||
|
||||
@GetMapping("/")
|
||||
public String index(@NonNull final Model model) {
|
||||
final Stream<Series> series = seriesRepository.findAll().stream().sorted(Comparator.comparing(Series::getName));
|
||||
model.addAttribute("series", series);
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String numberFormat(@Nullable final Double value, final int decimals) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return "%%.%df".formatted(decimals).formatted(value);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String dateTimeFormat(@Nullable final ZonedDateTime date) {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
return date.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
@GetMapping("findAll")
|
||||
public List<SeriesDto> findAll() {
|
||||
return seriesService.findAllDto();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
57
src/main/java/de/ph87/data/series/SeriesDto.java
Normal file
57
src/main/java/de/ph87/data/series/SeriesDto.java
Normal file
@ -0,0 +1,57 @@
|
||||
package de.ph87.data.series;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import de.ph87.data.series.consumption.period.PeriodDto;
|
||||
import de.ph87.data.web.IWebSocketMessage;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class SeriesDto implements IWebSocketMessage {
|
||||
|
||||
@JsonIgnore
|
||||
private final List<Object> websocketTopic = List.of("Series");
|
||||
|
||||
private final long id;
|
||||
|
||||
@NonNull
|
||||
private final String name;
|
||||
|
||||
@NonNull
|
||||
private final String unit;
|
||||
|
||||
@NonNull
|
||||
private final SeriesMode mode;
|
||||
|
||||
@Nullable
|
||||
@ToString.Exclude
|
||||
private final PeriodDto period;
|
||||
|
||||
@NonNull
|
||||
private final ZonedDateTime lastDate;
|
||||
|
||||
private final double lastValue;
|
||||
|
||||
@NonNull
|
||||
private final Set<String> aliases;
|
||||
|
||||
public SeriesDto(@NonNull final Series series) {
|
||||
this.id = series.getId();
|
||||
this.name = series.getName();
|
||||
this.unit = series.getUnit();
|
||||
this.mode = series.getMode();
|
||||
this.period = PeriodDto.orNull(series.getPeriod());
|
||||
this.lastDate = series.getLastDate();
|
||||
this.lastValue = series.getLastValue();
|
||||
this.aliases = new HashSet<>(series.getAliases());
|
||||
}
|
||||
|
||||
}
|
||||
@ -3,10 +3,12 @@ package de.ph87.data.series;
|
||||
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.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -16,13 +18,23 @@ public class SeriesService {
|
||||
|
||||
private final SeriesRepository seriesRepository;
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@NonNull
|
||||
public Series getOrCreateByName(@NonNull final String name, @NonNull final SeriesMode mode, @NonNull final ZonedDateTime date, final double value, @NonNull final String unit) {
|
||||
final Series series = seriesRepository.findByNameOrAliasesContains(name, name).orElseGet(() -> seriesRepository.save(new Series(name, mode, date, value, unit)));
|
||||
if (series.getMode() != mode) {
|
||||
throw new RuntimeException("'mode' argument for getOrCreateByName does not match 'mode' of existing Series: mode=%s, series=%s".formatted(mode, series));
|
||||
}
|
||||
series.update(date, value);
|
||||
final SeriesDto dto = new SeriesDto(series);
|
||||
applicationEventPublisher.publishEvent(dto);
|
||||
return series;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<SeriesDto> findAllDto() {
|
||||
return seriesRepository.findAll().stream().map(SeriesDto::new).toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -33,9 +33,6 @@ public class ConsumptionService {
|
||||
log.debug("Handling ConsumptionEvent: {}", event);
|
||||
|
||||
final Series series = seriesService.getOrCreateByName(event.getName(), event.isIncreasing() ? SeriesMode.INCREASING : SeriesMode.DECREASING, event.getDate(), event.getValue(), event.getUnit());
|
||||
series.setLastDate(event.getDate());
|
||||
series.setLastValue(series.getMode().update(series.getLastValue(), event.getValue()));
|
||||
|
||||
final Period period = periodService.getOrCreatePeriod(series, event);
|
||||
period.setLastDate(event.getDate());
|
||||
period.setLastValue(series.getMode().update(period.getLastValue(), event.getValue()));
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
package de.ph87.data.series.consumption.period;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class PeriodDto {
|
||||
|
||||
private final long id;
|
||||
|
||||
@NonNull
|
||||
private final String name;
|
||||
|
||||
@NonNull
|
||||
private final ZonedDateTime firstDate;
|
||||
|
||||
private final double firstValue;
|
||||
|
||||
@NonNull
|
||||
private final ZonedDateTime lastDate;
|
||||
|
||||
private final double lastValue;
|
||||
|
||||
public PeriodDto(@NonNull final Period period) {
|
||||
this.id = period.getId();
|
||||
this.name = period.getName();
|
||||
this.firstDate = period.getFirstDate();
|
||||
this.firstValue = period.getFirstValue();
|
||||
this.lastDate = period.getLastDate();
|
||||
this.lastValue = period.getLastValue();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PeriodDto orNull(@Nullable final Period period) {
|
||||
if (period == null) {
|
||||
return null;
|
||||
}
|
||||
return new PeriodDto(period);
|
||||
}
|
||||
|
||||
}
|
||||
@ -27,9 +27,6 @@ public class CounterService {
|
||||
log.debug("Handling CounterEvent: {}", event);
|
||||
|
||||
final Series series = seriesService.getOrCreateByName(event.getName(), SeriesMode.COUNTER, event.getDate(), event.getCount(), event.getUnit());
|
||||
series.setLastDate(event.getDate());
|
||||
series.setLastValue(series.getMode().update(series.getLastValue(), event.getCount()));
|
||||
|
||||
for (final Unit unit : Unit.values()) {
|
||||
final SeriesIntervalKey id = new SeriesIntervalKey(series, unit, event.getDate());
|
||||
counterRepository.findById(id)
|
||||
|
||||
@ -27,9 +27,6 @@ public class MeasureService {
|
||||
log.debug("Handling MeasureEvent: {}", event);
|
||||
|
||||
final Series series = seriesService.getOrCreateByName(event.getName(), SeriesMode.MEASURE, event.getDate(), event.getValue(), event.getUnit());
|
||||
series.setLastDate(event.getDate());
|
||||
series.setLastValue(series.getMode().update(series.getLastValue(), event.getValue()));
|
||||
|
||||
for (final Unit unit : Unit.values()) {
|
||||
final SeriesIntervalKey id = new SeriesIntervalKey(series, unit, event.getDate());
|
||||
measureRepository.findById(id)
|
||||
|
||||
12
src/main/java/de/ph87/data/web/IWebSocketMessage.java
Normal file
12
src/main/java/de/ph87/data/web/IWebSocketMessage.java
Normal file
@ -0,0 +1,12 @@
|
||||
package de.ph87.data.web;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IWebSocketMessage {
|
||||
|
||||
@JsonIgnore
|
||||
List<Object> getWebsocketTopic();
|
||||
|
||||
}
|
||||
39
src/main/java/de/ph87/data/web/WebConfig.java
Normal file
39
src/main/java/de/ph87/data/web/WebConfig.java
Normal file
@ -0,0 +1,39 @@
|
||||
package de.ph87.data.web;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**").allowedOrigins("*").allowedMethods("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
|
||||
registry
|
||||
.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/resources/")
|
||||
.resourceChain(true)
|
||||
.addResolver(new PathResourceResolver() {
|
||||
|
||||
protected Resource getResource(@NonNull String resourcePath, @NonNull Resource roomLocation) throws IOException {
|
||||
final Resource requestedResource = roomLocation.createRelative(resourcePath);
|
||||
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource : new ClassPathResource("/resources/index.html");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
35
src/main/java/de/ph87/data/web/WebSocketConfig.java
Normal file
35
src/main/java/de/ph87/data/web/WebSocketConfig.java
Normal file
@ -0,0 +1,35 @@
|
||||
package de.ph87.data.web;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@CrossOrigin
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
public static final String DESTINATION = "";
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws").setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.enableSimpleBroker(DESTINATION).setHeartbeatValue(new long[]{2000, 2000}).setTaskScheduler(heartBeatScheduler());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TaskScheduler heartBeatScheduler() {
|
||||
return new ThreadPoolTaskScheduler();
|
||||
}
|
||||
|
||||
}
|
||||
30
src/main/java/de/ph87/data/web/WebSocketService.java
Normal file
30
src/main/java/de/ph87/data/web/WebSocketService.java
Normal file
@ -0,0 +1,30 @@
|
||||
package de.ph87.data.web;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketService {
|
||||
|
||||
private final SimpMessageSendingOperations simpMessageSendingOperations;
|
||||
|
||||
@EventListener(IWebSocketMessage.class)
|
||||
public void send(@NonNull final IWebSocketMessage message) {
|
||||
String prefix = "";
|
||||
String topic = "";
|
||||
for (int i = 0; i < message.getWebsocketTopic().size(); i++) {
|
||||
topic += prefix + message.getWebsocketTopic().get(i);
|
||||
simpMessageSendingOperations.convertAndSend(topic, message);
|
||||
prefix = "/";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="5">
|
||||
<title>Data</title>
|
||||
<style>
|
||||
.section {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
font-family: monospace;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: 2px solid white;
|
||||
padding: 10px;
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: right;
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.unit {
|
||||
border-left: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="section">
|
||||
<table>
|
||||
<tr th:each="serie: ${series}">
|
||||
<td class="name" th:text="${serie.name}"></td>
|
||||
<td class="mode" th:text="${serie.mode}"></td>
|
||||
|
||||
<td class="value" th:text="${@seriesController.numberFormat(serie.lastValue, 1)}"></td>
|
||||
<td class="unit" th:text="${serie.unit}"></td>
|
||||
|
||||
<td class="date" th:text="${@seriesController.dateTimeFormat(serie.lastDate)}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user