Compare commits
10 Commits
073c42002a
...
277e321480
| Author | SHA1 | Date | |
|---|---|---|---|
| 277e321480 | |||
| bef7cc4a6e | |||
| c174a18a4a | |||
| 1c0ffc7b64 | |||
| e991b23479 | |||
| 188d13591f | |||
| 80e61a5c6c | |||
| 4ade9787db | |||
| d5ce06d4d7 | |||
| 44820f3201 |
@ -5,6 +5,8 @@ spring.datasource.driverClassName=org.h2.Driver
|
|||||||
spring.datasource.username=sa
|
spring.datasource.username=sa
|
||||||
spring.datasource.password=password
|
spring.datasource.password=password
|
||||||
#-
|
#-
|
||||||
|
spring.jackson.serialization.indent_output=true
|
||||||
|
#-
|
||||||
spring.jpa.hibernate.ddl-auto=create
|
spring.jpa.hibernate.ddl-auto=create
|
||||||
#-
|
#-
|
||||||
de.ph87.homeautomation.insert-demo-data=true
|
de.ph87.homeautomation.insert-demo-data=true
|
||||||
10
pom.xml
10
pom.xml
@ -41,6 +41,10 @@
|
|||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
@ -62,6 +66,12 @@
|
|||||||
<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>
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export class BulkService implements ISearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(next: (item: Bulk) => void): void {
|
create(next: (item: Bulk) => void): void {
|
||||||
this.api.postReturnItem("bulk/create/", new BulkCreate("Neu", true), Bulk.fromJson, next);
|
this.api.postReturnItem("bulk/create", new BulkCreate("Neu", true), Bulk.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(bulk: Bulk, next: () => void): void {
|
delete(bulk: Bulk, next: () => void): void {
|
||||||
|
|||||||
@ -72,6 +72,38 @@ 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 {
|
||||||
|
|||||||
@ -69,7 +69,7 @@ export class DeviceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(type: string, next: (item: Device) => void): void {
|
create(type: string, next: (item: Device) => void): void {
|
||||||
this.api.postReturnItem("device/create/", type, Device.fromJson, next);
|
this.api.postReturnItem("device/create", type, Device.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(device: Device, next: () => void): void {
|
delete(device: Device, next: () => void): void {
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import {validateListOrEmpty, validateNumberNotNull, validateStringEmptyToNull, validateStringNotEmptyNotNull} from "../validators";
|
import {undefinedOrNull, 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;
|
||||||
@ -11,7 +19,7 @@ export class Property {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public id: number,
|
public id: number,
|
||||||
public type: string,
|
public type: PropertyType,
|
||||||
public title: string,
|
public title: string,
|
||||||
public slug: string | null,
|
public slug: string | null,
|
||||||
public readChannel: Channel | null,
|
public readChannel: Channel | null,
|
||||||
@ -36,7 +44,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']),
|
validateStringNotEmptyNotNull(json['type']) as PropertyType,
|
||||||
validateStringNotEmptyNotNull(json['title']),
|
validateStringNotEmptyNotNull(json['title']),
|
||||||
validateStringEmptyToNull(json['slug']),
|
validateStringEmptyToNull(json['slug']),
|
||||||
Channel.fromJsonAllowNull(json['readChannel']),
|
Channel.fromJsonAllowNull(json['readChannel']),
|
||||||
@ -63,4 +71,32 @@ 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 {};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export class PropertyService implements ISearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(next: (item: Property) => void): void {
|
create(next: (item: Property) => void): void {
|
||||||
this.api.getReturnItem("property/create/", Property.fromJson, next);
|
this.api.getReturnItem("property/create", Property.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(property: Property, next: Next<void>): void {
|
delete(property: Property, next: Next<void>): void {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import {validateBooleanNotNull, validateListOrEmpty, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
|
import {validateBooleanNotNull, validateListOrEmpty, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
|
||||||
import {ScheduleEntry} from "./entry/ScheduleEntry";
|
import {ScheduleEntry} from "./entry/ScheduleEntry";
|
||||||
import {Astro} from "../astro/Astro";
|
|
||||||
|
|
||||||
export class Schedule {
|
export class Schedule {
|
||||||
|
|
||||||
@ -13,7 +12,6 @@ export class Schedule {
|
|||||||
readonly enabled: boolean,
|
readonly enabled: boolean,
|
||||||
readonly title: string,
|
readonly title: string,
|
||||||
readonly entries: ScheduleEntry[],
|
readonly entries: ScheduleEntry[],
|
||||||
readonly astros: Astro[],
|
|
||||||
) {
|
) {
|
||||||
this.next = entries.filter(e => e.nextFuzzyTimestamp).sort((a, b) => a.nextFuzzyTimestamp.date.getTime() - b.nextFuzzyTimestamp.date.getTime())[0];
|
this.next = entries.filter(e => e.nextFuzzyTimestamp).sort((a, b) => a.nextFuzzyTimestamp.date.getTime() - b.nextFuzzyTimestamp.date.getTime())[0];
|
||||||
this.last = entries.filter(e => e.lastFuzzyTimestamp).sort((a, b) => b.lastFuzzyTimestamp.date.getTime() - a.lastFuzzyTimestamp.date.getTime())[0];
|
this.last = entries.filter(e => e.lastFuzzyTimestamp).sort((a, b) => b.lastFuzzyTimestamp.date.getTime() - a.lastFuzzyTimestamp.date.getTime())[0];
|
||||||
@ -25,7 +23,6 @@ export class Schedule {
|
|||||||
validateBooleanNotNull(json['enabled']),
|
validateBooleanNotNull(json['enabled']),
|
||||||
validateStringNotEmptyNotNull(json['title']),
|
validateStringNotEmptyNotNull(json['title']),
|
||||||
validateListOrEmpty(json['entries'], ScheduleEntry.fromJson, ScheduleEntry.comparePosition),
|
validateListOrEmpty(json['entries'], ScheduleEntry.fromJson, ScheduleEntry.comparePosition),
|
||||||
validateListOrEmpty(json['astros'], Astro.fromJson),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class ScheduleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(next: Next<Schedule> = NO_OP): void {
|
create(next: Next<Schedule> = NO_OP): void {
|
||||||
this.api.getReturnItem("schedule/create/", Schedule.fromJson, next);
|
this.api.getReturnItem("schedule/create", Schedule.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(schedule: Schedule, key: string, value: any, next: Next<Schedule> = NO_OP): void {
|
set(schedule: Schedule, key: string, value: any, next: Next<Schedule> = NO_OP): void {
|
||||||
|
|||||||
@ -87,3 +87,7 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
@ -8,10 +8,6 @@
|
|||||||
Rollläden
|
Rollläden
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="item" [routerLink]="['/DeviceList', {'type': 'DeviceStateScene'}]" [class.itemActive]="isRouteActive('/DeviceList', 'DeviceStateScene')">-->
|
|
||||||
<!-- Gruppen-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<div class="item" routerLink="/BulkList" routerLinkActive="itemActive">
|
<div class="item" routerLink="/BulkList" routerLinkActive="itemActive">
|
||||||
Stapel
|
Stapel
|
||||||
</div>
|
</div>
|
||||||
@ -21,11 +17,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item itemSecondary" routerLink="/PropertyList" routerLinkActive="itemActive">
|
<div class="item itemSecondary" routerLink="/PropertyList" routerLinkActive="itemActive">
|
||||||
Eigenschaften
|
P
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item itemSecondary" routerLink="/ChannelList" routerLinkActive="itemActive">
|
<div class="item itemSecondary" routerLink="/ChannelList" routerLinkActive="itemActive">
|
||||||
Kanäle
|
C
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
|
@import "../config";
|
||||||
|
|
||||||
.menubar {
|
.menubar {
|
||||||
border-bottom: 1px solid black;
|
border-bottom: @border solid black;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
float: left;
|
float: left;
|
||||||
padding: 10px;
|
padding: @padding;
|
||||||
border-right: 1px solid black;
|
border-right: @border solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemSecondary {
|
.itemSecondary {
|
||||||
float: right;
|
float: right;
|
||||||
border-left: 1px solid black;
|
border-left: @border solid black;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
<table>
|
<div class="tiles">
|
||||||
<tr>
|
<div class="tile" *ngFor="let channel of channels">
|
||||||
<th>Titel</th>
|
<div>
|
||||||
<th>Typ</th>
|
{{ channel.title }}
|
||||||
<th colspan="3">Adresse</th>
|
</div>
|
||||||
<th>DPT</th>
|
<div class="left">
|
||||||
<th colspan="2">Wert</th>
|
{{ channel.type }}
|
||||||
</tr>
|
</div>
|
||||||
<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'">
|
||||||
<td class="first number">{{asKnxGroup(channel).addresMain}} / </td>
|
<div class="left">
|
||||||
<td class="middle number">{{asKnxGroup(channel).addresMid}} / </td>
|
{{ asKnxGroup(channel).addresMain }} / {{ asKnxGroup(channel).addresMid }} / {{ asKnxGroup(channel).addresSub }}
|
||||||
<td class="last number">{{asKnxGroup(channel).addresSub}}</td>
|
</div>
|
||||||
<td class="number">{{asKnxGroup(channel).dpt}}</td>
|
<div class="right">
|
||||||
|
DPT {{ asKnxGroup(channel).dpt }}
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="channel.type === 'Logic'">
|
<div class="timestamp">
|
||||||
<td colspan="4">{{asLogic(channel).operator}}</td>
|
{{ channel.timestamp | date:'yyyy-MM-dd HH:mm:ss' || '-' }}
|
||||||
</ng-container>
|
</div>
|
||||||
<td class="number">{{channel.value}}</td>
|
<div class="value">
|
||||||
<td>{{channel.timestamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
|
{{ channel.value || '-' }}
|
||||||
</tr>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,3 +1,23 @@
|
|||||||
table {
|
@import "../../../../config";
|
||||||
width: 100%;
|
|
||||||
|
div {
|
||||||
|
padding: @padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
clear: left;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
clear: right;
|
||||||
|
float: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,76 +1,84 @@
|
|||||||
<ng-container *ngFor="let device of devices; trackBy: Device.trackBy">
|
<div class="tiles">
|
||||||
<ng-container [ngSwitch]="device.type">
|
|
||||||
|
|
||||||
<div class="device" *ngSwitchCase="'DeviceSwitch'" [ngClass]="getSwitchClassList(device)">
|
<ng-container *ngFor="let device of devices; trackBy: Device.trackBy">
|
||||||
<div class="tileHeadTitle">
|
|
||||||
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
|
<div class="tile" *ngIf="device.type == 'DeviceSwitch'" [ngClass]="device.getSwitchClassList()">
|
||||||
|
<div class="tileHead">
|
||||||
|
<div class="flexGrow">
|
||||||
|
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
|
||||||
|
</div>
|
||||||
|
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]">
|
||||||
|
<fa-icon [icon]="faEdit"></fa-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit" [routerLink]="['/Device', {id: device.id}]">
|
<div class="tileBody">
|
||||||
<fa-icon [icon]="faEdit"></fa-icon>
|
<img alt="An" class="control" src="assets/switch-on.svg" (click)="deviceService.setSwitchState(device, true)"/>
|
||||||
</div>
|
<img alt="Aus" class="control" src="assets/switch-off.svg" (click)="deviceService.setSwitchState(device, false)"/>
|
||||||
<div class="controls">
|
|
||||||
<img alt="An" class="control" src="../../../../assets/switch-on.svg" (click)="deviceService.setSwitchState(device, true)"/>
|
|
||||||
<img alt="Aus" class="control" src="../../../../assets/switch-off.svg" (click)="deviceService.setSwitchState(device, false)"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="device" *ngSwitchCase="'DeviceStateScene'" [ngClass]="getStateSceneClassList(device)">
|
<div class="tile" *ngIf="device.type === 'DeviceStateScene'" [ngClass]="device.getStateSceneClassList()">
|
||||||
<div class="tileHeadTitle">
|
<div class="tileHead">
|
||||||
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
|
<div class="flexGrow">
|
||||||
|
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
|
||||||
|
</div>
|
||||||
|
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]">
|
||||||
|
<fa-icon [icon]="faEdit"></fa-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit" [routerLink]="['/Device', {id: device.id}]">
|
<div class="tileBody">
|
||||||
<fa-icon [icon]="faEdit"></fa-icon>
|
<div class="control" *ngFor="let scene of getStateScenes(device)" (click)="deviceService.setStateScene(device, scene.number)">
|
||||||
</div>
|
{{ scene.title }}
|
||||||
<div class="controls">
|
|
||||||
<div *ngFor="let scene of getStateScenes(device)" class="control button" (click)="deviceService.setStateScene(device, scene.number)">
|
|
||||||
<span class="center">{{scene.title}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="device" *ngSwitchCase="'DeviceShutter'" [ngClass]="getShutterClassList(device)">
|
<div class="tile" *ngIf="device.type === 'DeviceShutter'" [ngClass]="device.getShutterClassList()">
|
||||||
<div class="tileHeadTitle">
|
<div class="tileHead">
|
||||||
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
|
<div class="flexGrow">
|
||||||
|
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
|
||||||
|
</div>
|
||||||
|
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]">
|
||||||
|
<fa-icon [icon]="faEdit"></fa-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit" [routerLink]="['/Device', {id: device.id}]">
|
<div class="tileBody">
|
||||||
<fa-icon [icon]="faEdit"></fa-icon>
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 0)">
|
||||||
</div>
|
Auf
|
||||||
<div class="controls">
|
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 0)">
|
|
||||||
<span class="center">Auf</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 20)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 20)">
|
||||||
<span class="center">20%</span>
|
20%
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 30)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 30)">
|
||||||
<span class="center">30%</span>
|
30%
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 40)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 40)">
|
||||||
<span class="center">40%</span>
|
40%
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 50)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 50)">
|
||||||
<span class="center">50%</span>
|
50%
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 60)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 60)">
|
||||||
<span class="center">60%</span>
|
60%
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 70)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 70)">
|
||||||
<span class="center">70%</span>
|
70%
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 80)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 80)">
|
||||||
<span class="center">80%</span>
|
80%
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 90)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 90)">
|
||||||
<span class="center">90%</span>
|
90%
|
||||||
</div>
|
</div>
|
||||||
<div class="control button" (click)="deviceService.setShutterPosition(device, 100)">
|
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 100)">
|
||||||
<span class="center">Zu</span>
|
Zu
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="config">
|
<div class="config">
|
||||||
<button (click)="create()">+ Hinzufügen</button>
|
<button (click)="create()">+ Hinzufügen</button>
|
||||||
|
|||||||
@ -1,71 +1,17 @@
|
|||||||
.device {
|
@import "../../../../config";
|
||||||
padding: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
@media (min-width: 1000px) {
|
.control {
|
||||||
float: left;
|
float: left;
|
||||||
width: 400px;
|
width: 4em;
|
||||||
margin-right: 5px;
|
aspect-ratio: 1;
|
||||||
}
|
margin: @margin;
|
||||||
|
border-radius: 25%;
|
||||||
.tileHeadTitle {
|
|
||||||
float: left;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
clear: both;
|
|
||||||
|
|
||||||
.control {
|
|
||||||
position: relative;
|
|
||||||
float: left;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
padding: 5px;
|
|
||||||
margin: 5px;
|
|
||||||
border-radius: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background-color: lightblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control:hover {
|
|
||||||
background-color: lightskyblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.switchOn {
|
.controlShutter {
|
||||||
background-color: palegreen;
|
width: 17.3%;
|
||||||
}
|
padding-top: 1.1em;
|
||||||
|
text-align: center;
|
||||||
.switchOff {
|
margin: calc(@margin / 2);
|
||||||
background-color: indianred;
|
background-color: lightsteelblue;
|
||||||
}
|
|
||||||
|
|
||||||
.switchUnknown {
|
|
||||||
background-color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shutterOpen {
|
|
||||||
background-color: palegreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shutterBetween {
|
|
||||||
background-color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shutterClosed {
|
|
||||||
background-color: indianred;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shutterUnknown {
|
|
||||||
background-color: gray;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, DeviceShutter, DeviceStateScene, DeviceSwitch} from "../../../api/device/Device";
|
import {Device, DeviceStateScene} 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,32 +78,9 @@ export class DeviceListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSwitchClassList(device: Device): object {
|
getStateScenes(device: Device): Scene[] {
|
||||||
const value: number | null | undefined = (device as DeviceSwitch).stateProperty?.readChannel?.value;
|
const casted: DeviceStateScene = device as DeviceStateScene;
|
||||||
return {
|
return casted.sceneNumbers.map(sceneNumber => this.scenes.find(scene => scene.number === sceneNumber)).filter(scene => scene !== undefined).map(s => s as Scene);
|
||||||
switchOn: value === 1,
|
|
||||||
switchOff: value === 0,
|
|
||||||
switchUnknown: 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 {
|
|
||||||
shutterOpen: value === 0,
|
|
||||||
shutterBetween: value !== null && value !== undefined && value > 0 && value < 100,
|
|
||||||
shutterClosed: value === 100,
|
|
||||||
shutterUnknown: 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 {
|
||||||
|
|||||||
@ -2,53 +2,17 @@
|
|||||||
<td class="empty">-</td>
|
<td class="empty">-</td>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div class="config">
|
<div class="tiles">
|
||||||
<button (click)="create()">+ Hinzufügen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table>
|
<div *ngFor="let list of listLists()">
|
||||||
<tr>
|
<div class="tile" *ngFor="let property of list">
|
||||||
<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>
|
|
||||||
|
|
||||||
<td>
|
<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>
|
||||||
<div *ngIf="property.hrefId">{{property.hrefId}}0</div>
|
|
||||||
<div *ngIf="property.hrefId">{{property.hrefId}}1</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>
|
|
||||||
|
|
||||||
<td>
|
<div class="tileBody">
|
||||||
<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>
|
||||||
@ -57,44 +21,22 @@
|
|||||||
<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>
|
|
||||||
|
|
||||||
<td class="delete">
|
<div 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>
|
||||||
</td>
|
</div>
|
||||||
|
|
||||||
</tr>
|
</div>
|
||||||
</ng-container>
|
|
||||||
</table>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config">
|
||||||
|
<button (click)="create()">+ Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,29 @@
|
|||||||
table {
|
@import "../../../../config";
|
||||||
width: 100%;
|
|
||||||
|
.propertyStateBooleanUnknown {
|
||||||
|
background-color: @COLOR_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
.links {
|
.propertyStateBooleanTrue {
|
||||||
color: gray;
|
background-color: @COLOR_ACTIVE;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {Property} from "../../../api/property/Property";
|
import {Property, PropertyType} 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,16 +14,18 @@ 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,
|
||||||
@ -33,7 +35,10 @@ export class PropertyListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.propertyService.findAll(properties => this.properties = properties, Property.compareTypeThenTitle);
|
this.propertyService.findAll(properties => {
|
||||||
|
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);
|
||||||
@ -49,15 +54,20 @@ export class PropertyListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateProperty(property: Property, existing: boolean): void {
|
private updateProperty(property: Property, existing: boolean): void {
|
||||||
const index: number = this.properties.findIndex(p => p.id === property.id);
|
this.updateProperty2(this.booleans, property, existing);
|
||||||
|
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) {
|
||||||
this.properties[index] = property;
|
properties[index] = property;
|
||||||
} else {
|
} else {
|
||||||
this.properties.slice(index, 1);
|
properties.slice(index, 1);
|
||||||
}
|
}
|
||||||
} else if (existing) {
|
} else if (existing) {
|
||||||
this.properties.push(property);
|
properties.push(property);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,8 +98,22 @@ 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.properties.splice(this.properties.findIndex(p => p.id === property.id), 1));
|
this.propertyService.delete(property, () => {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,12 +13,15 @@
|
|||||||
<fa-icon *ngIf="entry.enabled" [icon]="faCheckCircle"></fa-icon>
|
<fa-icon *ngIf="entry.enabled" [icon]="faCheckCircle"></fa-icon>
|
||||||
<fa-icon *ngIf="!entry.enabled" [icon]="faCircle"></fa-icon>
|
<fa-icon *ngIf="!entry.enabled" [icon]="faCircle"></fa-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="tileHeadTitle">
|
<div class="flexGrow">
|
||||||
<select [ngModel]="entry.bulk?.id" (ngModelChange)="entryService.set(entry, 'bulk', $event)">
|
<select [ngModel]="entry.bulk?.id" (ngModelChange)="entryService.set(entry, 'bulk', $event)">
|
||||||
<option [ngValue]="null">-</option>
|
<option [ngValue]="null">-</option>
|
||||||
<option [ngValue]="bulk.id" *ngFor="let bulk of bulks.sort(Bulk.compareName)">{{ bulk.name }}</option>
|
<option [ngValue]="bulk.id" *ngFor="let bulk of bulks.sort(Bulk.compareName)">{{ bulk.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tileHeadDelete">
|
||||||
|
<fa-icon [icon]="faTimesCircle" (click)="delete(entry)"></fa-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tileBodyFlex">
|
<div class="tileBodyFlex">
|
||||||
@ -72,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); trackBy: trackByZenith">
|
<div *ngFor="let zenith of getZenithEntries(entry.type)">
|
||||||
<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>
|
||||||
@ -116,4 +119,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="config">
|
||||||
|
<button (click)="create()">+ Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@ -12,10 +12,22 @@ import {Bulk} from "../../../api/bulk/Bulk";
|
|||||||
import {BulkService} from "../../../api/bulk/BulkService";
|
import {BulkService} from "../../../api/bulk/BulkService";
|
||||||
import {Zenith} from "../../../api/schedule/entry/Zenith";
|
import {Zenith} from "../../../api/schedule/entry/Zenith";
|
||||||
import {TimeService} from "../../../api/time.service";
|
import {TimeService} from "../../../api/time.service";
|
||||||
import {faCheckCircle, faCircle} from "@fortawesome/free-regular-svg-icons";
|
import {faCheckCircle, faCircle, faTimesCircle} from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
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',
|
||||||
@ -23,6 +35,12 @@ const DAY_MINUTES: number = 24 * 60;
|
|||||||
})
|
})
|
||||||
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;
|
||||||
@ -39,14 +57,6 @@ 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,
|
||||||
@ -99,17 +109,6 @@ 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]);
|
||||||
@ -119,7 +118,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +129,11 @@ export class ScheduleEditorComponent implements OnInit {
|
|||||||
this.entryService.set(entry, 'daySecond', newMinutes * 60);
|
this.entryService.set(entry, 'daySecond', newMinutes * 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly faCheckCircle = faCheckCircle;
|
getZenithEntries(type: string) {
|
||||||
|
if (type === 'SUNRISE') {
|
||||||
|
return ZENITH_SUNRISE;
|
||||||
|
}
|
||||||
|
return ZENITH_SUNSET;
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly faCircle = faCircle;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<fa-icon *ngIf="!schedule.enabled" [icon]="faCircle"></fa-icon>
|
<fa-icon *ngIf="!schedule.enabled" [icon]="faCircle"></fa-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tileHeadTitle" [routerLink]="['/Schedule', {id: schedule.id}]">
|
<div class="flexGrow" [routerLink]="['/Schedule', {id: schedule.id}]">
|
||||||
{{ schedule.title }}
|
{{ schedule.title }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
@import "../../../config";
|
||||||
|
|
||||||
input {
|
input {
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 5px;
|
padding: @padding;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|||||||
@ -7,9 +7,8 @@
|
|||||||
<ng-container *ngIf="!selected">-</ng-container>
|
<ng-container *ngIf="!selected">-</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input #input type="text" *ngIf="searching" [(ngModel)]="term" (ngModelChange)="changed()" (keydown.enter)="doSearch()" (keydown.escape)="cancelSearch()" (focus)="cancelOnBlur=true" (blur)="blur()">
|
<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()">
|
||||||
<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>
|
||||||
|
|||||||
@ -1,22 +1,30 @@
|
|||||||
|
@import "../../../config";
|
||||||
|
|
||||||
.all {
|
.all {
|
||||||
|
|
||||||
.initial {
|
.initial {
|
||||||
padding: 5px;
|
padding: @padding;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: @border solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultList {
|
.resultList {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
min-width: 200px;
|
min-width: 10em;
|
||||||
border: 1px solid black;
|
border: @border solid black;
|
||||||
|
|
||||||
.result {
|
.result {
|
||||||
padding: 5px;
|
padding: @padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result:hover {
|
.result:hover {
|
||||||
|
|||||||
@ -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<T> implements OnInit {
|
export class SearchComponent implements OnInit {
|
||||||
|
|
||||||
private changedTimeout: number | undefined;
|
private changedTimeout: number | undefined;
|
||||||
|
|
||||||
@ -19,9 +19,6 @@ export class SearchComponent<T> implements OnInit {
|
|||||||
@ViewChild('input')
|
@ViewChild('input')
|
||||||
input?: HTMLInputElement;
|
input?: HTMLInputElement;
|
||||||
|
|
||||||
@ViewChild('resultList')
|
|
||||||
resultList?: HTMLDivElement;
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
searchService!: ISearchService;
|
searchService!: ISearchService;
|
||||||
|
|
||||||
@ -75,9 +72,6 @@ export class SearchComponent<T> 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();
|
||||||
|
|||||||
@ -3,20 +3,17 @@
|
|||||||
@border: 0.05em;
|
@border: 0.05em;
|
||||||
@border-radius: 0.2em;
|
@border-radius: 0.2em;
|
||||||
|
|
||||||
.disabledFont {
|
@COLOR_UNKNOWN: gray;
|
||||||
color: gray;
|
@COLOR_ACTIVE: #8fbc8f;
|
||||||
}
|
@COLOR_BETWEEN: #e4db9c;
|
||||||
|
@COLOR_INACTIVE: #bc8f8f;
|
||||||
|
|
||||||
.disabledBack {
|
.disabledBack {
|
||||||
background-color: gray;
|
background-color: @COLOR_UNKNOWN;
|
||||||
}
|
|
||||||
|
|
||||||
.enabledFont {
|
|
||||||
color: #8fbc8f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.enabledBack {
|
.enabledBack {
|
||||||
background-color: #8fbc8f;
|
background-color: @COLOR_ACTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skipBack {
|
.skipBack {
|
||||||
@ -34,3 +31,23 @@
|
|||||||
.fuzzyBack {
|
.fuzzyBack {
|
||||||
background-color: #88c0ff;
|
background-color: #88c0ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deviceSwitchOnBack {
|
||||||
|
background-color: @COLOR_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceSwitchOffBack {
|
||||||
|
background-color: @COLOR_INACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceShutterOpenBack {
|
||||||
|
background-color: @COLOR_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceShutterIntermediateBack {
|
||||||
|
background-color: @COLOR_BETWEEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceShutterClosedBack {
|
||||||
|
background-color: @COLOR_INACTIVE;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
@import "config";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@ -54,11 +56,11 @@ table {
|
|||||||
|
|
||||||
td, th {
|
td, th {
|
||||||
height: 0; // (=> auto growth) enables use of height percent for children
|
height: 0; // (=> auto growth) enables use of height percent for children
|
||||||
padding: 5px;
|
padding: @padding;
|
||||||
border: 1px solid black;
|
border: @border solid black;
|
||||||
|
|
||||||
img.fullCell {
|
img.fullCell {
|
||||||
margin: -5px;
|
margin: calc(-@margin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,14 +72,6 @@ table.vertical {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.center {
|
|
||||||
position: absolute;
|
|
||||||
margin: 0;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: gray;
|
color: gray;
|
||||||
|
|||||||
@ -13,8 +13,6 @@ 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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,6 @@ public class RepositorySearchHelper {
|
|||||||
.replaceAll("([A-Z])([A-Z])", "$1 $2")
|
.replaceAll("([A-Z])([A-Z])", "$1 $2")
|
||||||
.replaceAll("([a-z])([A-Z])", "$1 $2")
|
.replaceAll("([a-z])([A-Z])", "$1 $2")
|
||||||
.toLowerCase(Locale.ROOT);
|
.toLowerCase(Locale.ROOT);
|
||||||
System.out.println(term2);
|
|
||||||
final List<Predicate> predicates = Arrays.stream(term2.split("\\s"))
|
final List<Predicate> predicates = Arrays.stream(term2.split("\\s"))
|
||||||
.filter(word -> !word.isEmpty())
|
.filter(word -> !word.isEmpty())
|
||||||
.map(word -> criteriaBuilder.like(field, "%" + word + "%"))
|
.map(word -> criteriaBuilder.like(field, "%" + word + "%"))
|
||||||
|
|||||||
@ -11,6 +11,8 @@ 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;
|
||||||
@ -55,12 +57,12 @@ public class ScheduleCalculator {
|
|||||||
entry.setNextClearTimestamp(null);
|
entry.setNextClearTimestamp(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ZonedDateTime midnight = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
|
LocalDate day = now.toLocalDate();
|
||||||
ZonedDateTime next = calculateEntryForDay(entry, midnight);
|
ZonedDateTime next = calculateEntryForDay(entry, day);
|
||||||
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);
|
||||||
midnight = midnight.plusDays(1);
|
day = day.plusDays(1);
|
||||||
next = calculateEntryForDay(entry, midnight);
|
next = calculateEntryForDay(entry, day);
|
||||||
}
|
}
|
||||||
log.debug(" => {}", next);
|
log.debug(" => {}", next);
|
||||||
entry.setNextClearTimestamp(next);
|
entry.setNextClearTimestamp(next);
|
||||||
@ -70,14 +72,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 ZonedDateTime midnight) {
|
private ZonedDateTime calculateEntryForDay(final ScheduleEntry entry, final LocalDate day) {
|
||||||
switch (entry.getType()) {
|
switch (entry.getType()) {
|
||||||
case TIME:
|
case TIME:
|
||||||
return midnight.withHour(entry.getHour()).withMinute(entry.getMinute()).withSecond(entry.getSecond());
|
return day.atStartOfDay().atZone(ZoneId.systemDefault()).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(midnight, sunrise, entry.getZenith());
|
return astroCalculator.forDay(day, sunrise, entry.getZenith()).withLaterOffsetAtOverlap();
|
||||||
default:
|
default:
|
||||||
log.error("AstroEvent not implemented: {}", entry.getType());
|
log.error("AstroEvent not implemented: {}", entry.getType());
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
package de.ph87.homeautomation.schedule;
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
import de.ph87.homeautomation.schedule.astro.AstroDto;
|
|
||||||
import de.ph87.homeautomation.schedule.entry.ScheduleEntryDto;
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntryDto;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ -19,14 +17,11 @@ public class ScheduleDto implements Serializable {
|
|||||||
|
|
||||||
private final Set<ScheduleEntryDto> entries;
|
private final Set<ScheduleEntryDto> entries;
|
||||||
|
|
||||||
private final List<AstroDto> astros;
|
public ScheduleDto(final Schedule schedule, final Set<ScheduleEntryDto> entries) {
|
||||||
|
|
||||||
public ScheduleDto(final Schedule schedule, final Set<ScheduleEntryDto> entries, final List<AstroDto> astros) {
|
|
||||||
this.id = schedule.getId();
|
this.id = schedule.getId();
|
||||||
this.enabled = schedule.isEnabled();
|
this.enabled = schedule.isEnabled();
|
||||||
this.title = schedule.getTitle();
|
this.title = schedule.getTitle();
|
||||||
this.entries = entries;
|
this.entries = entries;
|
||||||
this.astros = astros;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
package de.ph87.homeautomation.schedule;
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
import de.ph87.homeautomation.schedule.astro.AstroDto;
|
|
||||||
import de.ph87.homeautomation.schedule.astro.AstroService;
|
|
||||||
import de.ph87.homeautomation.schedule.entry.ScheduleEntryDto;
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntryDto;
|
||||||
import de.ph87.homeautomation.schedule.entry.ScheduleEntryMapper;
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntryMapper;
|
||||||
import de.ph87.homeautomation.web.WebSocketService;
|
import de.ph87.homeautomation.web.WebSocketService;
|
||||||
@ -10,7 +8,6 @@ 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 java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -24,12 +21,9 @@ public class ScheduleMapper {
|
|||||||
|
|
||||||
private final WebSocketService webSocketService;
|
private final WebSocketService webSocketService;
|
||||||
|
|
||||||
private final AstroService astroService;
|
|
||||||
|
|
||||||
public ScheduleDto toDto(final Schedule schedule) {
|
public ScheduleDto toDto(final Schedule schedule) {
|
||||||
final Set<ScheduleEntryDto> entries = schedule.getEntries().stream().map(scheduleEntryMapper::toDto).collect(Collectors.toSet());
|
final Set<ScheduleEntryDto> entries = schedule.getEntries().stream().map(scheduleEntryMapper::toDto).collect(Collectors.toSet());
|
||||||
final List<AstroDto> astros = astroService.findAllNext();
|
return new ScheduleDto(schedule, entries);
|
||||||
return new ScheduleDto(schedule, entries, astros);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void publish(final Schedule schedule, final boolean existing) {
|
public void publish(final Schedule schedule, final boolean existing) {
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
package de.ph87.homeautomation.schedule.astro;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.lang.Nullable;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Getter
|
|
||||||
@ToString
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class Astro {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue
|
|
||||||
private long id;
|
|
||||||
|
|
||||||
@Version
|
|
||||||
private long version;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
private boolean enabled = true;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
private String error;
|
|
||||||
|
|
||||||
@Column(unique = true)
|
|
||||||
private double zenith;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
private String differentNameForSunset;
|
|
||||||
|
|
||||||
public Astro(final double zenith, @NonNull final String name, @Nullable final String differentNameForSunset) {
|
|
||||||
this.zenith = zenith;
|
|
||||||
this.name = name;
|
|
||||||
this.differentNameForSunset = differentNameForSunset;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -8,10 +8,12 @@ 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
|
||||||
@ -19,25 +21,15 @@ public class AstroCalculator {
|
|||||||
|
|
||||||
private final Config config;
|
private final Config config;
|
||||||
|
|
||||||
public ZonedDateTime next(final ZonedDateTime now, final boolean sunrise, final double zenith) {
|
public ZonedDateTime forDay(final LocalDate day, 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, config.getTimezone());
|
final SolarEventCalculator calculator = new SolarEventCalculator(location, TimeZone.getTimeZone("UTC"));
|
||||||
final Calendar calendar = GregorianCalendar.from(midnight);
|
final Calendar calendar = GregorianCalendar.from(day.atStartOfDay(ZoneId.of("UTC")));
|
||||||
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()), midnight.getZone());
|
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(nextCalendar.getTimeInMillis()), ZoneId.systemDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
package de.ph87.homeautomation.schedule.astro;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.lang.Nullable;
|
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@ToString
|
|
||||||
public class AstroDto {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final double zenith;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final ZonedDateTime sunrise;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final ZonedDateTime sunset;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final String sunriseName;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private final String sunsetName;
|
|
||||||
|
|
||||||
public AstroDto(@NonNull final Astro astro, @NonNull final ZonedDateTime sunrise, @NonNull final ZonedDateTime sunset) {
|
|
||||||
this.zenith = astro.getZenith();
|
|
||||||
this.sunrise = sunrise;
|
|
||||||
this.sunset = sunset;
|
|
||||||
this.sunriseName = astro.getName();
|
|
||||||
this.sunsetName = astro.getDifferentNameForSunset() == null ? astro.getName() : astro.getDifferentNameForSunset();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package de.ph87.homeautomation.schedule.astro;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface AstroRepository extends JpaRepository<Astro, Long> {
|
|
||||||
|
|
||||||
List<Astro> findAllByEnabledTrue();
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
package de.ph87.homeautomation.schedule.astro;
|
|
||||||
|
|
||||||
import lombok.*;
|
|
||||||
import org.springframework.boot.context.event.*;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.transaction.annotation.*;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.time.*;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@Transactional
|
|
||||||
@RequestMapping("Astro")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AstroService {
|
|
||||||
|
|
||||||
private final AstroRepository astroRepository;
|
|
||||||
|
|
||||||
private final AstroCalculator astroCalculator;
|
|
||||||
|
|
||||||
@EventListener(ApplicationStartedEvent.class)
|
|
||||||
public void startup() {
|
|
||||||
if (astroRepository.count() == 0) {
|
|
||||||
astroRepository.save(new Astro(71.0000, "Aufgang +++++", "Untergang +++++"));
|
|
||||||
astroRepository.save(new Astro(75.0000, "Aufgang ++++", "Untergang ++++"));
|
|
||||||
astroRepository.save(new Astro(80.0000, "Aufgang +++", "Untergang +++"));
|
|
||||||
astroRepository.save(new Astro(85.0000, "Aufgang ++", "Untergang ++"));
|
|
||||||
astroRepository.save(new Astro(90.0000, "Aufgang +", "Untergang +"));
|
|
||||||
astroRepository.save(new Astro(90.8333, "Aufgang", "Untergang"));
|
|
||||||
astroRepository.save(new Astro(92.0000, "Aufgang -", "Untergang -"));
|
|
||||||
astroRepository.save(new Astro(93.0000, "Aufgang --", "Untergang --"));
|
|
||||||
astroRepository.save(new Astro(94.0000, "Bürgerlich ++", null));
|
|
||||||
astroRepository.save(new Astro(95.0000, "Bürgerlich +", null));
|
|
||||||
astroRepository.save(new Astro(96.0000, "Bürgerlich", null));
|
|
||||||
astroRepository.save(new Astro(97.0000, "Bürgerlich -", null));
|
|
||||||
astroRepository.save(new Astro(98.0000, "Bürgerlich --", null));
|
|
||||||
astroRepository.save(new Astro(99.0000, "Bürgerlich ---", null));
|
|
||||||
astroRepository.save(new Astro(100.000, "Nautisch ++", null));
|
|
||||||
astroRepository.save(new Astro(101.000, "Nautisch +", null));
|
|
||||||
astroRepository.save(new Astro(102.000, "Nautisch", null));
|
|
||||||
astroRepository.save(new Astro(103.000, "Nautisch -", null));
|
|
||||||
astroRepository.save(new Astro(104.000, "Nautisch --", null));
|
|
||||||
astroRepository.save(new Astro(105.000, "Nautisch ---", null));
|
|
||||||
astroRepository.save(new Astro(106.000, "Astronomisch ++", null));
|
|
||||||
astroRepository.save(new Astro(107.000, "Astronomisch +", null));
|
|
||||||
astroRepository.save(new Astro(108.000, "Astronomisch", null));
|
|
||||||
astroRepository.save(new Astro(110.000, "Astronomisch -", null));
|
|
||||||
astroRepository.save(new Astro(120.000, "Astronomisch --", null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<AstroDto> findAllNext() {
|
|
||||||
final ZonedDateTime now = ZonedDateTime.now();
|
|
||||||
return astroRepository.findAllByEnabledTrue().stream().map(astro -> next(now, astro)).filter(Objects::nonNull).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private AstroDto next(final ZonedDateTime now, final Astro astro) {
|
|
||||||
final ZonedDateTime sunrise = astroCalculator.next(now, true, astro.getZenith());
|
|
||||||
final ZonedDateTime sunset = astroCalculator.next(now, false, astro.getZenith());
|
|
||||||
if (sunrise == null || sunset == null) {
|
|
||||||
astro.setEnabled(false);
|
|
||||||
astro.setError("sunrise (%s) or sunset (%s) NULL for %s".formatted(sunrise, sunset, now));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new AstroDto(astro, sunrise, sunset);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -48,6 +48,7 @@ public class ScheduleEntry {
|
|||||||
private boolean sunday = true;
|
private boolean sunday = true;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
private ScheduleEntryType type = ScheduleEntryType.TIME;
|
private ScheduleEntryType type = ScheduleEntryType.TIME;
|
||||||
|
|
||||||
private double zenith = Zenith.CIVIL.degrees().doubleValue();
|
private double zenith = Zenith.CIVIL.degrees().doubleValue();
|
||||||
@ -91,12 +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 int fuzzySeconds = rangeFull > 0 ? RANDOM.nextInt(rangeFull) - this.fuzzySeconds : 0;
|
final ZonedDateTime min = ZonedDateTime.now().plusMinutes(1);
|
||||||
nextFuzzyTimestamp = nextClearTimestamp.plusSeconds(fuzzySeconds);
|
while (nextFuzzyTimestamp == null || nextFuzzyTimestamp.isBefore(min)) {
|
||||||
|
final int fuzzySeconds = rangeFull > 0 ? RANDOM.nextInt(rangeFull) - this.fuzzySeconds : 0;
|
||||||
|
nextFuzzyTimestamp = nextClearTimestamp.plusSeconds(fuzzySeconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.ph87.homeautomation.web;
|
package de.ph87.homeautomation.web;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
@ -27,7 +28,7 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
.addResourceLocations("classpath:/resources/")
|
.addResourceLocations("classpath:/resources/")
|
||||||
.resourceChain(true)
|
.resourceChain(true)
|
||||||
.addResolver(new PathResourceResolver() {
|
.addResolver(new PathResourceResolver() {
|
||||||
protected Resource getResource(String resourcePath, Resource roomLocation) throws IOException {
|
protected Resource getResource(@NonNull String resourcePath, @NonNull Resource roomLocation) throws IOException {
|
||||||
Resource requestedResource = roomLocation.createRelative(resourcePath);
|
Resource requestedResource = roomLocation.createRelative(resourcePath);
|
||||||
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource : new ClassPathResource("/resources/index.html");
|
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource : new ClassPathResource("/resources/index.html");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,4 @@ spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.Im
|
|||||||
spring.jpa.hibernate.ddl-auto=update
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
spring.jpa.open-in-view=false
|
spring.jpa.open-in-view=false
|
||||||
#-
|
#-
|
||||||
spring.jackson.serialization.indent_output=true
|
|
||||||
#-
|
|
||||||
spring.main.banner-mode=off
|
spring.main.banner-mode=off
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user