UI ScheduleList including editing + FIX fuzzy premature scheduling

This commit is contained in:
Patrick Haßel 2021-10-28 17:00:16 +02:00
parent d8664a800a
commit bb7eb594a0
84 changed files with 14311 additions and 305 deletions

View File

@ -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
View File

@ -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>

View 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.

View 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
View 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

View 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.

View 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"
}

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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();
});
});

View 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);
};
}
}

View 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);
}
}

View 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;
}
}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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();
});
});

View 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);
}
}

View 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;
}

View 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 {
}

View 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>

View 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;
}

View 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!');
});
});

View 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
}
}

View 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 {
}

View 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();
});
});

View File

@ -0,0 +1,11 @@
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor() {
}
}

View 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;
}

View File

@ -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">&nbsp;</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>
[&nbsp;90°]
</option>
<option value="93">[&nbsp;93°]</option>
<option value="96">Bürgeliche Dämmerung [&nbsp;96°]</option>
<option value="99">[&nbsp;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">:&nbsp;</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">:&nbsp;</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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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)
}
}

View File

@ -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>

View File

@ -0,0 +1,3 @@
.empty {
color: gray;
}

View File

@ -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();
});
});

View File

@ -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();
}
}
}

View File

@ -0,0 +1 @@
<input type="number" [style]="{width: width}" [(ngModel)]="value" (change)="update()" (blur)="update()" (keypress)="$event.key === 'Enter' ? update() : {}">

View File

@ -0,0 +1,9 @@
input {
outline: none;
border: none;
padding: 5px;
margin: 0;
width: 100%;
background-color: transparent;
height: 1.5em;
}

View File

@ -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();
});
});

View 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);
}
}
}

View File

@ -0,0 +1,2 @@
<fa-icon [icon]="faCheck" *ngIf="initial" (click)="toggle()"></fa-icon>
<fa-icon [icon]="faTimes" *ngIf="!initial" (click)="toggle()"></fa-icon>

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

View File

@ -0,0 +1,4 @@
export const environment = {
production: true,
apiBasePath: '/',
};

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View 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>

View 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));

View 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
*/

View 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;
}
}

View 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);

View 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"
]
}

View 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
}
}

View 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"
]
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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();
}
} }

View File

@ -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);
}
} }

View File

@ -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);
} }

View File

@ -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) {

View File

@ -0,0 +1,5 @@
package de.ph87.homeautomation.knx.group;
public class KnxThreadWakeUpEvent {
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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));
}
} }
} }

View File

@ -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();

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
} }

View File

@ -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();
}
} }

View File

@ -0,0 +1,5 @@
package de.ph87.homeautomation.schedule;
public class ScheduleThreadWakeUpEvent {
}

View File

@ -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);
} }
} }

View File

@ -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);
} }
} }

View File

@ -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);
}
}

View File

@ -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());
} }
} }

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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();
} }