Compare commits
10 Commits
03631e17c2
...
6ebe41c8d2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ebe41c8d2 | |||
| b14c8d63d2 | |||
| fdf64ebd17 | |||
| aa90d8125d | |||
| 551efdbcff | |||
| 8e3e763e59 | |||
| e3d5523ac7 | |||
| 4fd227b19c | |||
| cd84f25577 | |||
| 201cfd9342 |
18
src/main/angular/.editorconfig
Normal file
18
src/main/angular/.editorconfig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
# noinspection EditorConfigKeyCorrectness
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.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.11.
|
||||||
|
|
||||||
|
## 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14182
src/main/angular/package-lock.json
generated
Normal file
14182
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.11",
|
||||||
|
"@angular/cli": "^18.2.11",
|
||||||
|
"@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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/main/angular/public/favicon.ico
Normal file
BIN
src/main/angular/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
25
src/main/angular/public/switchOff.svg
Normal file
25
src/main/angular/public/switchOff.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
fill: #bc1c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stroke {
|
||||||
|
fill: none;
|
||||||
|
stroke: #FFFFFF;
|
||||||
|
stroke-width: 4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<circle class="fill" cx="32" cy="32" r="32"/>
|
||||||
|
<path d="M32,52c-9.9,0-18-8.1-18-18c0-6.4,3.4-12.3,8.9-15.5c1-0.6,2.2-0.2,2.7,0.7c0.6,1,0.2,2.2-0.7,2.7
|
||||||
|
C20.7,24.4,18,29.1,18,34c0,7.7,6.3,14,14,14c7.7,0,14-6.3,14-14c0-5.1-2.7-9.7-7.2-12.2c-1-0.5-1.3-1.8-0.8-2.7
|
||||||
|
c0.5-1,1.8-1.3,2.7-0.8C46.5,21.5,50,27.5,50,34C50,43.9,41.9,52,32,52z"/>
|
||||||
|
<path d="M32,36c-1.1,0-2-0.9-2-2V14c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2v20C34,35.1,33.1,36,32,36z"/>
|
||||||
|
<path class="stroke" d="M39.8,18c4.9,2.7,8.2,8,8.2,14c0,8.8-7.2,16-16,16c-8.8,0-16-7.2-16-16c0-5.9,3.2-11,7.9-13.8"/>
|
||||||
|
<line class="stroke" x1="32" y1="32" x2="32" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 944 B |
25
src/main/angular/public/switchOn.svg
Normal file
25
src/main/angular/public/switchOn.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
fill: #5cc75e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stroke {
|
||||||
|
fill: none;
|
||||||
|
stroke: #FFFFFF;
|
||||||
|
stroke-width: 4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<circle class="fill" cx="32" cy="32" r="32"/>
|
||||||
|
<path d="M32,52c-9.9,0-18-8.1-18-18c0-6.4,3.4-12.3,8.9-15.5c1-0.6,2.2-0.2,2.7,0.7c0.6,1,0.2,2.2-0.7,2.7
|
||||||
|
C20.7,24.4,18,29.1,18,34c0,7.7,6.3,14,14,14c7.7,0,14-6.3,14-14c0-5.1-2.7-9.7-7.2-12.2c-1-0.5-1.3-1.8-0.8-2.7
|
||||||
|
c0.5-1,1.8-1.3,2.7-0.8C46.5,21.5,50,27.5,50,34C50,43.9,41.9,52,32,52z"/>
|
||||||
|
<path d="M32,36c-1.1,0-2-0.9-2-2V14c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2v20C34,35.1,33.1,36,32,36z"/>
|
||||||
|
<path class="stroke" d="M39.8,18c4.9,2.7,8.2,8,8.2,14c0,8.8-7.2,16-16,16c-8.8,0-16-7.2-16-16c0-5.9,3.2-11,7.9-13.8"/>
|
||||||
|
<line class="stroke" x1="32" y1="32" x2="32" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 944 B |
30
src/main/angular/src/app/api/Device/Device.ts
Normal file
30
src/main/angular/src/app/api/Device/Device.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {Property} from "../Property/Property";
|
||||||
|
import {orNull, validateString} from "../common/validators";
|
||||||
|
|
||||||
|
export class Device {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly uuid: string,
|
||||||
|
readonly name: string,
|
||||||
|
readonly slug: string,
|
||||||
|
readonly statePropertyId: string,
|
||||||
|
readonly stateProperty: Property | null,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Device {
|
||||||
|
return new Device(
|
||||||
|
validateString(json.uuid),
|
||||||
|
validateString(json.name),
|
||||||
|
validateString(json.slug),
|
||||||
|
validateString(json.statePropertyId),
|
||||||
|
orNull(json.stateProperty, Property.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static trackBy(index: number, device: Device) {
|
||||||
|
return device.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
5
src/main/angular/src/app/api/Device/DeviceFilter.ts
Normal file
5
src/main/angular/src/app/api/Device/DeviceFilter.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class DeviceFilter {
|
||||||
|
|
||||||
|
search: string = "";
|
||||||
|
|
||||||
|
}
|
||||||
32
src/main/angular/src/app/api/Device/device.service.ts
Normal file
32
src/main/angular/src/app/api/Device/device.service.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {CrudService} from '../common/CrudService';
|
||||||
|
import {Device} from './Device';
|
||||||
|
import {ApiService} from '../common/api.service';
|
||||||
|
import {Next} from '../common/types';
|
||||||
|
|
||||||
|
import {DeviceFilter} from './DeviceFilter';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DeviceService extends CrudService<Device> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
api: ApiService,
|
||||||
|
) {
|
||||||
|
super(api, ['Device'], Device.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
getByUuid(uuid: string, next: Next<Device>): void {
|
||||||
|
this.getSingle(['getByUuid', uuid], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
list(filter: DeviceFilter | null, next: Next<Device[]>): void {
|
||||||
|
this.postList(['list'], filter, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(device: Device, state: boolean, next?: Next<void>): void {
|
||||||
|
this.getNone(['setState', device.uuid, state], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
src/main/angular/src/app/api/Group/Group.ts
Normal file
33
src/main/angular/src/app/api/Group/Group.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {orNull, validateDateOrNull, validateString} from '../common/validators';
|
||||||
|
import {State} from '../State/State';
|
||||||
|
|
||||||
|
export class Group {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly address: string,
|
||||||
|
readonly name: string,
|
||||||
|
readonly description: string,
|
||||||
|
readonly dpt: string,
|
||||||
|
readonly state: State | null,
|
||||||
|
readonly lastValueChange: Date | null,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Group {
|
||||||
|
return new Group(
|
||||||
|
validateString(json.address),
|
||||||
|
validateString(json.name),
|
||||||
|
validateString(json.description),
|
||||||
|
validateString(json.dpt),
|
||||||
|
orNull(json.state, State.fromJson),
|
||||||
|
validateDateOrNull(json.lastValueChange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static trackBy(index: number, group: Group) {
|
||||||
|
return group.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
5
src/main/angular/src/app/api/Group/GroupFilter.ts
Normal file
5
src/main/angular/src/app/api/Group/GroupFilter.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class GroupFilter {
|
||||||
|
|
||||||
|
search: string = "";
|
||||||
|
|
||||||
|
}
|
||||||
28
src/main/angular/src/app/api/Group/group.service.ts
Normal file
28
src/main/angular/src/app/api/Group/group.service.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {CrudService} from '../common/CrudService';
|
||||||
|
import {Group} from './Group';
|
||||||
|
import {ApiService} from '../common/api.service';
|
||||||
|
import {Next} from '../common/types';
|
||||||
|
|
||||||
|
import {GroupFilter} from './GroupFilter';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class GroupService extends CrudService<Group> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
api: ApiService,
|
||||||
|
) {
|
||||||
|
super(api, ['Knx', 'Group'], Group.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
getByAddress(address: string, next: Next<Group>): void {
|
||||||
|
this.getSingle(['getByAddress', address], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
list(filter: GroupFilter | null, next: Next<Group[]>): void {
|
||||||
|
this.postList(['list'], filter, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
src/main/angular/src/app/api/Property/Property.ts
Normal file
24
src/main/angular/src/app/api/Property/Property.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {State} from "../State/State";
|
||||||
|
import {orNull, validateDateOrNull, validateString} from "../common/validators";
|
||||||
|
|
||||||
|
export class Property {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly id: string,
|
||||||
|
readonly type: string,
|
||||||
|
readonly state: State | null,
|
||||||
|
readonly lastValueChange: Date | null,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Property {
|
||||||
|
return new Property(
|
||||||
|
validateString(json.id),
|
||||||
|
validateString(json.type),
|
||||||
|
orNull(json.state, State.fromJson),
|
||||||
|
validateDateOrNull(json.lastValueChange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
src/main/angular/src/app/api/Shutter/Shutter.ts
Normal file
30
src/main/angular/src/app/api/Shutter/Shutter.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {Property} from "../Property/Property";
|
||||||
|
import {orNull, validateString} from "../common/validators";
|
||||||
|
|
||||||
|
export class Shutter {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly uuid: string,
|
||||||
|
readonly name: string,
|
||||||
|
readonly slug: string,
|
||||||
|
readonly positionPropertyId: string,
|
||||||
|
readonly positionProperty: Property | null,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Shutter {
|
||||||
|
return new Shutter(
|
||||||
|
validateString(json.uuid),
|
||||||
|
validateString(json.name),
|
||||||
|
validateString(json.slug),
|
||||||
|
validateString(json.positionPropertyId),
|
||||||
|
orNull(json.positionProperty, Property.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static trackBy(index: number, shutter: Shutter) {
|
||||||
|
return shutter.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
5
src/main/angular/src/app/api/Shutter/ShutterFilter.ts
Normal file
5
src/main/angular/src/app/api/Shutter/ShutterFilter.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class ShutterFilter {
|
||||||
|
|
||||||
|
search: string = "";
|
||||||
|
|
||||||
|
}
|
||||||
32
src/main/angular/src/app/api/Shutter/shutter.service.ts
Normal file
32
src/main/angular/src/app/api/Shutter/shutter.service.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {CrudService} from '../common/CrudService';
|
||||||
|
import {Shutter} from './Shutter';
|
||||||
|
import {ApiService} from '../common/api.service';
|
||||||
|
import {Next} from '../common/types';
|
||||||
|
|
||||||
|
import {ShutterFilter} from './ShutterFilter';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ShutterService extends CrudService<Shutter> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
api: ApiService,
|
||||||
|
) {
|
||||||
|
super(api, ['Shutter'], Shutter.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
getByUuid(uuid: string, next: Next<Shutter>): void {
|
||||||
|
this.getSingle(['getByUuid', uuid], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
list(filter: ShutterFilter | null, next: Next<Shutter[]>): void {
|
||||||
|
this.postList(['list'], filter, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition(shutter: Shutter, position: number, next?: Next<void>): void {
|
||||||
|
this.getNone(['setPosition', shutter.uuid, position], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
src/main/angular/src/app/api/State/State.ts
Normal file
23
src/main/angular/src/app/api/State/State.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {validateDate, validateString} from "../common/validators";
|
||||||
|
|
||||||
|
export class State {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly type: string,
|
||||||
|
readonly timestamp: Date,
|
||||||
|
readonly value: any,
|
||||||
|
readonly string: string,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): State {
|
||||||
|
return new State(
|
||||||
|
validateString(json.type),
|
||||||
|
validateDate(json.timestamp),
|
||||||
|
json.value,
|
||||||
|
validateString(json.string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
4
src/main/angular/src/app/api/common/CrudDirection.ts
Normal file
4
src/main/angular/src/app/api/common/CrudDirection.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum CrudDirection {
|
||||||
|
ASC = 'ASC',
|
||||||
|
DESC = 'DESC',
|
||||||
|
}
|
||||||
52
src/main/angular/src/app/api/common/CrudService.ts
Normal file
52
src/main/angular/src/app/api/common/CrudService.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import {ApiService} from "./api.service";
|
||||||
|
import {FromJson, Next} from "./types";
|
||||||
|
import {Page} from "./Page";
|
||||||
|
import {Subscription} from "rxjs";
|
||||||
|
|
||||||
|
export abstract class CrudService<ENTITY> {
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
readonly api: ApiService,
|
||||||
|
readonly path: any[],
|
||||||
|
readonly fromJson: FromJson<ENTITY>,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(next: Next<ENTITY>): Subscription {
|
||||||
|
return this.api.subscribe([...this.path], this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getNone(path: any[], next?: Next<void>): void {
|
||||||
|
this.api.getNone([...this.path, ...path], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getSingle(path: any[], next?: Next<ENTITY>): void {
|
||||||
|
this.api.getSingle([...this.path, ...path], this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getList(path: any[], next?: Next<ENTITY[]>): void {
|
||||||
|
this.api.getList([...this.path, ...path], this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPage(path: any[], next?: Next<Page<ENTITY>>): void {
|
||||||
|
this.api.getPage([...this.path, ...path], this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected postNone(path: any[], data: any, next?: Next<void>): void {
|
||||||
|
this.api.postNone([...this.path, ...path], data, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected postSingle(path: any[], data: any, next?: Next<ENTITY>): void {
|
||||||
|
this.api.postSingle([...this.path, ...path], data, this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected postList(path: any[], data: any, next?: Next<ENTITY[]>): void {
|
||||||
|
this.api.postList([...this.path, ...path], data, this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected postPage(path: any[], data: any, next?: Next<Page<ENTITY>>): void {
|
||||||
|
this.api.postPage([...this.path, ...path], data, this.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
src/main/angular/src/app/api/common/Order.ts
Normal file
13
src/main/angular/src/app/api/common/Order.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {CrudDirection} from "./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/common/Page.ts
Normal file
28
src/main/angular/src/app/api/common/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/common/Sort.ts
Normal file
65
src/main/angular/src/app/api/common/Sort.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {CrudDirection} from "./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 = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
79
src/main/angular/src/app/api/common/api.service.ts
Normal file
79
src/main/angular/src/app/api/common/api.service.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {map, Subscription} from "rxjs";
|
||||||
|
import {FromJson, getApiUrl, 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>): Subscription {
|
||||||
|
return this.http.get<void>(getApiUrl('http', path), {withCredentials: true}).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
getString(path: any[], next: Next<string>): Subscription {
|
||||||
|
return this.http.get(getApiUrl('http', path), {responseType: "text", withCredentials: true}).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSingle<T>(path: any[], fromJson: FromJson<T>, next?: Next<T>): Subscription {
|
||||||
|
return this.http.get<any>(getApiUrl('http', path), {withCredentials: true}).pipe(map(fromJson)).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPage<T>(path: any[], fromJson: FromJson<T>, next: Next<Page<T>> | undefined = undefined): Subscription {
|
||||||
|
return this.http.get<any>(getApiUrl('http', path), {withCredentials: true}).pipe(map(Page.fromJson(fromJson))).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
getList<T>(path: any[], fromJson: FromJson<T>, next: Next<T[]> | undefined = undefined): Subscription {
|
||||||
|
return this.http.get<any[]>(getApiUrl('http', path), {withCredentials: true}).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
postNone(path: any[], data: any, next?: Next<void>): Subscription {
|
||||||
|
return this.http.post<void>(getApiUrl('http', path), data, {withCredentials: true}).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
postSingle<T>(path: any[], data: any, fromJson: FromJson<T>, next?: Next<T>): Subscription {
|
||||||
|
return this.http.post<any>(getApiUrl('http', path), data, {withCredentials: true}).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>(getApiUrl('http', path), data, {withCredentials: true}).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[]>(getApiUrl('http', path), data, {withCredentials: true}).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
40
src/main/angular/src/app/api/common/relative.pipe.ts
Normal file
40
src/main/angular/src/app/api/common/relative.pipe.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'relative',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class RelativePipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(date: Date | undefined | null, now: Date, future: string = 'In', past: string = 'Seit', nowStr: string = 'Gerade eben'): string | null | undefined {
|
||||||
|
if (date === undefined || date === null) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = past;
|
||||||
|
let totalMillis = now.getTime() - date.getTime();
|
||||||
|
if (totalMillis < 0) {
|
||||||
|
totalMillis = -totalMillis;
|
||||||
|
prefix = future;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSeconds = Math.floor(totalMillis / 1000);
|
||||||
|
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||||
|
const totalHours = Math.floor(totalMinutes / 60);
|
||||||
|
const days = Math.floor(totalHours / 24);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
const minutes = totalMinutes % 60;
|
||||||
|
const hours = totalHours % 24;
|
||||||
|
if (totalSeconds < 1) {
|
||||||
|
return nowStr;
|
||||||
|
} else if (days > 0) {
|
||||||
|
return `${prefix} ${days} Tag${days === 1 ? '' : 'en'}`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${prefix} ${hours} Stunde${hours === 1 ? '' : 'n'}`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${prefix} ${minutes} Minute${minutes === 1 ? '' : 'n'}`;
|
||||||
|
}
|
||||||
|
return `${prefix} ${seconds} Sekunde${seconds === 1 ? '' : 'n'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
src/main/angular/src/app/api/common/types.ts
Normal file
9
src/main/angular/src/app/api/common/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 getApiUrl(protocol: string, path: any[]): string {
|
||||||
|
return protocol + (environment.secure ? 's' : '') + '://' + environment.host + ':' + environment.port + '/' + environment.base + path.join('/');
|
||||||
|
}
|
||||||
80
src/main/angular/src/app/api/common/validators.ts
Normal file
80
src/main/angular/src/app/api/common/validators.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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 || json === undefined) {
|
||||||
|
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 || json === undefined) {
|
||||||
|
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 || json === undefined) {
|
||||||
|
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 || json === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validateDate(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateListIgnoreNullItems<T>(jsonList: any, fromJson: FromJson<T | null>): T[] {
|
||||||
|
return jsonList.map(fromJson).filter((item: T | null) => item !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateList<T>(jsonList: any, fromJson: FromJson<T>): T[] {
|
||||||
|
return jsonList.map(fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLocalDateOrNull(json: any): Date | null {
|
||||||
|
if (json === null || json === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validateLocalDate(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLocalDate(json: any): Date {
|
||||||
|
return new Date(validateString(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orNull<T, R>(item: T | null | undefined, map: (t: T) => R): R | null {
|
||||||
|
if (item === null || item === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return map(item);
|
||||||
|
}
|
||||||
17
src/main/angular/src/app/api/common/ws.ts
Normal file
17
src/main/angular/src/app/api/common/ws.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {StompService} from "@stomp/ng2-stompjs";
|
||||||
|
import {getApiUrl} from "./types";
|
||||||
|
|
||||||
|
export function stompServiceFactory() {
|
||||||
|
const stomp = new StompService({
|
||||||
|
url: getApiUrl('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;
|
||||||
|
}
|
||||||
16
src/main/angular/src/app/app.component.html
Normal file
16
src/main/angular/src/app/app.component.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<div class="flexBox">
|
||||||
|
<div class="flexBoxFixed menu">
|
||||||
|
<div class="item itemLeft" routerLink="DeviceList" routerLinkActive="active">Geräte</div>
|
||||||
|
<div class="item itemLeft" routerLink="ShutterList" routerLinkActive="active">Rollläden</div>
|
||||||
|
<div class="item itemRight" routerLink="GroupList" routerLinkActive="active">KNX</div>
|
||||||
|
</div>
|
||||||
|
<div class="flexBoxRest">
|
||||||
|
<router-outlet/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="notConnected" *ngIf="apiService.websocketError">
|
||||||
|
<div>
|
||||||
|
Nicht verbunden
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
43
src/main/angular/src/app/app.component.less
Normal file
43
src/main/angular/src/app/app.component.less
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
@import "../config";
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
border-bottom: @border solid black;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: @space;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemLeft {
|
||||||
|
float: left;
|
||||||
|
border-right: @border solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemRight {
|
||||||
|
float: right;
|
||||||
|
border-left: @border solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: lightskyblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#notConnected {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
border: @space solid red;
|
||||||
|
color: red;
|
||||||
|
|
||||||
|
div {
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
font-size: 200%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/angular/src/app/app.component.ts
Normal file
22
src/main/angular/src/app/app.component.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||||
|
import {ApiService} from './api/common/api.service';
|
||||||
|
import {NgIf} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [RouterOutlet, RouterLink, RouterLinkActive, NgIf],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.less'
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'angular';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly apiService: ApiService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
src/main/angular/src/app/app.config.ts
Normal file
22
src/main/angular/src/app/app.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||||
|
import {provideRouter} from '@angular/router';
|
||||||
|
|
||||||
|
import {routes} from './app.routes';
|
||||||
|
import {provideHttpClient} from '@angular/common/http';
|
||||||
|
import {registerLocaleData} from '@angular/common';
|
||||||
|
|
||||||
|
import localeDe from '@angular/common/locales/de';
|
||||||
|
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||||
|
import {stompServiceFactory} from './api/common/ws';
|
||||||
|
import {StompService} from '@stomp/ng2-stompjs';
|
||||||
|
|
||||||
|
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideZoneChangeDetection({eventCoalescing: true}),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(),
|
||||||
|
{provide: StompService, useFactory: stompServiceFactory},
|
||||||
|
]
|
||||||
|
};
|
||||||
11
src/main/angular/src/app/app.routes.ts
Normal file
11
src/main/angular/src/app/app.routes.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {Routes} from '@angular/router';
|
||||||
|
import {KnxGroupListPageComponent} from './pages/knx-group-list-page/knx-group-list-page.component';
|
||||||
|
import {DeviceListPageComponent} from './pages/device-list-page/device-list-page.component';
|
||||||
|
import {ShutterListPageComponent} from './pages/shutter-list-page/shutter-list-page.component';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{path: 'DeviceList', component: DeviceListPageComponent},
|
||||||
|
{path: 'ShutterList', component: ShutterListPageComponent},
|
||||||
|
{path: 'GroupList', component: KnxGroupListPageComponent},
|
||||||
|
{path: '**', redirectTo: 'GroupList'},
|
||||||
|
];
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<div class="flexBox">
|
||||||
|
<div class="flexBoxFixed">
|
||||||
|
<input [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
|
||||||
|
</div>
|
||||||
|
<div class="flexBoxRest">
|
||||||
|
<app-device-list [deviceList]="deviceList"></app-device-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
@import "../../../config";
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-bottom: @border solid lightgray;
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {DeviceListComponent} from '../../shared/device-list/device-list.component';
|
||||||
|
import {Device} from '../../api/Device/Device';
|
||||||
|
import {DeviceService} from '../../api/Device/device.service';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {DeviceFilter} from '../../api/Device/DeviceFilter';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {KnxGroupListComponent} from '../../shared/knx-group-list/knx-group-list.component';
|
||||||
|
import {ApiService} from '../../api/common/api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-device-list-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
DeviceListComponent,
|
||||||
|
FormsModule,
|
||||||
|
KnxGroupListComponent
|
||||||
|
],
|
||||||
|
templateUrl: './device-list-page.component.html',
|
||||||
|
styleUrl: './device-list-page.component.less'
|
||||||
|
})
|
||||||
|
export class DeviceListPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
protected deviceList: Device[] = [];
|
||||||
|
|
||||||
|
protected filter: DeviceFilter = new DeviceFilter();
|
||||||
|
|
||||||
|
private fetchTimeout: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly deviceService: DeviceService,
|
||||||
|
protected readonly apiService: ApiService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.fetch();
|
||||||
|
this.subs.push(this.deviceService.subscribe(device => this.updateDevice(device)));
|
||||||
|
this.apiService.connected(() => this.fetch());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDelayed() {
|
||||||
|
if (this.fetchTimeout) {
|
||||||
|
clearTimeout(this.fetchTimeout);
|
||||||
|
this.fetchTimeout = undefined;
|
||||||
|
}
|
||||||
|
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetch() {
|
||||||
|
this.deviceService.list(this.filter, list => this.deviceList = list)
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateDevice(device: Device) {
|
||||||
|
const index = this.deviceList.findIndex(d => d.uuid === device.uuid);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.deviceList.splice(index, 1, device);
|
||||||
|
} else {
|
||||||
|
this.fetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<div class="flexBox">
|
||||||
|
<div class="flexBoxFixed">
|
||||||
|
<input [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
|
||||||
|
</div>
|
||||||
|
<div class="flexBoxRest">
|
||||||
|
<app-knx-group-list [groupList]="groupList"></app-knx-group-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
@import "../../../config";
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-bottom: @border solid lightgray;
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {KnxGroupListComponent} from '../../shared/knx-group-list/knx-group-list.component';
|
||||||
|
import {Group} from '../../api/Group/Group';
|
||||||
|
import {GroupService} from '../../api/Group/group.service';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {GroupFilter} from '../../api/Group/GroupFilter';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {ApiService} from '../../api/common/api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-knx-group-list-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
KnxGroupListComponent,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './knx-group-list-page.component.html',
|
||||||
|
styleUrl: './knx-group-list-page.component.less'
|
||||||
|
})
|
||||||
|
export class KnxGroupListPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
protected groupList: Group[] = [];
|
||||||
|
|
||||||
|
protected filter: GroupFilter = new GroupFilter();
|
||||||
|
|
||||||
|
private fetchTimeout: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly groupService: GroupService,
|
||||||
|
protected readonly apiService: ApiService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.fetch();
|
||||||
|
this.subs.push(this.groupService.subscribe(group => this.updateGroup(group)));
|
||||||
|
this.apiService.connected(() => this.fetch());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDelayed() {
|
||||||
|
if (this.fetchTimeout) {
|
||||||
|
clearTimeout(this.fetchTimeout);
|
||||||
|
this.fetchTimeout = undefined;
|
||||||
|
}
|
||||||
|
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetch() {
|
||||||
|
this.groupService.list(this.filter, list => this.groupList = list)
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateGroup(group: Group) {
|
||||||
|
const index = this.groupList.findIndex(g => g.address === group.address);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.groupList.splice(index, 1, group);
|
||||||
|
} else {
|
||||||
|
this.fetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<div class="flexBox">
|
||||||
|
<div class="flexBoxFixed">
|
||||||
|
<input [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
|
||||||
|
</div>
|
||||||
|
<div class="flexBoxRest">
|
||||||
|
<app-shutter-list [shutterList]="shutterList"></app-shutter-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
@import "../../../config";
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-bottom: @border solid lightgray;
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {ShutterListComponent} from '../../shared/shutter-list/shutter-list.component';
|
||||||
|
import {Shutter} from '../../api/Shutter/Shutter';
|
||||||
|
import {ShutterService} from '../../api/Shutter/shutter.service';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {ShutterFilter} from '../../api/Shutter/ShutterFilter';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {KnxGroupListComponent} from '../../shared/knx-group-list/knx-group-list.component';
|
||||||
|
import {ApiService} from '../../api/common/api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-shutter-list-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ShutterListComponent,
|
||||||
|
FormsModule,
|
||||||
|
KnxGroupListComponent
|
||||||
|
],
|
||||||
|
templateUrl: './shutter-list-page.component.html',
|
||||||
|
styleUrl: './shutter-list-page.component.less'
|
||||||
|
})
|
||||||
|
export class ShutterListPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
protected shutterList: Shutter[] = [];
|
||||||
|
|
||||||
|
protected filter: ShutterFilter = new ShutterFilter();
|
||||||
|
|
||||||
|
private fetchTimeout: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly shutterService: ShutterService,
|
||||||
|
protected readonly apiService: ApiService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.fetch();
|
||||||
|
this.subs.push(this.shutterService.subscribe(shutter => this.updateShutter(shutter)));
|
||||||
|
this.apiService.connected(() => this.fetch());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDelayed() {
|
||||||
|
if (this.fetchTimeout) {
|
||||||
|
clearTimeout(this.fetchTimeout);
|
||||||
|
this.fetchTimeout = undefined;
|
||||||
|
}
|
||||||
|
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetch() {
|
||||||
|
this.shutterService.list(this.filter, list => this.shutterList = list)
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateShutter(shutter: Shutter) {
|
||||||
|
const index = this.shutterList.findIndex(d => d.uuid === shutter.uuid);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.shutterList.splice(index, 1, shutter);
|
||||||
|
} else {
|
||||||
|
this.fetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<div class="deviceList tileContainer">
|
||||||
|
|
||||||
|
<div class="tile" *ngFor="let device of deviceList; trackBy: Device.trackBy">
|
||||||
|
|
||||||
|
<div class="device tileInner" [ngClass]="ngClass(device)">
|
||||||
|
|
||||||
|
<div class="name">
|
||||||
|
{{ device.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<div class="switchOn" (click)="deviceService.setState(device, true)"></div>
|
||||||
|
<div class="switchOff" (click)="deviceService.setState(device, false)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timestamp details">
|
||||||
|
{{ device.stateProperty?.lastValueChange | relative:now }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
@import "../../../config";
|
||||||
|
|
||||||
|
.deviceList {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.device {
|
||||||
|
|
||||||
|
.name {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
clear: left;
|
||||||
|
float: left;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
div {
|
||||||
|
float: left;
|
||||||
|
margin-left: @space;
|
||||||
|
width: 4em;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchOn {
|
||||||
|
//noinspection CssUnknownTarget
|
||||||
|
background-image: url("/switchOn.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchOff {
|
||||||
|
//noinspection CssUnknownTarget
|
||||||
|
background-image: url("/switchOff.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {NgClass, NgForOf} from '@angular/common';
|
||||||
|
import {Device} from '../../api/Device/Device';
|
||||||
|
import {DeviceService} from '../../api/Device/device.service';
|
||||||
|
import {RelativePipe} from '../../api/common/relative.pipe';
|
||||||
|
import {Subscription, timer} from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-device-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
NgClass,
|
||||||
|
RelativePipe
|
||||||
|
],
|
||||||
|
templateUrl: './device-list.component.html',
|
||||||
|
styleUrl: './device-list.component.less'
|
||||||
|
})
|
||||||
|
export class DeviceListComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly Device = Device;
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
protected now: Date = new Date();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
deviceList: Device[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly deviceService: DeviceService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.now = new Date();
|
||||||
|
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngClass(device: Device) {
|
||||||
|
return {
|
||||||
|
"stateOn": device.stateProperty?.state?.value === true,
|
||||||
|
"stateOff": device.stateProperty?.state?.value === false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<div class="groupList tileContainer">
|
||||||
|
|
||||||
|
<div class="tile" *ngFor="let group of groupList; trackBy: Group.trackBy">
|
||||||
|
|
||||||
|
<div class="group tileInner" [ngClass]="ngClass(group)">
|
||||||
|
|
||||||
|
<div class="name">
|
||||||
|
{{ group.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details">
|
||||||
|
|
||||||
|
<div class="stackLeft address">
|
||||||
|
{{ group.address }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stackLeft dpt">
|
||||||
|
DPT {{ group.dpt }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stackRight state">
|
||||||
|
{{ group.state?.string || '-' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stackRight timestamp">
|
||||||
|
{{ group.lastValueChange | relative:now }}:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
@import "../../../config";
|
||||||
|
|
||||||
|
.groupList {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.group {
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin-bottom: @space;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {NgClass, NgForOf} from '@angular/common';
|
||||||
|
import {Group} from '../../api/Group/Group';
|
||||||
|
import {RelativePipe} from '../../api/common/relative.pipe';
|
||||||
|
import {Subscription, timer} from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-knx-group-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
NgClass,
|
||||||
|
RelativePipe
|
||||||
|
],
|
||||||
|
templateUrl: './knx-group-list.component.html',
|
||||||
|
styleUrl: './knx-group-list.component.less'
|
||||||
|
})
|
||||||
|
export class KnxGroupListComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly Group = Group;
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
protected now: Date = new Date();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
groupList: Group[] = [];
|
||||||
|
|
||||||
|
ngClass(group: Group) {
|
||||||
|
return {
|
||||||
|
"stateOn": group.state?.value === true,
|
||||||
|
"stateOff": group.state?.value === false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.now = new Date();
|
||||||
|
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<div class="window" (click)="activate.emit(position)">
|
||||||
|
<div class="shutter" [style.height]="position + '%'">
|
||||||
|
<!-- -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
@import "../../../../config";
|
||||||
|
|
||||||
|
.window {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: lightskyblue;
|
||||||
|
border: @border solid black;
|
||||||
|
|
||||||
|
.shutter {
|
||||||
|
background-color: saddlebrown;
|
||||||
|
border-bottom: @border solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-shutter-icon',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './shutter-icon.component.html',
|
||||||
|
styleUrl: './shutter-icon.component.less'
|
||||||
|
})
|
||||||
|
export class ShutterIconComponent {
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
position?: number
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
activate: EventEmitter<number> = new EventEmitter();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<div class="shutterList tileContainer">
|
||||||
|
|
||||||
|
<div class="tile" *ngFor="let shutter of shutterList; trackBy: Shutter.trackBy">
|
||||||
|
|
||||||
|
<div class="shutter tileInner">
|
||||||
|
|
||||||
|
<div class="name">
|
||||||
|
{{ shutter.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="icon">
|
||||||
|
<app-shutter-icon [position]="shutter.positionProperty?.state?.value"></app-shutter-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timestamp details">
|
||||||
|
{{ shutter.positionProperty?.lastValueChange | relative:now }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<div class="action">
|
||||||
|
<app-shutter-icon [position]="0" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<app-shutter-icon [position]="50" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<app-shutter-icon [position]="80" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<app-shutter-icon [position]="90" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<app-shutter-icon [position]="100" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
@import "../../../config";
|
||||||
|
|
||||||
|
.shutterList {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.shutter {
|
||||||
|
|
||||||
|
.name {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
clear: left;
|
||||||
|
float: left;
|
||||||
|
width: 4em;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
float: right;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
clear: right;
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
div {
|
||||||
|
float: left;
|
||||||
|
margin-left: @space;
|
||||||
|
width: 3em;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {NgClass, NgForOf} from '@angular/common';
|
||||||
|
import {Shutter} from '../../api/Shutter/Shutter';
|
||||||
|
import {ShutterService} from '../../api/Shutter/shutter.service';
|
||||||
|
import {RelativePipe} from '../../api/common/relative.pipe';
|
||||||
|
import {Subscription, timer} from 'rxjs';
|
||||||
|
import {ShutterIconComponent} from './shutter-icon/shutter-icon.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-shutter-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
NgClass,
|
||||||
|
RelativePipe,
|
||||||
|
ShutterIconComponent
|
||||||
|
],
|
||||||
|
templateUrl: './shutter-list.component.html',
|
||||||
|
styleUrl: './shutter-list.component.less'
|
||||||
|
})
|
||||||
|
export class ShutterListComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly Shutter = Shutter;
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
protected now: Date = new Date();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
shutterList: Shutter[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly shutterService: ShutterService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.now = new Date();
|
||||||
|
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
110
src/main/angular/src/config.less
Normal file
110
src/main/angular/src/config.less
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
@space: 0.4em;
|
||||||
|
@border: 0.05em;
|
||||||
|
|
||||||
|
.flexBox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.flexBoxFixed {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexBoxRest {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
font-size: 80%;
|
||||||
|
color: gray;
|
||||||
|
|
||||||
|
.stackLeft {
|
||||||
|
float: left;
|
||||||
|
margin-right: @space;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stackRight {
|
||||||
|
float: right;
|
||||||
|
margin-left: @space;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.stateOn {
|
||||||
|
background-color: palegreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stateOff {
|
||||||
|
background-color: indianred;
|
||||||
|
|
||||||
|
.details {
|
||||||
|
color: #575757;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tileContainer {
|
||||||
|
padding: calc(@space / 2);
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
padding: calc(@space / 2);
|
||||||
|
|
||||||
|
.tileInner {
|
||||||
|
padding: @space;
|
||||||
|
border: @border solid lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
|
||||||
|
.tileContainer {
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1300px) {
|
||||||
|
|
||||||
|
.tileContainer {
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
width: 33.33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1700px) {
|
||||||
|
|
||||||
|
.tileContainer {
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 2100px) {
|
||||||
|
|
||||||
|
.tileContainer {
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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: 'Home/',
|
||||||
|
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="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Angular</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<!--suppress HtmlUnknownTarget -->
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</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));
|
||||||
33
src/main/angular/src/styles.less
Normal file
33
src/main/angular/src/styles.less
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@import "./config";
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 4vmin;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
all: unset;
|
||||||
|
width: calc(100% - 2 * 0.2em - @border);
|
||||||
|
padding-left: 0.2em;
|
||||||
|
padding-right: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package de.ph87.home.common.crud;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public abstract class AbstractSearchFilter implements ISearch {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@JsonProperty
|
||||||
|
private String search;
|
||||||
|
|
||||||
|
}
|
||||||
5
src/main/java/de/ph87/home/common/crud/CrudAction.java
Normal file
5
src/main/java/de/ph87/home/common/crud/CrudAction.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.ph87.home.common.crud;
|
||||||
|
|
||||||
|
public enum CrudAction {
|
||||||
|
CREATED, UPDATED, DELETED
|
||||||
|
}
|
||||||
14
src/main/java/de/ph87/home/common/crud/EntityNotFound.java
Normal file
14
src/main/java/de/ph87/home/common/crud/EntityNotFound.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package de.ph87.home.common.crud;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
public class EntityNotFound extends RuntimeException {
|
||||||
|
|
||||||
|
public EntityNotFound(@NonNull final String key, @NonNull final Object value) {
|
||||||
|
super("Entity not found: %s=%s".formatted(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
src/main/java/de/ph87/home/common/crud/ISearch.java
Normal file
34
src/main/java/de/ph87/home/common/crud/ISearch.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package de.ph87.home.common.crud;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public interface ISearch {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String getSearch();
|
||||||
|
|
||||||
|
default boolean search(@NonNull final String... fields) {
|
||||||
|
final String term = getSearch();
|
||||||
|
if (term == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final List<String> haystack = Arrays.stream(fields).map(String::toString).map(String::toLowerCase).toList();
|
||||||
|
return splitWords(term).allMatch(word -> anyMatch(word, haystack));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
default Stream<String> splitWords(@NonNull final String term) {
|
||||||
|
return Arrays.stream(term.toLowerCase(Locale.ROOT).replaceAll("^\\s|\\s$", "").split("\\s+"));
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean anyMatch(@NonNull final String needle, @NonNull final List<String> haystack) {
|
||||||
|
return haystack.stream().anyMatch(lowerCaseHayStack -> lowerCaseHayStack.contains(needle));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.ph87.home.common.json;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ClassSimpleNameSerializer extends JsonSerializer<Class<?>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(final Class<?> aClass, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException {
|
||||||
|
if (aClass == null) {
|
||||||
|
jsonGenerator.writeNull();
|
||||||
|
} else {
|
||||||
|
jsonGenerator.writeString(aClass.getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package de.ph87.home.common;
|
package de.ph87.home.common.json;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
@ -9,7 +9,7 @@ import java.time.Instant;
|
|||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
public class EpochSecondsToZonedDateTime extends JsonDeserializer<ZonedDateTime> {
|
public class EpochSecondsToZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ZonedDateTime deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
|
public ZonedDateTime deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package de.ph87.home.common;
|
package de.ph87.home.common.json;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
public class SecondsToDuration extends JsonDeserializer<Duration> {
|
public class SecondsToDurationDeserializer extends JsonDeserializer<Duration> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Duration deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
|
public Duration deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
|
||||||
@ -3,6 +3,7 @@ package de.ph87.home.demo;
|
|||||||
import de.ph87.home.device.DeviceService;
|
import de.ph87.home.device.DeviceService;
|
||||||
import de.ph87.home.knx.property.KnxPropertyService;
|
import de.ph87.home.knx.property.KnxPropertyService;
|
||||||
import de.ph87.home.knx.property.KnxPropertyType;
|
import de.ph87.home.knx.property.KnxPropertyType;
|
||||||
|
import de.ph87.home.shutter.ShutterService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
@ -21,18 +22,42 @@ public class DemoService {
|
|||||||
|
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
|
private final ShutterService shutterService;
|
||||||
|
|
||||||
@EventListener(ApplicationStartedEvent.class)
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
public void startup() {
|
public void startup() {
|
||||||
knxPropertyService.create("fernseher", KnxPropertyType.BOOLEAN, adr(20), adr(4));
|
knxPropertyService.create("eg_ambiente", KnxPropertyType.BOOLEAN, adr(849), adr(848));
|
||||||
knxPropertyService.create("verstaerker", KnxPropertyType.BOOLEAN, adr(825), adr(824));
|
|
||||||
knxPropertyService.create("receiver", KnxPropertyType.BOOLEAN, adr(2561), adr(2560));
|
|
||||||
|
|
||||||
deviceService.create("EG Ambiente", "eg_ambiente", "eg_ambiente");
|
deviceService.create("EG Ambiente", "eg_ambiente", "eg_ambiente");
|
||||||
|
|
||||||
|
knxPropertyService.create("fernseher", KnxPropertyType.BOOLEAN, adr(20), adr(4));
|
||||||
deviceService.create("Wohnzimmer Fernseher", "fernseher", "fernseher");
|
deviceService.create("Wohnzimmer Fernseher", "fernseher", "fernseher");
|
||||||
|
|
||||||
|
knxPropertyService.create("verstaerker", KnxPropertyType.BOOLEAN, adr(825), adr(824));
|
||||||
deviceService.create("Wohnzimmer Verstärker", "verstaerker", "verstaerker");
|
deviceService.create("Wohnzimmer Verstärker", "verstaerker", "verstaerker");
|
||||||
deviceService.create("Wohnzimmer Fensterdeko", "fensterdeko", "fensterdeko");
|
|
||||||
|
knxPropertyService.create("fensterdeko", KnxPropertyType.BOOLEAN, adr(1823), adr(1822));
|
||||||
|
deviceService.create("Wohnzimmer Fenster", "fensterdeko", "fensterdeko");
|
||||||
|
|
||||||
|
knxPropertyService.create("haengelampe", KnxPropertyType.BOOLEAN, adr(1794), adr(1799));
|
||||||
deviceService.create("Wohnzimmer Hängelampe", "haengelampe", "haengelampe");
|
deviceService.create("Wohnzimmer Hängelampe", "haengelampe", "haengelampe");
|
||||||
|
|
||||||
|
knxPropertyService.create("receiver", KnxPropertyType.BOOLEAN, adr(2561), adr(2560));
|
||||||
deviceService.create("Receiver", "receiver", "receiver");
|
deviceService.create("Receiver", "receiver", "receiver");
|
||||||
|
|
||||||
|
knxPropertyService.create("wohnzimmer_links", KnxPropertyType.DOUBLE, adr(1048), adr(1048));
|
||||||
|
shutterService.create("Wohnzimmer Links", "wohnzimmer_links", "wohnzimmer_links");
|
||||||
|
|
||||||
|
knxPropertyService.create("wohnzimmer_rechts", KnxPropertyType.DOUBLE, adr(1811), adr(1811));
|
||||||
|
shutterService.create("Wohnzimmer Rechts", "wohnzimmer_rechts", "wohnzimmer_rechts");
|
||||||
|
|
||||||
|
knxPropertyService.create("kueche_seite", KnxPropertyType.DOUBLE, adr(2316), adr(2316));
|
||||||
|
shutterService.create("Küche Seite", "kueche_seite", "kueche_seite");
|
||||||
|
|
||||||
|
knxPropertyService.create("kueche_theke", KnxPropertyType.DOUBLE, adr(2320), adr(2320));
|
||||||
|
shutterService.create("Küche Theke", "kueche_theke", "kueche_theke");
|
||||||
|
|
||||||
|
knxPropertyService.create("kueche_tuer", KnxPropertyType.DOUBLE, adr(2324), adr(2324));
|
||||||
|
shutterService.create("Küche Tür", "kueche_tuer", "kueche_tuer");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GroupAddress adr(final int rawGroupAddress) {
|
private static GroupAddress adr(final int rawGroupAddress) {
|
||||||
|
|||||||
@ -28,12 +28,12 @@ public class Device {
|
|||||||
@Setter
|
@Setter
|
||||||
@NonNull
|
@NonNull
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String stateProperty;
|
private String statePropertyId;
|
||||||
|
|
||||||
public Device(@NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty) {
|
public Device(@NonNull final String name, @NonNull final String slug, @NonNull final String statePropertyId) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.slug = slug;
|
this.slug = slug;
|
||||||
this.stateProperty = stateProperty;
|
this.statePropertyId = statePropertyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import lombok.NonNull;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tuwien.auto.calimero.KNXFormatException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -20,6 +21,14 @@ public class DeviceController {
|
|||||||
|
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@GetMapping("getByUuid/{id}")
|
||||||
|
@ExceptionHandler(KNXFormatException.class)
|
||||||
|
private DeviceDto getByUuid(@PathVariable final String id, @NonNull final HttpServletRequest request) {
|
||||||
|
log.debug("getByUuid: path={}", request.getServletPath());
|
||||||
|
return deviceService.getByUuidDto(id);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@RequestMapping(value = "list", method = {RequestMethod.GET, RequestMethod.POST})
|
@RequestMapping(value = "list", method = {RequestMethod.GET, RequestMethod.POST})
|
||||||
private List<DeviceDto> list(@RequestBody(required = false) @Nullable final DeviceFilter filter, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch {
|
private List<DeviceDto> list(@RequestBody(required = false) @Nullable final DeviceFilter filter, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch {
|
||||||
@ -29,20 +38,20 @@ public class DeviceController {
|
|||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@GetMapping("get/{uuidOrSlug}")
|
@GetMapping("get/{uuidOrSlug}")
|
||||||
private DeviceDto get(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws DeviceNotFound {
|
private DeviceDto get(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) {
|
||||||
log.debug("get: path={}", request.getServletPath());
|
log.debug("get: path={}", request.getServletPath());
|
||||||
return deviceService.toDto(uuidOrSlug);
|
return deviceService.getByUuidOrSlugDto(uuidOrSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@GetMapping("getState/{uuidOrSlug}")
|
@GetMapping("getState/{uuidOrSlug}")
|
||||||
private Boolean getState(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws DeviceNotFound, PropertyTypeMismatch {
|
private Boolean getState(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch {
|
||||||
log.debug("getState: path={}", request.getServletPath());
|
log.debug("getState: path={}", request.getServletPath());
|
||||||
return deviceService.toDto(uuidOrSlug).getStateValue();
|
return deviceService.getByUuidOrSlugDto(uuidOrSlug).getStateValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("setState/{uuidOrSlug}/{state}")
|
@GetMapping("setState/{uuidOrSlug}/{state}")
|
||||||
private void setState(@PathVariable @NonNull final String uuidOrSlug, @PathVariable final boolean state, @NonNull final HttpServletRequest request) throws PropertyNotFound, DeviceNotFound, PropertyNotWritable, PropertyTypeMismatch {
|
private void setState(@PathVariable @NonNull final String uuidOrSlug, @PathVariable final boolean state, @NonNull final HttpServletRequest request) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch {
|
||||||
log.debug("setState: path={}", request.getServletPath());
|
log.debug("setState: path={}", request.getServletPath());
|
||||||
deviceService.setState(uuidOrSlug, state);
|
deviceService.setState(uuidOrSlug, state);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,20 @@ package de.ph87.home.device;
|
|||||||
|
|
||||||
import de.ph87.home.property.PropertyDto;
|
import de.ph87.home.property.PropertyDto;
|
||||||
import de.ph87.home.property.PropertyTypeMismatch;
|
import de.ph87.home.property.PropertyTypeMismatch;
|
||||||
|
import de.ph87.home.web.IWebSocketMessage;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString
|
@ToString
|
||||||
public class DeviceDto {
|
public class DeviceDto implements IWebSocketMessage {
|
||||||
|
|
||||||
|
@ToString.Exclude
|
||||||
|
private final List<Object> websocketTopic = List.of("Device");
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String uuid;
|
private final String uuid;
|
||||||
@ -21,18 +27,18 @@ public class DeviceDto {
|
|||||||
private final String slug;
|
private final String slug;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String stateConfig;
|
private final String statePropertyId;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private final PropertyDto<Boolean> state;
|
private final PropertyDto<Boolean> stateProperty;
|
||||||
|
|
||||||
public DeviceDto(@NonNull final Device device, @Nullable final PropertyDto<Boolean> state) {
|
public DeviceDto(@NonNull final Device device, @Nullable final PropertyDto<Boolean> stateProperty) {
|
||||||
this.uuid = device.getUuid();
|
this.uuid = device.getUuid();
|
||||||
this.name = device.getName();
|
this.name = device.getName();
|
||||||
this.slug = device.getSlug();
|
this.slug = device.getSlug();
|
||||||
this.stateConfig = device.getStateProperty();
|
this.statePropertyId = device.getStatePropertyId();
|
||||||
this.state = state;
|
this.stateProperty = stateProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -47,10 +53,10 @@ public class DeviceDto {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Boolean getStateValue() throws PropertyTypeMismatch {
|
public Boolean getStateValue() throws PropertyTypeMismatch {
|
||||||
if (state == null) {
|
if (stateProperty == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return state.getStateValueAs(Boolean.class);
|
return stateProperty.getStateValueAs(Boolean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.ph87.home.device;
|
package de.ph87.home.device;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import de.ph87.home.common.crud.AbstractSearchFilter;
|
||||||
import de.ph87.home.property.PropertyTypeMismatch;
|
import de.ph87.home.property.PropertyTypeMismatch;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -9,7 +10,7 @@ import lombok.ToString;
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString
|
@ToString
|
||||||
public class DeviceFilter {
|
public class DeviceFilter extends AbstractSearchFilter {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@ -23,19 +24,18 @@ public class DeviceFilter {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private Boolean stateFalse;
|
private Boolean stateFalse;
|
||||||
|
|
||||||
@SuppressWarnings("RedundantIfStatement")
|
|
||||||
public boolean filter(@NonNull final DeviceDto dto) throws PropertyTypeMismatch {
|
public boolean filter(@NonNull final DeviceDto dto) throws PropertyTypeMismatch {
|
||||||
if (stateNull != null && stateNull != (dto.getState() == null)) {
|
if (stateNull != null && stateNull != (dto.getStateProperty() == null)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final Boolean value = dto.getStateValue();
|
final Boolean value = dto.getStateValue();
|
||||||
if (stateTrue != null && (dto.getState() == null || stateTrue != value)) {
|
if (stateTrue != null && (value == null || stateTrue != value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (stateFalse != null && (dto.getState() == null || stateFalse == value)) {
|
if (stateFalse != null && (value == null || stateFalse == value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return search(dto.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
package de.ph87.home.device;
|
|
||||||
|
|
||||||
import lombok.NonNull;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
||||||
|
|
||||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
|
||||||
public class DeviceNotFound extends Exception {
|
|
||||||
|
|
||||||
public DeviceNotFound(@NonNull final String key, final @NonNull String value) {
|
|
||||||
super("Device not found: %s=%s".formatted(key, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -10,6 +10,6 @@ public interface DeviceRepository extends ListCrudRepository<Device, String> {
|
|||||||
|
|
||||||
Optional<Device> findByUuidOrSlug(@NonNull String uuid, @NonNull String slug);
|
Optional<Device> findByUuidOrSlug(@NonNull String uuid, @NonNull String slug);
|
||||||
|
|
||||||
List<Device> findAllByStateProperty(@NonNull String propertyId);
|
List<Device> findAllByStatePropertyId(@NonNull String propertyId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package de.ph87.home.device;
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import de.ph87.home.common.crud.CrudAction;
|
||||||
|
import de.ph87.home.common.crud.EntityNotFound;
|
||||||
import de.ph87.home.property.*;
|
import de.ph87.home.property.*;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
@ -27,29 +29,34 @@ public class DeviceService {
|
|||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public DeviceDto create(@NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty) {
|
public DeviceDto create(@NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty) {
|
||||||
return toDto(deviceRepository.save(new Device(name, slug, stateProperty)));
|
return publish(deviceRepository.save(new Device(name, slug, stateProperty)), CrudAction.UPDATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setState(@NonNull final String uuidOrSlug, final boolean state) throws DeviceNotFound, PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch {
|
public void setState(@NonNull final String uuidOrSlug, final boolean state) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch {
|
||||||
log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state);
|
log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state);
|
||||||
final Device device = getByUuidOrSlug(uuidOrSlug);
|
final Device device = getByUuidOrSlug(uuidOrSlug);
|
||||||
propertyService.write(device.getStateProperty(), state, Boolean.class);
|
propertyService.write(device.getStatePropertyId(), state, Boolean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public DeviceDto toDto(final @NonNull String uuidOrSlug) throws DeviceNotFound {
|
public DeviceDto getByUuidOrSlugDto(final @NonNull String uuidOrSlug) {
|
||||||
return toDto(getByUuidOrSlug(uuidOrSlug));
|
return toDto(getByUuidOrSlug(uuidOrSlug));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Device getByUuidOrSlug(@NonNull final String uuidOrSlug) {
|
||||||
|
return deviceRepository.findByUuidOrSlug(uuidOrSlug, uuidOrSlug).orElseThrow(() -> new EntityNotFound("uuidOrSlug", uuidOrSlug));
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public DeviceDto toDto(@NonNull final Device device) {
|
public DeviceDto toDto(@NonNull final Device device) {
|
||||||
final PropertyDto<Boolean> state = propertyService.dtoByIdAndTypeOrNull(device.getStateProperty(), Boolean.class);
|
final PropertyDto<Boolean> state = propertyService.dtoByIdAndTypeOrNull(device.getStatePropertyId(), Boolean.class);
|
||||||
return new DeviceDto(device, state);
|
return new DeviceDto(device, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Device getByUuidOrSlug(@NonNull final String uuidOrSlug) throws DeviceNotFound {
|
private Device getByUuid(@NonNull final String uuid) {
|
||||||
return deviceRepository.findByUuidOrSlug(uuidOrSlug, uuidOrSlug).orElseThrow(() -> new DeviceNotFound("uuidOrSlug", uuidOrSlug));
|
return deviceRepository.findById(uuid).orElseThrow(() -> new EntityNotFound("uuid", uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -69,13 +76,20 @@ public class DeviceService {
|
|||||||
|
|
||||||
@EventListener(PropertyDto.class)
|
@EventListener(PropertyDto.class)
|
||||||
public void onPropertyChange(@NonNull final PropertyDto<?> dto) {
|
public void onPropertyChange(@NonNull final PropertyDto<?> dto) {
|
||||||
deviceRepository.findAllByStateProperty(dto.getId()).forEach(this::publish);
|
deviceRepository.findAllByStatePropertyId(dto.getId()).forEach(device -> publish(device, CrudAction.CREATED));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void publish(@NonNull final Device device) {
|
@NonNull
|
||||||
final DeviceDto deviceDto = toDto(device);
|
private DeviceDto publish(@NonNull final Device device, @NonNull final CrudAction action) {
|
||||||
log.info("Device updated: {}", deviceDto);
|
final DeviceDto dto = toDto(device);
|
||||||
applicationEventPublisher.publishEvent(deviceDto);
|
log.info("Device {}: {}", action, dto);
|
||||||
|
applicationEventPublisher.publishEvent(dto);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public DeviceDto getByUuidDto(@NonNull final String uuid) {
|
||||||
|
return toDto(getByUuid(uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
package de.ph87.home.knx;
|
package de.ph87.home.knx.group;
|
||||||
|
|
||||||
|
import de.ph87.home.knx.group.dpt.DPT;
|
||||||
|
import de.ph87.home.property.State;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import tuwien.auto.calimero.GroupAddress;
|
import tuwien.auto.calimero.GroupAddress;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ -31,6 +35,12 @@ public class Group {
|
|||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private long puid;
|
private long puid;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private State<?> state;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ZonedDateTime lastValueChange = null;
|
||||||
|
|
||||||
public Group(@NonNull final String id, @NonNull final GroupAddress address, @NonNull final String name, @NonNull final String description, @NonNull final DPT dpt, final long puid) {
|
public Group(@NonNull final String id, @NonNull final GroupAddress address, @NonNull final String name, @NonNull final String description, @NonNull final DPT dpt, final long puid) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.address = address;
|
this.address = address;
|
||||||
@ -40,6 +50,13 @@ public class Group {
|
|||||||
this.puid = puid;
|
this.puid = puid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setState(@NonNull final State<?> newState) {
|
||||||
|
if (newState.valueChanged(this.state)) {
|
||||||
|
lastValueChange = ZonedDateTime.now();
|
||||||
|
}
|
||||||
|
this.state = newState;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object o) {
|
public boolean equals(final Object o) {
|
||||||
if (this == o) {
|
if (this == o) {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package de.ph87.home.knx;
|
package de.ph87.home.knx.group;
|
||||||
|
|
||||||
import io.micrometer.common.lang.Nullable;
|
import io.micrometer.common.lang.Nullable;
|
||||||
import jakarta.persistence.AttributeConverter;
|
import jakarta.persistence.AttributeConverter;
|
||||||
37
src/main/java/de/ph87/home/knx/group/GroupController.java
Normal file
37
src/main/java/de/ph87/home/knx/group/GroupController.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package de.ph87.home.knx.group;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tuwien.auto.calimero.GroupAddress;
|
||||||
|
import tuwien.auto.calimero.KNXFormatException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Knx/Group")
|
||||||
|
public class GroupController {
|
||||||
|
|
||||||
|
private final GroupService groupService;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@GetMapping("getByAddress/{id}")
|
||||||
|
@ExceptionHandler(KNXFormatException.class)
|
||||||
|
private GroupDto getByAddress(@PathVariable final String id, @NonNull final HttpServletRequest request) throws KNXFormatException {
|
||||||
|
log.debug("getByAddress: path={}", request.getServletPath());
|
||||||
|
return groupService.getByAddressDto(GroupAddress.from(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@RequestMapping(value = "list", method = {RequestMethod.GET, RequestMethod.POST})
|
||||||
|
private List<GroupDto> list(@RequestBody(required = false) @Nullable final GroupFilter filter, @NonNull final HttpServletRequest request) {
|
||||||
|
log.debug("list: path={} filter={}", request.getServletPath(), filter);
|
||||||
|
return groupService.list(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
57
src/main/java/de/ph87/home/knx/group/GroupDto.java
Normal file
57
src/main/java/de/ph87/home/knx/group/GroupDto.java
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package de.ph87.home.knx.group;
|
||||||
|
|
||||||
|
import de.ph87.home.property.State;
|
||||||
|
import de.ph87.home.web.IWebSocketMessage;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class GroupDto implements IWebSocketMessage {
|
||||||
|
|
||||||
|
@ToString.Exclude
|
||||||
|
private final List<Object> websocketTopic = List.of("Knx", "Group");
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String address;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String dpt;
|
||||||
|
|
||||||
|
@ToString.Exclude
|
||||||
|
private final long puid;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final State<?> state;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final ZonedDateTime lastValueChange;
|
||||||
|
|
||||||
|
public GroupDto(@NonNull final Group group) {
|
||||||
|
this.id = group.getId();
|
||||||
|
this.address = group.getAddress().toString();
|
||||||
|
this.name = group.getName();
|
||||||
|
this.description = group.getDescription();
|
||||||
|
this.dpt = group.getDpt().toString();
|
||||||
|
this.puid = group.getPuid();
|
||||||
|
this.state = group.getState();
|
||||||
|
this.lastValueChange = group.getLastValueChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
src/main/java/de/ph87/home/knx/group/GroupFilter.java
Normal file
16
src/main/java/de/ph87/home/knx/group/GroupFilter.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package de.ph87.home.knx.group;
|
||||||
|
|
||||||
|
import de.ph87.home.common.crud.AbstractSearchFilter;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class GroupFilter extends AbstractSearchFilter {
|
||||||
|
|
||||||
|
public boolean filter(@NonNull final Group group) {
|
||||||
|
return search(group.getName(), group.getAddress().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package de.ph87.home.knx;
|
package de.ph87.home.knx.group;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
@ -1,5 +1,8 @@
|
|||||||
package de.ph87.home.knx;
|
package de.ph87.home.knx.group;
|
||||||
|
|
||||||
|
import de.ph87.home.knx.group.dpt.DPT;
|
||||||
|
import de.ph87.home.knx.group.dpt.DPTException;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -23,7 +26,7 @@ import java.util.Optional;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GroupService {
|
public class GroupRepository {
|
||||||
|
|
||||||
private final List<Group> groupList = new ArrayList<>();
|
private final List<Group> groupList = new ArrayList<>();
|
||||||
|
|
||||||
@ -77,4 +80,14 @@ public class GroupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<Group> findAll(@Nullable final GroupFilter filter) {
|
||||||
|
synchronized (groupList) {
|
||||||
|
if (filter == null) {
|
||||||
|
return new ArrayList<>(groupList);
|
||||||
|
}
|
||||||
|
return groupList.stream().filter(filter::filter).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
54
src/main/java/de/ph87/home/knx/group/GroupService.java
Normal file
54
src/main/java/de/ph87/home/knx/group/GroupService.java
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package de.ph87.home.knx.group;
|
||||||
|
|
||||||
|
import de.ph87.home.common.crud.EntityNotFound;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import tuwien.auto.calimero.GroupAddress;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GroupService {
|
||||||
|
|
||||||
|
private final GroupRepository groupRepository;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public GroupDto toDto(@NonNull final Group group) {
|
||||||
|
return new GroupDto(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<GroupDto> list() {
|
||||||
|
return list(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<GroupDto> list(@Nullable final GroupFilter filter) {
|
||||||
|
return groupRepository.findAll(filter).stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publish(@NonNull final Group group) {
|
||||||
|
final GroupDto groupDto = toDto(group);
|
||||||
|
log.info("Group updated: {}", groupDto);
|
||||||
|
applicationEventPublisher.publishEvent(groupDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public GroupDto getByAddressDto(@NonNull final GroupAddress address) {
|
||||||
|
return toDto(getByAddress(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Group getByAddress(@NonNull final GroupAddress address) {
|
||||||
|
return groupRepository.findByAddress(address).orElseThrow(() -> new EntityNotFound("address", address));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package de.ph87.home.knx;
|
package de.ph87.home.knx.group.dpt;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package de.ph87.home.knx;
|
package de.ph87.home.knx.group.dpt;
|
||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package de.ph87.home.knx.link;
|
package de.ph87.home.knx.link;
|
||||||
|
|
||||||
import de.ph87.home.knx.Group;
|
import de.ph87.home.knx.group.Group;
|
||||||
import de.ph87.home.knx.KnxConfig;
|
import de.ph87.home.knx.KnxConfig;
|
||||||
import de.ph87.home.knx.link.request.Request;
|
import de.ph87.home.knx.link.request.Request;
|
||||||
import de.ph87.home.knx.link.request.RequestRead;
|
import de.ph87.home.knx.link.request.RequestRead;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package de.ph87.home.knx.link.request;
|
package de.ph87.home.knx.link.request;
|
||||||
|
|
||||||
import de.ph87.home.knx.Group;
|
import de.ph87.home.knx.group.Group;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package de.ph87.home.knx.link.request;
|
package de.ph87.home.knx.link.request;
|
||||||
|
|
||||||
import de.ph87.home.knx.Group;
|
import de.ph87.home.knx.group.Group;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
|
||||||
public class RequestRead extends Request {
|
public class RequestRead extends Request {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package de.ph87.home.knx.link.request;
|
package de.ph87.home.knx.link.request;
|
||||||
|
|
||||||
import de.ph87.home.knx.Group;
|
import de.ph87.home.knx.group.Group;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import tuwien.auto.calimero.dptxlator.DPTXlator;
|
import tuwien.auto.calimero.dptxlator.DPTXlator;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package de.ph87.home.knx.property;
|
package de.ph87.home.knx.property;
|
||||||
|
|
||||||
import de.ph87.home.knx.GroupAddressJpaConverter;
|
import de.ph87.home.knx.group.GroupAddressJpaConverter;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Convert;
|
import jakarta.persistence.Convert;
|
||||||
|
|||||||
@ -8,6 +8,6 @@ import java.util.List;
|
|||||||
|
|
||||||
public interface KnxPropertyRepository extends ListCrudRepository<KnxProperty, String> {
|
public interface KnxPropertyRepository extends ListCrudRepository<KnxProperty, String> {
|
||||||
|
|
||||||
List<KnxProperty> findDistinctByReadOrWrite(@NonNull GroupAddress read, @NonNull GroupAddress write);
|
List<KnxProperty> findAllByRead(@NonNull GroupAddress read);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
package de.ph87.home.knx.property;
|
package de.ph87.home.knx.property;
|
||||||
|
|
||||||
import de.ph87.home.knx.Group;
|
import de.ph87.home.knx.group.Group;
|
||||||
import de.ph87.home.knx.GroupLoaded;
|
import de.ph87.home.knx.group.GroupLoaded;
|
||||||
import de.ph87.home.knx.GroupService;
|
import de.ph87.home.knx.group.GroupRepository;
|
||||||
|
import de.ph87.home.knx.group.GroupService;
|
||||||
import de.ph87.home.knx.link.KnxLinkService;
|
import de.ph87.home.knx.link.KnxLinkService;
|
||||||
import de.ph87.home.property.PropertyNotFound;
|
import de.ph87.home.property.*;
|
||||||
import de.ph87.home.property.PropertyNotOwned;
|
|
||||||
import de.ph87.home.property.PropertyService;
|
|
||||||
import de.ph87.home.property.PropertyTypeMismatch;
|
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -38,10 +36,12 @@ public class KnxPropertyService {
|
|||||||
|
|
||||||
private final PropertyService propertyService;
|
private final PropertyService propertyService;
|
||||||
|
|
||||||
private final GroupService groupService;
|
private final GroupRepository groupRepository;
|
||||||
|
|
||||||
private final KnxLinkService knxLinkService;
|
private final KnxLinkService knxLinkService;
|
||||||
|
|
||||||
|
private final GroupService groupService;
|
||||||
|
|
||||||
@Scheduled(initialDelay = 1, fixedDelay = 1, timeUnit = TimeUnit.HOURS)
|
@Scheduled(initialDelay = 1, fixedDelay = 1, timeUnit = TimeUnit.HOURS)
|
||||||
public void readAll() {
|
public void readAll() {
|
||||||
knxPropertyRepository.findAll().forEach(this::read);
|
knxPropertyRepository.findAll().forEach(this::read);
|
||||||
@ -49,7 +49,7 @@ public class KnxPropertyService {
|
|||||||
|
|
||||||
@EventListener(GroupLoaded.class)
|
@EventListener(GroupLoaded.class)
|
||||||
public void onGroupLoad(@NonNull final GroupLoaded groupLoaded) {
|
public void onGroupLoad(@NonNull final GroupLoaded groupLoaded) {
|
||||||
findAllByAddress(groupLoaded.getGroup().getAddress()).forEach(this::read);
|
findAllByReadAddress(groupLoaded.getGroup().getAddress()).forEach(this::read);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void create(@NonNull final String id, @NonNull final KnxPropertyType type, @Nullable final GroupAddress read, @Nullable final GroupAddress write) {
|
public void create(@NonNull final String id, @NonNull final KnxPropertyType type, @Nullable final GroupAddress read, @Nullable final GroupAddress write) {
|
||||||
@ -68,17 +68,12 @@ public class KnxPropertyService {
|
|||||||
|
|
||||||
@EventListener(ProcessEvent.class)
|
@EventListener(ProcessEvent.class)
|
||||||
public void onProcessEvent(@NonNull final ProcessEvent event) {
|
public void onProcessEvent(@NonNull final ProcessEvent event) {
|
||||||
findAllByAddress(event.getDestination()).forEach(knxProperty -> onProcessEvent(knxProperty, event));
|
findAllByReadAddress(event.getDestination()).forEach(knxProperty -> onProcessEvent(knxProperty, event));
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private List<KnxProperty> findAllByAddress(@NonNull final GroupAddress address) {
|
|
||||||
return knxPropertyRepository.findDistinctByReadOrWrite(address, address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onProcessEvent(@NonNull final KnxProperty knxProperty, @NonNull final ProcessEvent event) {
|
private void onProcessEvent(@NonNull final KnxProperty knxProperty, @NonNull final ProcessEvent event) {
|
||||||
log.debug("onProcessEvent: knxProperty={}, event={}", knxProperty, event);
|
log.debug("onProcessEvent: knxProperty={}, event={}", knxProperty, event);
|
||||||
groupService.findByAddress(event.getDestination()).ifPresent(group -> onProcessEvent(knxProperty, event, group));
|
groupRepository.findByAddress(event.getDestination()).ifPresent(group -> onProcessEvent(knxProperty, event, group));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onProcessEvent(@NonNull final KnxProperty knxProperty, @NonNull final ProcessEvent event, @NonNull final Group group) {
|
private void onProcessEvent(@NonNull final KnxProperty knxProperty, @NonNull final ProcessEvent event, @NonNull final Group group) {
|
||||||
@ -87,15 +82,20 @@ public class KnxPropertyService {
|
|||||||
final DPTXlator translator = group.getDpt().createTranslator();
|
final DPTXlator translator = group.getDpt().createTranslator();
|
||||||
translator.setData(event.getASDU());
|
translator.setData(event.getASDU());
|
||||||
log.debug("translator: {}", translator);
|
log.debug("translator: {}", translator);
|
||||||
switch (knxProperty.getType()) {
|
final Property<?> property = switch (knxProperty.getType()) {
|
||||||
case BOOLEAN -> {
|
case BOOLEAN -> {
|
||||||
if (!(translator instanceof final DPTXlatorBoolean booleanTranslator)) {
|
if (!(translator instanceof final DPTXlatorBoolean booleanTranslator)) {
|
||||||
throw new RuntimeException("DPTXlator type should be DPTXlatorBoolean for property.type = BOOLEAN but is: " + translator.getClass().getSimpleName());
|
throw new RuntimeException("DPTXlator type should be DPTXlatorBoolean for property.type = BOOLEAN but is: " + translator.getClass().getSimpleName());
|
||||||
}
|
}
|
||||||
propertyService.update(this, knxProperty.getId(), Boolean.class, booleanTranslator.getValueBoolean(), booleanTranslator.getValue());
|
yield propertyService.update(this, knxProperty.getId(), Boolean.class, booleanTranslator.getValueBoolean(), booleanTranslator.getValue());
|
||||||
}
|
}
|
||||||
case DOUBLE -> propertyService.update(this, knxProperty.getId(), Double.class, translator.getNumericValue(), translator.getValue());
|
case DOUBLE -> propertyService.update(this, knxProperty.getId(), Double.class, translator.getNumericValue(), translator.getValue());
|
||||||
|
};
|
||||||
|
if (property.getState() == null) {
|
||||||
|
throw new RuntimeException();
|
||||||
}
|
}
|
||||||
|
group.setState(property.getState());
|
||||||
|
groupService.publish(group);
|
||||||
} catch (KNXException | PropertyNotFound | PropertyTypeMismatch | PropertyNotOwned e) {
|
} catch (KNXException | PropertyNotFound | PropertyTypeMismatch | PropertyNotOwned e) {
|
||||||
log.error("Failed to handle ProcessEvent: knxProperty={}, error={}", knxProperty, e.toString());
|
log.error("Failed to handle ProcessEvent: knxProperty={}, error={}", knxProperty, e.toString());
|
||||||
}
|
}
|
||||||
@ -104,12 +104,12 @@ public class KnxPropertyService {
|
|||||||
private void read(@NonNull final KnxProperty knxProperty) {
|
private void read(@NonNull final KnxProperty knxProperty) {
|
||||||
final Optional<KnxProperty> property = knxPropertyRepository.findById(knxProperty.getId());
|
final Optional<KnxProperty> property = knxPropertyRepository.findById(knxProperty.getId());
|
||||||
final Optional<GroupAddress> address = property.map(KnxProperty::getRead);
|
final Optional<GroupAddress> address = property.map(KnxProperty::getRead);
|
||||||
final Optional<Group> group = address.map(groupService::findByAddress).filter(Optional::isPresent).map(Optional::get);
|
final Optional<Group> group = address.map(groupRepository::findByAddress).filter(Optional::isPresent).map(Optional::get);
|
||||||
group.ifPresent(knxLinkService::queueRead);
|
group.ifPresent(knxLinkService::queueRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void write(@NonNull final KnxProperty knxProperty, @NonNull final Object value) {
|
private void write(@NonNull final KnxProperty knxProperty, @NonNull final Object value) {
|
||||||
knxPropertyRepository.findById(knxProperty.getId()).map(KnxProperty::getWrite).map(groupService::findByAddress).filter(Optional::isPresent).map(Optional::get).ifPresent(group -> write(knxProperty, group, value));
|
knxPropertyRepository.findById(knxProperty.getId()).map(KnxProperty::getWrite).map(groupRepository::findByAddress).filter(Optional::isPresent).map(Optional::get).ifPresent(group -> write(knxProperty, group, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void write(@NonNull final KnxProperty knxProperty, @NonNull final Group group, @NonNull final Object value) {
|
private void write(@NonNull final KnxProperty knxProperty, @NonNull final Group group, @NonNull final Object value) {
|
||||||
@ -133,4 +133,9 @@ public class KnxPropertyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private List<KnxProperty> findAllByReadAddress(@NonNull final GroupAddress address) {
|
||||||
|
return knxPropertyRepository.findAllByRead(address);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import lombok.NonNull;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@ -43,16 +44,17 @@ public class Property<T> implements IProperty<T> {
|
|||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private final Consumer<Property<T>> onStateSet;
|
private final Consumer<Property<T>> onStateSet;
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@ToString.Exclude
|
|
||||||
private State<T> lastState = null;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private State<T> state = null;
|
private State<T> state = null;
|
||||||
|
|
||||||
public void update(@Nullable final State<T> state) {
|
@Nullable
|
||||||
this.lastState = this.state;
|
private ZonedDateTime lastValueChange = null;
|
||||||
this.state = state;
|
|
||||||
|
public void update(@NonNull final State<T> newState) {
|
||||||
|
if (newState.valueChanged(this.state)) {
|
||||||
|
lastValueChange = ZonedDateTime.now();
|
||||||
|
}
|
||||||
|
this.state = newState;
|
||||||
this.onStateSet.accept(this);
|
this.onStateSet.accept(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import lombok.Getter;
|
|||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString
|
@ToString
|
||||||
public class PropertyDto<T> implements IProperty<T> {
|
public class PropertyDto<T> implements IProperty<T> {
|
||||||
@ -22,17 +24,16 @@ public class PropertyDto<T> implements IProperty<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ToString.Exclude
|
private final State<T> state;
|
||||||
private final State<T> lastState;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private final State<T> state;
|
private final ZonedDateTime lastValueChange;
|
||||||
|
|
||||||
public PropertyDto(@NonNull final Property<T> property) {
|
public PropertyDto(@NonNull final Property<T> property) {
|
||||||
this.id = property.getId();
|
this.id = property.getId();
|
||||||
this.type = property.getType();
|
this.type = property.getType();
|
||||||
this.state = property.getState();
|
this.state = property.getState();
|
||||||
this.lastState = property.getLastState();
|
this.lastValueChange = property.getLastValueChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,9 +104,10 @@ public class PropertyService {
|
|||||||
return new PropertyDto<>(property);
|
return new PropertyDto<>(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> void update(@NonNull final Object owner, @NonNull final String id, @NonNull final Class<T> type, @Nullable final T value, @NonNull final String string) throws PropertyNotFound, PropertyNotOwned, PropertyTypeMismatch {
|
public <T> Property<T> update(@NonNull final Object owner, @NonNull final String id, @NonNull final Class<T> type, @Nullable final T value, @NonNull final String string) throws PropertyNotFound, PropertyNotOwned, PropertyTypeMismatch {
|
||||||
final Property<T> property = getByIdAndTypeAndOwner(id, type, owner);
|
final Property<T> property = getByIdAndTypeAndOwner(id, type, owner);
|
||||||
property.update(new State<>(property.getType().cast(value), string));
|
property.update(new State<>(type, property.getType().cast(value), string));
|
||||||
|
return property;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user