Compare commits

..

No commits in common. "master" and "deploy---2024-09-11---16-32-31" have entirely different histories.

31 changed files with 247 additions and 423 deletions

View File

@ -66,12 +66,6 @@
<version>1.2</version> <version>1.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

View File

@ -72,38 +72,6 @@ export abstract class Device {
return d => d.areaId === areaId && d.roomId === roomId; return d => d.areaId === areaId && d.roomId === roomId;
} }
getSwitchClassList(): object {
if (!(this instanceof DeviceSwitch)) {
throw Error();
}
const value: number | null | undefined = (this as DeviceSwitch).stateProperty?.readChannel?.value;
return {
deviceSwitchOnBack: value === 1,
deviceSwitchOffBack: value === 0,
disabledBack: value === null || value === undefined,
};
}
getStateSceneClassList(): object {
if (!(this instanceof DeviceStateScene)) {
throw Error();
}
return this.getSwitchClassList();
}
getShutterClassList(): object {
if (!(this instanceof DeviceShutter)) {
throw Error();
}
const value: number | null | undefined = (this as DeviceShutter).positionProperty?.readChannel?.value;
return {
deviceShutterOpenBack: value === 0,
deviceShutterIntermediateBack: value !== null && value !== undefined && value > 0 && value < 100,
deviceShutterClosedBack: value === 100,
disabledBack: value === null || value === undefined,
};
}
} }
export class DeviceSwitch extends Device { export class DeviceSwitch extends Device {

View File

@ -1,16 +1,8 @@
import {undefinedOrNull, validateListOrEmpty, validateNumberNotNull, validateStringEmptyToNull, validateStringNotEmptyNotNull} from "../validators"; import {validateListOrEmpty, validateNumberNotNull, validateStringEmptyToNull, validateStringNotEmptyNotNull} from "../validators";
import {Channel} from "../channel/Channel"; import {Channel} from "../channel/Channel";
import {SearchResult} from "../SearchResult"; import {SearchResult} from "../SearchResult";
import {environment} from "../../../environments/environment"; import {environment} from "../../../environments/environment";
export enum PropertyType {
BOOLEAN = 'BOOLEAN',
SHUTTER = 'SHUTTER',
BRIGHTNESS_PERCENT = 'BRIGHTNESS_PERCENT',
COLOR_TEMPERATURE = 'COLOR_TEMPERATURE',
LUX = 'LUX',
}
export class Property { export class Property {
readonly hrefId: string | null; readonly hrefId: string | null;
@ -19,7 +11,7 @@ export class Property {
constructor( constructor(
public id: number, public id: number,
public type: PropertyType, public type: string,
public title: string, public title: string,
public slug: string | null, public slug: string | null,
public readChannel: Channel | null, public readChannel: Channel | null,
@ -44,7 +36,7 @@ export class Property {
static fromJson(json: any): Property { static fromJson(json: any): Property {
return new Property( return new Property(
validateNumberNotNull(json['id']), validateNumberNotNull(json['id']),
validateStringNotEmptyNotNull(json['type']) as PropertyType, validateStringNotEmptyNotNull(json['type']),
validateStringNotEmptyNotNull(json['title']), validateStringNotEmptyNotNull(json['title']),
validateStringEmptyToNull(json['slug']), validateStringEmptyToNull(json['slug']),
Channel.fromJsonAllowNull(json['readChannel']), Channel.fromJsonAllowNull(json['readChannel']),
@ -71,32 +63,4 @@ export class Property {
} }
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
} }
getStateClassList() {
const value = this.readChannel?.value;
switch (this.type) {
case PropertyType.BOOLEAN:
return {
propertyStateBooleanUnknown: undefinedOrNull(value),
propertyStateBooleanTrue: value > 0,
propertyStateBooleanFalse: value === 0,
};
case PropertyType.SHUTTER:
return {
propertyStatePercentUnknown: undefinedOrNull(value),
propertyStatePercentActive: value === 0,
propertyStatePercentBetween: value > 0 && value < 100,
propertyStatePercentInactive: value === 100,
};
case PropertyType.BRIGHTNESS_PERCENT:
return {
propertyStatePercentUnknown: undefinedOrNull(value),
propertyStatePercentActive: value === 100,
propertyStatePercentBetween: value > 0 && value < 100,
propertyStatePercentInactive: value === 0,
};
}
return {};
}
} }

View File

@ -87,7 +87,3 @@ export function validateMap<T>(json: any, valueFromJson: (json: any) => T): Map<
} }
return map; return map;
} }
export function undefinedOrNull(value: any) {
return value === undefined || value === null;
}

View File

@ -1,24 +1,24 @@
<div class="tiles"> <table>
<div class="tile" *ngFor="let channel of channels"> <tr>
<div> <th>Titel</th>
{{ channel.title }} <th>Typ</th>
</div> <th colspan="3">Adresse</th>
<div class="left"> <th>DPT</th>
{{ channel.type }} <th colspan="2">Wert</th>
</div> </tr>
<tr *ngFor="let channel of channels">
<td>{{channel.title}}</td>
<td>{{channel.type}}</td>
<ng-container *ngIf="channel.type === 'KnxGroup'"> <ng-container *ngIf="channel.type === 'KnxGroup'">
<div class="left"> <td class="first number">{{asKnxGroup(channel).addresMain}}&nbsp;/&nbsp;</td>
{{ asKnxGroup(channel).addresMain }}&nbsp;/&nbsp;{{ asKnxGroup(channel).addresMid }}&nbsp;/&nbsp;{{ asKnxGroup(channel).addresSub }} <td class="middle number">{{asKnxGroup(channel).addresMid}}&nbsp;/&nbsp;</td>
</div> <td class="last number">{{asKnxGroup(channel).addresSub}}</td>
<div class="right"> <td class="number">{{asKnxGroup(channel).dpt}}</td>
DPT {{ asKnxGroup(channel).dpt }}
</div>
</ng-container> </ng-container>
<div class="timestamp"> <ng-container *ngIf="channel.type === 'Logic'">
{{ channel.timestamp | date:'yyyy-MM-dd HH:mm:ss' || '-' }} <td colspan="4">{{asLogic(channel).operator}}</td>
</div> </ng-container>
<div class="value"> <td class="number">{{channel.value}}</td>
{{ channel.value || '-' }} <td>{{channel.timestamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
</div> </tr>
</div> </table>
</div>

View File

@ -1,23 +1,3 @@
@import "../../../../config"; table {
width: 100%;
div {
padding: @padding;
}
.left {
float: left;
}
.right {
float: right;
}
.timestamp {
clear: left;
float: left;
}
.value {
clear: right;
float: right;
} }

View File

@ -2,7 +2,7 @@
<ng-container *ngFor="let device of devices; trackBy: Device.trackBy"> <ng-container *ngFor="let device of devices; trackBy: Device.trackBy">
<div class="tile" *ngIf="device.type == 'DeviceSwitch'" [ngClass]="device.getSwitchClassList()"> <div class="tile" *ngIf="device.type == 'DeviceSwitch'" [ngClass]="getSwitchClassList(device)">
<div class="tileHead"> <div class="tileHead">
<div class="flexGrow"> <div class="flexGrow">
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text> <app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="tile" *ngIf="device.type === 'DeviceStateScene'" [ngClass]="device.getStateSceneClassList()"> <div class="tile" *ngIf="device.type === 'DeviceStateScene'" [ngClass]="getStateSceneClassList(device)">
<div class="tileHead"> <div class="tileHead">
<div class="flexGrow"> <div class="flexGrow">
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text> <app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
@ -33,7 +33,7 @@
</div> </div>
</div> </div>
<div class="tile" *ngIf="device.type === 'DeviceShutter'" [ngClass]="device.getShutterClassList()"> <div class="tile" *ngIf="device.type === 'DeviceShutter'" [ngClass]="getShutterClassList(device)">
<div class="tileHead"> <div class="tileHead">
<div class="flexGrow"> <div class="flexGrow">
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text> <app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>

View File

@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {DeviceService} from "../../../api/device/device.service"; import {DeviceService} from "../../../api/device/device.service";
import {PropertyService} from "../../../api/property/property.service"; import {PropertyService} from "../../../api/property/property.service";
import {Device, DeviceStateScene} from "../../../api/device/Device"; import {Device, DeviceShutter, DeviceStateScene, DeviceSwitch} from "../../../api/device/Device";
import {faEdit} from '@fortawesome/free-regular-svg-icons'; import {faEdit} from '@fortawesome/free-regular-svg-icons';
import {Scene} from "../../../api/scene/Scene"; import {Scene} from "../../../api/scene/Scene";
import {SceneService} from "../../../api/scene/scene.service"; import {SceneService} from "../../../api/scene/scene.service";
@ -78,9 +78,32 @@ export class DeviceListComponent implements OnInit {
} }
} }
getStateScenes(device: Device): Scene[] { getSwitchClassList(device: Device): object {
const casted: DeviceStateScene = device as DeviceStateScene; const value: number | null | undefined = (device as DeviceSwitch).stateProperty?.readChannel?.value;
return casted.sceneNumbers.map(sceneNumber => this.scenes.find(scene => scene.number === sceneNumber)).filter(scene => scene !== undefined).map(s => s as Scene); return {
deviceSwitchOnBack: value === 1,
deviceSwitchOffBack: value === 0,
disabledBack: value === null || value === undefined,
};
}
getStateSceneClassList(device: Device): object {
return this.getSwitchClassList(device);
}
getShutterClassList(device: Device): object {
const value: number | null | undefined = (device as DeviceShutter).positionProperty?.readChannel?.value;
return {
deviceShutterOpenBack: value === 0,
deviceShutterIntermediateBack: value !== null && value !== undefined && value > 0 && value < 100,
deviceShutterClosedBack: value === 100,
disabledBack: value === null || value === undefined,
};
}
getStateScenes(d: Device): Scene[] {
const device: DeviceStateScene = d as DeviceStateScene;
return device.sceneNumbers.map(sceneNumber => this.scenes.find(scene => scene.number === sceneNumber)).filter(scene => scene !== undefined).map(s => s as Scene);
} }
set(device: Device, key: string, value: any): void { set(device: Device, key: string, value: any): void {

View File

@ -2,17 +2,49 @@
<td class="empty">-</td> <td class="empty">-</td>
</ng-template> </ng-template>
<div class="tiles"> <table>
<tr>
<th>Bezeichnung</th>
<th>Slug</th>
<th>Typ</th>
<th>Wert</th>
<th>Zeitstempel</th>
<th>Lesekanal</th>
<th>Schreibkanal</th>
<th>
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</th>
</tr>
<ng-container *ngFor="let property of properties.sort(Property.compareTypeThenTitle)">
<tr>
<div *ngFor="let list of listLists()"> <td>
<div class="tile" *ngFor="let property of list">
<div class="tileHead" [ngClass]="property.getStateClassList()">
<app-text [initial]="property.title" (valueChange)="edit(property, 'title', $event)"></app-text> <app-text [initial]="property.title" (valueChange)="edit(property, 'title', $event)"></app-text>
<div class="links" *ngIf="property.type === 'BOOLEAN'">
<div *ngIf="property.hrefId">{{property.hrefId}}0</div>
<div *ngIf="property.hrefId">{{property.hrefId}}1</div>
</div> </div>
<div class="links" *ngIf="property.type === 'SHUTTER'">
<div *ngIf="property.hrefId">{{property.hrefId}}0</div>
<div *ngIf="property.hrefId">{{property.hrefId}}90</div>
<div *ngIf="property.hrefId">{{property.hrefId}}100</div>
</div>
</td>
<div class="tileBody"> <td>
<app-text [initial]="property.slug" (valueChange)="edit(property, 'slug', $event)"></app-text>
<div class="links" *ngIf="property.type === 'BOOLEAN'">
<div *ngIf="property.hrefSlug">{{property.hrefSlug}}0</div>
<div *ngIf="property.hrefSlug">{{property.hrefSlug}}1</div>
</div>
<div class="links" *ngIf="property.type === 'SHUTTER'">
<div *ngIf="property.hrefSlug">{{property.hrefSlug}}0</div>
<div *ngIf="property.hrefSlug">{{property.hrefSlug}}90</div>
<div *ngIf="property.hrefSlug">{{property.hrefSlug}}100</div>
</div>
</td>
<td>
<select [(ngModel)]="property.type" (ngModelChange)="edit(property, 'type', property.type)"> <select [(ngModel)]="property.type" (ngModelChange)="edit(property, 'type', property.type)">
<option value="BOOLEAN">Schalter</option> <option value="BOOLEAN">Schalter</option>
<option value="SHUTTER">Rollladen</option> <option value="SHUTTER">Rollladen</option>
@ -21,21 +53,47 @@
<option value="LUX">Helligkeit [lux]</option> <option value="LUX">Helligkeit [lux]</option>
<option value="SCENE">Szene</option> <option value="SCENE">Szene</option>
</select> </select>
</td>
<ng-container *ngIf="property.readChannel?.value !== null else empty">
<td *ngIf="property.type === 'BOOLEAN'" class="boolean" [class.true]="property.readChannel?.value" [class.false]="!property.readChannel?.value" (click)="edit(property, 'value', property.readChannel?.value > 0 ? 0 : 1)">
{{property.readChannel?.value ? "An" : "Aus"}}
</td>
<td *ngIf="property.type === 'SHUTTER'" class="number" [class.true]="property.readChannel?.value === 0" [class.false]="property.readChannel?.value === 100" [class.tristate]="0 < property.readChannel?.value && property.readChannel?.value < 100">
{{property.readChannel?.value}} %
</td>
<td *ngIf="property.type === 'BRIGHTNESS_PERCENT'" class="number">
{{property.readChannel?.value}} %
</td>
<td *ngIf="property.type === 'COLOR_TEMPERATURE'" class="number">
{{property.readChannel?.value}} K
</td>
<td *ngIf="property.type === 'LUX'" class="number">
{{property.readChannel?.value | number:'0.0-0'}} lux
</td>
<td *ngIf="property.type === 'SCENE'">
{{findScene(property)?.title || "Unbekannt: " + property.readChannel?.value}}
</td>
</ng-container>
<td *ngIf="property.readChannel?.timestamp !== null else empty">
{{property.readChannel?.timestamp | date:'yyyy-MM-dd HH:mm:ss'}}
</td>
<td class="full">
<app-search [searchService]="channelService" [initial]="property.readChannel?.id" (valueChange)="set(property, 'readChannel', $event)"></app-search> <app-search [searchService]="channelService" [initial]="property.readChannel?.id" (valueChange)="set(property, 'readChannel', $event)"></app-search>
</td>
<td class="full">
<app-search [searchService]="channelService" [initial]="property.writeChannel?.id" (valueChange)="set(property, 'writeChannel', $event)"></app-search> <app-search [searchService]="channelService" [initial]="property.writeChannel?.id" (valueChange)="set(property, 'writeChannel', $event)"></app-search>
</td>
<div class="delete"> <td class="delete">
<fa-icon title="Löschen" *ngIf="property.usages.length === 0" [icon]="faTimes" (click)="delete(property)"></fa-icon> <fa-icon title="Löschen" *ngIf="property.usages.length === 0" [icon]="faTimes" (click)="delete(property)"></fa-icon>
</div> </td>
</div> </tr>
</ng-container>
</div> </table>
</div>
</div>
<div class="config"> <div class="config">
<button (click)="create()">+ Hinzufügen</button> <button (click)="create()">+ Hinzufügen</button>

View File

@ -1,29 +1,8 @@
@import "../../../../config"; table {
width: 100%;
.propertyStateBooleanUnknown {
background-color: @COLOR_UNKNOWN;
} }
.propertyStateBooleanTrue { .links {
background-color: @COLOR_ACTIVE; color: gray;
} font-size: 80%;
.propertyStateBooleanFalse {
background-color: @COLOR_INACTIVE;
}
.propertyStatePercentUnknown {
background-color: @COLOR_UNKNOWN;
}
.propertyStatePercentActive {
background-color: @COLOR_ACTIVE;
}
.propertyStatePercentBetween {
background-color: @COLOR_BETWEEN;
}
.propertyStatePercentInactive {
background-color: @COLOR_INACTIVE;
} }

View File

@ -1,5 +1,5 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Property, PropertyType} from "../../../api/property/Property"; import {Property} from "../../../api/property/Property";
import {PropertyService} from "../../../api/property/property.service"; import {PropertyService} from "../../../api/property/property.service";
import {Scene} from "../../../api/scene/Scene"; import {Scene} from "../../../api/scene/Scene";
import {SceneService} from "../../../api/scene/scene.service"; import {SceneService} from "../../../api/scene/scene.service";
@ -14,18 +14,16 @@ import {environment} from 'src/environments/environment';
}) })
export class PropertyListComponent implements OnInit { export class PropertyListComponent implements OnInit {
readonly environment = environment;
readonly faTimes = faTimesCircle; readonly faTimes = faTimesCircle;
protected booleans: Property[] = [];
protected shutters: Property[] = [];
Property = Property; Property = Property;
properties: Property[] = [];
scenes: Scene[] = []; scenes: Scene[] = [];
readonly environment = environment;
constructor( constructor(
readonly propertyService: PropertyService, readonly propertyService: PropertyService,
readonly sceneService: SceneService, readonly sceneService: SceneService,
@ -35,10 +33,7 @@ export class PropertyListComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.propertyService.findAll(properties => { this.propertyService.findAll(properties => this.properties = properties, Property.compareTypeThenTitle);
this.booleans = properties.filter(p => p.type === PropertyType.BOOLEAN).sort(Property.compareTypeThenTitle);
this.shutters = properties.filter(p => p.type === PropertyType.SHUTTER).sort(Property.compareTypeThenTitle);
}, Property.compareTypeThenTitle);
this.propertyService.subscribe(update => this.updateProperty(update.payload, update.existing)); this.propertyService.subscribe(update => this.updateProperty(update.payload, update.existing));
this.sceneService.findAll(scenes => this.scenes = scenes, Scene.compareNumber); this.sceneService.findAll(scenes => this.scenes = scenes, Scene.compareNumber);
@ -54,23 +49,15 @@ export class PropertyListComponent implements OnInit {
} }
private updateProperty(property: Property, existing: boolean): void { private updateProperty(property: Property, existing: boolean): void {
if (property.type === PropertyType.BOOLEAN) { const index: number = this.properties.findIndex(p => p.id === property.id);
this.updateProperty2(this.booleans, property, existing);
} else if (property.type === PropertyType.SHUTTER) {
this.updateProperty2(this.shutters, property, existing);
}
}
private updateProperty2(properties: Property[], property: Property, existing: boolean) {
const index: number = properties.findIndex(p => p.id === property.id);
if (index >= 0) { if (index >= 0) {
if (existing) { if (existing) {
properties[index] = property; this.properties[index] = property;
} else { } else {
properties.slice(index, 1); this.properties.slice(index, 1);
} }
} else if (existing) { } else if (existing) {
properties.push(property); this.properties.push(property);
} }
} }
@ -101,22 +88,8 @@ export class PropertyListComponent implements OnInit {
delete(property: Property): void { delete(property: Property): void {
if (confirm(`Eigenschaft "${property.title}" wirklich löschen?`)) { if (confirm(`Eigenschaft "${property.title}" wirklich löschen?`)) {
this.propertyService.delete(property, () => { this.propertyService.delete(property, () => this.properties.splice(this.properties.findIndex(p => p.id === property.id), 1));
this.delete2(this.booleans, property);
this.delete2(this.shutters, property);
});
} }
} }
private delete2(properties: Property[], property: Property) {
const index = properties.findIndex(p => p.id === property.id);
if (index >= 0) {
properties.splice(index, 1);
}
}
listLists(): Property[][] {
return [this.shutters, this.booleans];
}
} }

View File

@ -75,7 +75,7 @@
<div class="tileBodyFlex" *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'"> <div class="tileBodyFlex" *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
<div class="flexGrow sun"> <div class="flexGrow sun">
<div *ngFor="let zenith of getZenithEntries(entry.type)"> <div *ngFor="let zenith of getZenithEntries(entry.type); trackBy: trackByZenith">
<app-bool [label]="zenith.title" [value]="entry.zenith === zenith.value" (onChange)="entryService.set(entry, 'zenith', zenith.value)"></app-bool> <app-bool [label]="zenith.title" [value]="entry.zenith === zenith.value" (onChange)="entryService.set(entry, 'zenith', zenith.value)"></app-bool>
</div> </div>
<div> <div>

View File

@ -16,18 +16,6 @@ import {faCheckCircle, faCircle, faTimesCircle} from "@fortawesome/free-regular-
const DAY_MINUTES: number = 24 * 60; const DAY_MINUTES: number = 24 * 60;
const ZENITH_ENTRIES: Zenith[] = [
new Zenith("Astr.", 107, true, true),
new Zenith("Naut.", 102, true, true),
new Zenith("Bürg.", 96, true, true),
new Zenith("Aufg.", 90.8, true, false),
new Zenith("Unterg.", 90.8, false, true),
];
const ZENITH_SUNRISE = ZENITH_ENTRIES.filter(zenith => zenith.sunrise);
const ZENITH_SUNSET = ZENITH_ENTRIES.filter(zenith => zenith.sunset).reverse();
@Component({ @Component({
selector: 'app-schedule-editor', selector: 'app-schedule-editor',
templateUrl: './schedule-editor.component.html', templateUrl: './schedule-editor.component.html',
@ -35,12 +23,6 @@ const ZENITH_SUNSET = ZENITH_ENTRIES.filter(zenith => zenith.sunset).reverse();
}) })
export class ScheduleEditorComponent implements OnInit { export class ScheduleEditorComponent implements OnInit {
protected readonly faCheckCircle = faCheckCircle;
protected readonly faTimesCircle = faTimesCircle;
protected readonly faCircle = faCircle;
protected readonly ScheduleEntry = ScheduleEntry; protected readonly ScheduleEntry = ScheduleEntry;
protected readonly Schedule = Schedule; protected readonly Schedule = Schedule;
@ -57,6 +39,14 @@ export class ScheduleEditorComponent implements OnInit {
protected bulks: Bulk[] = []; protected bulks: Bulk[] = [];
protected readonly ZENITH_ENTRIES: Zenith[] = [
new Zenith("Astr.", 107, true, true),
new Zenith("Naut.", 102, true, true),
new Zenith("Bürg.", 96, true, true),
new Zenith("Aufg.", 90.8, true, false),
new Zenith("Unterg.", 90.8, false, true),
];
constructor( constructor(
readonly router: Router, readonly router: Router,
readonly activatedRoute: ActivatedRoute, readonly activatedRoute: ActivatedRoute,
@ -109,6 +99,17 @@ export class ScheduleEditorComponent implements OnInit {
} }
} }
getZenithEntries(type: string): Zenith[] {
if (type === 'SUNRISE') {
return this.ZENITH_ENTRIES.filter(zenith => zenith.sunrise);
}
return this.ZENITH_ENTRIES.reverse().filter(zenith => zenith.sunset);
}
trackByZenith(index: number, zenith: Zenith) {
return zenith.value;
}
timeFromString(entry: ScheduleEntry, time: string) { timeFromString(entry: ScheduleEntry, time: string) {
const parts = time.split(':'); const parts = time.split(':');
const hour = parseInt(parts[0]); const hour = parseInt(parts[0]);
@ -118,6 +119,7 @@ export class ScheduleEditorComponent implements OnInit {
second = parseInt(parts[2]); second = parseInt(parts[2]);
} }
const daySecond = (hour * 24 + minute) * 60 + second; const daySecond = (hour * 24 + minute) * 60 + second;
console.log(hour, minute, second, daySecond);
this.entryService.set(entry, 'daySecond', daySecond); this.entryService.set(entry, 'daySecond', daySecond);
} }
@ -129,11 +131,9 @@ export class ScheduleEditorComponent implements OnInit {
this.entryService.set(entry, 'daySecond', newMinutes * 60); this.entryService.set(entry, 'daySecond', newMinutes * 60);
} }
getZenithEntries(type: string) { protected readonly faCheckCircle = faCheckCircle;
if (type === 'SUNRISE') {
return ZENITH_SUNRISE;
}
return ZENITH_SUNSET;
}
protected readonly faCircle = faCircle;
protected readonly faTimesCircle = faTimesCircle;
} }

