UI ScheduleList including editing + FIX fuzzy premature scheduling
This commit is contained in:
parent
d8664a800a
commit
bb7eb594a0
@ -3,5 +3,5 @@
|
|||||||
cd "$(dirname "$0")" || exit 1
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
|
||||||
mvn clean package spring-boot:repackage && \
|
mvn clean package spring-boot:repackage && \
|
||||||
scp target/Homeautomation.jar media@10.0.0.50:/home/media/Homeautomation/Homeautomation.jar.update && \
|
scp target/Homeautomation.jar media@10.0.0.50:/home/media/java/Homeautomation/Homeautomation.jar.update && \
|
||||||
curl -m 2 -s http://10.0.0.50:8080/server/shutdown && echo "Server restarting..." || echo "Failed to restart server!"
|
curl -m 2 -s http://10.0.0.50:8080/server/shutdown && echo "Server restarting..." || echo "Failed to restart server!"
|
||||||
63
pom.xml
63
pom.xml
@ -73,6 +73,67 @@
|
|||||||
<build>
|
<build>
|
||||||
<finalName>${project.artifactId}</finalName>
|
<finalName>${project.artifactId}</finalName>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.github.eirslett</groupId>
|
||||||
|
<artifactId>frontend-maven-plugin</artifactId>
|
||||||
|
<version>1.10.3</version>
|
||||||
|
<configuration>
|
||||||
|
<workingDirectory>src/main/angular</workingDirectory>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>install-node-and-npm</id>
|
||||||
|
<goals>
|
||||||
|
<goal>install-node-and-npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<nodeVersion>v14.15.5</nodeVersion>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>npm install</id>
|
||||||
|
<goals>
|
||||||
|
<goal>npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<arguments>install</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>npm build</id>
|
||||||
|
<phase>generate-resources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>npm</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<arguments>run build</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-resources-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-resources</id>
|
||||||
|
<phase>process-resources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>copy-resources</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<outputDirectory>target/classes/resources/</outputDirectory>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/angular/dist/angular/</directory>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
@ -81,6 +142,7 @@
|
|||||||
<release>11</release>
|
<release>11</release>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
@ -92,6 +154,7 @@
|
|||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
17
src/main/angular/.browserslistrc
Normal file
17
src/main/angular/.browserslistrc
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
last 1 Chrome version
|
||||||
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
|
Firefox ESR
|
||||||
|
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||||
16
src/main/angular/.editorconfig
Normal file
16
src/main/angular/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 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]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
46
src/main/angular/.gitignore
vendored
Normal file
46
src/main/angular/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
/node/
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# Only exists if Bazel was run
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# profiling files
|
||||||
|
chrome-profiler-events*.json
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.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 12.2.0.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app 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.io/cli) page.
|
||||||
111
src/main/angular/angular.json
Normal file
111
src/main/angular/angular.json
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"angular": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "less"
|
||||||
|
},
|
||||||
|
"@schematics/angular:application": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/angular",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "less",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"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": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "angular:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "angular:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "angular:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"karmaConfig": "karma.conf.js",
|
||||||
|
"inlineStyleLanguage": "less",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.less"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "angular"
|
||||||
|
}
|
||||||
44
src/main/angular/karma.conf.js
Normal file
44
src/main/angular/karma.conf.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Karma configuration file, see link for more information
|
||||||
|
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
config.set({
|
||||||
|
basePath: '',
|
||||||
|
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
jasmine: {
|
||||||
|
// you can add configuration options for Jasmine here
|
||||||
|
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||||
|
// for example, you can disable the random execution with `random: false`
|
||||||
|
// or set a specific seed with `seed: 4321`
|
||||||
|
},
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
jasmineHtmlReporter: {
|
||||||
|
suppressAll: true // removes the duplicated traces
|
||||||
|
},
|
||||||
|
coverageReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage/angular'),
|
||||||
|
subdir: '.',
|
||||||
|
reporters: [
|
||||||
|
{type: 'html'},
|
||||||
|
{type: 'text-summary'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
singleRun: false,
|
||||||
|
restartOnFileChange: true
|
||||||
|
});
|
||||||
|
};
|
||||||
12083
src/main/angular/package-lock.json
generated
Normal file
12083
src/main/angular/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
src/main/angular/package.json
Normal file
42
src/main/angular/package.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"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": "~12.2.0",
|
||||||
|
"@angular/common": "~12.2.0",
|
||||||
|
"@angular/compiler": "~12.2.0",
|
||||||
|
"@angular/core": "~12.2.0",
|
||||||
|
"@angular/forms": "~12.2.0",
|
||||||
|
"@angular/platform-browser": "~12.2.0",
|
||||||
|
"@angular/platform-browser-dynamic": "~12.2.0",
|
||||||
|
"@angular/router": "~12.2.0",
|
||||||
|
"@fortawesome/angular-fontawesome": "^0.9.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
|
"rxjs": "~6.6.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.11.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "~12.2.0",
|
||||||
|
"@angular/cli": "~12.2.0",
|
||||||
|
"@angular/compiler-cli": "~12.2.0",
|
||||||
|
"@types/jasmine": "~3.8.0",
|
||||||
|
"@types/node": "^12.11.1",
|
||||||
|
"jasmine-core": "~3.8.0",
|
||||||
|
"karma": "~6.3.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage": "~2.0.3",
|
||||||
|
"karma-jasmine": "~4.0.0",
|
||||||
|
"karma-jasmine-html-reporter": "~1.7.0",
|
||||||
|
"typescript": "~4.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main/angular/src/app/api/api.service.spec.ts
Normal file
16
src/main/angular/src/app/api/api.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {ApiService} from './api.service';
|
||||||
|
|
||||||
|
describe('ApiService', () => {
|
||||||
|
let service: ApiService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ApiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
51
src/main/angular/src/app/api/api.service.ts
Normal file
51
src/main/angular/src/app/api/api.service.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {map} from "rxjs/operators";
|
||||||
|
import {environment} from "../../environments/environment";
|
||||||
|
|
||||||
|
export function NO_OP() {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NO_SORT<T>(a: T, b: T): number {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly http: HttpClient,
|
||||||
|
) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem<T>(path: string, fromJson: (json: any) => T, next: (item: T) => void = NO_OP, errorHandler: (error: any) => void = NO_OP) {
|
||||||
|
this.http.get<any>(environment.apiBasePath + path).pipe(map(fromJson)).subscribe(next, this.getErrorHandler(errorHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
getList<T>(path: string, fromJson: (json: any) => T, compare: (a: T, b: T) => number = NO_SORT, next: (list: T[]) => void = NO_OP, errorHandler: (error: any) => void = NO_OP) {
|
||||||
|
this.http.get<any[]>(environment.apiBasePath + path).pipe(map(list => list.map(fromJson).sort(compare))).subscribe(next, this.getErrorHandler(errorHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
postReturnNone<T>(path: string, data: any, next: (_: void) => void = NO_OP, errorHandler: (error: any) => void = NO_OP) {
|
||||||
|
this.http.post<any>(environment.apiBasePath + path, data).subscribe(next, this.getErrorHandler(errorHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
postReturnItem<T>(path: string, data: any, fromJson: (json: any) => T, next: (item: T) => void = NO_OP, errorHandler: (error: any) => void = NO_OP) {
|
||||||
|
this.http.post<any>(environment.apiBasePath + path, data).pipe(map(fromJson)).subscribe(next, this.getErrorHandler(errorHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
postReturnList<T>(path: string, data: any, fromJson: (json: any) => T, next: (list: T[]) => void = NO_OP, errorHandler: (error: any) => void = NO_OP) {
|
||||||
|
this.http.post<any>(environment.apiBasePath + path, data).pipe(map(list => list.map(fromJson))).subscribe(next, this.getErrorHandler(errorHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getErrorHandler(errorHandler: (error: any) => void): ((error: any) => void) {
|
||||||
|
return error => {
|
||||||
|
console.error(error);
|
||||||
|
errorHandler(error);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
36
src/main/angular/src/app/api/schedule/Schedule.ts
Normal file
36
src/main/angular/src/app/api/schedule/Schedule.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {validateBooleanNotNull, validateListOrEmpty, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
|
||||||
|
import {ScheduleEntry} from "./entry/ScheduleEntry";
|
||||||
|
|
||||||
|
export class Schedule {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public enabled: boolean,
|
||||||
|
public name: string,
|
||||||
|
public entries: ScheduleEntry[],
|
||||||
|
) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Schedule {
|
||||||
|
return new Schedule(
|
||||||
|
validateNumberNotNull(json['id']),
|
||||||
|
validateBooleanNotNull(json['enabled']),
|
||||||
|
validateStringNotEmptyNotNull(json['name']),
|
||||||
|
validateListOrEmpty(json['entries'], ScheduleEntry.fromJson, ScheduleEntry.compareId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static trackBy(index: number, item: Schedule): number {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static compareId(a: Schedule, b: Schedule): number {
|
||||||
|
return a.id - b.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static compareName(a: Schedule, b: Schedule): number {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
96
src/main/angular/src/app/api/schedule/entry/ScheduleEntry.ts
Normal file
96
src/main/angular/src/app/api/schedule/entry/ScheduleEntry.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import {validateBooleanNotNull, validateDateAllowNull, validateMap, validateNumberNotNull, validateStringNotEmptyNotNull} from "../../validators";
|
||||||
|
import {prefix} from "../../../helpers";
|
||||||
|
|
||||||
|
class Timestamp {
|
||||||
|
|
||||||
|
public readonly WEEKDAY: string[] = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
|
||||||
|
|
||||||
|
public readonly dayName;
|
||||||
|
|
||||||
|
public readonly timeString;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
readonly date: Date,
|
||||||
|
) {
|
||||||
|
const now = new Date();
|
||||||
|
const minutes: string = prefix(this.date.getMinutes(), '0', 2);
|
||||||
|
if (date.getDate() === now.getDate()) {
|
||||||
|
this.dayName = "Heute";
|
||||||
|
this.timeString = date.getHours() + ":" + minutes;
|
||||||
|
} else if (date.getDate() === now.getDate() + 1) {
|
||||||
|
this.dayName = "Morgen";
|
||||||
|
this.timeString = date.getHours() + ":" + minutes;
|
||||||
|
} else {
|
||||||
|
this.dayName = this.WEEKDAY[date.getDay()];
|
||||||
|
this.timeString = date.getHours() + ":" + minutes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromDateOrNull(date: Date | null): Timestamp | null {
|
||||||
|
if (date === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Timestamp(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScheduleEntry {
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
public id: number,
|
||||||
|
public enabled: boolean,
|
||||||
|
public monday: boolean,
|
||||||
|
public tuesday: boolean,
|
||||||
|
public wednesday: boolean,
|
||||||
|
public thursday: boolean,
|
||||||
|
public friday: boolean,
|
||||||
|
public saturday: boolean,
|
||||||
|
public sunday: boolean,
|
||||||
|
public type: string,
|
||||||
|
public zenith: number,
|
||||||
|
public hour: number,
|
||||||
|
public minute: number,
|
||||||
|
public second: number,
|
||||||
|
public fuzzySeconds: number,
|
||||||
|
public lastClearTimestamp: Timestamp | null,
|
||||||
|
public nextClearTimestamp: Timestamp | null,
|
||||||
|
public nextFuzzyTimestamp: Timestamp | null,
|
||||||
|
public properties: Map<String, String>,
|
||||||
|
) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): ScheduleEntry {
|
||||||
|
return new ScheduleEntry(
|
||||||
|
validateNumberNotNull(json['id']),
|
||||||
|
validateBooleanNotNull(json['enabled']),
|
||||||
|
validateBooleanNotNull(json['monday']),
|
||||||
|
validateBooleanNotNull(json['tuesday']),
|
||||||
|
validateBooleanNotNull(json['wednesday']),
|
||||||
|
validateBooleanNotNull(json['thursday']),
|
||||||
|
validateBooleanNotNull(json['friday']),
|
||||||
|
validateBooleanNotNull(json['saturday']),
|
||||||
|
validateBooleanNotNull(json['sunday']),
|
||||||
|
validateStringNotEmptyNotNull(json['type']),
|
||||||
|
validateNumberNotNull(json['zenith']),
|
||||||
|
validateNumberNotNull(json['hour']),
|
||||||
|
validateNumberNotNull(json['minute']),
|
||||||
|
validateNumberNotNull(json['second']),
|
||||||
|
validateNumberNotNull(json['fuzzySeconds']),
|
||||||
|
Timestamp.fromDateOrNull(validateDateAllowNull(json['lastClearTimestamp'])),
|
||||||
|
Timestamp.fromDateOrNull(validateDateAllowNull(json['nextClearTimestamp'])),
|
||||||
|
Timestamp.fromDateOrNull(validateDateAllowNull(json['nextFuzzyTimestamp'])),
|
||||||
|
validateMap(json['properties'], validateStringNotEmptyNotNull),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static trackBy(index: number, item: ScheduleEntry): number {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static compareId(a: ScheduleEntry, b: ScheduleEntry): number {
|
||||||
|
return a.id - b.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {ScheduleEntryService} from './schedule-entry.service';
|
||||||
|
|
||||||
|
describe('ScheduleEntryService', () => {
|
||||||
|
let service: ScheduleEntryService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ScheduleEntryService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ScheduleEntry} from "./ScheduleEntry";
|
||||||
|
import {ApiService, NO_OP} from "../../api.service";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ScheduleEntryService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly api: ApiService,
|
||||||
|
) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(next: (list: ScheduleEntry[]) => void, compare: (a: ScheduleEntry, b: ScheduleEntry) => number, errorHandler: (error: any) => void = NO_OP): void {
|
||||||
|
this.api.getList("scheduleEntry/findAll", ScheduleEntry.fromJson, compare, next, errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(entry: ScheduleEntry, key: string, value: any, next: (item: ScheduleEntry) => void, errorHandler: (error: any) => void = NO_OP): void {
|
||||||
|
this.api.postReturnItem("schedule/entry/set/" + entry.id + "/" + key, value, ScheduleEntry.fromJson, next, errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {ScheduleService} from './schedule.service';
|
||||||
|
|
||||||
|
describe('ScheduleService', () => {
|
||||||
|
let service: ScheduleService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ScheduleService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
24
src/main/angular/src/app/api/schedule/schedule.service.ts
Normal file
24
src/main/angular/src/app/api/schedule/schedule.service.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ApiService, NO_OP} from "../api.service";
|
||||||
|
import {Schedule} from "./Schedule";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ScheduleService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly api: ApiService,
|
||||||
|
) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(next: (list: Schedule[]) => void, compare: (a: Schedule, b: Schedule) => number, errorHandler: (error: any) => void = NO_OP): void {
|
||||||
|
this.api.getList("schedule/findAll", Schedule.fromJson, compare, next, errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(schedule: Schedule, key: string, value: any, next: (item: Schedule) => void, errorHandler: (error: any) => void = NO_OP): void {
|
||||||
|
this.api.postReturnItem("schedule/set/" + schedule.id + "/" + key, value, Schedule.fromJson, next, errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
src/main/angular/src/app/api/validators.ts
Normal file
65
src/main/angular/src/app/api/validators.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
export function validateBooleanNotNull(value: any): boolean {
|
||||||
|
if (value !== true && value !== false) {
|
||||||
|
throw new Error("Not a boolean: " + value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateNumberNotNull(value: any): number {
|
||||||
|
const number: number = parseFloat(value);
|
||||||
|
if (isNaN(number)) {
|
||||||
|
throw new Error("Not a number: " + value);
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateStringNotEmptyNotNull(value: any): string {
|
||||||
|
if (!(typeof value === 'string')) {
|
||||||
|
throw new Error("Not a string: " + value);
|
||||||
|
}
|
||||||
|
if (value === '') {
|
||||||
|
throw new Error("String cannot be empty!");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateStringNotEmpty_Nullable(value: any): string | null {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!(typeof value === 'string')) {
|
||||||
|
throw new Error("Not a string: " + value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDateAllowNull(value: any): Date | null {
|
||||||
|
if (value === null || value === undefined || value === 0 || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const number: number = Date.parse(value);
|
||||||
|
if (isNaN(number)) {
|
||||||
|
throw new Error("Not a Date: " + value);
|
||||||
|
}
|
||||||
|
return new Date(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateListOrEmpty<T>(json: any, fromJson: (json: any) => T, compare: (a: T, b: T) => number): T[] {
|
||||||
|
if (!Array.isArray(json)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return json.map(fromJson).sort(compare);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMap<T>(json: any, valueFromJson: (json: any) => T): Map<String, T> {
|
||||||
|
if (typeof json !== 'object') {
|
||||||
|
throw new Error("Not a Map: " + json);
|
||||||
|
}
|
||||||
|
const map = new Map<String, T>();
|
||||||
|
for (const key in json) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
||||||
|
map.set(key, valueFromJson(json[key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
15
src/main/angular/src/app/app-routing.module.ts
Normal file
15
src/main/angular/src/app/app-routing.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
|
import {ScheduleListComponent} from "./pages/schedule-list/schedule-list.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{path: 'ScheduleList', component: ScheduleListComponent},
|
||||||
|
{path: '**', redirectTo: '/ScheduleList'},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule {
|
||||||
|
}
|
||||||
9
src/main/angular/src/app/app.component.html
Normal file
9
src/main/angular/src/app/app.component.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div class="menubar">
|
||||||
|
<div class="item" routerLink="/ScheduleList" routerLinkActive="itemActive">
|
||||||
|
Zeitplan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
18
src/main/angular/src/app/app.component.less
Normal file
18
src/main/angular/src/app/app.component.less
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.menubar {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
float: left;
|
||||||
|
padding: 10px;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemActive {
|
||||||
|
background-color: palegreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
35
src/main/angular/src/app/app.component.spec.ts
Normal file
35
src/main/angular/src/app/app.component.spec.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {AppComponent} from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should have as title 'angular'`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app.title).toEqual('angular');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('.content span')?.textContent).toContain('angular app is running!');
|
||||||
|
});
|
||||||
|
});
|
||||||
18
src/main/angular/src/app/app.component.ts
Normal file
18
src/main/angular/src/app/app.component.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {DataService} from "./data.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.less']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'angular';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly dataService: DataService,
|
||||||
|
) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
src/main/angular/src/app/app.module.ts
Normal file
33
src/main/angular/src/app/app.module.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
|
import {AppComponent} from './app.component';
|
||||||
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
|
import {FormsModule} from "@angular/forms";
|
||||||
|
import {EditFieldComponent} from './shared/edit-field/edit-field.component';
|
||||||
|
import {ScheduleListComponent} from './pages/schedule-list/schedule-list.component';
|
||||||
|
import {ToggleSwitchComponent} from './shared/toggle-switch/toggle-switch.component';
|
||||||
|
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {NumberComponent} from './shared/number/number.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
EditFieldComponent,
|
||||||
|
ScheduleListComponent,
|
||||||
|
ToggleSwitchComponent,
|
||||||
|
NumberComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
HttpClientModule,
|
||||||
|
FormsModule,
|
||||||
|
FontAwesomeModule,
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
16
src/main/angular/src/app/data.service.spec.ts
Normal file
16
src/main/angular/src/app/data.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {DataService} from './data.service';
|
||||||
|
|
||||||
|
describe('DataService', () => {
|
||||||
|
let service: DataService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(DataService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/main/angular/src/app/data.service.ts
Normal file
11
src/main/angular/src/app/data.service.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DataService {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/main/angular/src/app/helpers.ts
Normal file
7
src/main/angular/src/app/helpers.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function prefix(value: number, prefix: any, length: number): string {
|
||||||
|
let result = "" + value;
|
||||||
|
while (result.length < length) {
|
||||||
|
result = prefix + result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
<table>
|
||||||
|
<ng-container *ngFor="let schedule of schedules; let first = first; trackBy: Schedule.trackBy">
|
||||||
|
|
||||||
|
<tr *ngIf="!first" class="blank">
|
||||||
|
<td colspan="22"> </td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="blank">
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="schedule.enabled" (onChange)="scheduleUpdate(schedule, 'enabled', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
<td colspan="20">{{schedule.name}}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Mo</th>
|
||||||
|
<th>Di</th>
|
||||||
|
<th>Mi</th>
|
||||||
|
<th>Do</th>
|
||||||
|
<th>Fr</th>
|
||||||
|
<th>Sa</th>
|
||||||
|
<th>So</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Sonnenstand</th>
|
||||||
|
<th colspan="3">Uhrzeit</th>
|
||||||
|
<th>Unschärfe</th>
|
||||||
|
<th colspan="6">Nächste Ausführung</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr *ngFor="let entry of schedule.entries" [class.disabled]="entry.nextClearTimestamp === null">
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="entry.enabled" (onChange)="entryUpdate(schedule, entry, 'enabled', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="entry.monday" (onChange)="entryUpdate(schedule, entry, 'monday', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="entry.tuesday" (onChange)="entryUpdate(schedule, entry, 'tuesday', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="entry.wednesday" (onChange)="entryUpdate(schedule, entry, 'wednesday', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="entry.thursday" (onChange)="entryUpdate(schedule, entry, 'thursday', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="entry.friday" (onChange)="entryUpdate(schedule, entry, 'friday', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="entry.saturday" (onChange)="entryUpdate(schedule, entry, 'saturday', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
<td class="boolean">
|
||||||
|
<app-toggle-switch [initial]="entry.sunday" (onChange)="entryUpdate(schedule, entry, 'sunday', $event)"></app-toggle-switch>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<select [(ngModel)]="entry.type" (ngModelChange)="entryUpdate(schedule, entry, 'type', entry.type)">
|
||||||
|
<option value="TIME">Uhrzeit</option>
|
||||||
|
<option value="SUNRISE">Sonnenaufgang</option>
|
||||||
|
<option value="SUNSET">Sonnenuntergang</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<ng-container *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
|
||||||
|
<td>
|
||||||
|
<select class="number" [(ngModel)]="entry.zenith" (ngModelChange)="entryUpdate(schedule, entry, 'zenith', entry.zenith)">
|
||||||
|
<option value="90.8333">
|
||||||
|
<ng-container *ngIf="entry.type === 'SUNRISE'">Sonnenaufgang</ng-container>
|
||||||
|
<ng-container *ngIf="entry.type === 'SUNSET'">Sonnenuntergang</ng-container>
|
||||||
|
[ 90°]
|
||||||
|
</option>
|
||||||
|
<option value="93">[ 93°]</option>
|
||||||
|
<option value="96">Bürgeliche Dämmerung [ 96°]</option>
|
||||||
|
<option value="99">[ 99°]</option>
|
||||||
|
<option value="102">Nautische Dämmerung [102°]</option>
|
||||||
|
<option value="105">[105°]</option>
|
||||||
|
<option value="108">Astronomische Dämmerung [108°]</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<td *ngIf="entry.type !== 'SUNRISE' && entry.type !== 'SUNSET'" class="empty"></td>
|
||||||
|
|
||||||
|
<ng-container *ngIf="entry.type === 'TIME'">
|
||||||
|
<td class="first">
|
||||||
|
<select class="number" [(ngModel)]="entry.hour" (ngModelChange)="entryUpdate(schedule, entry, 'hour', entry.hour)">
|
||||||
|
<option *ngFor="let _ of [].constructor(60); let value = index" [ngValue]="value">{{value}}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="middle">:</td>
|
||||||
|
<td class="last">
|
||||||
|
<select class="number" [(ngModel)]="entry.minute" (ngModelChange)="entryUpdate(schedule, entry, 'minute', entry.minute)">
|
||||||
|
<option *ngFor="let _ of [].constructor(60); let value = index" [ngValue]="value">{{value | number:'2.0'}}</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<td *ngIf="entry.type !== 'TIME'" colspan="3" class="empty"></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<select class="number" [(ngModel)]="entry.fuzzySeconds" (ngModelChange)="entryUpdate(schedule, entry, 'fuzzySeconds', entry.fuzzySeconds)">
|
||||||
|
<option [ngValue]="0">Keine</option>
|
||||||
|
<option [ngValue]="60">1 Minute</option>
|
||||||
|
<option [ngValue]="300">5 Minuten</option>
|
||||||
|
<option [ngValue]="600">10 Minuten</option>
|
||||||
|
<option [ngValue]="1800">30 Minuten</option>
|
||||||
|
<option [ngValue]="3600">1 Stunde</option>
|
||||||
|
<option [ngValue]="7200">2 Stunden</option>
|
||||||
|
<option [ngValue]="10800">3 Stunden</option>
|
||||||
|
<option [ngValue]="21600">6 Stunden</option>
|
||||||
|
<option [ngValue]="43200">12 Stunden</option>
|
||||||
|
<option [ngValue]="86400">1 Tag</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<ng-container *ngIf="entry.nextClearTimestamp">
|
||||||
|
<td class="number first" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.dayName}}</td>
|
||||||
|
<td class="number middle" [class.empty]="entry.fuzzySeconds > 0">: </td>
|
||||||
|
<td class="number last" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.timeString}}</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!entry.nextClearTimestamp">
|
||||||
|
<td class="empty first"></td>
|
||||||
|
<td class="empty middle"></td>
|
||||||
|
<td class="empty last"></td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="entry.nextFuzzyTimestamp && entry.fuzzySeconds > 0">
|
||||||
|
<td class="number first">{{entry.nextFuzzyTimestamp.dayName}}</td>
|
||||||
|
<td class="number middle">: </td>
|
||||||
|
<td class="number last">{{entry.nextFuzzyTimestamp.timeString}}</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!entry.nextFuzzyTimestamp || entry.fuzzySeconds <= 0">
|
||||||
|
<td class="empty first"></td>
|
||||||
|
<td class="empty middle"></td>
|
||||||
|
<td class="empty last"></td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
</table>
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
select {
|
||||||
|
background-color: transparent;
|
||||||
|
border-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.blank {
|
||||||
|
th, td {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: palegreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
color: gray;
|
||||||
|
background-color: #DDDDDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boolean {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
border-right-width: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minute {
|
||||||
|
border-right-width: 0;
|
||||||
|
border-left-width: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last {
|
||||||
|
border-left-width: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle {
|
||||||
|
border-right-width: 0;
|
||||||
|
border-left-width: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
* {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {ScheduleListComponent} from './schedule-list.component';
|
||||||
|
|
||||||
|
describe('ScheduleListComponent', () => {
|
||||||
|
let component: ScheduleListComponent;
|
||||||
|
let fixture: ComponentFixture<ScheduleListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ScheduleListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ScheduleListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {ScheduleService} from "../../api/schedule/schedule.service";
|
||||||
|
import {Schedule} from "../../api/schedule/Schedule";
|
||||||
|
import {ScheduleEntry} from "../../api/schedule/entry/ScheduleEntry";
|
||||||
|
import {ScheduleEntryService} from "../../api/schedule/entry/schedule-entry.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-schedule-list',
|
||||||
|
templateUrl: './schedule-list.component.html',
|
||||||
|
styleUrls: ['./schedule-list.component.less']
|
||||||
|
})
|
||||||
|
export class ScheduleListComponent implements OnInit {
|
||||||
|
|
||||||
|
readonly Schedule = Schedule;
|
||||||
|
|
||||||
|
schedules: Schedule[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly scheduleService: ScheduleService,
|
||||||
|
readonly scheduleEntryService: ScheduleEntryService,
|
||||||
|
) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.scheduleService.findAll(schedules => this.schedules = schedules, Schedule.compareName);
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleUpdate(schedule: Schedule, key: string, value: any): void {
|
||||||
|
this.scheduleService.set(schedule, key, value, schedule => this.updateSchedule(schedule));
|
||||||
|
}
|
||||||
|
|
||||||
|
entryUpdate(schedule: Schedule, entry: ScheduleEntry, key: string, value: any): void {
|
||||||
|
this.scheduleEntryService.set(entry, key, value, entry => this.updateEntry(schedule, entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSchedule(schedule: Schedule): void {
|
||||||
|
const index: number = this.schedules.findIndex(s => s.id === schedule.id);
|
||||||
|
if (index < 0) {
|
||||||
|
this.schedules.push(schedule);
|
||||||
|
} else {
|
||||||
|
this.schedules[index] = schedule;
|
||||||
|
}
|
||||||
|
this.schedules = this.schedules.sort(Schedule.compareName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateEntry(schedule: Schedule, entry: ScheduleEntry): void {
|
||||||
|
const index: number = schedule.entries.findIndex(s => s.id === entry.id);
|
||||||
|
if (index < 0) {
|
||||||
|
schedule.entries.push(entry);
|
||||||
|
} else {
|
||||||
|
schedule.entries[index] = entry;
|
||||||
|
}
|
||||||
|
schedule.entries = schedule.entries.sort(ScheduleEntry.compareId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<input *ngIf="editing" type="text" [(ngModel)]="value" (keypress)="keypress($event)" (blur)="finish()">
|
||||||
|
<div *ngIf="!editing" [class.empty]="initial == ''" (click)="editing=true">
|
||||||
|
<ng-container *ngIf="initial != ''">{{initial}}</ng-container>
|
||||||
|
<ng-container *ngIf="initial == ''">- LEER -</ng-container>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
.empty {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {EditFieldComponent} from './edit-field.component';
|
||||||
|
|
||||||
|
describe('EditFieldComponent', () => {
|
||||||
|
let component: EditFieldComponent;
|
||||||
|
let fixture: ComponentFixture<EditFieldComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [EditFieldComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditFieldComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-field',
|
||||||
|
templateUrl: './edit-field.component.html',
|
||||||
|
styleUrls: ['./edit-field.component.less']
|
||||||
|
})
|
||||||
|
export class EditFieldComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
initial!: string;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
private valueChange: EventEmitter<string> = new EventEmitter<string>();
|
||||||
|
|
||||||
|
editing: boolean = false;
|
||||||
|
|
||||||
|
value: string = this.initial;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.value = this.initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(): void {
|
||||||
|
this.editing = false;
|
||||||
|
if (this.value != this.initial) {
|
||||||
|
this.valueChange.emit(this.value);
|
||||||
|
} else {
|
||||||
|
this.value = this.initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keypress($event: KeyboardEvent): void {
|
||||||
|
if ($event.key == "Enter") {
|
||||||
|
this.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<input type="number" [style]="{width: width}" [(ngModel)]="value" (change)="update()" (blur)="update()" (keypress)="$event.key === 'Enter' ? update() : {}">
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
input {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {NumberComponent} from './number.component';
|
||||||
|
|
||||||
|
describe('NumberComponent', () => {
|
||||||
|
let component: NumberComponent;
|
||||||
|
let fixture: ComponentFixture<NumberComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [NumberComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NumberComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
34
src/main/angular/src/app/shared/number/number.component.ts
Normal file
34
src/main/angular/src/app/shared/number/number.component.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-number',
|
||||||
|
templateUrl: './number.component.html',
|
||||||
|
styleUrls: ['./number.component.less']
|
||||||
|
})
|
||||||
|
export class NumberComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
width: string = "100%";
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
initial!: number;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
onChange: EventEmitter<number> = new EventEmitter<number>();
|
||||||
|
|
||||||
|
value!: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.value = this.initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(): void {
|
||||||
|
if (this.value !== this.initial) {
|
||||||
|
this.onChange.emit(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
<fa-icon [icon]="faCheck" *ngIf="initial" (click)="toggle()"></fa-icon>
|
||||||
|
<fa-icon [icon]="faTimes" *ngIf="!initial" (click)="toggle()"></fa-icon>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {ToggleSwitchComponent} from './toggle-switch.component';
|
||||||
|
|
||||||
|
describe('ToggleSwitchComponent', () => {
|
||||||
|
let component: ToggleSwitchComponent;
|
||||||
|
let fixture: ComponentFixture<ToggleSwitchComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ToggleSwitchComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ToggleSwitchComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
|
|
||||||
|
import {faCheck, faTimes} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-toggle-switch',
|
||||||
|
templateUrl: './toggle-switch.component.html',
|
||||||
|
styleUrls: ['./toggle-switch.component.less']
|
||||||
|
})
|
||||||
|
export class ToggleSwitchComponent implements OnInit {
|
||||||
|
|
||||||
|
faCheck = faCheck;
|
||||||
|
faTimes = faTimes;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
initial!: boolean;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
active: string = "Aktiv";
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
inactive: string = "Inaktiv";
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
onChange: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
value!: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.value = this.initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(): void {
|
||||||
|
this.onChange.emit(!this.initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
0
src/main/angular/src/assets/.gitkeep
Normal file
0
src/main/angular/src/assets/.gitkeep
Normal file
4
src/main/angular/src/environments/environment.prod.ts
Normal file
4
src/main/angular/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
apiBasePath: '/',
|
||||||
|
};
|
||||||
18
src/main/angular/src/environments/environment.ts
Normal file
18
src/main/angular/src/environments/environment.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// This file can be replaced during build by using the `fileReplacements` array.
|
||||||
|
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||||
|
// The list of file replacements can be found in `angular.json`.
|
||||||
|
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
apiBasePath: 'http://localhost:8080/',
|
||||||
|
// apiBasePath: 'http://10.0.0.50:8080/',
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For easier debugging in development mode, you can import the following file
|
||||||
|
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||||
|
*
|
||||||
|
* This import should be commented out in production mode because it will have a negative impact
|
||||||
|
* on performance if an error is thrown.
|
||||||
|
*/
|
||||||
|
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||||
BIN
src/main/angular/src/favicon.ico
Normal file
BIN
src/main/angular/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
13
src/main/angular/src/index.html
Normal file
13
src/main/angular/src/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!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">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
src/main/angular/src/main.ts
Normal file
12
src/main/angular/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import {enableProdMode} from '@angular/core';
|
||||||
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
|
import {AppModule} from './app/app.module';
|
||||||
|
import {environment} from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
65
src/main/angular/src/polyfills.ts
Normal file
65
src/main/angular/src/polyfills.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||||
|
* You can add your own extra polyfills to this file.
|
||||||
|
*
|
||||||
|
* This file is divided into 2 sections:
|
||||||
|
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||||
|
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||||
|
* file.
|
||||||
|
*
|
||||||
|
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||||
|
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||||
|
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||||
|
*
|
||||||
|
* Learn more in https://angular.io/guide/browser-support
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* BROWSER POLYFILLS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IE11 requires the following for NgClass support on SVG elements
|
||||||
|
*/
|
||||||
|
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web Animations `@angular/platform-browser/animations`
|
||||||
|
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||||
|
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||||
|
*/
|
||||||
|
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||||
|
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||||
|
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||||
|
* will put import in the top of bundle, so user need to create a separate file
|
||||||
|
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||||
|
* into that file, and then add the following code before importing zone.js.
|
||||||
|
* import './zone-flags';
|
||||||
|
*
|
||||||
|
* The flags allowed in zone-flags.ts are listed here.
|
||||||
|
*
|
||||||
|
* The following flags will work for all browsers.
|
||||||
|
*
|
||||||
|
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||||
|
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||||
|
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||||
|
*
|
||||||
|
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||||
|
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||||
|
*
|
||||||
|
* (window as any).__Zone_enable_cross_context_check = true;
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Zone JS is required by default for Angular itself.
|
||||||
|
*/
|
||||||
|
import 'zone.js'; // Included with Angular CLI.
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* APPLICATION IMPORTS
|
||||||
|
*/
|
||||||
37
src/main/angular/src/styles.less
Normal file
37
src/main/angular/src/styles.less
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: arial, sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid black;
|
||||||
|
|
||||||
|
img.fullCell {
|
||||||
|
margin: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table.vertical {
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/angular/src/test.ts
Normal file
24
src/main/angular/src/test.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||||
|
|
||||||
|
import 'zone.js/testing';
|
||||||
|
import {getTestBed} from '@angular/core/testing';
|
||||||
|
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
|
||||||
|
|
||||||
|
declare const require: {
|
||||||
|
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||||
|
keys(): string[];
|
||||||
|
<T>(id: string): T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// First, initialize the Angular testing environment.
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting(),
|
||||||
|
{teardown: {destroyAfterEach: true}},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then we find all the tests.
|
||||||
|
const context = require.context('./', true, /\.spec\.ts$/);
|
||||||
|
// And load the modules.
|
||||||
|
context.keys().map(context);
|
||||||
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 this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
30
src/main/angular/tsconfig.json
Normal file
30
src/main/angular/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"module": "es2020",
|
||||||
|
"lib": [
|
||||||
|
"es2018",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/angular/tsconfig.spec.json
Normal file
18
src/main/angular/tsconfig.spec.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/test.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -19,13 +19,13 @@ import java.time.ZonedDateTime;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "UnusedReturnValue", "SameParameterValue", "UnusedAssignment"})
|
@SuppressWarnings({"unchecked", "UnusedReturnValue", "SameParameterValue", "UnusedAssignment", "RedundantSuppression"})
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DemoDataService {
|
public class DemoDataService {
|
||||||
|
|
||||||
public static final int MIN30_SEC = 30 * 60;
|
public static final int MIN30 = 30 * 60;
|
||||||
|
|
||||||
private final KnxGroupWriteService knxGroupWriteService;
|
private final KnxGroupWriteService knxGroupWriteService;
|
||||||
|
|
||||||
@ -41,77 +41,68 @@ public class DemoDataService {
|
|||||||
final KnxGroupDto wohnzimmer_rollladen_position_anfahren = createKnxGroupIfNotExists("Wohnzimmer Rollladen Position Anfahren", new GroupAddress(0, 4, 24), "5.001", false, false);
|
final KnxGroupDto wohnzimmer_rollladen_position_anfahren = createKnxGroupIfNotExists("Wohnzimmer Rollladen Position Anfahren", new GroupAddress(0, 4, 24), "5.001", false, false);
|
||||||
final KnxGroupDto schlafzimmer_rollladen_position_anfahren = createKnxGroupIfNotExists("Schlafzimmer Rollladen Position Anfahren", new GroupAddress(0, 3, 3), "5.001", false, false);
|
final KnxGroupDto schlafzimmer_rollladen_position_anfahren = createKnxGroupIfNotExists("Schlafzimmer Rollladen Position Anfahren", new GroupAddress(0, 3, 3), "5.001", false, false);
|
||||||
final KnxGroupDto flur_rollladen_position_anfahren = createKnxGroupIfNotExists("Flur Rollladen Position Anfahren", new GroupAddress(0, 5, 13), "5.001", false, false);
|
final KnxGroupDto flur_rollladen_position_anfahren = createKnxGroupIfNotExists("Flur Rollladen Position Anfahren", new GroupAddress(0, 5, 13), "5.001", false, false);
|
||||||
final KnxGroupDto badewanne_schalten = createKnxGroupIfNotExists("Badewanne Schalten", 781, "1.001", false, false);
|
|
||||||
final KnxGroupDto bad_licht_mitteschalten = createKnxGroupIfNotExists("Bad Licht Mitte Schalten", 797, "1.001", false, false);
|
final KnxGroupDto bad_licht_mitteschalten = createKnxGroupIfNotExists("Bad Licht Mitte Schalten", 797, "1.001", false, false);
|
||||||
final KnxGroupDto helligkeit = createKnxGroupIfNotExists("Helligkeit", 1286, "9.004", false, true);
|
final KnxGroupDto helligkeit = createKnxGroupIfNotExists("Helligkeit", 1286, "9.004", false, true);
|
||||||
|
|
||||||
final Schedule scheduleEgFlurLicht = new Schedule();
|
if (scheduleRepository.count() == 0) {
|
||||||
scheduleEgFlurLicht.setName("EG Flur Licht");
|
final Schedule scheduleEgFlurLicht = new Schedule();
|
||||||
createTime(scheduleEgFlurLicht, 11, 30, 0, MIN30_SEC, new PropertyEntry(eg_flur_licht_schalten, true));
|
scheduleEgFlurLicht.setName("EG Flur Licht");
|
||||||
createTime(scheduleEgFlurLicht, 12, 30, 0, MIN30_SEC, new PropertyEntry(eg_flur_licht_schalten, false));
|
createTime(scheduleEgFlurLicht, 11, 30, 0, MIN30, new PropertyEntry(eg_flur_licht_schalten, true));
|
||||||
createTime(scheduleEgFlurLicht, 16, 30, 0, MIN30_SEC, new PropertyEntry(eg_flur_licht_schalten, true));
|
createTime(scheduleEgFlurLicht, 12, 30, 0, MIN30, new PropertyEntry(eg_flur_licht_schalten, false));
|
||||||
createTime(scheduleEgFlurLicht, 17, 30, 0, MIN30_SEC, new PropertyEntry(eg_flur_licht_schalten, false));
|
createTime(scheduleEgFlurLicht, 16, 30, 0, MIN30, new PropertyEntry(eg_flur_licht_schalten, true));
|
||||||
createTime(scheduleEgFlurLicht, 22, 0, 0, MIN30_SEC, new PropertyEntry(eg_flur_licht_schalten, true));
|
createTime(scheduleEgFlurLicht, 17, 30, 0, MIN30, new PropertyEntry(eg_flur_licht_schalten, false));
|
||||||
createTime(scheduleEgFlurLicht, 23, 0, 0, MIN30_SEC, new PropertyEntry(eg_flur_licht_schalten, false));
|
createTime(scheduleEgFlurLicht, 22, 0, 0, MIN30, new PropertyEntry(eg_flur_licht_schalten, true));
|
||||||
createTime(scheduleEgFlurLicht, 1, 0, 0, MIN30_SEC, new PropertyEntry(eg_flur_licht_schalten, true));
|
createTime(scheduleEgFlurLicht, 23, 0, 0, MIN30, new PropertyEntry(eg_flur_licht_schalten, false));
|
||||||
createTime(scheduleEgFlurLicht, 2, 0, 0, MIN30_SEC, new PropertyEntry(eg_flur_licht_schalten, false));
|
createTime(scheduleEgFlurLicht, 1, 0, 0, MIN30, new PropertyEntry(eg_flur_licht_schalten, true));
|
||||||
scheduleRepository.save(scheduleEgFlurLicht);
|
createTime(scheduleEgFlurLicht, 2, 0, 0, MIN30, new PropertyEntry(eg_flur_licht_schalten, false));
|
||||||
|
scheduleRepository.save(scheduleEgFlurLicht);
|
||||||
|
|
||||||
final Schedule scheduleEgAmbiente = new Schedule();
|
final Schedule scheduleEgAmbiente = new Schedule();
|
||||||
scheduleEgAmbiente.setName("EG Ambiente");
|
scheduleEgAmbiente.setName("EG Ambiente");
|
||||||
createTime(scheduleEgAmbiente, 7, 15, 0, MIN30_SEC, new PropertyEntry(eg_ambiente_schalten, true));
|
createTime(scheduleEgAmbiente, 7, 15, 0, MIN30, new PropertyEntry(eg_ambiente_schalten, true));
|
||||||
createTime(scheduleEgAmbiente, 9, 30, 0, MIN30_SEC, new PropertyEntry(eg_ambiente_schalten, false));
|
createTime(scheduleEgAmbiente, 9, 30, 0, MIN30, new PropertyEntry(eg_ambiente_schalten, false));
|
||||||
createSunset(scheduleEgAmbiente, Zenith.OFFICIAL, MIN30_SEC, new PropertyEntry(eg_ambiente_schalten, true));
|
createSunset(scheduleEgAmbiente, Zenith.OFFICIAL, MIN30, new PropertyEntry(eg_ambiente_schalten, true));
|
||||||
createSunset(scheduleEgAmbiente, Zenith.ASTRONOMICAL, MIN30_SEC, new PropertyEntry(eg_ambiente_schalten, false));
|
createSunset(scheduleEgAmbiente, Zenith.ASTRONOMICAL, MIN30, new PropertyEntry(eg_ambiente_schalten, false));
|
||||||
scheduleRepository.save(scheduleEgAmbiente);
|
scheduleRepository.save(scheduleEgAmbiente);
|
||||||
|
|
||||||
final Schedule scheduleOgAmbiente = new Schedule();
|
final Schedule scheduleOgAmbiente = new Schedule();
|
||||||
scheduleOgAmbiente.setName("OG Ambiente");
|
scheduleOgAmbiente.setName("OG Ambiente");
|
||||||
createTime(scheduleOgAmbiente, 7, 15, 0, MIN30_SEC, new PropertyEntry(og_ambiente_schalten, true));
|
createTime(scheduleOgAmbiente, 7, 15, 0, MIN30, new PropertyEntry(og_ambiente_schalten, true));
|
||||||
createTime(scheduleOgAmbiente, 9, 30, 0, MIN30_SEC, new PropertyEntry(og_ambiente_schalten, false));
|
createTime(scheduleOgAmbiente, 9, 30, 0, MIN30, new PropertyEntry(og_ambiente_schalten, false));
|
||||||
createSunset(scheduleOgAmbiente, Zenith.OFFICIAL, MIN30_SEC, new PropertyEntry(og_ambiente_schalten, true));
|
createSunset(scheduleOgAmbiente, Zenith.OFFICIAL, MIN30, new PropertyEntry(og_ambiente_schalten, true));
|
||||||
createSunset(scheduleOgAmbiente, Zenith.ASTRONOMICAL, MIN30_SEC, new PropertyEntry(og_ambiente_schalten, false));
|
createSunset(scheduleOgAmbiente, Zenith.ASTRONOMICAL, MIN30, new PropertyEntry(og_ambiente_schalten, false));
|
||||||
scheduleRepository.save(scheduleOgAmbiente);
|
scheduleRepository.save(scheduleOgAmbiente);
|
||||||
|
|
||||||
final Schedule scheduleWohnzimmerRollladen = new Schedule();
|
final Schedule scheduleWohnzimmerRollladen = new Schedule();
|
||||||
scheduleWohnzimmerRollladen.setName("Rollläden Wohnzimmer");
|
scheduleWohnzimmerRollladen.setName("Rollläden Wohnzimmer");
|
||||||
createSunrise(scheduleWohnzimmerRollladen, Zenith.CIVIL, 0, new PropertyEntry(wohnzimmer_rollladen_position_anfahren, 0));
|
createSunrise(scheduleWohnzimmerRollladen, Zenith.CIVIL, 0, new PropertyEntry(wohnzimmer_rollladen_position_anfahren, 0));
|
||||||
createSunset(scheduleWohnzimmerRollladen, Zenith.CIVIL, 0, new PropertyEntry(wohnzimmer_rollladen_position_anfahren, 100));
|
createSunset(scheduleWohnzimmerRollladen, Zenith.CIVIL, 0, new PropertyEntry(wohnzimmer_rollladen_position_anfahren, 100));
|
||||||
scheduleRepository.save(scheduleWohnzimmerRollladen);
|
scheduleRepository.save(scheduleWohnzimmerRollladen);
|
||||||
|
|
||||||
final Schedule scheduleSchlafzimmerRollladen = new Schedule();
|
final Schedule scheduleSchlafzimmerRollladen = new Schedule();
|
||||||
scheduleSchlafzimmerRollladen.setName("Rollläden Schlafzimmer");
|
scheduleSchlafzimmerRollladen.setName("Rollläden Schlafzimmer");
|
||||||
createTime(scheduleSchlafzimmerRollladen, 7, 0, 0, 0, new PropertyEntry(schlafzimmer_rollladen_position_anfahren, 0));
|
createTime(scheduleSchlafzimmerRollladen, 7, 0, 0, 0, new PropertyEntry(schlafzimmer_rollladen_position_anfahren, 0));
|
||||||
createSunset(scheduleSchlafzimmerRollladen, Zenith.CIVIL, 0, new PropertyEntry(schlafzimmer_rollladen_position_anfahren, 100));
|
createSunset(scheduleSchlafzimmerRollladen, Zenith.CIVIL, 0, new PropertyEntry(schlafzimmer_rollladen_position_anfahren, 100));
|
||||||
scheduleRepository.save(scheduleSchlafzimmerRollladen);
|
scheduleRepository.save(scheduleSchlafzimmerRollladen);
|
||||||
|
|
||||||
final Schedule scheduleFlurRollladen = new Schedule();
|
final Schedule scheduleFlurRollladen = new Schedule();
|
||||||
scheduleFlurRollladen.setName("Rollläden Flur");
|
scheduleFlurRollladen.setName("Rollläden Flur");
|
||||||
createSunrise(scheduleFlurRollladen, Zenith.CIVIL, 0, new PropertyEntry(flur_rollladen_position_anfahren, 0));
|
createSunrise(scheduleFlurRollladen, Zenith.CIVIL, 0, new PropertyEntry(flur_rollladen_position_anfahren, 0));
|
||||||
createSunset(scheduleFlurRollladen, Zenith.CIVIL, 0, new PropertyEntry(flur_rollladen_position_anfahren, 100));
|
createSunset(scheduleFlurRollladen, Zenith.CIVIL, 0, new PropertyEntry(flur_rollladen_position_anfahren, 100));
|
||||||
scheduleRepository.save(scheduleFlurRollladen);
|
scheduleRepository.save(scheduleFlurRollladen);
|
||||||
|
|
||||||
final Schedule scheduleBadewanneBlinken = new Schedule();
|
final Schedule scheduleBadLichtMitte = new Schedule();
|
||||||
scheduleBadewanneBlinken.setName("Badewanne");
|
scheduleBadLichtMitte.setName("Bad Licht Mitte");
|
||||||
final int interval = 2;
|
createTime(scheduleBadLichtMitte, 10, 30, 0, MIN30, new PropertyEntry(bad_licht_mitteschalten, true));
|
||||||
int seconds = interval;
|
createTime(scheduleBadLichtMitte, 11, 30, 0, MIN30, new PropertyEntry(bad_licht_mitteschalten, false));
|
||||||
createRelative(scheduleBadewanneBlinken, seconds += interval, 0, new PropertyEntry(badewanne_schalten, true));
|
createTime(scheduleBadLichtMitte, 15, 30, 0, MIN30, new PropertyEntry(bad_licht_mitteschalten, true));
|
||||||
createRelative(scheduleBadewanneBlinken, seconds += interval, 0, new PropertyEntry(badewanne_schalten, false));
|
createTime(scheduleBadLichtMitte, 16, 30, 0, MIN30, new PropertyEntry(bad_licht_mitteschalten, false));
|
||||||
createRelative(scheduleBadewanneBlinken, seconds += interval, 0, new PropertyEntry(badewanne_schalten, true));
|
createTime(scheduleBadLichtMitte, 21, 0, 0, MIN30, new PropertyEntry(bad_licht_mitteschalten, true));
|
||||||
createRelative(scheduleBadewanneBlinken, seconds += interval, 0, new PropertyEntry(badewanne_schalten, false));
|
createTime(scheduleBadLichtMitte, 22, 0, 0, MIN30, new PropertyEntry(bad_licht_mitteschalten, false));
|
||||||
scheduleRepository.save(scheduleBadewanneBlinken);
|
createTime(scheduleBadLichtMitte, 0, 0, 0, MIN30, new PropertyEntry(bad_licht_mitteschalten, true));
|
||||||
|
createTime(scheduleBadLichtMitte, 1, 0, 0, MIN30, new PropertyEntry(bad_licht_mitteschalten, false));
|
||||||
final Schedule scheduleBadLichtMitte = new Schedule();
|
scheduleRepository.save(scheduleBadLichtMitte);
|
||||||
scheduleBadLichtMitte.setName("Bad Licht Mitte");
|
}
|
||||||
createTime(scheduleBadLichtMitte, 10, 30, 0, MIN30_SEC, new PropertyEntry(bad_licht_mitteschalten, true));
|
|
||||||
createTime(scheduleBadLichtMitte, 11, 30, 0, MIN30_SEC, new PropertyEntry(bad_licht_mitteschalten, false));
|
|
||||||
createTime(scheduleBadLichtMitte, 15, 30, 0, MIN30_SEC, new PropertyEntry(bad_licht_mitteschalten, true));
|
|
||||||
createTime(scheduleBadLichtMitte, 16, 30, 0, MIN30_SEC, new PropertyEntry(bad_licht_mitteschalten, false));
|
|
||||||
createTime(scheduleBadLichtMitte, 21, 0, 0, MIN30_SEC, new PropertyEntry(bad_licht_mitteschalten, true));
|
|
||||||
createTime(scheduleBadLichtMitte, 22, 0, 0, MIN30_SEC, new PropertyEntry(bad_licht_mitteschalten, false));
|
|
||||||
createTime(scheduleBadLichtMitte, 0, 0, 0, MIN30_SEC, new PropertyEntry(bad_licht_mitteschalten, true));
|
|
||||||
createTime(scheduleBadLichtMitte, 1, 0, 0, MIN30_SEC, new PropertyEntry(bad_licht_mitteschalten, false));
|
|
||||||
scheduleRepository.save(scheduleBadLichtMitte);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private KnxGroupDto createKnxGroupIfNotExists(final String name, final int address, final String dpt, final boolean readable, final boolean multiGroup) {
|
private KnxGroupDto createKnxGroupIfNotExists(final String name, final int address, final String dpt, final boolean readable, final boolean multiGroup) {
|
||||||
@ -122,24 +113,24 @@ public class DemoDataService {
|
|||||||
return knxGroupRepository.findByAddressRaw(address.getRawAddress()).map(KnxGroupDto::new).orElseGet(() -> knxGroupWriteService.create(name, address, dpt, readable, multiGroup));
|
return knxGroupRepository.findByAddressRaw(address.getRawAddress()).map(KnxGroupDto::new).orElseGet(() -> knxGroupWriteService.create(name, address, dpt, readable, multiGroup));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduleEntry createRelative(final Schedule schedule, final int inSeconds, final int plusMinusSeconds, final Map.Entry<String, String>... entries) {
|
private ScheduleEntry createRelative(final Schedule schedule, final int inSeconds, final int fuzzySeconds, final Map.Entry<String, String>... entries) {
|
||||||
final ZonedDateTime now = ZonedDateTime.now().plusSeconds(inSeconds).withNano(0);
|
final ZonedDateTime now = ZonedDateTime.now().plusSeconds(inSeconds).withNano(0);
|
||||||
return create(schedule, ScheduleEntryType.TIME, null, now.getHour(), now.getMinute(), now.getSecond(), plusMinusSeconds, entries);
|
return create(schedule, ScheduleEntryType.TIME, null, now.getHour(), now.getMinute(), now.getSecond(), fuzzySeconds, entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduleEntry createTime(final Schedule schedule, final int hour, final int minute, final int second, final int plusMinusSeconds, final Map.Entry<String, String>... entries) {
|
private ScheduleEntry createTime(final Schedule schedule, final int hour, final int minute, final int second, final int fuzzySeconds, final Map.Entry<String, String>... entries) {
|
||||||
return create(schedule, ScheduleEntryType.TIME, null, hour, minute, second, plusMinusSeconds, entries);
|
return create(schedule, ScheduleEntryType.TIME, null, hour, minute, second, fuzzySeconds, entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduleEntry createSunrise(final Schedule schedule, final Zenith zenith, final int plusMinusSeconds, final Map.Entry<String, String>... entries) {
|
private ScheduleEntry createSunrise(final Schedule schedule, final Zenith zenith, final int fuzzySeconds, final Map.Entry<String, String>... entries) {
|
||||||
return create(schedule, ScheduleEntryType.SUNRISE, zenith, 0, 0, 0, plusMinusSeconds, entries);
|
return create(schedule, ScheduleEntryType.SUNRISE, zenith, 0, 0, 0, fuzzySeconds, entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduleEntry createSunset(final Schedule schedule, final Zenith zenith, final int plusMinusSeconds, final Map.Entry<String, String>... entries) {
|
private ScheduleEntry createSunset(final Schedule schedule, final Zenith zenith, final int fuzzySeconds, final Map.Entry<String, String>... entries) {
|
||||||
return create(schedule, ScheduleEntryType.SUNSET, zenith, 0, 0, 0, plusMinusSeconds, entries);
|
return create(schedule, ScheduleEntryType.SUNSET, zenith, 0, 0, 0, fuzzySeconds, entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduleEntry create(final Schedule schedule, final ScheduleEntryType type, final Zenith zenith, final int hour, final int minute, final int second, final int plusMinusSeconds, final Map.Entry<String, String>... entries) {
|
private ScheduleEntry create(final Schedule schedule, final ScheduleEntryType type, final Zenith zenith, final int hour, final int minute, final int second, final int fuzzySeconds, final Map.Entry<String, String>... entries) {
|
||||||
final ScheduleEntry entry = new ScheduleEntry();
|
final ScheduleEntry entry = new ScheduleEntry();
|
||||||
entry.setType(type);
|
entry.setType(type);
|
||||||
if (zenith != null) {
|
if (zenith != null) {
|
||||||
@ -148,7 +139,7 @@ public class DemoDataService {
|
|||||||
entry.setHour(hour);
|
entry.setHour(hour);
|
||||||
entry.setMinute(minute);
|
entry.setMinute(minute);
|
||||||
entry.setSecond(second);
|
entry.setSecond(second);
|
||||||
entry.setPlusMinusSeconds(plusMinusSeconds);
|
entry.setFuzzySeconds(fuzzySeconds);
|
||||||
Arrays.stream(entries).forEach(p -> entry.getProperties().put(p.getKey(), p.getValue()));
|
Arrays.stream(entries).forEach(p -> entry.getProperties().put(p.getKey(), p.getValue()));
|
||||||
schedule.getEntries().add(entry);
|
schedule.getEntries().add(entry);
|
||||||
return entry;
|
return entry;
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package de.ph87.homeautomation.device;
|
package de.ph87.homeautomation.device;
|
||||||
|
|
||||||
import de.ph87.homeautomation.device.devices.Device;
|
import de.ph87.homeautomation.device.devices.Device;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
public abstract class DeviceDto {
|
public abstract class DeviceDto {
|
||||||
|
|
||||||
public final long id;
|
public final long id;
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package de.ph87.homeautomation.device.devices;
|
package de.ph87.homeautomation.device.devices;
|
||||||
|
|
||||||
import de.ph87.homeautomation.device.DeviceDto;
|
import de.ph87.homeautomation.device.DeviceDto;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
public class DeviceNumberDto extends DeviceDto {
|
public class DeviceNumberDto extends DeviceDto {
|
||||||
|
|
||||||
public final Number value;
|
public final Number value;
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package de.ph87.homeautomation.device.devices;
|
package de.ph87.homeautomation.device.devices;
|
||||||
|
|
||||||
import de.ph87.homeautomation.device.DeviceDto;
|
import de.ph87.homeautomation.device.DeviceDto;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
public class DeviceSwitchDto extends DeviceDto {
|
public class DeviceSwitchDto extends DeviceDto {
|
||||||
|
|
||||||
public final Boolean state;
|
public final Boolean state;
|
||||||
|
|||||||
@ -2,11 +2,14 @@ package de.ph87.homeautomation.knx;
|
|||||||
|
|
||||||
import de.ph87.homeautomation.knx.group.KnxGroupLinkService;
|
import de.ph87.homeautomation.knx.group.KnxGroupLinkService;
|
||||||
import de.ph87.homeautomation.knx.group.KnxGroupWriteService;
|
import de.ph87.homeautomation.knx.group.KnxGroupWriteService;
|
||||||
|
import de.ph87.homeautomation.knx.group.KnxThreadWakeUpEvent;
|
||||||
import de.ph87.homeautomation.shared.AbstractThreadService;
|
import de.ph87.homeautomation.shared.AbstractThreadService;
|
||||||
import de.ph87.network.router.Router;
|
import de.ph87.network.router.Router;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
import tuwien.auto.calimero.CloseEvent;
|
import tuwien.auto.calimero.CloseEvent;
|
||||||
import tuwien.auto.calimero.DetachEvent;
|
import tuwien.auto.calimero.DetachEvent;
|
||||||
import tuwien.auto.calimero.FrameEvent;
|
import tuwien.auto.calimero.FrameEvent;
|
||||||
@ -150,4 +153,10 @@ public class KnxThreadService extends AbstractThreadService implements NetworkLi
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventListener(KnxThreadWakeUpEvent.class)
|
||||||
|
@TransactionalEventListener
|
||||||
|
public void wakeUp() {
|
||||||
|
_wakeUp();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
package de.ph87.homeautomation.knx.group;
|
package de.ph87.homeautomation.knx.group;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Getter;
|
||||||
import tuwien.auto.calimero.GroupAddress;
|
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
@Data
|
@Getter
|
||||||
public class KnxGroupDto {
|
public class KnxGroupDto {
|
||||||
|
|
||||||
public final long id;
|
public final long id;
|
||||||
@ -38,8 +37,4 @@ public class KnxGroupDto {
|
|||||||
valueTimestamp = knxGroup.getValueTimestamp();
|
valueTimestamp = knxGroup.getValueTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupAddress getAddress() {
|
|
||||||
return new GroupAddress(addressRaw);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import lombok.Getter;
|
|||||||
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;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import tuwien.auto.calimero.GroupAddress;
|
import tuwien.auto.calimero.GroupAddress;
|
||||||
@ -33,10 +34,12 @@ public class KnxGroupSetService implements IPropertyOwner {
|
|||||||
|
|
||||||
private final KnxGroupRepository knxGroupRepository;
|
private final KnxGroupRepository knxGroupRepository;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
@EventListener(ApplicationStartedEvent.class)
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
public void requestAll() {
|
public void requestAll() {
|
||||||
knxGroupWriteService.markAllForRead();
|
knxGroupWriteService.markAllForRead();
|
||||||
knxThreadService.notifyActionPending();
|
eventPublisher.publishEvent(new KnxThreadWakeUpEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -44,7 +47,7 @@ public class KnxGroupSetService implements IPropertyOwner {
|
|||||||
final GroupAddress groupAddress = parseGroupAddress(propertyName);
|
final GroupAddress groupAddress = parseGroupAddress(propertyName);
|
||||||
try {
|
try {
|
||||||
if (knxGroupWriteService.setSendValue(groupAddress, value)) {
|
if (knxGroupWriteService.setSendValue(groupAddress, value)) {
|
||||||
knxThreadService.notifyActionPending();
|
eventPublisher.publishEvent(new KnxThreadWakeUpEvent());
|
||||||
} else {
|
} else {
|
||||||
log.error("No such KnxGroup.address = {}", groupAddress);
|
log.error("No such KnxGroup.address = {}", groupAddress);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,7 +89,7 @@ public class KnxGroupWriteService {
|
|||||||
if (knxGroup.getNumberValue() != null && !knxGroup.getNumberValue().isNaN()) {
|
if (knxGroup.getNumberValue() != null && !knxGroup.getNumberValue().isNaN()) {
|
||||||
final String message = knxGroup.getPropertyName() + (knxGroup.isMultiGroup() ? ".from." + knxGroup.getLastDeviceAddressString() : "") + " " + knxGroup.getNumberValue();
|
final String message = knxGroup.getPropertyName() + (knxGroup.isMultiGroup() ? ".from." + knxGroup.getLastDeviceAddressString() : "") + " " + knxGroup.getNumberValue();
|
||||||
try {
|
try {
|
||||||
log.info("UDP Broadcast {}:{}: {}", broadcastAddress, broadcastPort, message);
|
log.debug("UDP Broadcast {}:{}: {}", broadcastAddress, broadcastPort, message);
|
||||||
final byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
|
final byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
|
||||||
broadcastDatagramSocket.send(new DatagramPacket(bytes, bytes.length, broadcastAddress, broadcastPort));
|
broadcastDatagramSocket.send(new DatagramPacket(bytes, bytes.length, broadcastAddress, broadcastPort));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
package de.ph87.homeautomation.knx.group;
|
||||||
|
|
||||||
|
public class KnxThreadWakeUpEvent {
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
package de.ph87.homeautomation.property;
|
package de.ph87.homeautomation.property;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Getter
|
||||||
public class PropertyDto {
|
public class PropertyDto {
|
||||||
|
|
||||||
public final String name;
|
public final String name;
|
||||||
|
|||||||
@ -0,0 +1,135 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import com.luckycatlabs.sunrisesunset.Zenith;
|
||||||
|
import com.luckycatlabs.sunrisesunset.calculator.SolarEventCalculator;
|
||||||
|
import com.luckycatlabs.sunrisesunset.dto.Location;
|
||||||
|
import de.ph87.homeautomation.Config;
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntryType;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleCalculationService {
|
||||||
|
|
||||||
|
private final Config config;
|
||||||
|
|
||||||
|
private final ScheduleReadService scheduleReadService;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
|
public void calculateAllNext() {
|
||||||
|
final ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
scheduleReadService.findAll().forEach(schedule -> calculateSchedule(schedule, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void calculateSchedule(final Schedule schedule, final ZonedDateTime now) {
|
||||||
|
schedule.getEntries().forEach(scheduleEntry -> calculateEntry(schedule, scheduleEntry, now));
|
||||||
|
final Optional<ScheduleEntry> nextEntry = schedule.getEntries().stream()
|
||||||
|
.filter(entry -> entry.getNextFuzzyTimestamp() != null && entry.getNextFuzzyTimestamp().isAfter(now))
|
||||||
|
.min(Comparator.comparing(ScheduleEntry::getNextFuzzyTimestamp));
|
||||||
|
if (nextEntry.isEmpty()) {
|
||||||
|
log.info("No next schedule for \"{}\"", schedule.getName());
|
||||||
|
} else {
|
||||||
|
log.info("Next schedule for \"{}\": {}", schedule.getName(), nextEntry.get().getNextFuzzyTimestamp());
|
||||||
|
}
|
||||||
|
eventPublisher.publishEvent(new ScheduleThreadWakeUpEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void calculateEntry(final Schedule schedule, final ScheduleEntry entry, final ZonedDateTime now) {
|
||||||
|
log.debug("calculateNext \"{}\", {}:", schedule.getName(), entry);
|
||||||
|
if (!schedule.isEnabled() || !entry.isEnabled() || !isAnyWeekdayEnabled(entry)) {
|
||||||
|
entry.setNextClearTimestamp(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ZonedDateTime midnight = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
|
||||||
|
ZonedDateTime next = calculateEntryForDay(entry, midnight);
|
||||||
|
while (next != null && (!next.isAfter(now) || !isAfterLast(entry, next) || !isWeekdayEnabled(entry, next))) {
|
||||||
|
log.debug(" -- skipping: {}", next);
|
||||||
|
midnight = midnight.plusDays(1);
|
||||||
|
next = calculateEntryForDay(entry, midnight);
|
||||||
|
}
|
||||||
|
log.debug(" => {}", next);
|
||||||
|
entry.setNextClearTimestamp(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAfterLast(final ScheduleEntry entry, final ZonedDateTime next) {
|
||||||
|
return entry.getLastClearTimestamp() == null || !next.isAfter(entry.getLastClearTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZonedDateTime calculateEntryForDay(final ScheduleEntry entry, final ZonedDateTime midnight) {
|
||||||
|
switch (entry.getType()) {
|
||||||
|
case TIME:
|
||||||
|
return midnight.withHour(entry.getHour()).withMinute(entry.getMinute()).withSecond(entry.getSecond());
|
||||||
|
case SUNRISE:
|
||||||
|
case SUNSET:
|
||||||
|
return astroNext(entry, midnight);
|
||||||
|
default:
|
||||||
|
log.error("AstroEvent not implemented: {}", entry.getType());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZonedDateTime astroNext(final ScheduleEntry entry, ZonedDateTime midnight) {
|
||||||
|
final Location location = new Location(config.getLatitude(), config.getLongitude());
|
||||||
|
final SolarEventCalculator calculator = new SolarEventCalculator(location, config.getTimezone());
|
||||||
|
final Calendar calendar = GregorianCalendar.from(midnight);
|
||||||
|
final Calendar nextCalendar = astroNext(calculator, entry.getType(), new Zenith(entry.getZenith()), calendar);
|
||||||
|
if (nextCalendar == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(nextCalendar.getTimeInMillis()), midnight.getZone());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Calendar astroNext(final SolarEventCalculator calculator, final ScheduleEntryType type, final Zenith solarZenith, final Calendar calendar) {
|
||||||
|
switch (type) {
|
||||||
|
case SUNRISE:
|
||||||
|
return calculator.computeSunriseCalendar(solarZenith, calendar);
|
||||||
|
case SUNSET:
|
||||||
|
return calculator.computeSunsetCalendar(solarZenith, calendar);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAnyWeekdayEnabled(final ScheduleEntry entry) {
|
||||||
|
return entry.isMonday() || entry.isTuesday() || entry.isWednesday() || entry.isThursday() || entry.isFriday() || entry.isSaturday() || entry.isSunday();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWeekdayEnabled(final ScheduleEntry entry, final ZonedDateTime value) {
|
||||||
|
switch (value.getDayOfWeek()) {
|
||||||
|
case MONDAY:
|
||||||
|
return entry.isMonday();
|
||||||
|
case TUESDAY:
|
||||||
|
return entry.isTuesday();
|
||||||
|
case WEDNESDAY:
|
||||||
|
return entry.isWednesday();
|
||||||
|
case THURSDAY:
|
||||||
|
return entry.isThursday();
|
||||||
|
case FRIDAY:
|
||||||
|
return entry.isFriday();
|
||||||
|
case SATURDAY:
|
||||||
|
return entry.isSaturday();
|
||||||
|
case SUNDAY:
|
||||||
|
return entry.isSunday();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,64 +1,33 @@
|
|||||||
package de.ph87.homeautomation.schedule;
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
|
import de.ph87.homeautomation.schedule.entry.ScheduleNextExecutionDto;
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("schedule")
|
@RequestMapping("schedule")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ScheduleController {
|
public class ScheduleController {
|
||||||
|
|
||||||
private final ScheduleRepository scheduleRepository;
|
private final ScheduleReadService scheduleReadService;
|
||||||
|
|
||||||
|
private final ScheduleWriteService scheduleWriteService;
|
||||||
|
|
||||||
@GetMapping("findAll")
|
@GetMapping("findAll")
|
||||||
public List<ScheduleDto> findAll() {
|
public List<ScheduleDto> findAll() {
|
||||||
return scheduleRepository.findAll().stream().map(ScheduleDto::new).collect(Collectors.toList());
|
return scheduleReadService.findAllDtos();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("findAllNext")
|
@GetMapping("findAllNext")
|
||||||
public List<ScheduleEntryNextDto> findAllNext() {
|
public List<ScheduleNextExecutionDto> findAllNext() {
|
||||||
final ZonedDateTime now = ZonedDateTime.now();
|
return scheduleReadService.findAllNextExecutionDtos();
|
||||||
return scheduleRepository.findAll().stream()
|
|
||||||
.map(schedule -> ScheduleEntryNextDto.create(schedule, now))
|
|
||||||
.filter(Optional::isPresent)
|
|
||||||
.map(Optional::get)
|
|
||||||
.sorted(Comparator.comparing(ScheduleEntryNextDto::getNextTimestamp))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@PostMapping("set/{id}/enabled")
|
||||||
static class ScheduleEntryNextDto {
|
public ScheduleDto setEnabled(@PathVariable final long id, @RequestBody final boolean enabled) {
|
||||||
|
return scheduleWriteService.setEnabled(id, enabled);
|
||||||
public final String name;
|
|
||||||
|
|
||||||
public final ZonedDateTime nextTimestamp;
|
|
||||||
|
|
||||||
public final HashMap<String, String> properties;
|
|
||||||
|
|
||||||
private ScheduleEntryNextDto(final Schedule schedule, final ScheduleEntry entry) {
|
|
||||||
this.name = schedule.getName();
|
|
||||||
this.nextTimestamp = entry.getNextFuzzyTimestamp();
|
|
||||||
this.properties = new HashMap<>(entry.getProperties());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Optional<ScheduleEntryNextDto> create(final Schedule schedule, final ZonedDateTime now) {
|
|
||||||
return schedule.getEntries().stream()
|
|
||||||
.filter(entry -> entry.getNextFuzzyTimestamp() != null && entry.getNextFuzzyTimestamp().isAfter(now))
|
|
||||||
.min(Comparator.comparing(ScheduleEntry::getNextFuzzyTimestamp))
|
|
||||||
.map(scheduleEntry -> new ScheduleEntryNextDto(schedule, scheduleEntry));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
package de.ph87.homeautomation.schedule;
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
import de.ph87.homeautomation.schedule.entry.ScheduleEntryDto;
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntryDto;
|
||||||
import lombok.Data;
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Data
|
@Getter
|
||||||
public class ScheduleDto {
|
public class ScheduleDto {
|
||||||
|
|
||||||
private long id;
|
public final long id;
|
||||||
|
|
||||||
private boolean enabled;
|
public final boolean enabled;
|
||||||
|
|
||||||
private String name;
|
public final String name;
|
||||||
|
|
||||||
private Set<ScheduleEntryDto> entries;
|
public final Set<ScheduleEntryDto> entries;
|
||||||
|
|
||||||
public ScheduleDto(final Schedule schedule) {
|
public ScheduleDto(final Schedule schedule) {
|
||||||
id = schedule.getId();
|
id = schedule.getId();
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import de.ph87.homeautomation.property.PropertyService;
|
||||||
|
import de.ph87.homeautomation.property.PropertySetException;
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleExecutionService {
|
||||||
|
|
||||||
|
private final ScheduleReadService scheduleReadService;
|
||||||
|
|
||||||
|
private final ScheduleCalculationService scheduleCalculationService;
|
||||||
|
|
||||||
|
private final PropertyService propertyService;
|
||||||
|
|
||||||
|
public void executeAllLastDue() {
|
||||||
|
final ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
scheduleReadService.findAll().forEach(schedule -> executeLastDue(schedule, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeLastDue(final Schedule schedule, final ZonedDateTime now) {
|
||||||
|
schedule.getEntries().stream()
|
||||||
|
.filter(entry -> entry.getNextFuzzyTimestamp() != null && !entry.getNextFuzzyTimestamp().isAfter(now))
|
||||||
|
.max(Comparator.comparing(ScheduleEntry::getNextFuzzyTimestamp))
|
||||||
|
.ifPresent(entry -> {
|
||||||
|
entry.setLastClearTimestamp(entry.getNextClearTimestamp());
|
||||||
|
log.info("Executing Schedule \"{}\" Entry {}", schedule.getName(), entry);
|
||||||
|
entry.getProperties().forEach(this::applyPropertyMapEntry);
|
||||||
|
scheduleCalculationService.calculateSchedule(schedule, now);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPropertyMapEntry(final String propertyName, final String value) {
|
||||||
|
try {
|
||||||
|
propertyService.set(propertyName, value);
|
||||||
|
} catch (PropertySetException e) {
|
||||||
|
log.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleMapper {
|
||||||
|
|
||||||
|
public ScheduleDto toDto(final Schedule schedule) {
|
||||||
|
return new ScheduleDto(schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleNextExecutionDto;
|
||||||
|
import de.ph87.office.web.NotFoundException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleReadService {
|
||||||
|
|
||||||
|
private final ScheduleRepository scheduleRepository;
|
||||||
|
|
||||||
|
private final ScheduleMapper scheduleMapper;
|
||||||
|
|
||||||
|
public Schedule get(final long id) {
|
||||||
|
return scheduleRepository.findById(id).orElseThrow(() -> new NotFoundException("Schedule.id=%d", id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Schedule> findAll() {
|
||||||
|
return scheduleRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ScheduleDto> findAllDtos() {
|
||||||
|
return findAll().stream().map(scheduleMapper::toDto).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ScheduleNextExecutionDto> findAllNextExecutionDtos() {
|
||||||
|
final ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
return scheduleRepository.findAll().stream()
|
||||||
|
.map(schedule -> ScheduleNextExecutionDto.create(schedule, now))
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.sorted(Comparator.comparing(ScheduleNextExecutionDto::getNextTimestamp))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Schedule getByEntry(final ScheduleEntry entry) {
|
||||||
|
return scheduleRepository.getByEntriesContaining(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package de.ph87.homeautomation.schedule;
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
|
||||||
import org.springframework.data.repository.CrudRepository;
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -8,4 +9,6 @@ public interface ScheduleRepository extends CrudRepository<Schedule, Long> {
|
|||||||
|
|
||||||
List<Schedule> findAll();
|
List<Schedule> findAll();
|
||||||
|
|
||||||
|
Schedule getByEntriesContaining(ScheduleEntry entry);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
package de.ph87.homeautomation.schedule;
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntryReadService;
|
||||||
import de.ph87.homeautomation.shared.AbstractThreadService;
|
import de.ph87.homeautomation.shared.AbstractThreadService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
@ -13,7 +16,9 @@ import java.time.ZonedDateTime;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ScheduleThreadService extends AbstractThreadService {
|
public class ScheduleThreadService extends AbstractThreadService {
|
||||||
|
|
||||||
private final ScheduleWriteService scheduleWriteService;
|
private final ScheduleExecutionService scheduleExecutionService;
|
||||||
|
|
||||||
|
private final ScheduleEntryReadService scheduleEntryReadService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getThreadName() {
|
protected String getThreadName() {
|
||||||
@ -22,13 +27,13 @@ public class ScheduleThreadService extends AbstractThreadService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doStart() throws Exception {
|
protected void doStart() throws Exception {
|
||||||
scheduleWriteService.calculateAllNext();
|
// nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long doStep() throws InterruptedException {
|
protected long doStep() throws InterruptedException {
|
||||||
scheduleWriteService.executeAllLastDue();
|
scheduleExecutionService.executeAllLastDue();
|
||||||
return scheduleWriteService.getNextTimestamp().map(nextTimestamp -> Duration.between(ZonedDateTime.now(), nextTimestamp).toMillis()).orElse(0L);
|
return scheduleEntryReadService.getNextTimestamp().map(nextTimestamp -> Duration.between(ZonedDateTime.now(), nextTimestamp).toMillis()).orElse(0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -41,4 +46,10 @@ public class ScheduleThreadService extends AbstractThreadService {
|
|||||||
// nothing
|
// nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventListener(ScheduleThreadWakeUpEvent.class)
|
||||||
|
@TransactionalEventListener
|
||||||
|
public void wakeUp() {
|
||||||
|
_wakeUp();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
public class ScheduleThreadWakeUpEvent {
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,158 +1,29 @@
|
|||||||
package de.ph87.homeautomation.schedule;
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
import com.luckycatlabs.sunrisesunset.Zenith;
|
|
||||||
import com.luckycatlabs.sunrisesunset.calculator.SolarEventCalculator;
|
|
||||||
import com.luckycatlabs.sunrisesunset.dto.Location;
|
|
||||||
import de.ph87.homeautomation.Config;
|
|
||||||
import de.ph87.homeautomation.property.PropertyService;
|
|
||||||
import de.ph87.homeautomation.property.PropertySetException;
|
|
||||||
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
|
|
||||||
import de.ph87.homeautomation.schedule.entry.ScheduleEntryRepository;
|
|
||||||
import de.ph87.homeautomation.schedule.entry.ScheduleEntryType;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.GregorianCalendar;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@EnableScheduling
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ScheduleWriteService {
|
public class ScheduleWriteService {
|
||||||
|
|
||||||
private final Config config;
|
private final ScheduleReadService scheduleReadService;
|
||||||
|
|
||||||
private final ScheduleRepository scheduleRepository;
|
private final ScheduleCalculationService scheduleCalculationService;
|
||||||
|
|
||||||
private final PropertyService propertyService;
|
private final ScheduleMapper scheduleMapper;
|
||||||
|
|
||||||
private final ScheduleEntryRepository scheduleEntryRepository;
|
public ScheduleDto setEnabled(final long id, final boolean enabled) {
|
||||||
|
final Schedule schedule = scheduleReadService.get(id);
|
||||||
public void calculateAllNext() {
|
schedule.setEnabled(enabled);
|
||||||
final ZonedDateTime now = ZonedDateTime.now();
|
scheduleCalculationService.calculateSchedule(schedule, ZonedDateTime.now());
|
||||||
scheduleRepository.findAll().forEach(schedule -> calculateSchedule(schedule, now));
|
return scheduleMapper.toDto(schedule);
|
||||||
}
|
|
||||||
|
|
||||||
public void executeAllLastDue() {
|
|
||||||
final ZonedDateTime now = ZonedDateTime.now();
|
|
||||||
scheduleRepository.findAll().forEach(schedule -> executeLastDue(schedule, now));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeLastDue(final Schedule schedule, final ZonedDateTime now) {
|
|
||||||
schedule.getEntries().stream()
|
|
||||||
.filter(entry -> entry.getNextFuzzyTimestamp() != null && !entry.getNextFuzzyTimestamp().isAfter(now))
|
|
||||||
.max(Comparator.comparing(ScheduleEntry::getNextFuzzyTimestamp))
|
|
||||||
.ifPresent(entry -> {
|
|
||||||
entry.setLastClearTimestamp(entry.getNextClearTimestamp());
|
|
||||||
log.info("Executing ScheduleEntry {}", entry);
|
|
||||||
entry.getProperties().forEach(this::applyPropertyMapEntry);
|
|
||||||
calculateSchedule(schedule, now);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyPropertyMapEntry(final String propertyName, final String value) {
|
|
||||||
try {
|
|
||||||
propertyService.set(propertyName, value);
|
|
||||||
} catch (PropertySetException e) {
|
|
||||||
log.error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void calculateSchedule(final Schedule schedule, final ZonedDateTime now) {
|
|
||||||
schedule.getEntries().forEach(scheduleEntry -> calculateEntry(schedule, scheduleEntry, now));
|
|
||||||
schedule.getEntries().stream()
|
|
||||||
.filter(entry -> entry.getNextFuzzyTimestamp() != null && entry.getNextFuzzyTimestamp().isAfter(now))
|
|
||||||
.min(Comparator.comparing(ScheduleEntry::getNextFuzzyTimestamp))
|
|
||||||
.ifPresent(scheduleEntry -> log.info("Next schedule for \"{}\": {}", schedule.getName(), scheduleEntry.getNextFuzzyTimestamp()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void calculateEntry(final Schedule schedule, final ScheduleEntry entry, final ZonedDateTime now) {
|
|
||||||
log.debug("calculateNext \"{}\", {}:", schedule.getName(), entry);
|
|
||||||
if (!entry.isEnabled() || !isAnyWeekdayEnabled(entry)) {
|
|
||||||
entry.setNextClearTimestamp(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ZonedDateTime midnight = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
|
|
||||||
ZonedDateTime next = calculateEntryForDay(entry, midnight);
|
|
||||||
while (next != null && (!next.isAfter(now) || !isWeekdayValid(entry, next))) {
|
|
||||||
log.debug(" -- skipping: {}", next);
|
|
||||||
midnight = midnight.plusDays(1);
|
|
||||||
next = calculateEntryForDay(entry, midnight);
|
|
||||||
}
|
|
||||||
log.debug(" => {}", next);
|
|
||||||
entry.setNextClearTimestamp(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ZonedDateTime calculateEntryForDay(final ScheduleEntry entry, final ZonedDateTime midnight) {
|
|
||||||
switch (entry.getType()) {
|
|
||||||
case TIME:
|
|
||||||
return midnight.withHour(entry.getHour()).withMinute(entry.getMinute()).withSecond(entry.getSecond());
|
|
||||||
case SUNRISE:
|
|
||||||
case SUNSET:
|
|
||||||
return astroNext(entry, midnight);
|
|
||||||
default:
|
|
||||||
log.error("AstroEvent not implemented: {}", entry.getType());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ZonedDateTime astroNext(final ScheduleEntry entry, ZonedDateTime midnight) {
|
|
||||||
final Location location = new Location(config.getLatitude(), config.getLongitude());
|
|
||||||
final SolarEventCalculator calculator = new SolarEventCalculator(location, config.getTimezone());
|
|
||||||
final Calendar calendar = GregorianCalendar.from(midnight);
|
|
||||||
final Calendar nextCalendar = astroNext(calculator, entry.getType(), new Zenith(entry.getZenith()), calendar);
|
|
||||||
if (nextCalendar == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(nextCalendar.getTimeInMillis()), midnight.getZone());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Calendar astroNext(final SolarEventCalculator calculator, final ScheduleEntryType type, final Zenith solarZenith, final Calendar calendar) {
|
|
||||||
switch (type) {
|
|
||||||
case SUNRISE:
|
|
||||||
return calculator.computeSunriseCalendar(solarZenith, calendar);
|
|
||||||
case SUNSET:
|
|
||||||
return calculator.computeSunsetCalendar(solarZenith, calendar);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAnyWeekdayEnabled(final ScheduleEntry entry) {
|
|
||||||
return entry.isMonday() || entry.isTuesday() || entry.isWednesday() || entry.isThursday() || entry.isFriday() || entry.isSaturday() || entry.isSunday();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isWeekdayValid(final ScheduleEntry entry, final ZonedDateTime value) {
|
|
||||||
switch (value.getDayOfWeek()) {
|
|
||||||
case MONDAY:
|
|
||||||
return entry.isMonday();
|
|
||||||
case TUESDAY:
|
|
||||||
return entry.isTuesday();
|
|
||||||
case WEDNESDAY:
|
|
||||||
return entry.isWednesday();
|
|
||||||
case THURSDAY:
|
|
||||||
return entry.isThursday();
|
|
||||||
case FRIDAY:
|
|
||||||
return entry.isFriday();
|
|
||||||
case SATURDAY:
|
|
||||||
return entry.isSaturday();
|
|
||||||
case SUNDAY:
|
|
||||||
return entry.isSunday();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<ZonedDateTime> getNextTimestamp() {
|
|
||||||
return scheduleEntryRepository.findFirstByNextFuzzyTimestampNotNullOrderByNextFuzzyTimestampAsc().map(ScheduleEntry::getNextFuzzyTimestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ public class ScheduleEntry {
|
|||||||
|
|
||||||
private int second;
|
private int second;
|
||||||
|
|
||||||
private int plusMinusSeconds = 0;
|
private int fuzzySeconds = 0;
|
||||||
|
|
||||||
private ZonedDateTime nextClearTimestamp;
|
private ZonedDateTime nextClearTimestamp;
|
||||||
|
|
||||||
@ -64,12 +64,12 @@ public class ScheduleEntry {
|
|||||||
|
|
||||||
public void setNextClearTimestamp(final ZonedDateTime next) {
|
public void setNextClearTimestamp(final ZonedDateTime next) {
|
||||||
nextClearTimestamp = next;
|
nextClearTimestamp = next;
|
||||||
if (nextClearTimestamp != null && (lastClearTimestamp == null || nextClearTimestamp.compareTo(lastClearTimestamp) != 0)) {
|
if (nextClearTimestamp == null) {
|
||||||
final int secondsRange = 2 * plusMinusSeconds;
|
|
||||||
final int fuzzySeconds = secondsRange > 0 ? RANDOM.nextInt(secondsRange) - plusMinusSeconds : 0;
|
|
||||||
nextFuzzyTimestamp = nextClearTimestamp.plusSeconds(fuzzySeconds);
|
|
||||||
} else {
|
|
||||||
nextFuzzyTimestamp = null;
|
nextFuzzyTimestamp = null;
|
||||||
|
} else {
|
||||||
|
final int rangeFull = 2 * fuzzySeconds;
|
||||||
|
final int fuzzySeconds = rangeFull > 0 ? RANDOM.nextInt(rangeFull) - this.fuzzySeconds : 0;
|
||||||
|
nextFuzzyTimestamp = nextClearTimestamp.plusSeconds(fuzzySeconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
package de.ph87.homeautomation.schedule.entry;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("schedule/entry")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleEntryController {
|
||||||
|
|
||||||
|
private final ScheduleEntryWriteService scheduleEntryWriteService;
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/enabled")
|
||||||
|
public ScheduleEntryDto setEnabled(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/monday")
|
||||||
|
public ScheduleEntryDto setMonday(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setMonday, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/tuesday")
|
||||||
|
public ScheduleEntryDto setTuesday(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setTuesday, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/wednesday")
|
||||||
|
public ScheduleEntryDto setWednesday(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setWednesday, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/thursday")
|
||||||
|
public ScheduleEntryDto setThursday(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setThursday, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/friday")
|
||||||
|
public ScheduleEntryDto setFriday(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setFriday, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/saturday")
|
||||||
|
public ScheduleEntryDto setSaturday(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setSaturday, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/sunday")
|
||||||
|
public ScheduleEntryDto setSunday(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setSunday, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/hour")
|
||||||
|
public ScheduleEntryDto setHour(@PathVariable final long id, @RequestBody final int value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setHour, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/minute")
|
||||||
|
public ScheduleEntryDto setMinute(@PathVariable final long id, @RequestBody final int value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setMinute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("set/{id}/fuzzySeconds")
|
||||||
|
public ScheduleEntryDto setFuzzySeconds(@PathVariable final long id, @RequestBody final int value) {
|
||||||
|
return scheduleEntryWriteService.set(id, ScheduleEntry::setFuzzySeconds, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,45 +1,50 @@
|
|||||||
package de.ph87.homeautomation.schedule.entry;
|
package de.ph87.homeautomation.schedule.entry;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Getter
|
||||||
public class ScheduleEntryDto {
|
public class ScheduleEntryDto {
|
||||||
|
|
||||||
private long id;
|
public final long id;
|
||||||
|
|
||||||
private boolean enabled;
|
public final boolean enabled;
|
||||||
|
|
||||||
private boolean monday;
|
public final boolean monday;
|
||||||
|
|
||||||
private boolean tuesday;
|
public final boolean tuesday;
|
||||||
|
|
||||||
private boolean wednesday;
|
public final boolean wednesday;
|
||||||
|
|
||||||
private boolean thursday;
|
public final boolean thursday;
|
||||||
|
|
||||||
private boolean friday;
|
public final boolean friday;
|
||||||
|
|
||||||
private boolean saturday;
|
public final boolean saturday;
|
||||||
|
|
||||||
private boolean sunday;
|
public final boolean sunday;
|
||||||
|
|
||||||
private ScheduleEntryType type;
|
public final ScheduleEntryType type;
|
||||||
|
|
||||||
private double zenith;
|
public final double zenith;
|
||||||
|
|
||||||
private int hour;
|
public final int hour;
|
||||||
|
|
||||||
private int minute;
|
public final int minute;
|
||||||
|
|
||||||
private int second;
|
public final int second;
|
||||||
|
|
||||||
private ZonedDateTime nextDateTime;
|
public final int fuzzySeconds;
|
||||||
|
|
||||||
private Map<String, String> properties;
|
public final ZonedDateTime nextClearTimestamp;
|
||||||
|
|
||||||
|
public final ZonedDateTime lastClearTimestamp;
|
||||||
|
|
||||||
|
public final ZonedDateTime nextFuzzyTimestamp;
|
||||||
|
|
||||||
|
public final Map<String, String> properties;
|
||||||
|
|
||||||
public ScheduleEntryDto(final ScheduleEntry scheduleEntry) {
|
public ScheduleEntryDto(final ScheduleEntry scheduleEntry) {
|
||||||
id = scheduleEntry.getId();
|
id = scheduleEntry.getId();
|
||||||
@ -56,8 +61,11 @@ public class ScheduleEntryDto {
|
|||||||
hour = scheduleEntry.getHour();
|
hour = scheduleEntry.getHour();
|
||||||
minute = scheduleEntry.getMinute();
|
minute = scheduleEntry.getMinute();
|
||||||
second = scheduleEntry.getSecond();
|
second = scheduleEntry.getSecond();
|
||||||
nextDateTime = scheduleEntry.getNextFuzzyTimestamp();
|
fuzzySeconds = scheduleEntry.getFuzzySeconds();
|
||||||
properties = new HashMap<>(scheduleEntry.getProperties());
|
nextClearTimestamp = scheduleEntry.getNextClearTimestamp();
|
||||||
|
lastClearTimestamp = scheduleEntry.getLastClearTimestamp();
|
||||||
|
nextFuzzyTimestamp = scheduleEntry.getNextFuzzyTimestamp();
|
||||||
|
properties = Map.copyOf(scheduleEntry.getProperties());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
package de.ph87.homeautomation.schedule.entry;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleEntryMapper {
|
||||||
|
|
||||||
|
public ScheduleEntryDto toDto(final ScheduleEntry scheduleEntry) {
|
||||||
|
return new ScheduleEntryDto(scheduleEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package de.ph87.homeautomation.schedule.entry;
|
||||||
|
|
||||||
|
import de.ph87.office.web.NotFoundException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleEntryReadService {
|
||||||
|
|
||||||
|
private final ScheduleEntryRepository scheduleEntryRepository;
|
||||||
|
|
||||||
|
public ScheduleEntry get(final long id) {
|
||||||
|
return scheduleEntryRepository.findById(id).orElseThrow(() -> new NotFoundException("ScheduleEntry.id=%d", id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ZonedDateTime> getNextTimestamp() {
|
||||||
|
return scheduleEntryRepository.findFirstByNextFuzzyTimestampNotNullOrderByNextFuzzyTimestampAsc().map(ScheduleEntry::getNextFuzzyTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package de.ph87.homeautomation.schedule.entry;
|
||||||
|
|
||||||
|
import de.ph87.homeautomation.schedule.Schedule;
|
||||||
|
import de.ph87.homeautomation.schedule.ScheduleCalculationService;
|
||||||
|
import de.ph87.homeautomation.schedule.ScheduleReadService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleEntryWriteService {
|
||||||
|
|
||||||
|
private final ScheduleEntryReadService scheduleEntryReadService;
|
||||||
|
|
||||||
|
private final ScheduleReadService scheduleReadService;
|
||||||
|
|
||||||
|
private final ScheduleCalculationService scheduleCalculationService;
|
||||||
|
|
||||||
|
private final ScheduleEntryMapper scheduleEntryMapper;
|
||||||
|
|
||||||
|
public <T> ScheduleEntryDto set(final long id, final BiConsumer<ScheduleEntry, T> setter, final T value) {
|
||||||
|
final ScheduleEntry entry = scheduleEntryReadService.get(id);
|
||||||
|
setter.accept(entry, value);
|
||||||
|
final Schedule schedule = scheduleReadService.getByEntry(entry);
|
||||||
|
scheduleCalculationService.calculateSchedule(schedule, ZonedDateTime.now());
|
||||||
|
return scheduleEntryMapper.toDto(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package de.ph87.homeautomation.schedule.entry;
|
||||||
|
|
||||||
|
import de.ph87.homeautomation.schedule.Schedule;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class ScheduleNextExecutionDto {
|
||||||
|
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
public final ZonedDateTime nextTimestamp;
|
||||||
|
|
||||||
|
public final HashMap<String, String> properties;
|
||||||
|
|
||||||
|
private ScheduleNextExecutionDto(final Schedule schedule, final ScheduleEntry entry) {
|
||||||
|
this.name = schedule.getName();
|
||||||
|
this.nextTimestamp = entry.getNextFuzzyTimestamp();
|
||||||
|
this.properties = new HashMap<>(entry.getProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<ScheduleNextExecutionDto> create(final Schedule schedule, final ZonedDateTime now) {
|
||||||
|
return schedule.getEntries().stream()
|
||||||
|
.filter(entry -> entry.getNextFuzzyTimestamp() != null && entry.getNextFuzzyTimestamp().isAfter(now))
|
||||||
|
.min(Comparator.comparing(ScheduleEntry::getNextFuzzyTimestamp))
|
||||||
|
.map(scheduleEntry -> new ScheduleNextExecutionDto(schedule, scheduleEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -36,7 +36,7 @@ public abstract class AbstractThreadService {
|
|||||||
protected abstract void postStop();
|
protected abstract void postStop();
|
||||||
|
|
||||||
@EventListener(ApplicationStartedEvent.class)
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
public void afterStartup() {
|
public final void afterStartup() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
started = true;
|
started = true;
|
||||||
}
|
}
|
||||||
@ -44,7 +44,7 @@ public abstract class AbstractThreadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
public void preDestroy() {
|
public final void preDestroy() {
|
||||||
log.debug("{} stopping...", getThreadName());
|
log.debug("{} stopping...", getThreadName());
|
||||||
stop = true;
|
stop = true;
|
||||||
preStop();
|
preStop();
|
||||||
@ -65,7 +65,6 @@ public abstract class AbstractThreadService {
|
|||||||
try {
|
try {
|
||||||
doStart();
|
doStart();
|
||||||
while (!stop) {
|
while (!stop) {
|
||||||
|
|
||||||
doWait(doStep());
|
doWait(doStep());
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@ -93,7 +92,7 @@ public abstract class AbstractThreadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyActionPending() {
|
protected final void _wakeUp() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
lock.notifyAll();
|
lock.notifyAll();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user