View File

@ -7,8 +7,9 @@
<ng-container *ngIf="!selected">-</ng-container> <ng-container *ngIf="!selected">-</ng-container>
</div> </div>
<div *ngIf="searching" class="resultList">
<input #input type="text" *ngIf="searching" [(ngModel)]="term" (ngModelChange)="changed()" (keydown.enter)="doSearch()" (keydown.escape)="cancelSearch()" (focus)="cancelOnBlur=true" (blur)="blur()"> <input #input type="text" *ngIf="searching" [(ngModel)]="term" (ngModelChange)="changed()" (keydown.enter)="doSearch()" (keydown.escape)="cancelSearch()" (focus)="cancelOnBlur=true" (blur)="blur()">
<div #resultList *ngIf="searching" class="resultList">
<div *ngIf="allowEmpty" class="result" (mousedown)="dontCancelOnBlur()" (click)="select(undefined)"> <div *ngIf="allowEmpty" class="result" (mousedown)="dontCancelOnBlur()" (click)="select(undefined)">
- -
</div> </div>

View File

@ -1,7 +1,6 @@
@import "../../../config"; @import "../../../config";
.all { .all {
.initial { .initial {
padding: @padding; padding: @padding;
height: 100%; height: 100%;
@ -13,12 +12,7 @@
} }
.resultList { .resultList {
position: fixed; position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: lightgray; background-color: lightgray;
min-width: 10em; min-width: 10em;
border: @border solid black; border: @border solid black;

View File

@ -7,7 +7,7 @@ import {ISearchService} from "../../api/ISearchService";
templateUrl: './search.component.html', templateUrl: './search.component.html',
styleUrls: ['./search.component.less'] styleUrls: ['./search.component.less']
}) })
export class SearchComponent implements OnInit { export class SearchComponent<T> implements OnInit {
private changedTimeout: number | undefined; private changedTimeout: number | undefined;
@ -19,6 +19,9 @@ export class SearchComponent implements OnInit {
@ViewChild('input') @ViewChild('input')
input?: HTMLInputElement; input?: HTMLInputElement;
@ViewChild('resultList')
resultList?: HTMLDivElement;
@Input() @Input()
searchService!: ISearchService; searchService!: ISearchService;
@ -72,6 +75,9 @@ export class SearchComponent implements OnInit {
start(): void { start(): void {
this.term = this.selected?.title || ""; this.term = this.selected?.title || "";
if (this.resultList && this.input) {
this.resultList.style.left = this.input.style.left;
}
this.searching = true; this.searching = true;
setTimeout(() => this.input2?.nativeElement.focus(), 0); setTimeout(() => this.input2?.nativeElement.focus(), 0);
this.doSearch(); this.doSearch();

View File

@ -3,17 +3,12 @@
@border: 0.05em; @border: 0.05em;
@border-radius: 0.2em; @border-radius: 0.2em;
@COLOR_UNKNOWN: gray;
@COLOR_ACTIVE: #8fbc8f;
@COLOR_BETWEEN: #e4db9c;
@COLOR_INACTIVE: #bc8f8f;
.disabledBack { .disabledBack {
background-color: @COLOR_UNKNOWN; background-color: gray;
} }
.enabledBack { .enabledBack {
background-color: @COLOR_ACTIVE; background-color: #8fbc8f;
} }
.skipBack { .skipBack {
@ -33,21 +28,21 @@
} }
.deviceSwitchOnBack { .deviceSwitchOnBack {
background-color: @COLOR_ACTIVE; background-color: #8fbc8f;
} }
.deviceSwitchOffBack { .deviceSwitchOffBack {
background-color: @COLOR_INACTIVE; background-color: #bc8f8f;
} }
.deviceShutterOpenBack { .deviceShutterOpenBack {
background-color: @COLOR_ACTIVE; background-color: #8fbc8f;
} }
.deviceShutterIntermediateBack { .deviceShutterIntermediateBack {
background-color: @COLOR_BETWEEN; background-color: #e4db9c;
} }
.deviceShutterClosedBack { .deviceShutterClosedBack {
background-color: @COLOR_INACTIVE; background-color: #bc8f8f;
} }

View File

@ -13,6 +13,8 @@ public class Config {
private double longitude = 6.9645334; private double longitude = 6.9645334;
private String timezone = "Europe/Berlin";
private boolean insertDemoData = false; private boolean insertDemoData = false;
} }

View File

@ -2,7 +2,6 @@ package de.ph87.homeautomation.channel;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import de.ph87.homeautomation.shared.Helpers; import de.ph87.homeautomation.shared.Helpers;
import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -30,11 +29,12 @@ public class ChannelReader {
return findByChannel(channel).orElseThrow(RuntimeException::new); return findByChannel(channel).orElseThrow(RuntimeException::new);
} }
@NonNull public Double read(final Property property) {
public Optional<Double> read(@NonNull final Property property) { final Channel channel = property.getReadChannel();
return Optional.ofNullable(property.getReadChannel()) if (channel == null) {
.map(this::getByChannel) return null;
.map(owner -> owner.read(property.getReadChannel().getId())); }
return getByChannel(channel).read(property.getReadChannel().getId());
} }
public ChannelDto toDtoAllowNull(final Channel channel) { public ChannelDto toDtoAllowNull(final Channel channel) {

View File

@ -1,20 +0,0 @@
package de.ph87.homeautomation.knx.group;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("knx/group")
public class KnxGroupImportController {
private final KnxGroupImportService knxGroupImportService;
@GetMapping("import")
public void doImport() {
knxGroupImportService.importGroups();
}
}

View File

@ -1,6 +1,5 @@
package de.ph87.homeautomation.knx.group; package de.ph87.homeautomation.knx.group;
import de.ph87.homeautomation.property.PropertyReader;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
@ -24,8 +23,6 @@ public class KnxGroupImportService {
private final KnxGroupRepository knxGroupRepository; private final KnxGroupRepository knxGroupRepository;
private final PropertyReader propertyReader;
public void importGroups() { public void importGroups() {
try { try {
execute("/usr/bin/git", "fetch", "--all"); execute("/usr/bin/git", "fetch", "--all");
@ -37,18 +34,6 @@ public class KnxGroupImportService {
knxGroupRepository.findAll().forEach(knxGroup -> knxGroup.setEts(false)); knxGroupRepository.findAll().forEach(knxGroup -> knxGroup.setEts(false));
try { try {
Jsoup.parse(new File(ETS_HOME, "G"), "UTF-8").select("GA").forEach(this::importGroup); Jsoup.parse(new File(ETS_HOME, "G"), "UTF-8").select("GA").forEach(this::importGroup);
knxGroupRepository.findAllByEtsFalse().forEach(group -> {
log.warn("Removing obsolete group: {}", group);
propertyReader.findAllByReadChannel_Id(group.getId()).forEach(property -> {
property.setReadChannel(null);
log.warn(" - removed group as Property readChannel: {}", property);
});
propertyReader.findAllByWriteChannel_Id(group.getId()).forEach(property -> {
property.setWriteChannel(null);
log.warn(" - removed group as Property writeChannel: {}", property);
});
knxGroupRepository.delete(group);
});
} catch (IOException e) { } catch (IOException e) {
log.error("Failed to import KnxGroups: {}", e.toString()); log.error("Failed to import KnxGroups: {}", e.toString());
} }
@ -88,13 +73,7 @@ public class KnxGroupImportService {
private void setDpt(final KnxGroup knxGroup, final String dptString) { private void setDpt(final KnxGroup knxGroup, final String dptString) {
final Matcher mainSub = Pattern.compile("^DPST-(?<main>\\d+)-(?<sub>\\d+)$").matcher(dptString); final Matcher mainSub = Pattern.compile("^DPST-(?<main>\\d+)-(?<sub>\\d+)$").matcher(dptString);
if (mainSub.matches()) { if (mainSub.matches()) {
final int main = Integer.parseInt(mainSub.group("main")); knxGroup.setDptMain(Integer.parseInt(mainSub.group("main")));
if (knxGroup.getDptMain() != main) {
knxGroup.setLastTelegram(null);
knxGroup.setValue(null);
knxGroup.setTimestamp(null);
}
knxGroup.setDptMain(main);
knxGroup.setDptSub(Integer.parseInt(mainSub.group("sub"))); knxGroup.setDptSub(Integer.parseInt(mainSub.group("sub")));
return; return;
} }

View File

@ -4,7 +4,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import tuwien.auto.calimero.*; import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXFormatException;
import tuwien.auto.calimero.KNXTimeoutException;
import tuwien.auto.calimero.datapoint.StateDP; import tuwien.auto.calimero.datapoint.StateDP;
import tuwien.auto.calimero.dptxlator.TranslatorTypes; import tuwien.auto.calimero.dptxlator.TranslatorTypes;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl; import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
@ -71,12 +74,6 @@ public class KnxGroupLinkService {
knxGroup.getRead().setNextTimestamp(null); knxGroup.getRead().setNextTimestamp(null);
log.debug("Successfully sent KnxGroup: {}", knxGroup); log.debug("Successfully sent KnxGroup: {}", knxGroup);
return true; return true;
} catch (KNXIllegalArgumentException e) {
log.error("Failed to read KnxGroup {}", knxGroup);
knxGroup.getRead().setErrorCount(knxGroup.getRead().getErrorCount() + 1);
knxGroup.getRead().setErrorMessage(e.getMessage());
knxGroup.getRead().setNextTimestamp(null);
return true;
} catch (KNXTimeoutException | KNXFormatException e) { } catch (KNXTimeoutException | KNXFormatException e) {
log.error("Failed to read KnxGroup {}", knxGroup); log.error("Failed to read KnxGroup {}", knxGroup);
knxGroup.getRead().setErrorCount(knxGroup.getRead().getErrorCount() + 1); knxGroup.getRead().setErrorCount(knxGroup.getRead().getErrorCount() + 1);

View File

@ -1,16 +1,18 @@
package de.ph87.homeautomation.knx.group; package de.ph87.homeautomation.knx.group;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.CrudRepository;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface KnxGroupRepository extends ListCrudRepository<KnxGroup, Long>, JpaSpecificationExecutor<KnxGroup> { public interface KnxGroupRepository extends CrudRepository<KnxGroup, Long>, JpaSpecificationExecutor<KnxGroup> {
Optional<KnxGroup> findByAddressRaw(int rawAddress); Optional<KnxGroup> findByAddressRaw(int rawAddress);
List<KnxGroup> findAll();
Optional<KnxGroup> findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc(); Optional<KnxGroup> findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc();
Optional<KnxGroup> findFirstByRead_NextTimestampLessThanEqualOrderByRead_NextTimestampAsc(ZonedDateTime timestamp); Optional<KnxGroup> findFirstByRead_NextTimestampLessThanEqualOrderByRead_NextTimestampAsc(ZonedDateTime timestamp);
@ -19,6 +21,4 @@ public interface KnxGroupRepository extends ListCrudRepository<KnxGroup, Long>,
List<KnxGroup> findAllByNameContainsIgnoreCaseOrAddressStrContainsIgnoreCase(String name, String addressStr); List<KnxGroup> findAllByNameContainsIgnoreCaseOrAddressStrContainsIgnoreCase(String name, String addressStr);
List<KnxGroup> findAllByEtsFalse();
} }

View File

@ -44,11 +44,6 @@ public class PropertyController implements ISearchController {
return propertyWriter.set(id, Property::setSlug, value == null || value.isEmpty() ? null : value); return propertyWriter.set(id, Property::setSlug, value == null || value.isEmpty() ? null : value);
} }
@GetMapping("get/{id}/value")
public Double getValue(@PathVariable final long id) {
return channelReader.read(propertyReader.getById(id)).orElse(null);
}
@PostMapping("set/{id}/value") @PostMapping("set/{id}/value")
public PropertyDto setValue(@PathVariable final long id, @RequestBody final double value) { public PropertyDto setValue(@PathVariable final long id, @RequestBody final double value) {
return propertyWriter.set(id, propertyWriter::writeToChannel, value); return propertyWriter.set(id, propertyWriter::writeToChannel, value);
@ -71,10 +66,18 @@ public class PropertyController implements ISearchController {
@PostMapping("toggle/{id}") @PostMapping("toggle/{id}")
public PropertyDto setValue(@PathVariable final long id) { public PropertyDto setValue(@PathVariable final long id) {
final boolean oldState = channelReader.read(propertyReader.getById(id)).map(v -> v > 0.0).orElse(false); final boolean oldState = getOldStateBoolean(id, false);
return propertyWriter.set(id, propertyWriter::writeToChannel, oldState ? 0.0 : 1.0); return propertyWriter.set(id, propertyWriter::writeToChannel, oldState ? 0.0 : 1.0);
} }
private boolean getOldStateBoolean(final long id, final boolean orElse) {
final Double oldValue = channelReader.read(propertyReader.getById(id));
if (oldValue == null || oldValue.isNaN()) {
return orElse;
}
return oldValue > 0.0;
}
@Override @Override
@GetMapping("searchById/{id}") @GetMapping("searchById/{id}")
public SearchResult searchById(@PathVariable final long id) { public SearchResult searchById(@PathVariable final long id) {

View File

@ -23,10 +23,6 @@ public class PropertyReader {
return propertyRepository.findAllByReadChannel_Id(readChannelId); return propertyRepository.findAllByReadChannel_Id(readChannelId);
} }
public List<Property> findAllByWriteChannel_Id(final long writeChannelId) {
return propertyRepository.findAllByWriteChannel_Id(writeChannelId);
}
public List<PropertyDto> search(final String term) { public List<PropertyDto> search(final String term) {
return RepositorySearchHelper.search(term, propertyRepository, "title", propertyMapper::toDto); return RepositorySearchHelper.search(term, propertyRepository, "title", propertyMapper::toDto);
} }

View File

@ -12,7 +12,7 @@ public interface PropertyRepository extends JpaRepository<Property, Long>, JpaSp
List<Property> findAllByReadChannel_Id(long readChannelId); List<Property> findAllByReadChannel_Id(long readChannelId);
List<Property> findAllByWriteChannel_Id(final long writeChannelId); List<Property> findAllByTitleLikeIgnoreCase(final String like);
boolean existsByTitle(String title); boolean existsByTitle(String title);

View File

@ -11,8 +11,6 @@ import org.springframework.context.event.EventListener;
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.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Comparator; import java.util.Comparator;
import java.util.Optional; import java.util.Optional;
@ -57,12 +55,12 @@ public class ScheduleCalculator {
entry.setNextClearTimestamp(null); entry.setNextClearTimestamp(null);
return; return;
} }
LocalDate day = now.toLocalDate(); ZonedDateTime midnight = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
ZonedDateTime next = calculateEntryForDay(entry, day); ZonedDateTime next = calculateEntryForDay(entry, midnight);
while (next != null && (!next.isAfter(now) || !isAfterLast(entry, next) || !isWeekdayEnabled(entry, next))) { while (next != null && (!next.isAfter(now) || !isAfterLast(entry, next) || !isWeekdayEnabled(entry, next))) {
log.debug(" -- skipping: next={}", next); log.debug(" -- skipping: next={}", next);
day = day.plusDays(1); midnight = midnight.plusDays(1);
next = calculateEntryForDay(entry, day); next = calculateEntryForDay(entry, midnight);
} }
log.debug(" => {}", next); log.debug(" => {}", next);
entry.setNextClearTimestamp(next); entry.setNextClearTimestamp(next);
@ -72,14 +70,14 @@ public class ScheduleCalculator {
return entry.getLastClearTimestamp() == null || next.isAfter(entry.getLastClearTimestamp()); return entry.getLastClearTimestamp() == null || next.isAfter(entry.getLastClearTimestamp());
} }
private ZonedDateTime calculateEntryForDay(final ScheduleEntry entry, final LocalDate day) { private ZonedDateTime calculateEntryForDay(final ScheduleEntry entry, final ZonedDateTime midnight) {
switch (entry.getType()) { switch (entry.getType()) {
case TIME: case TIME:
return day.atStartOfDay().atZone(ZoneId.systemDefault()).withHour(entry.getHour()).withMinute(entry.getMinute()).withSecond(entry.getSecond()); return midnight.withHour(entry.getHour()).withMinute(entry.getMinute()).withSecond(entry.getSecond());
case SUNRISE: case SUNRISE:
case SUNSET: case SUNSET:
final boolean sunrise = entry.getType() == ScheduleEntryType.SUNRISE; final boolean sunrise = entry.getType() == ScheduleEntryType.SUNRISE;
return astroCalculator.forDay(day, sunrise, entry.getZenith()).withLaterOffsetAtOverlap(); return astroCalculator.forDay(midnight, sunrise, entry.getZenith());
default: default:
log.error("AstroEvent not implemented: {}", entry.getType()); log.error("AstroEvent not implemented: {}", entry.getType());
break; break;

View File

@ -8,12 +8,10 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.TimeZone;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -21,15 +19,25 @@ public class AstroCalculator {
private final Config config; private final Config config;
public ZonedDateTime forDay(final LocalDate day, final boolean sunrise, final double zenith) { public ZonedDateTime next(final ZonedDateTime now, final boolean sunrise, final double zenith) {
ZonedDateTime day = now.truncatedTo(ChronoUnit.DAYS);
ZonedDateTime next;
do {
next = forDay(day, sunrise, zenith);
day = day.plusDays(1);
} while (next != null && !next.isAfter(now));
return next;
}
public ZonedDateTime forDay(final ZonedDateTime midnight, final boolean sunrise, final double zenith) {
final Location location = new Location(config.getLatitude(), config.getLongitude()); final Location location = new Location(config.getLatitude(), config.getLongitude());
final SolarEventCalculator calculator = new SolarEventCalculator(location, TimeZone.getTimeZone("UTC")); final SolarEventCalculator calculator = new SolarEventCalculator(location, config.getTimezone());
final Calendar calendar = GregorianCalendar.from(day.atStartOfDay(ZoneId.of("UTC"))); final Calendar calendar = GregorianCalendar.from(midnight);
final Calendar nextCalendar = forDay(calculator, sunrise, new Zenith(zenith), calendar); final Calendar nextCalendar = forDay(calculator, sunrise, new Zenith(zenith), calendar);
if (nextCalendar == null) { if (nextCalendar == null) {
return null; return null;
} }
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(nextCalendar.getTimeInMillis()), ZoneId.systemDefault()); return ZonedDateTime.ofInstant(Instant.ofEpochMilli(nextCalendar.getTimeInMillis()), midnight.getZone());
} }
private Calendar forDay(final SolarEventCalculator calculator, final boolean sunrise, final Zenith solarZenith, final Calendar calendar) { private Calendar forDay(final SolarEventCalculator calculator, final boolean sunrise, final Zenith solarZenith, final Calendar calendar) {

View File

@ -92,16 +92,14 @@ public class ScheduleEntry {
public void setNextClearTimestamp(final ZonedDateTime next) { public void setNextClearTimestamp(final ZonedDateTime next) {
nextClearTimestamp = next; nextClearTimestamp = next;
if (nextClearTimestamp == null) {
nextFuzzyTimestamp = null; nextFuzzyTimestamp = null;
if (nextClearTimestamp != null) { } else {
final int rangeFull = 2 * fuzzySeconds; final int rangeFull = 2 * fuzzySeconds;
final ZonedDateTime min = ZonedDateTime.now().plusMinutes(1);
while (nextFuzzyTimestamp == null || nextFuzzyTimestamp.isBefore(min)) {
final int fuzzySeconds = rangeFull > 0 ? RANDOM.nextInt(rangeFull) - this.fuzzySeconds : 0; final int fuzzySeconds = rangeFull > 0 ? RANDOM.nextInt(rangeFull) - this.fuzzySeconds : 0;
nextFuzzyTimestamp = nextClearTimestamp.plusSeconds(fuzzySeconds); nextFuzzyTimestamp = nextClearTimestamp.plusSeconds(fuzzySeconds);
} }
} }
}
@Override @Override
public String toString() { public String toString() {

View File

@ -2,10 +2,12 @@ package de.ph87.homeautomation.web;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@CrossOrigin
@Configuration @Configuration
@EnableWebSocketMessageBroker @EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

View File

@ -1,70 +0,0 @@
package de.ph87.homeautomation.schedule;
import de.ph87.homeautomation.Config;
import de.ph87.homeautomation.schedule.astro.AstroCalculator;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Slf4j
class ScheduleCalculatorDaylightSavingTimeTransitionTest {
private static final ZonedDateTime DAY_OF_DST_SET_BACK = ZonedDateTime.of(2024, 10, 27, 0, 0, 0, 0, ZoneId.of("Europe/Berlin"));
private static final AstroCalculator astroCalculator = new AstroCalculator(new Config());
@Test
void withHour() {
final ZonedDateTime morning = DAY_OF_DST_SET_BACK.withHour(6);
assertEquals(6, morning.getHour());
assertEquals(0, morning.getMinute());
assertEquals(0, morning.getSecond());
assertEquals(0, morning.getNano());
assertEquals(3600, morning.getOffset().getTotalSeconds());
}
@Test
void sunrise_dstSetBack() {
final ZonedDateTime morning = astroCalculator.forDay(DAY_OF_DST_SET_BACK.toLocalDate(), true, 90);
assertEquals(7, morning.getHour());
assertEquals(19, morning.getMinute());
assertEquals(0, morning.getSecond());
assertEquals(0, morning.getNano());
assertEquals(3600, morning.getOffset().getTotalSeconds());
}
@Test
void sunset_dstSetBack() {
final ZonedDateTime morning = astroCalculator.forDay(DAY_OF_DST_SET_BACK.toLocalDate(), false, 90);
assertEquals(17, morning.getHour());
assertEquals(13, morning.getMinute());
assertEquals(0, morning.getSecond());
assertEquals(0, morning.getNano());
assertEquals(3600, morning.getOffset().getTotalSeconds());
}
@Test
void sunrise_noDstChange() {
final ZonedDateTime morning = astroCalculator.forDay(DAY_OF_DST_SET_BACK.minusDays(1).toLocalDate(), true, 90);
assertEquals(8, morning.getHour());
assertEquals(17, morning.getMinute());
assertEquals(0, morning.getSecond());
assertEquals(0, morning.getNano());
assertEquals(7200, morning.getOffset().getTotalSeconds());
}
@Test
void sunset_noDstChange() {
final ZonedDateTime morning = astroCalculator.forDay(DAY_OF_DST_SET_BACK.minusDays(1).toLocalDate(), false, 90);
assertEquals(18, morning.getHour());
assertEquals(14, morning.getMinute());
assertEquals(0, morning.getSecond());
assertEquals(0, morning.getNano());
assertEquals(7200, morning.getOffset().getTotalSeconds());
}
}