Compare commits

..

No commits in common. "master" and "deploy---2023-07-05---23-31-50" have entirely different histories.

108 changed files with 1036 additions and 1684 deletions

View File

@ -4,8 +4,7 @@ spring.datasource.url=jdbc:h2:./Homeautomation;AUTO_SERVER=TRUE
spring.datasource.driverClassName=org.h2.Driver spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa spring.datasource.username=sa
spring.datasource.password=password spring.datasource.password=password
#- spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jackson.serialization.indent_output=true
#- #-
spring.jpa.hibernate.ddl-auto=create spring.jpa.hibernate.ddl-auto=create
#- #-

18
pom.xml
View File

@ -9,14 +9,14 @@
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
</properties> </properties>
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version> <version>2.7.5</version>
</parent> </parent>
<dependencies> <dependencies>
@ -41,10 +41,6 @@
<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>
@ -58,7 +54,7 @@
<dependency> <dependency>
<groupId>com.github.calimero</groupId> <groupId>com.github.calimero</groupId>
<artifactId>calimero-core</artifactId> <artifactId>calimero-core</artifactId>
<version>2.5.1</version> <version>2.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.luckycatlabs</groupId> <groupId>com.luckycatlabs</groupId>
@ -66,12 +62,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

@ -125,8 +125,5 @@
} }
} }
} }
},
"cli": {
"analytics": false
} }
} }

View File

@ -1,82 +0,0 @@
export class Duration {
static readonly ZERO: Duration = new Duration('0');
readonly totalSeconds: number;
readonly days: number;
readonly hours: number;
readonly minutes: number;
readonly seconds: number;
readonly code: string;
readonly zero: boolean;
private constructor(input: string) {
this.totalSeconds = Duration.parse(input);
const sign = Math.sign(this.totalSeconds);
const abs = Math.abs(this.totalSeconds);
this.days = sign * Math.floor(abs / (24 * 60 * 60));
this.hours = sign * Math.floor(abs / (60 * 60)) % 24;
this.minutes = sign * Math.floor(abs / 60) % 60;
this.seconds = sign * Math.floor(abs) % 60;
this.zero = this.totalSeconds === 0;
this.code = this.buildCode();
}
static ofCode(code: string) {
return new Duration(code);
}
private static parse(input: string) {
const regex = /(?<signs>[+-]*)(?<value>\d+(?:[.,]\d+)?)(?<unit>d|h|m|s|ms)/g;
let totalSeconds = 0;
for (const match of input.matchAll(regex)) {
if (!match?.groups) {
continue;
}
const sign = ((match.groups["signs"].replace(/\++/g, '').length % 2) === 1) ? -1 : 1;
const value = sign * parseInt(match.groups["value"]);
switch (match.groups["unit"]) {
case 'd':
totalSeconds += value * 24 * 60 * 60;
break;
case 'h':
totalSeconds += value * 60 * 60;
break;
case 'm':
totalSeconds += value * 60;
break;
case 's':
totalSeconds += value;
break;
}
}
return totalSeconds;
}
private buildCode() {
if (this.zero) {
return '0';
}
let code = "";
if (this.days != 0) {
code += this.days + "d";
}
if (this.hours != 0) {
code += this.hours + "h";
}
if (this.minutes != 0) {
code += this.minutes + "m";
}
if (this.seconds != 0) {
code += this.seconds + "s";
}
return code;
}
}

View File

@ -1,9 +1,28 @@
import {prefix} from "../helpers";
export class Timestamp { export class Timestamp {
public readonly WEEKDAY: string[] = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
public readonly dayName;
public readonly timeString;
public constructor( public constructor(
readonly date: Date, readonly date: Date,
) { ) {
// - const now = new Date();
const minutes: string = prefix(this.date.getMinutes(), '0', 2);
if (date.getDate() === now.getDate()) {
this.dayName = "Heute";
this.timeString = date.getHours() + ":" + minutes;
} else if (date.getDate() === now.getDate() + 1) {
this.dayName = "Morgen";
this.timeString = date.getHours() + ":" + minutes;
} else {
this.dayName = this.WEEKDAY[date.getDay()];
this.timeString = date.getHours() + ":" + minutes;
}
} }
public static fromDateOrNull(date: Date | null): Timestamp | null { public static fromDateOrNull(date: Date | null): Timestamp | null {

View File

@ -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 {
@ -66,10 +66,6 @@ export class BulkService implements ISearchService {
this.api.getReturnItem("bulk/run/" + bulk.id, _ => _, next); this.api.getReturnItem("bulk/run/" + bulk.id, _ => _, next);
} }
duplicate(bulk: Bulk, next: (item: Bulk) => void): void {
this.api.getReturnItem("bulk/duplicate/" + bulk.id, _ => _, next);
}
setEntry(entry: BulkEntry, key: string, value: any, next: (result: BulkEntry) => void = NO_OP): void { setEntry(entry: BulkEntry, key: string, value: any, next: (result: BulkEntry) => void = NO_OP): void {
this.api.putReturnItem("BulkEntry/" + entry.id + "/set/" + key, value, next); this.api.putReturnItem("BulkEntry/" + entry.id + "/set/" + key, value, next);
} }

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

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

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

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

View File

@ -1,5 +1,6 @@
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 {
@ -12,6 +13,7 @@ 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];
@ -23,6 +25,7 @@ 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),
); );
} }

View File

@ -1,19 +1,14 @@
import {validateBooleanNotNull, validateDateAllowNull, validateNumberAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull} from "../../validators"; import {validateBooleanNotNull, validateDateAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull} from "../../validators";
import {Timestamp} from "../../Timestamp"; import {Timestamp} from "../../Timestamp";
import {Property} from "../../property/Property"; import {Property} from "../../property/Property";
import {Bulk} from "../../bulk/Bulk"; import {Bulk} from "../../bulk/Bulk";
import {formatNumber} from "@angular/common";
function getDaySeconds(date: Date): number {
return date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
}
export class ScheduleEntry { export class ScheduleEntry {
readonly todo: string;
readonly dayMinute: number;
readonly anyWeekday: boolean;
readonly executable: boolean;
private constructor( private constructor(
readonly id: number, readonly id: number,
readonly position: number, readonly position: number,
@ -31,7 +26,6 @@ export class ScheduleEntry {
readonly minute: number, readonly minute: number,
readonly second: number, readonly second: number,
readonly fuzzySeconds: number, readonly fuzzySeconds: number,
readonly skip: number,
readonly lastClearTimestamp: Timestamp | null, readonly lastClearTimestamp: Timestamp | null,
readonly nextClearTimestamp: Timestamp | null, readonly nextClearTimestamp: Timestamp | null,
readonly nextFuzzyTimestamp: Timestamp | null, readonly nextFuzzyTimestamp: Timestamp | null,
@ -40,24 +34,7 @@ export class ScheduleEntry {
readonly value: number, readonly value: number,
readonly bulk: Bulk | null, readonly bulk: Bulk | null,
) { ) {
this.dayMinute = hour * 60 + minute; // nothing
this.anyWeekday = this.monday || this.tuesday || this.wednesday || this.thursday || this.friday || this.saturday || this.sunday;
this.executable = this.enabled && this.anyWeekday && !!this.bulk;
this.todo = this.getToDo();
}
getToDo(): string {
let result: string[] = [];
if (!this.enabled) {
result.push("inaktiv");
}
if (!this.bulk) {
result.push("keine Aktion");
}
if (!this.anyWeekday) {
result.push("kein Wochentag");
}
return result.join(", ");
} }
static fromJson(json: any): ScheduleEntry { static fromJson(json: any): ScheduleEntry {
@ -78,7 +55,6 @@ export class ScheduleEntry {
validateNumberNotNull(json['minute']), validateNumberNotNull(json['minute']),
validateNumberNotNull(json['second']), validateNumberNotNull(json['second']),
validateNumberNotNull(json['fuzzySeconds']), validateNumberNotNull(json['fuzzySeconds']),
validateNumberAllowNull(json['skip']) || 0,
Timestamp.fromDateOrNull(validateDateAllowNull(json['lastClearTimestamp'])), Timestamp.fromDateOrNull(validateDateAllowNull(json['lastClearTimestamp'])),
Timestamp.fromDateOrNull(validateDateAllowNull(json['nextClearTimestamp'])), Timestamp.fromDateOrNull(validateDateAllowNull(json['nextClearTimestamp'])),
Timestamp.fromDateOrNull(validateDateAllowNull(json['nextFuzzyTimestamp'])), Timestamp.fromDateOrNull(validateDateAllowNull(json['nextFuzzyTimestamp'])),
@ -97,8 +73,4 @@ export class ScheduleEntry {
return a.position - b.position; return a.position - b.position;
} }
get time(): string {
return formatNumber(this.hour, 'de-DE', '2.0-0') + ':' + formatNumber(this.minute, 'de-DE', '2.0-0');
}
} }

View File

@ -1,12 +0,0 @@
export class Zenith {
constructor(
readonly title: string,
readonly value: number,
readonly sunrise: boolean,
readonly sunset: boolean,
) {
// -
}
}

View File

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

View File

@ -1,65 +0,0 @@
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
import {DatePipe} from "@angular/common";
import {Timestamp} from "./Timestamp";
@Injectable({
providedIn: 'root'
})
export class TimeService {
readonly datePipe: DatePipe = new DatePipe(this.locale);
private _now: Date = new Date();
constructor(
@Inject(LOCALE_ID) readonly locale: string,
) {
// -
}
get now(): Date {
return this._now;
}
relativeDate(timestamp: Timestamp | undefined) {
const date: Date = timestamp?.date;
if (date === undefined || date === null) {
return "";
}
const relativeName = this.relativeCalendarDaysName(date);
return relativeName + " " + this.datePipe.transform(date, 'HH:mm');
}
relativeCalendarDaysName(date: Date): string {
const prefix = this.relativeCalendarDaysPrefix(date);
const weekday = date.toLocaleDateString(this.locale, {weekday: 'long'});
return prefix + ", " + weekday;
}
private relativeCalendarDaysPrefix(date: Date): string {
const days = this.calendarDays(date);
if (days < -2) {
return "Vor " + -days + " Tagen";
} else if (days === -2) {
return "Vorgestern";
} else if (days === -1) {
return "Gestern";
} else if (days === 1) {
return "Morgen";
} else if (days === 2) {
return "Übermorgen";
} else if (days > 2) {
return "In " + days + " Tagen";
}
return "Heute";
}
private calendarDays(date: Date) {
const DAY_MS = 1000 * 60 * 60 * 24;
const aMidnight = new Date(this._now.getFullYear(), this._now.getMonth(), this._now.getDate());
const bMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const milliseconds = bMidnight.getTime() - aMidnight.getTime();
return Math.floor(milliseconds / DAY_MS);
}
}

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

@ -8,6 +8,10 @@
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>
@ -17,11 +21,11 @@
</div> </div>
<div class="item itemSecondary" routerLink="/PropertyList" routerLinkActive="itemActive"> <div class="item itemSecondary" routerLink="/PropertyList" routerLinkActive="itemActive">
P Eigenschaften
</div> </div>
<div class="item itemSecondary" routerLink="/ChannelList" routerLinkActive="itemActive"> <div class="item itemSecondary" routerLink="/ChannelList" routerLinkActive="itemActive">
C Kanäle
</div> </div>
</div> </div>

View File

@ -1,17 +1,15 @@
@import "../config";
.menubar { .menubar {
border-bottom: @border solid black; border-bottom: 1px solid black;
.item { .item {
float: left; float: left;
padding: @padding; padding: 10px;
border-right: @border solid black; border-right: 1px solid black;
} }
.itemSecondary { .itemSecondary {
float: right; float: right;
border-left: @border solid black; border-left: 1px solid black;
border-right: none; border-right: none;
} }

View File

@ -1,4 +1,4 @@
import {LOCALE_ID, NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {AppRoutingModule} from './app-routing.module'; import {AppRoutingModule} from './app-routing.module';
@ -20,14 +20,6 @@ import {BulkEditorComponent} from './pages/bulk/editor/bulk-editor.component';
import {LeftPadDirective} from './pipes/left-pad.directive'; import {LeftPadDirective} from './pipes/left-pad.directive';
import {EntryValueComponent} from './shared/entry-value/entry-value.component'; import {EntryValueComponent} from './shared/entry-value/entry-value.component';
import {registerLocaleData} from '@angular/common';
import localeDe from '@angular/common/locales/de';
import localeDeExtra from '@angular/common/locales/extra/de';
import {BoolComponent} from './shared/bool/bool.component';
import {DurationComponent} from './shared/duration/duration.component';
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
@ -45,8 +37,6 @@ registerLocaleData(localeDe, 'de-DE', localeDeExtra);
BulkEditorComponent, BulkEditorComponent,
LeftPadDirective, LeftPadDirective,
EntryValueComponent, EntryValueComponent,
BoolComponent,
DurationComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -55,9 +45,7 @@ registerLocaleData(localeDe, 'de-DE', localeDeExtra);
FormsModule, FormsModule,
FontAwesomeModule, FontAwesomeModule,
], ],
providers: [ providers: [],
{provide: LOCALE_ID, useValue: 'de-DE'}
],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { export class AppModule {

View File

@ -1,9 +1,12 @@
<ng-container *ngIf="bulk"> <ng-container *ngIf="bulk">
<h1> <h1>
<app-text [initial]="bulk.name" (valueChange)="set(bulk, 'name', $event)"></app-text> <app-text [initial]="bulk.name" (valueChange)="set(bulk, 'name', $event)"></app-text>
</h1> </h1>
<p>
<button (click)="create()">+ Hinzufügen</button>
</p>
<table> <table>
<tr> <tr>
<th>Eigenschaft</th> <th>Eigenschaft</th>
@ -53,6 +56,4 @@
</ng-container> </ng-container>
</table> </table>
<button (click)="create()">+ Hinzufügen</button>
</ng-container> </ng-container>

View File

@ -1,3 +0,0 @@
table {
width: 100%;
}

View File

@ -1,32 +1,32 @@
<div class="tiles"> <p>
<div class="tile" *ngFor="let bulk of bulks.sort(Bulk.compareName); trackBy: Bulk.trackBy"> <button (click)="create()">+ Hinzufügen</button>
<div class="tileHead disabledBack" [class.enabledBack]="bulk.enabled"> </p>
<div (click)="set(bulk, 'enabled', !bulk.enabled)"> <table>
<tr>
<th>&nbsp;</th>
<th>Bezeichnung</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
<tr *ngFor="let bulk of bulks.sort(Bulk.compareName); trackBy: Bulk.trackBy">
<td class="boolean" (click)="set(bulk, 'enabled', !bulk.enabled)" [class.true]="bulk.enabled" [class.false]="!bulk.enabled">
<fa-icon *ngIf="bulk.enabled" [icon]="faCheckCircle"></fa-icon> <fa-icon *ngIf="bulk.enabled" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!bulk.enabled" [icon]="faCircle"></fa-icon> <fa-icon *ngIf="!bulk.enabled" [icon]="faCircle"></fa-icon>
</div> </td>
<div class="flexGrow" [routerLink]="['/Bulk', {id: bulk.id}]"> <td [routerLink]="['/Bulk', {id: bulk.id}]">
{{bulk.name}} {{bulk.name}}
</div> </td>
<div class="tileHeadRight" (click)="run(bulk)"> <td class="run" (click)="run(bulk)">
<fa-icon title="Ausführen" [icon]="faPlay"></fa-icon> <fa-icon title="Ausführen" [icon]="faPlay"></fa-icon>
</div> </td>
<div class="tileHeadRight" (click)="duplicate(bulk)"> <td class="delete" (click)="delete(bulk)">
<fa-icon title="Duplizieren" [icon]="faCopy"></fa-icon>
</div>
<div class="tileHeadDelete" (click)="delete(bulk)">
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon> <fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</div> </td>
</div> </tr>
</div> </table>
</div>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>

View File

@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {BulkService} from "../../../api/bulk/BulkService"; import {BulkService} from "../../../api/bulk/BulkService";
import {Bulk} from "../../../api/bulk/Bulk"; import {Bulk} from "../../../api/bulk/Bulk";
import {faCheckCircle, faCircle, faCopy, faPlayCircle, faTimesCircle} from "@fortawesome/free-regular-svg-icons"; import {faCheckCircle, faCircle, faPlayCircle, faTimesCircle} from "@fortawesome/free-regular-svg-icons";
@Component({ @Component({
selector: 'app-bulk-list', selector: 'app-bulk-list',
@ -60,15 +60,10 @@ export class BulkListComponent implements OnInit {
} }
} }
duplicate(bulk: Bulk): void {
this.bulkService.duplicate(bulk, bulk => this.addOrReplace(bulk));
}
delete(bulk: Bulk): void { delete(bulk: Bulk): void {
if (confirm("Zeitplan \"" + bulk.name + "\" wirklich löschen?")) { if (confirm("Zeitplan \"" + bulk.name + "\" wirklich löschen?")) {
this.bulkService.delete(bulk, () => this.remove(bulk)); this.bulkService.delete(bulk, () => this.remove(bulk));
} }
} }
protected readonly faCopy = faCopy;
} }

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

@ -1,3 +0,0 @@
table {
width: 100%;
}

View File

@ -1,85 +1,77 @@
<div class="tiles"> <div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>
<ng-container *ngFor="let device of devices; trackBy: Device.trackBy"> <ng-container *ngFor="let device of devices; trackBy: Device.trackBy">
<ng-container [ngSwitch]="device.type">
<div class="tile" *ngIf="device.type == 'DeviceSwitch'" [ngClass]="device.getSwitchClassList()"> <div class="device" *ngSwitchCase="'DeviceSwitch'" [ngClass]="getSwitchClassList(device)">
<div class="tileHead"> <div class="title">
<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>
</div> </div>
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]"> <div class="edit" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon> <fa-icon [icon]="faEdit"></fa-icon>
</div> </div>
</div> <div class="controls">
<div class="tileBody"> <img alt="An" class="control" src="../../../../assets/switch-on.svg" (click)="deviceService.setSwitchState(device, true)"/>
<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)"/>
<img alt="Aus" class="control" src="assets/switch-off.svg" (click)="deviceService.setSwitchState(device, false)"/>
</div> </div>
</div> </div>
<div class="tile" *ngIf="device.type === 'DeviceStateScene'" [ngClass]="device.getStateSceneClassList()"> <div class="device" *ngSwitchCase="'DeviceStateScene'" [ngClass]="getStateSceneClassList(device)">
<div class="tileHead"> <div class="title">
<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>
</div> </div>
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]"> <div class="edit" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon> <fa-icon [icon]="faEdit"></fa-icon>
</div> </div>
</div> <div class="controls">
<div class="tileBody"> <div *ngFor="let scene of getStateScenes(device)" class="control button" (click)="deviceService.setStateScene(device, scene.number)">
<div class="control" *ngFor="let scene of getStateScenes(device)" (click)="deviceService.setStateScene(device, scene.number)"> <span class="center">{{scene.title}}</span>
{{ scene.title }}
</div> </div>
</div> </div>
</div> </div>
<div class="tile" *ngIf="device.type === 'DeviceShutter'" [ngClass]="device.getShutterClassList()"> <div class="device" *ngSwitchCase="'DeviceShutter'" [ngClass]="getShutterClassList(device)">
<div class="tileHead"> <div class="title">
<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>
</div> </div>
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]"> <div class="edit" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon> <fa-icon [icon]="faEdit"></fa-icon>
</div> </div>
<div class="controls">
<div class="control button" (click)="deviceService.setShutterPosition(device, 0)">
<span class="center">Auf</span>
</div> </div>
<div class="tileBody"> <div class="control button" (click)="deviceService.setShutterPosition(device, 20)">
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 0)"> <span class="center">20%</span>
Auf
</div> </div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 20)"> <div class="control button" (click)="deviceService.setShutterPosition(device, 30)">
20% <span class="center">30%</span>
</div> </div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 30)"> <div class="control button" (click)="deviceService.setShutterPosition(device, 40)">
30% <span class="center">40%</span>
</div> </div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 40)"> <div class="control button" (click)="deviceService.setShutterPosition(device, 50)">
40% <span class="center">50%</span>
</div> </div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 50)"> <div class="control button" (click)="deviceService.setShutterPosition(device, 60)">
50% <span class="center">60%</span>
</div> </div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 60)"> <div class="control button" (click)="deviceService.setShutterPosition(device, 70)">
60% <span class="center">70%</span>
</div> </div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 70)"> <div class="control button" (click)="deviceService.setShutterPosition(device, 80)">
70% <span class="center">80%</span>
</div> </div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 80)"> <div class="control button" (click)="deviceService.setShutterPosition(device, 90)">
80% <span class="center">90%</span>
</div> </div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 90)"> <div class="control button" (click)="deviceService.setShutterPosition(device, 100)">
90% <span class="center">Zu</span>
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 100)">
Zu
</div> </div>
</div> </div>
</div> </div>
</ng-container> </ng-container>
</ng-container>
</div>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>

View File

@ -1,17 +1,71 @@
@import "../../../../config"; .device {
padding: 5px;
margin-bottom: 5px;
border-radius: 10px;
@media (min-width: 1000px) {
float: left;
width: 400px;
margin-right: 5px;
}
.title {
float: left;
font-weight: bold;
}
.edit {
float: right;
}
.controls {
clear: both;
.control { .control {
position: relative;
float: left; float: left;
width: 4em; width: 60px;
aspect-ratio: 1; height: 60px;
margin: @margin; padding: 5px;
margin: 5px;
border-radius: 25%; border-radius: 25%;
} }
.controlShutter { .button {
width: 17.3%; background-color: lightblue;
padding-top: 1.1em; }
text-align: center;
margin: calc(@margin / 2); .control:hover {
background-color: lightsteelblue; background-color: lightskyblue;
}
}
}
.switchOn {
background-color: palegreen;
}
.switchOff {
background-color: indianred;
}
.switchUnknown {
background-color: gray;
}
.shutterOpen {
background-color: palegreen;
}
.shutterBetween {
background-color: yellow;
}
.shutterClosed {
background-color: indianred;
}
.shutterUnknown {
background-color: gray;
} }

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

View File

@ -2,17 +2,53 @@
<td class="empty">-</td> <td class="empty">-</td>
</ng-template> </ng-template>
<div class="tiles"> <div class="config">
<button (click)="create()">+ Hinzufügen</button>
<div *ngFor="let list of listLists()">
<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>
</div> </div>
<div class="tileBody"> <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>
<td>
<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 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>
<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,22 +57,44 @@
<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">
<button (click)="create()">+ Hinzufügen</button>
</div>

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

@ -1,126 +1,151 @@
<ng-container *ngIf="schedule"> <ng-container *ngIf="schedule">
<div id="title"> <ng-template #boolean let-entry="entry" let-value="value" let-key="key">
<app-text [initial]="schedule.title" (valueChange)="scheduleService.set(schedule, 'title', $event)"></app-text> <td class="boolean" (click)="set(entry, key, !value)" [class.true]="value" [class.false]="!value">
</div> <fa-icon *ngIf="value" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!value" [icon]="faCircle"></fa-icon>
</td>
</ng-template>
<div class="tiles"> <p>
<div class="tile" *ngFor="let entry of schedule.entries; trackBy: ScheduleEntry.trackBy">
<div class="tileHead disabledBack" [class.enabledBack]="entry.executable" [class.skipBack]="entry.skip">
<div class="enabled" (click)="set(entry, 'enabled', !entry.enabled)">
<fa-icon *ngIf="entry.enabled" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!entry.enabled" [icon]="faCircle"></fa-icon>
</div>
<div class="flexGrow">
<select [ngModel]="entry.bulk?.id" (ngModelChange)="entryService.set(entry, 'bulk', $event)">
<option [ngValue]="null">-</option>
<option [ngValue]="bulk.id" *ngFor="let bulk of bulks.sort(Bulk.compareName)">{{ bulk.name }}</option>
</select>
</div>
<div class="tileHeadDelete">
<fa-icon [icon]="faTimesCircle" (click)="delete(entry)"></fa-icon>
</div>
</div>
<div class="tileBodyFlex">
<div class="weekdays">
<div>
<app-bool label="Mo" [value]="entry.monday" (onChange)="entryService.set(entry, 'monday', $event)"></app-bool>
</div>
<div>
<app-bool label="Di" [value]="entry.tuesday" (onChange)="entryService.set(entry, 'tuesday', $event)"></app-bool>
</div>
<div>
<app-bool label="Mi" [value]="entry.wednesday" (onChange)="entryService.set(entry, 'wednesday', $event)"></app-bool>
</div>
<div>
<app-bool label="Do" [value]="entry.thursday" (onChange)="entryService.set(entry, 'thursday', $event)"></app-bool>
</div>
<div>
<app-bool label="Fr" [value]="entry.friday" (onChange)="entryService.set(entry, 'friday', $event)"></app-bool>
</div>
<div>
<app-bool label="Sa" [value]="entry.saturday" (onChange)="entryService.set(entry, 'saturday', $event)"></app-bool>
</div>
<div>
<app-bool label="So" [value]="entry.sunday" (onChange)="entryService.set(entry, 'sunday', $event)"></app-bool>
</div>
</div>
<div class="modes">
<div class="_inner_">
<div>
<app-bool icon="faClock" [value]="entry.type === 'TIME'" (onChange)="entryService.set(entry, 'type', 'TIME')"></app-bool>
</div>
<div>
<app-bool icon="faArrowAltCircleUp" [value]="entry.type === 'SUNRISE'" (onChange)="entryService.set(entry, 'type', 'SUNRISE')"></app-bool>
</div>
<div>
<app-bool icon="faArrowAltCircleDown" [value]="entry.type === 'SUNSET'" (onChange)="entryService.set(entry, 'type', 'SUNSET')"></app-bool>
</div>
</div>
</div>
</div>
<div class="tileBodyFlex" *ngIf="entry.type === 'TIME'">
<div class="flexGrow time">
<button class="buttonPlus" (click)="dayMinuteAdd(entry, +60)">+</button>
<button class="buttonMinus" (click)="dayMinuteAdd(entry, -60)">-</button>
<input type="time" [ngModel]="entry.time" (ngModelChange)="timeFromString(entry, $event)">
<button class="buttonPlus" (click)="dayMinuteAdd(entry, +1)">+</button>
<button class="buttonMinus" (click)="dayMinuteAdd(entry, -1)">-</button>
</div>
</div>
<div class="tileBodyFlex" *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
<div class="flexGrow sun">
<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>
</div>
<div>
<input type="number" min="45" max="120" [ngModel]="entry.zenith" (ngModelChange)="entryService.set(entry, 'zenith', $event || 0)">
</div>
</div>
</div>
<div class="tileBodyFlex">
<div class="flexHalf">
<div class="flexIcon">
<img class="icon" src="assets/dice.svg" alt="+/-" title="Zufallsabweichung +/-">
</div>
<div class="flexIconInput">
<app-duration [duration]="Duration.ofCode(entry.fuzzySeconds + 's')" [inputClass]="entry.fuzzySeconds ? 'fuzzyBack' : ''" [min]="Duration.ofCode('')" (onChange)="entryService.set(entry, 'fuzzySeconds', $event.totalSeconds)"></app-duration>
</div>
</div>
<div class="flexHalf">
<div class="flexIcon">
<img class="icon" src="assets/skip.svg" alt="Überspringen">
</div>
<div class="flexIconInput flexInputLast">
<input type="number" min="0" [class.skipBack]="entry.skip" [ngModel]="entry.skip" (ngModelChange)="entryService.set(entry, 'skip', $event || 0)">
</div>
</div>
</div>
<div class="tileBodyFlex">
<div class="flexGrow timestamp" [class.skipFont]="entry.skip" *ngIf="entry.executable">
{{ timeService.relativeDate(entry.nextFuzzyTimestamp) }}
<span [class.fuzzyFont]="entry.fuzzySeconds" *ngIf="entry.fuzzySeconds">
(eig: {{ entry.nextClearTimestamp.date | date:'HH:mm' }})
</span>
</div>
<div class="flexGrow inactive" *ngIf="entry.todo">
{{ entry.todo }}
</div>
</div>
</div>
</div>
<div class="config">
<button (click)="create()">+ Hinzufügen</button> <button (click)="create()">+ Hinzufügen</button>
</div> </p>
<table>
<tr class="header">
<ng-container *ngTemplateOutlet="boolean;context:{schedule: schedule, value: schedule.enabled, key:'enabled'}"></ng-container>
<td colspan="24">
<app-text [initial]="schedule.title" (valueChange)="set(null, 'title', $event)"></app-text>
</td>
</tr>
<tr [class.disabled]="!schedule.enabled">
<th>&nbsp;</th>
<th>Mo</th>
<th>Di</th>
<th>Mi</th>
<th>Do</th>
<th>Fr</th>
<th>Sa</th>
<th>So</th>
<th>Typ</th>
<th>Sonnenstand</th>
<th colspan="3">Uhrzeit</th>
<th>Unschärfe</th>
<th colspan="6">Nächste Ausführung</th>
<th colspan="2">Eingeschaft setzen</th>
<th>Massenausführung</th>
<th class="noBorderFirst">&nbsp;</th>
<th class="noBorderMiddle">&nbsp;</th>
<th class="noBorderLast">&nbsp;</th>
</tr>
<tr *ngFor="let entry of schedule.entries; trackBy: ScheduleEntry.trackBy" [class.disabled]="entry.nextClearTimestamp === null">
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.enabled, key:'enabled'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.monday, key:'monday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.tuesday, key:'tuesday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.wednesday, key:'wednesday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.thursday, key:'thursday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.friday, key:'friday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.saturday, key:'saturday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.sunday, key:'sunday'}"></ng-container>
<td>
<select [ngModel]="entry.type" (ngModelChange)="set(entry, 'type', $event)">
<option value="TIME">Uhrzeit</option>
<option value="SUNRISE">Sonnenaufgang</option>
<option value="SUNSET">Sonnenuntergang</option>
</select>
</td>
<ng-container *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
<td>
<select [ngModel]="entry.zenith" (ngModelChange)="set(entry, 'zenith', $event)">
<option *ngFor="let event of schedule.astros; let index = index" [value]="event.zenith">
[{{event.zenith | number:'0.1-1' | leftPad:5}}°, {{(entry.type === 'SUNRISE' ? event.sunrise : event.sunset) | date:'HH:mm'}}]&nbsp;{{entry.type === 'SUNRISE' ? event.sunriseName : event.sunsetName}}
</option>
</select>
</td>
</ng-container>
<td *ngIf="entry.type !== 'SUNRISE' && entry.type !== 'SUNSET'" class="empty"></td>
<ng-container *ngIf="entry.type === 'TIME'">
<td class="first">
<select [ngModel]="entry.hour" (ngModelChange)="set(entry, 'hour', $event)">
<option *ngFor="let _ of [].constructor(24); let value = index" [ngValue]="value">{{value}}</option>
</select>
</td>
<td class="middle">:</td>
<td class="last">
<select [ngModel]="entry.minute" (ngModelChange)="set(entry, 'minute', $event)">
<option *ngFor="let _ of [].constructor(12); let value = index" [ngValue]="value * 5">{{value * 5 | number:'2.0'}}</option>
</select>
</td>
</ng-container>
<td *ngIf="entry.type !== 'TIME'" colspan="3" class="empty"></td>
<td>
<select [ngModel]="entry.fuzzySeconds" (ngModelChange)="set(entry, 'fuzzySeconds', $event)">
<option [ngValue]="0">Keine</option>
<option [ngValue]="60">1 Minute</option>
<option [ngValue]="300">5 Minuten</option>
<option [ngValue]="600">10 Minuten</option>
<option [ngValue]="1800">30 Minuten</option>
<option [ngValue]="3600">1 Stunde</option>
<option [ngValue]="7200">2 Stunden</option>
<option [ngValue]="10800">3 Stunden</option>
<option [ngValue]="21600">6 Stunden</option>
<option [ngValue]="43200">12 Stunden</option>
<option [ngValue]="86400">1 Tag</option>
</select>
</td>
<ng-container *ngIf="entry.nextClearTimestamp">
<td class="number first" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.dayName}}</td>
<td class="number middle" [class.empty]="entry.fuzzySeconds > 0">:&nbsp;</td>
<td class="number last" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.timeString}}</td>
</ng-container>
<ng-container *ngIf="!entry.nextClearTimestamp">
<td class="empty first"></td>
<td class="empty middle"></td>
<td class="empty last"></td>
</ng-container>
<ng-container *ngIf="entry.nextFuzzyTimestamp && entry.fuzzySeconds > 0">
<td class="number first">{{entry.nextFuzzyTimestamp.dayName}}</td>
<td class="number middle">:&nbsp;</td>
<td class="number last">{{entry.nextFuzzyTimestamp.timeString}}</td>
</ng-container>
<ng-container *ngIf="!entry.nextFuzzyTimestamp || entry.fuzzySeconds <= 0">
<td class="empty first"></td>
<td class="empty middle"></td>
<td class="empty last"></td>
</ng-container>
<td>
<app-search [searchService]="propertyService" [initial]="entry.property?.id" (valueChange)="set(entry, 'property', $event)"></app-search>
</td>
<td>
<app-entry-value [entry]="entry" [allowChange]="true" (onSet)="set(entry, $event.key, $event.value)"></app-entry-value>
</td>
<td>
<app-search [searchService]="bulkService" [initial]="entry.bulk?.id" (valueChange)="set(entry, 'bulk', $event)"></app-search>
</td>
<td class="delete noBorderFirst" (click)="delete(entry)">
<fa-icon [icon]="faTimes"></fa-icon>
</td>
<td class="noBorderMiddle" (click)="set(entry, 'position', entry.position - 1)">
<fa-icon *ngIf="entry.position > 0" [icon]="faUp"></fa-icon>
</td>
<td class="noBorderLast" (click)="set(entry, 'position', entry.position + 1)">
<fa-icon *ngIf="entry.position < schedule.entries.length - 1" [icon]="faDown"></fa-icon>
</td>
</tr>
</table>
</ng-container> </ng-container>

View File

@ -1,73 +1,19 @@
@import "../../../../config"; select {
background-color: transparent;
@time_input_width: 35%; border-width: 0;
#title {
margin: @margin;
}
.weekdays {
float: left;
width: 75%;
border-radius: @border-radius;
div {
float: left;
width: 14.2857%;
}
}
.modes {
float: left;
width: 25%;
._inner_ {
margin-left: @margin;
border-radius: @border-radius;
div {
float: left;
width: 33.3333%;
}
}
}
.time {
width: 100%; width: 100%;
outline: none;
input { font-family: monospace;
width: @time_input_width;
text-align: center;
margin-right: @margin;
} }
button { th {
width: calc(((100% - @time_input_width) - 4 * @margin) / 4); background-color: lightblue;
margin-right: @margin;
} }
button:last-child { tr.header {
margin-right: 0;
th:not(:first-child), td:not(:first-child) {
border: none;
} }
} }
.sun {
div {
float: left;
width: 20%;
}
}
.timestamp {
text-align: center;
}
.inactive {
color: gray;
text-align: center;
}

View File

@ -3,31 +3,13 @@ import {ScheduleService} from "../../../api/schedule/schedule.service";
import {Schedule} from "../../../api/schedule/Schedule"; import {Schedule} from "../../../api/schedule/Schedule";
import {ScheduleEntry} from "../../../api/schedule/entry/ScheduleEntry"; import {ScheduleEntry} from "../../../api/schedule/entry/ScheduleEntry";
import {ScheduleEntryService} from "../../../api/schedule/entry/schedule-entry.service"; import {ScheduleEntryService} from "../../../api/schedule/entry/schedule-entry.service";
import {faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {PropertyService} from "../../../api/property/property.service";
import {BulkService} from "../../../api/bulk/BulkService";
import {Update} from "../../../api/Update"; import {Update} from "../../../api/Update";
import {NO_OP} from "../../../api/api.service"; import {NO_OP} from "../../../api/api.service";
import {Duration} from "../../../api/Duration";
import {Bulk} from "../../../api/bulk/Bulk";
import {BulkService} from "../../../api/bulk/BulkService";
import {Zenith} from "../../../api/schedule/entry/Zenith";
import {TimeService} from "../../../api/time.service";
import {faCheckCircle, faCircle, faTimesCircle} from "@fortawesome/free-regular-svg-icons";
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,43 +17,36 @@ const ZENITH_SUNSET = ZENITH_ENTRIES.filter(zenith => zenith.sunset).reverse();
}) })
export class ScheduleEditorComponent implements OnInit { export class ScheduleEditorComponent implements OnInit {
protected readonly faCheckCircle = faCheckCircle; readonly ScheduleEntry = ScheduleEntry;
protected readonly faTimesCircle = faTimesCircle; readonly faCheckCircle = faCheckCircle;
protected readonly faCircle = faCircle; readonly faCircle = faCircle;
protected readonly ScheduleEntry = ScheduleEntry; readonly faTimes = faTimesCircle;
protected readonly Schedule = Schedule; readonly faUp = faArrowAltCircleUp;
protected readonly Duration = Duration; readonly faDown = faArrowAltCircleDown;
protected readonly Bulk = Bulk; readonly Schedule = Schedule;
protected readonly expanded: number[] = []; schedule!: Schedule;
protected now: Date = new Date(Date.now());
protected schedule!: Schedule;
protected bulks: Bulk[] = [];
constructor( constructor(
readonly router: Router, readonly router: Router,
readonly activatedRoute: ActivatedRoute, readonly activatedRoute: ActivatedRoute,
readonly scheduleService: ScheduleService, readonly scheduleService: ScheduleService,
readonly entryService: ScheduleEntryService, readonly scheduleEntryService: ScheduleEntryService,
readonly propertyService: PropertyService,
readonly bulkService: BulkService, readonly bulkService: BulkService,
readonly timeService: TimeService,
) { ) {
// - // nothing
} }
ngOnInit(): void { ngOnInit(): void {
this.scheduleService.subscribe(update => this.update(update)); this.scheduleService.subscribe(update => this.update(update));
this.bulkService.findAll(list => this.bulks = list); this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params['id'], schedule => this.schedule = schedule));
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params['id'], schedule => this.setSchedule(schedule)));
} }
private update(update: Update<Schedule>): void { private update(update: Update<Schedule>): void {
@ -82,58 +57,25 @@ export class ScheduleEditorComponent implements OnInit {
this.router.navigate(['/ScheduleList']); this.router.navigate(['/ScheduleList']);
return; return;
} }
this.setSchedule(update.payload); this.schedule = update.payload;
}
private setSchedule(schedule: Schedule) {
this.schedule = schedule;
this.expanded.length = 0;
this.expanded.push(...schedule.entries.map(e => e.id));
} }
create(): void { create(): void {
this.entryService.create(this.schedule, NO_OP); this.scheduleEntryService.create(this.schedule, NO_OP);
} }
delete(entry: ScheduleEntry): void { delete(entry: ScheduleEntry): void {
if (confirm("Eintrag \"" + this.timeService.relativeDate(entry.nextClearTimestamp) + " +/-" + entry.fuzzySeconds + "\" wirklich löschen?")) { if (confirm("Eintrag \"" + entry.nextClearTimestamp?.timeString + " +/-" + entry.fuzzySeconds + "\" wirklich löschen?")) {
this.entryService.delete(entry, NO_OP); this.scheduleEntryService.delete(entry, NO_OP);
} }
} }
set(entry: ScheduleEntry | null, key: string, value: any): void { set(entry: ScheduleEntry | null, key: string, value: any): void {
if (entry) { if (entry) {
this.entryService.set(entry, key, value, NO_OP); this.scheduleEntryService.set(entry, key, value, NO_OP);
} else { } else {
this.scheduleService.set(this.schedule, key, value, NO_OP); this.scheduleService.set(this.schedule, key, value, NO_OP);
} }
} }
timeFromString(entry: ScheduleEntry, time: string) {
const parts = time.split(':');
const hour = parseInt(parts[0]);
const minute = parseInt(parts[1]);
let second = 0;
if (parts.length === 3) {
second = parseInt(parts[2]);
}
const daySecond = (hour * 24 + minute) * 60 + second;
this.entryService.set(entry, 'daySecond', daySecond);
}
dayMinuteAdd(entry: ScheduleEntry, minutes: number) {
let newMinutes = entry.dayMinute + minutes;
while (newMinutes < 0 || newMinutes >= DAY_MINUTES) {
newMinutes = (newMinutes + DAY_MINUTES) % DAY_MINUTES;
}
this.entryService.set(entry, 'daySecond', newMinutes * 60);
}
getZenithEntries(type: string) {
if (type === 'SUNRISE') {
return ZENITH_SUNRISE;
}
return ZENITH_SUNSET;
}
} }

View File

@ -1,53 +1,49 @@
<div class="tiles"> <p>
<button (click)="create()">+ Hinzufügen</button>
</p>
<div class="tile" *ngFor="let schedule of schedules; trackBy: Schedule.trackBy"> <table>
<tr>
<th>&nbsp;</th>
<th>Bezeichnung</th>
<th colspan="3">Nächste</th>
<th colspan="3">Eigenschaft</th>
<th>Massenausführung</th>
<th colspan="3">Letzte</th>
<th>&nbsp;</th>
</tr>
<tr *ngFor="let schedule of schedules; trackBy: Schedule.trackBy">
<div class="tileHead disabledBack" [class.enabledBack]="schedule.next" [class.skipBack]="schedule.next?.skip"> <td class="boolean" (click)="set(schedule, 'enabled', !schedule.enabled)" [class.true]="schedule.enabled" [class.false]="!schedule.enabled">
<div class="enabled" (click)="set(schedule, 'enabled', !schedule.enabled)">
<fa-icon *ngIf="schedule.enabled" [icon]="faCheckCircle"></fa-icon> <fa-icon *ngIf="schedule.enabled" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!schedule.enabled" [icon]="faCircle"></fa-icon> <fa-icon *ngIf="!schedule.enabled" [icon]="faCircle"></fa-icon>
</div> </td>
<div class="flexGrow" [routerLink]="['/Schedule', {id: schedule.id}]"> <td [routerLink]="['/Schedule', {id: schedule.id}]">
{{schedule.title}} {{schedule.title}}
</div> </td>
<div class="tileHeadRight" (click)="skip(schedule.next)" *ngIf="schedule.next"> <td class="number first" [class.empty]="!schedule.next?.nextFuzzyTimestamp">{{schedule.next?.nextFuzzyTimestamp.dayName}}</td>
<img class="icon" src="assets/skip.svg" [alt]="'Über.'">{{ schedule.next?.skip }} <td class="number middle" [class.empty]="!schedule.next?.nextFuzzyTimestamp">:&nbsp;</td>
</div> <td class="number last" [class.empty]="!schedule.next?.nextFuzzyTimestamp">{{schedule.next?.nextFuzzyTimestamp.timeString}}</td>
<div class="tileHeadRight" (click)="delete(schedule)"> <td class="number first" [class.empty]="!schedule.next?.property">{{schedule.next?.property?.title}}</td>
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon> <td class="number middle" [class.empty]="!schedule.next?.property">&nbsp;=&nbsp;</td>
</div> <td class="number last" [class.empty]="!schedule.next?.property">
<app-entry-value *ngIf="schedule.next" [entry]="schedule.next" [allowChange]="false"></app-entry-value>
</td>
</div> <td [class.empty]="!schedule.next?.bulk">
<div class="tileBody next">
<div class="timestamp">
<ng-container *ngIf="schedule.next">{{ timeService.relativeDate(schedule.next?.nextFuzzyTimestamp) }}</ng-container>
</div>
<div class="bulk" [class.timestampBulkEmpty]="!schedule.next?.bulk" *ngIf="schedule.next?.bulk">
{{schedule.next?.bulk?.name}} {{schedule.next?.bulk?.name}}
<fa-icon [icon]="faPlayCircle" (click)="execute(schedule.next?.bulk)"></fa-icon> </td>
</div>
</div>
<div class="tileBody last"> <td class="number first" [class.empty]="!schedule.last?.lastFuzzyTimestamp">{{schedule.last?.lastFuzzyTimestamp.dayName}}</td>
<div class="timestamp"> <td class="number middle" [class.empty]="!schedule.last?.lastFuzzyTimestamp">:&nbsp;</td>
<ng-container *ngIf="schedule.last">{{ timeService.relativeDate(schedule.last?.lastFuzzyTimestamp) }}</ng-container> <td class="number last" [class.empty]="!schedule.last?.lastFuzzyTimestamp">{{schedule.last?.lastFuzzyTimestamp.timeString}}</td>
<ng-container *ngIf="!schedule.last">- - -</ng-container>
</div>
<div class="bulk" [class.timestampBulkEmpty]="!schedule.last?.bulk" *ngIf="schedule.last?.bulk">
{{ schedule.last?.bulk?.name }}
<fa-icon [icon]="faPlayCircle" (click)="execute(schedule.last?.bulk)"></fa-icon>
</div>
</div>
</div> <td class="delete" (click)="delete(schedule)">
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</td>
</div> </tr>
</table>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>

View File

@ -1,15 +1,11 @@
@import "../../../../config"; select {
background-color: transparent;
.last { border-width: 0;
color: gray; width: 100%;
font-size: 60%; outline: none;
border-top: @border solid gray; font-family: monospace;
} }
.timestamp { th {
float: left; background-color: lightblue;
}
.bulk {
float: right;
} }

View File

@ -1,14 +1,9 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ScheduleService} from "../../../api/schedule/schedule.service"; import {ScheduleService} from "../../../api/schedule/schedule.service";
import {Schedule} from "../../../api/schedule/Schedule"; import {Schedule} from "../../../api/schedule/Schedule";
import {faCheckCircle, faCircle, faPlayCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons'; import {faCheckCircle, faCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
import {NO_OP} from "../../../api/api.service"; import {NO_OP} from "../../../api/api.service";
import {Update} from "../../../api/Update"; import {Update} from "../../../api/Update";
import {ScheduleEntryService} from "../../../api/schedule/entry/schedule-entry.service";
import {ScheduleEntry} from "../../../api/schedule/entry/ScheduleEntry";
import {Bulk} from "../../../api/bulk/Bulk";
import {BulkService} from "../../../api/bulk/BulkService";
import {TimeService} from "../../../api/time.service";
@Component({ @Component({
selector: 'app-schedule-list', selector: 'app-schedule-list',
@ -29,9 +24,6 @@ export class ScheduleListComponent implements OnInit {
constructor( constructor(
readonly scheduleService: ScheduleService, readonly scheduleService: ScheduleService,
readonly entryService: ScheduleEntryService,
readonly bulkService: BulkService,
readonly timeService: TimeService,
) { ) {
// nothing // nothing
} }
@ -71,19 +63,4 @@ export class ScheduleListComponent implements OnInit {
} }
} }
skip(entry: ScheduleEntry) {
let skip = entry.skip + 1;
if (skip > 7) {
skip = 0;
}
this.entryService.set(entry, 'skip', skip);
}
execute(bulk: Bulk | undefined) {
if (bulk && confirm("Stapel Ausführen?\n\n" + bulk.name)) {
this.bulkService.run(bulk);
}
}
protected readonly faPlayCircle = faPlayCircle;
} }

View File

@ -1,18 +0,0 @@
<div type="checkbox" [style.background-color]="color()" (click)="onChange.emit(!value)">
<ng-container *ngIf="label">
{{ label }}
</ng-container>
<ng-container *ngIf="!label">
<ng-container *ngIf="icon">
<fa-icon *ngIf="icon === 'faCircle'" [icon]="faCircle"></fa-icon>
<fa-icon *ngIf="icon === 'faCheckCircle'" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="icon === 'faClock'" [icon]="faClock"></fa-icon>
<fa-icon *ngIf="icon === 'faArrowAltCircleUp'" [icon]="faArrowAltCircleUp"></fa-icon>
<fa-icon *ngIf="icon === 'faArrowAltCircleDown'" [icon]="faArrowAltCircleDown"></fa-icon>
</ng-container>
<ng-container *ngIf="!icon">
<fa-icon *ngIf="value" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!value" [icon]="faCircle"></fa-icon>
</ng-container>
</ng-container>
</div>

View File

@ -1,8 +0,0 @@
div {
width: 100%;
height: 100%;
padding-top: 0.1em;
padding-bottom: 0.1em;
text-align: center;
background-color: gray;
}

View File

@ -1,58 +0,0 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCircle, faClock} from "@fortawesome/free-regular-svg-icons";
@Component({
selector: 'app-bool',
templateUrl: './bool.component.html',
styleUrls: ['./bool.component.less']
})
export class BoolComponent implements OnInit {
protected readonly faCheckCircle = faCheckCircle;
protected readonly faCircle = faCircle;
protected readonly faClock = faClock;
protected readonly faArrowAltCircleUp = faArrowAltCircleUp;
protected readonly faArrowAltCircleDown = faArrowAltCircleDown;
@Input()
icon: string | undefined = undefined;
@Input()
colorActive: string = '#8FBC8FFF';
@Input()
colorInactive: string = '#8c8c8c';
@Input()
label: string | undefined = undefined;
@Input()
value: boolean | undefined = undefined;
@Output()
onChange: EventEmitter<boolean> = new EventEmitter();
constructor() {
}
ngOnInit(): void {
}
toggle() {
}
color(): string {
if (this.value === true) {
return this.colorActive;
} else if (this.value === false) {
return this.colorInactive;
}
return "";
}
}

View File

@ -1 +0,0 @@
<input type="text" [class]="inputClass" [(ngModel)]="code" (focus)="focus = true" (blur)="apply()" (keydown.enter)="apply()" (keydown.escape)="cancel()">

View File

@ -1,3 +0,0 @@
input {
width: 100%;
}

View File

@ -1,63 +0,0 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {Duration} from "../../api/Duration";
@Component({
selector: 'app-duration',
templateUrl: './duration.component.html',
styleUrls: ['./duration.component.less']
})
export class DurationComponent implements OnInit {
private _duration: Duration = Duration.ZERO;
@Input()
set duration(duration: Duration) {
this._duration = duration;
if (!this.focus) {
this.code = this.duration.code;
}
}
get duration(): Duration {
return this._duration;
}
@Input()
inputClass: string = '';
@Input()
min: Duration | undefined = undefined;
@Input()
max: Duration | undefined = undefined;
@Output()
readonly onChange: EventEmitter<Duration> = new EventEmitter();
protected code: string = '';
protected focus: boolean = false;
constructor() {
}
ngOnInit(): void {
}
apply() {
let duration = Duration.ofCode(this.code);
if (this.min !== undefined && duration.totalSeconds < this.min.totalSeconds) {
duration = this.min;
}
if (this.max !== undefined && duration.totalSeconds > this.max.totalSeconds) {
duration = this.max;
}
this.code = duration.code;
this.onChange.emit(duration);
}
cancel() {
this.code = this.duration.code;
}
}

View File

@ -1,9 +1,7 @@
@import "../../../config";
input { input {
outline: none; outline: none;
border: none; border: none;
padding: @padding; padding: 5px;
margin: 0; margin: 0;
width: 100%; width: 100%;
background-color: transparent; background-color: transparent;

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,30 +1,22 @@
@import "../../../config";
.all { .all {
.initial { .initial {
padding: @padding; padding: 5px;
height: 100%; height: 100%;
} }
.selected { .selected {
font-weight: bold; font-weight: bold;
border-bottom: @border solid black; border-bottom: 1px solid black;
} }
.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: 200px;
border: @border solid black; border: 1px solid black;
.result { .result {
padding: @padding; padding: 5px;
} }
.result:hover { .result:hover {

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

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M243.8 339.8C232.9 350.7 215.1 350.7 204.2 339.8L140.2 275.8C129.3 264.9 129.3 247.1 140.2 236.2C151.1 225.3 168.9 225.3 179.8 236.2L224 280.4L332.2 172.2C343.1 161.3 360.9 161.3 371.8 172.2C382.7 183.1 382.7 200.9 371.8 211.8L243.8 339.8zM512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 553 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M175 175C184.4 165.7 199.6 165.7 208.1 175L255.1 222.1L303 175C312.4 165.7 327.6 165.7 336.1 175C346.3 184.4 346.3 199.6 336.1 208.1L289.9 255.1L336.1 303C346.3 312.4 346.3 327.6 336.1 336.1C327.6 346.3 312.4 346.3 303 336.1L255.1 289.9L208.1 336.1C199.6 346.3 184.4 346.3 175 336.1C165.7 327.6 165.7 312.4 175 303L222.1 255.1L175 208.1C165.7 199.6 165.7 184.4 175 175V175zM512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 687 B

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="800px" height="800px" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-220.000000, -8079.000000)">
<g transform="translate(56.000000, 160.000000)">
<path
d="M174,7927.1047 C172.896,7927.1047 172,7927.9997 172,7929.1047 C172,7930.2097 172.896,7931.1047 174,7931.1047 C175.104,7931.1047 176,7930.2097 176,7929.1047 C176,7927.9997 175.104,7927.1047 174,7927.1047 L174,7927.1047 Z M182,7921.9997 C182,7921.4477 181.552,7920.9997 181,7920.9997 L167,7920.9997 C166.448,7920.9997 166,7921.4477 166,7921.9997 L166,7935.9997 C166,7936.5527 166.448,7936.9997 167,7936.9997 L181,7936.9997 C181.552,7936.9997 182,7936.5527 182,7935.9997 L182,7921.9997 Z M184,7920.9997 L184,7936.9997 C184,7938.1047 183.105,7938.9997 182,7938.9997 L166,7938.9997 C164.896,7938.9997 164,7938.1047 164,7936.9997 L164,7920.9997 C164,7919.8957 164.896,7918.9997 166,7918.9997 L182,7918.9997 C183.105,7918.9997 184,7919.8957 184,7920.9997 L184,7920.9997 Z M170,7927.1047 C171.104,7927.1047 172,7926.2097 172,7925.1047 C172,7923.9997 171.104,7923.1047 170,7923.1047 C168.896,7923.1047 168,7923.9997 168,7925.1047 C168,7926.2097 168.896,7927.1047 170,7927.1047 L170,7927.1047 Z M170,7931.1047 C168.896,7931.1047 168,7931.9997 168,7933.1047 C168,7934.2097 168.896,7935.1047 170,7935.1047 C171.104,7935.1047 172,7934.2097 172,7933.1047 C172,7931.9997 171.104,7931.1047 170,7931.1047 L170,7931.1047 Z M178,7923.1047 C176.896,7923.1047 176,7923.9997 176,7925.1047 C176,7926.2097 176.896,7927.1047 178,7927.1047 C179.104,7927.1047 180,7926.2097 180,7925.1047 C180,7923.9997 179.104,7923.1047 178,7923.1047 L178,7923.1047 Z M180,7933.1047 C180,7934.2097 179.104,7935.1047 178,7935.1047 C176.896,7935.1047 176,7934.2097 176,7933.1047 C176,7931.9997 176.896,7931.1047 178,7931.1047 C179.104,7931.1047 180,7931.9997 180,7933.1047 L180,7933.1047 Z"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 9L12 15L6 9" stroke="#33363F" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 292 B

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 6L15 12L9 18" stroke="#33363F" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 292 B

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="37" r="6"/>
<path d="M24,31a6,6,0,1,0,6,6A6,6,0,0,0,24,31Zm0,8a2,2,0,1,1,2-2A2,2,0,0,1,24,39Z"/>
<circle cx="40" cy="37" r="6"/>
<path d="M37.4,19.6a1.9,1.9,0,0,0-3,.2,2.1,2.1,0,0,0,.2,2.7l4,3.9a1.9,1.9,0,0,0,2.8,0l4-3.9a2.3,2.3,0,0,0,.3-2.7,2,2,0,0,0-3.1-.2l-.6.6A18,18,0,0,0,6,21v2a2,2,0,0,0,4,0V21a14,14,0,0,1,28-.9Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 486 B

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="5"/>
<path d="M12 2V4" stroke-linecap="round"/>
<path d="M12 20V22" stroke-linecap="round"/>
<path d="M4 12L2 12" stroke-linecap="round"/>
<path d="M22 12L20 12" stroke-linecap="round"/>
<path d="M19.7778 4.22266L17.5558 6.25424" stroke-linecap="round"/>
<path d="M4.22217 4.22266L6.44418 6.25424" stroke-linecap="round"/>
<path d="M6.44434 17.5557L4.22211 19.7779" stroke-linecap="round"/>
<path d="M19.7778 19.7773L17.5558 17.5551" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 684 B

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" xmlns="http://www.w3.org/2000/svg">
<path d="M12 7V12L10.5 14.5M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 349 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

View File

@ -1,53 +0,0 @@
@margin: 0.5em;
@padding: 0.2em;
@border: 0.05em;
@border-radius: 0.2em;
@COLOR_UNKNOWN: gray;
@COLOR_ACTIVE: #8fbc8f;
@COLOR_BETWEEN: #e4db9c;
@COLOR_INACTIVE: #bc8f8f;
.disabledBack {
background-color: @COLOR_UNKNOWN;
}
.enabledBack {
background-color: @COLOR_ACTIVE;
}
.skipBack {
background-color: #ffc059;
}
.skipFont {
color: #ff9a00;
}
.fuzzyFont {
color: #489dff;
}
.fuzzyBack {
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;
}

View File

@ -1,9 +1,22 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
import {getBaseUrl} from "./UrlHelper"; import {getBaseUrl} from "./UrlHelper";
const PROD: boolean = false; const PROD: boolean = true;
export const environment = { export const environment = {
production: false, production: false,
restBase: PROD ? 'http://10.0.0.50:8082' : getBaseUrl('http', 8080), restBase: PROD ? 'http://10.0.0.50:8082' : getBaseUrl('http', 8080),
websocketBase: PROD ? 'ws://10.0.0.50:8082' : getBaseUrl('ws', 8080), websocketBase: PROD ? 'ws://10.0.0.50:8082' : getBaseUrl('ws', 8080),
}; };
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

View File

@ -1,35 +1,6 @@
@import "config";
* {
box-sizing: border-box;
}
// font
body, input, select, button {
font-size: 5vw;
font-family: arial, sans-serif;
@media (min-width: 1001px) {
font-size: 18px;
}
}
// forms
input, select, button {
width: 100%;
margin: 0;
border: none;
outline: none;
padding: 0.1em;
border-radius: @border-radius;
}
select {
margin-top: -0.1em;
background-color: transparent;
}
body { body {
margin: 0; margin: 0;
font-family: arial, sans-serif;
width: 100%; width: 100%;
} }
@ -39,8 +10,6 @@ a {
div { div {
overflow: hidden; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
img { img {
@ -56,11 +25,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: @padding; padding: 5px;
border: @border solid black; border: 1px solid black;
img.fullCell { img.fullCell {
margin: calc(-@margin); margin: -5px;
} }
} }
@ -72,6 +41,14 @@ 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;
@ -145,19 +122,5 @@ table.vertical {
.config { .config {
clear: both; clear: both;
margin: 0 @margin @margin; margin-bottom: 5px;
@media (min-width: 1001px) {
width: 400px;
} }
}
.buttonPlus {
background-color: #8fbc8f;
}
.buttonMinus {
background-color: #ef8787;
}
@import "tile";

View File

@ -1,77 +0,0 @@
@import "config";
.tiles {
margin-top: @margin;
margin-left: @margin;
.tile {
border-radius: @border-radius;
margin-right: @margin;
margin-bottom: @margin;
background-color: #ececec;
@media (min-width: 1001px) {
width: 400px;
float: left;
}
.tileHead {
display: flex;
div {
float: left;
padding: @padding;
}
.tileHeadTitle {
flex-grow: 1;
}
.tileHeadRight {
float: right;
}
.tileHeadDelete {
float: right;
color: darkred;
}
}
.tileBody {
padding: @padding;
}
.tileBodyFlex {
display: flex;
padding: @padding;
}
.icon {
display: inline;
vertical-align: bottom;
height: 1.1em;
}
}
.flexGrow {
flex-grow: 1;
}
.flexHalf {
width: 50%;
}
.flexIcon {
float: left;
width: 1.5em;
text-align: center;
}
.flexIconInput {
float: left;
width: calc(100% - 1.5em);
}
}

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

@ -7,7 +7,6 @@ import de.ph87.homeautomation.bulk.BulkDto;
import de.ph87.homeautomation.bulk.entry.BulkEntryController; import de.ph87.homeautomation.bulk.entry.BulkEntryController;
import de.ph87.homeautomation.bulk.entry.BulkEntryCreateDto; import de.ph87.homeautomation.bulk.entry.BulkEntryCreateDto;
import de.ph87.homeautomation.channel.Channel; import de.ph87.homeautomation.channel.Channel;
import de.ph87.homeautomation.device.DeviceController;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import de.ph87.homeautomation.property.PropertyRepository; import de.ph87.homeautomation.property.PropertyRepository;
import de.ph87.homeautomation.property.PropertyType; import de.ph87.homeautomation.property.PropertyType;
@ -40,32 +39,23 @@ public class DemoDataService {
private final ScheduleEntryController scheduleEntryController; private final ScheduleEntryController scheduleEntryController;
private final DeviceController deviceController;
public void insertDemoData() { public void insertDemoData() {
if (!config.isInsertDemoData()) { if (!config.isInsertDemoData()) {
return; return;
} }
final Property propertyDirect = createProperty("propertyDirect", "direct", PropertyType.BOOLEAN, null, null);
final long schedule = createSchedule(true, "Schedule"); final Property propertyBulkBoolean = createProperty("propertyBulkBoolean", null, PropertyType.BOOLEAN, null, null);
final Property propertyBulkShutter = createProperty("propertyBulkShutter", null, PropertyType.SHUTTER, null, null);
deviceController.create("DeviceSwitch"); final Property propertyBulkBrightness = createProperty("propertyBulkBrightness", null, PropertyType.BRIGHTNESS_PERCENT, null, null);
deviceController.create("DeviceSwitch"); final Property propertyBulkColorTemperature = createProperty("propertyBulkColorTemperature", null, PropertyType.COLOR_TEMPERATURE, null, null);
deviceController.create("DeviceSwitch"); final BulkDto bulk = bulkController.create(new BulkCreateDto("bulk", true));
deviceController.create("DeviceShutter"); bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkBoolean.getId(), 1, 0));
deviceController.create("DeviceShutter"); bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkShutter.getId(), 35, 0));
deviceController.create("DeviceShutter"); bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkBrightness.getId(), 40, 0));
bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkColorTemperature.getId(), 55, 0));
final Property propWeihnachtsbeleuchtung = createProperty("Weihnachtsbeleuchtung", null, PropertyType.BOOLEAN, null, null); final long scheduleId = createSchedule(true, "schedule");
final BulkDto bulkWeihnachtsbeleuchtungAn = bulkController.create(new BulkCreateDto("Weihnachtsbeleuchtung An", true));
bulkEntryController.create(new BulkEntryCreateDto(bulkWeihnachtsbeleuchtungAn.getId(), propWeihnachtsbeleuchtung.getId(), 35, 0));
final ZonedDateTime now = ZonedDateTime.now().plusSeconds(3); final ZonedDateTime now = ZonedDateTime.now().plusSeconds(3);
createTime(schedule, true, now.getHour(), now.getMinute(), now.getSecond(), 0, 1, bulkWeihnachtsbeleuchtungAn); createTime(scheduleId, true, now.getHour(), now.getMinute(), now.getSecond(), 0, propertyDirect, 1, bulk);
final Property propRollladenSchlafzimmer = createProperty("Schlafzimmer", null, PropertyType.SHUTTER, null, null);
final BulkDto bulkRollladenSchlafzimmerAufstehen = bulkController.create(new BulkCreateDto("Rollladen Schlafzimmer Aufstehen", true));
bulkEntryController.create(new BulkEntryCreateDto(bulkRollladenSchlafzimmerAufstehen.getId(), propRollladenSchlafzimmer.getId(), 1, 0));
createSunrise(schedule, true, Zenith.CIVIL, 0, 1, bulkRollladenSchlafzimmerAufstehen);
} }
private Property createProperty(final String title, final String slug, final PropertyType type, final Channel readChannel, final Channel writeChannel) { private Property createProperty(final String title, final String slug, final PropertyType type, final Channel readChannel, final Channel writeChannel) {
@ -83,26 +73,24 @@ public class DemoDataService {
return id; return id;
} }
private void createTime(final long scheduleId, final boolean enabled, final int hour, final int minute, final int second, final int fuzzySeconds, final double value, final BulkDto bulk) { private void createTime(final long scheduleId, final boolean enabled, final int hour, final int minute, final int second, final int fuzzySeconds, final Property property, final double value, final BulkDto bulk) {
newScheduleEntry(scheduleId, enabled, ScheduleEntryType.TIME, null, hour, minute, second, fuzzySeconds, value, bulk); newScheduleEntry(scheduleId, enabled, ScheduleEntryType.TIME, null, hour, minute, second, fuzzySeconds, property, value, bulk);
} }
private void createSunrise(final long scheduleId, final boolean enabled, final Zenith zenith, final int fuzzySeconds, final double value, final BulkDto bulk) { private void newScheduleEntry(final long scheduleId, final boolean enabled, final ScheduleEntryType type, final Zenith zenith, final int hour, final int minute, final int second, final int fuzzySeconds, final Property property, final double value, final BulkDto bulk) {
newScheduleEntry(scheduleId, enabled, ScheduleEntryType.SUNRISE, zenith, 0, 0, 0, fuzzySeconds, value, bulk);
}
private void newScheduleEntry(final long scheduleId, final boolean enabled, final ScheduleEntryType type, final Zenith zenith, final int hour, final int minute, final int second, final int fuzzySeconds, final double value, final BulkDto bulk) {
final long id = scheduleEntryController.create(scheduleId).getId(); final long id = scheduleEntryController.create(scheduleId).getId();
scheduleEntryController.setEnabled(id, enabled); scheduleEntryController.setEnabled(id, enabled);
scheduleEntryController.setType(id, type.name()); scheduleEntryController.setType(id, type.name());
if (zenith != null) { if (zenith != null) {
scheduleEntryController.setZenith(id, zenith.degrees() + ""); scheduleEntryController.setZenith(id, zenith.degrees() + "");
} }
scheduleEntryController.daySecond(id, (hour * 60 + minute) * 60 + second); scheduleEntryController.setHour(id, hour);
scheduleEntryController.setMinute(id, minute);
scheduleEntryController.setSecond(id, second);
scheduleEntryController.setFuzzySeconds(id, fuzzySeconds); scheduleEntryController.setFuzzySeconds(id, fuzzySeconds);
scheduleEntryController.setProperty(id, property == null ? null : property.getId());
scheduleEntryController.setValue(id, value); scheduleEntryController.setValue(id, value);
scheduleEntryController.setBulk(id, bulk == null ? null : bulk.getId()); scheduleEntryController.setBulk(id, bulk == null ? null : bulk.getId());
scheduleEntryController.setSkip(id, 1);
} }
} }

View File

@ -1,12 +1,12 @@
package de.ph87.homeautomation; package de.ph87.homeautomation;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Predicate;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Predicate;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -23,6 +23,7 @@ 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 + "%"))

View File

@ -1,12 +1,12 @@
package de.ph87.homeautomation.bulk; package de.ph87.homeautomation.bulk;
import de.ph87.homeautomation.bulk.entry.BulkEntry; import de.ph87.homeautomation.bulk.entry.BulkEntry;
import jakarta.persistence.*;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import javax.persistence.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -40,12 +40,6 @@ public class Bulk {
return entries.size(); return entries.size();
} }
public Bulk(final Bulk original, final String name) {
this.enabled = original.isEnabled();
this.name = name;
this.entries = original.getEntries().stream().map(BulkEntry::new).toList();
}
public Bulk(final BulkCreateDto dto) { public Bulk(final BulkCreateDto dto) {
this.enabled = dto.isEnabled(); this.enabled = dto.isEnabled();
this.name = dto.getName(); this.name = dto.getName();

View File

@ -1,11 +1,12 @@
package de.ph87.homeautomation.bulk; package de.ph87.homeautomation.bulk;
import de.ph87.homeautomation.shared.*; import de.ph87.homeautomation.shared.ISearchController;
import lombok.*; import de.ph87.homeautomation.shared.SearchResult;
import org.springframework.data.domain.*; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.*; import java.util.List;
@RestController @RestController
@RequestMapping("bulk") @RequestMapping("bulk")
@ -36,11 +37,6 @@ public class BulkController implements ISearchController {
bulkWriter.run(id); bulkWriter.run(id);
} }
@GetMapping("duplicate/{id}")
public BulkDto duplicate(@PathVariable final long id) {
return bulkWriter.duplicate(id);
}
@PostMapping("set/{id}/name") @PostMapping("set/{id}/name")
public BulkDto name(@PathVariable final long id, @RequestBody final String name) { public BulkDto name(@PathVariable final long id, @RequestBody final String name) {
return bulkWriter.set(id, bulk -> bulk.setName(name)); return bulkWriter.set(id, bulk -> bulk.setName(name));

View File

@ -1,9 +1,9 @@
package de.ph87.homeautomation.bulk; package de.ph87.homeautomation.bulk;
import de.ph87.homeautomation.common.Filter; import de.ph87.homeautomation.common.Filter;
import jakarta.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.Predicate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;

View File

@ -1,9 +1,11 @@
package de.ph87.homeautomation.bulk; package de.ph87.homeautomation.bulk;
import de.ph87.homeautomation.property.*; import de.ph87.homeautomation.property.Property;
import org.springframework.data.jpa.repository.*; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.*; import java.util.List;
import java.util.Optional;
public interface BulkRepository extends JpaRepository<Bulk, Long>, JpaSpecificationExecutor<Bulk> { public interface BulkRepository extends JpaRepository<Bulk, Long>, JpaSpecificationExecutor<Bulk> {
@ -14,6 +16,4 @@ public interface BulkRepository extends JpaRepository<Bulk, Long>, JpaSpecificat
List<Bulk> findDistinctByEntries_Property(Property property); List<Bulk> findDistinctByEntries_Property(Property property);
boolean existsByName(String name);
} }

View File

@ -1,11 +1,11 @@
package de.ph87.homeautomation.bulk; package de.ph87.homeautomation.bulk;
import lombok.*; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.*; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.*; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.*; import org.springframework.transaction.annotation.Transactional;
import java.util.function.*; import java.util.function.Consumer;
@Slf4j @Slf4j
@Service @Service
@ -38,19 +38,6 @@ public class BulkWriter {
bulkExecutor.execute(bulk); bulkExecutor.execute(bulk);
} }
public BulkDto duplicate(final long id) {
final Bulk original = bulkReader.getById(id);
int number = 1;
while (true) {
final String copyName = original.getName() + " Kopie " + number;
if (!bulkRepository.existsByName(copyName)) {
final Bulk copy = bulkRepository.save(new Bulk(original, copyName));
log.info("Bulk duplicated: {}", copy);
return bulkMapper.toDto(copy);
}
}
}
public BulkDto set(final long id, final Consumer<Bulk> consumer) { public BulkDto set(final long id, final Consumer<Bulk> consumer) {
final Bulk bulk = bulkReader.getById(id); final Bulk bulk = bulkReader.getById(id);
consumer.accept(bulk); consumer.accept(bulk);

View File

@ -1,12 +1,13 @@
package de.ph87.homeautomation.bulk.entry; package de.ph87.homeautomation.bulk.entry;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import jakarta.persistence.*;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import javax.persistence.*;
@Entity @Entity
@Getter @Getter
@ToString @ToString
@ -40,11 +41,4 @@ public class BulkEntry {
this.value = dto.getValue(); this.value = dto.getValue();
} }
@SuppressWarnings("CopyConstructorMissesField")
public BulkEntry(final BulkEntry original) {
this.position = original.getPosition();
this.property = original.getProperty();
this.value = original.getValue();
}
} }

View File

@ -1,8 +1,8 @@
package de.ph87.homeautomation.channel; package de.ph87.homeautomation.channel;
import jakarta.persistence.*;
import lombok.*; import lombok.*;
import javax.persistence.*;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@Getter @Getter

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,11 +1,12 @@
package de.ph87.homeautomation.device.devices; package de.ph87.homeautomation.device.devices;
import jakarta.persistence.*;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import javax.persistence.*;
@Getter @Getter
@Setter @Setter
@ToString @ToString

View File

@ -1,12 +1,13 @@
package de.ph87.homeautomation.device.devices; package de.ph87.homeautomation.device.devices;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
@Getter @Getter
@Setter @Setter
@ToString @ToString

View File

@ -1,14 +1,14 @@
package de.ph87.homeautomation.device.devices; package de.ph87.homeautomation.device.devices;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ManyToOne;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;

View File

@ -1,12 +1,13 @@
package de.ph87.homeautomation.device.devices; package de.ph87.homeautomation.device.devices;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
@Getter @Getter
@Setter @Setter
@ToString @ToString

View File

@ -2,12 +2,12 @@ package de.ph87.homeautomation.knx.group;
import de.ph87.homeautomation.channel.Channel; import de.ph87.homeautomation.channel.Channel;
import de.ph87.homeautomation.channel.IChannelOwner; import de.ph87.homeautomation.channel.IChannelOwner;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import lombok.*; import lombok.*;
import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.GroupAddress;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@Getter @Getter

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

@ -1,11 +1,11 @@
package de.ph87.homeautomation.knx.group; package de.ph87.homeautomation.knx.group;
import jakarta.persistence.Embeddable;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.persistence.Embeddable;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@Slf4j @Slf4j

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

@ -1,11 +1,11 @@
package de.ph87.homeautomation.knx.group; package de.ph87.homeautomation.knx.group;
import jakarta.persistence.Embeddable;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import tuwien.auto.calimero.IndividualAddress; import tuwien.auto.calimero.IndividualAddress;
import javax.persistence.Embeddable;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@Getter @Getter

View File

@ -1,9 +1,10 @@
package de.ph87.homeautomation.property; package de.ph87.homeautomation.property;
import de.ph87.homeautomation.channel.Channel; import de.ph87.homeautomation.channel.Channel;
import jakarta.persistence.*;
import lombok.*; import lombok.*;
import javax.persistence.*;
@Getter @Getter
@Setter @Setter
@ToString @ToString

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

@ -1,11 +1,12 @@
package de.ph87.homeautomation.scene; package de.ph87.homeautomation.scene;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.*; import lombok.*;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Getter @Getter
@Setter @Setter
@ToString @ToString

View File

@ -1,9 +1,9 @@
package de.ph87.homeautomation.schedule; package de.ph87.homeautomation.schedule;
import de.ph87.homeautomation.schedule.entry.ScheduleEntry; import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
import jakarta.persistence.*;
import lombok.*; import lombok.*;
import javax.persistence.*;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;

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

@ -1,9 +1,11 @@
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
@ -17,11 +19,14 @@ public class ScheduleDto implements Serializable {
private final Set<ScheduleEntryDto> entries; private final Set<ScheduleEntryDto> entries;
public ScheduleDto(final Schedule schedule, final Set<ScheduleEntryDto> entries) { private final List<AstroDto> astros;
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;
} }
} }

View File

@ -38,7 +38,6 @@ public class ScheduleExecutor {
} }
private void executeEntry(final Schedule schedule, final ScheduleEntry entry) { private void executeEntry(final Schedule schedule, final ScheduleEntry entry) {
if (entry.getSkip() <= 0) {
log.info("Executing Schedule \"{}\" Entry {}", schedule.getTitle(), entry); log.info("Executing Schedule \"{}\" Entry {}", schedule.getTitle(), entry);
if (entry.getProperty() != null) { if (entry.getProperty() != null) {
log.debug("Schedule setting property: {} = {}", entry.getProperty().getTitle(), entry.getValue()); log.debug("Schedule setting property: {} = {}", entry.getProperty().getTitle(), entry.getValue());
@ -48,10 +47,6 @@ public class ScheduleExecutor {
log.debug("Schedule executing Bulk: {}", entry.getBulk()); log.debug("Schedule executing Bulk: {}", entry.getBulk());
bulkExecutor.execute(entry.getBulk()); bulkExecutor.execute(entry.getBulk());
} }
} else {
log.info("Skipping Schedule \"{}\" Entry {}", schedule.getTitle(), entry);
entry.setSkip(entry.getSkip() - 1);
}
entry.setLastClearTimestamp(entry.getNextClearTimestamp()); entry.setLastClearTimestamp(entry.getNextClearTimestamp());
entry.setLastFuzzyTimestamp(entry.getNextFuzzyTimestamp()); entry.setLastFuzzyTimestamp(entry.getNextFuzzyTimestamp());
scheduleWriter.notifyChanged(schedule); scheduleWriter.notifyChanged(schedule);

View File

@ -1,5 +1,7 @@
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;
@ -8,6 +10,7 @@ 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;
@ -21,9 +24,12 @@ 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());
return new ScheduleDto(schedule, entries); final List<AstroDto> astros = astroService.findAllNext();
return new ScheduleDto(schedule, entries, astros);
} }
public void publish(final Schedule schedule, final boolean existing) { public void publish(final Schedule schedule, final boolean existing) {

View File

@ -0,0 +1,45 @@
package de.ph87.homeautomation.schedule.astro;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import javax.persistence.*;
@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;
}
}

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

@ -0,0 +1,37 @@
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();
}
}

View File

@ -0,0 +1,11 @@
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();
}

View File

@ -0,0 +1,71 @@
package de.ph87.homeautomation.schedule.astro;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
@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 -", null));
astroRepository.save(new Astro(93.0000, "Aufgang --", null));
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);
}
}

View File

@ -4,12 +4,12 @@ import com.luckycatlabs.sunrisesunset.Zenith;
import de.ph87.homeautomation.bulk.Bulk; import de.ph87.homeautomation.bulk.Bulk;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import de.ph87.homeautomation.schedule.Schedule; import de.ph87.homeautomation.schedule.Schedule;
import jakarta.persistence.*;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import javax.persistence.*;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Random; import java.util.Random;
@ -48,7 +48,6 @@ 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();
@ -64,9 +63,6 @@ public class ScheduleEntry {
private int fuzzySeconds = 0; private int fuzzySeconds = 0;
@Column(nullable = false)
private int skip = 0;
@ManyToOne @ManyToOne
private Property property; private Property property;
@ -92,16 +88,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

@ -1,10 +1,8 @@
package de.ph87.homeautomation.schedule.entry; package de.ph87.homeautomation.schedule.entry;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController @RestController
@RequestMapping("schedule/entry") @RequestMapping("schedule/entry")
@RequiredArgsConstructor @RequiredArgsConstructor
@ -77,9 +75,19 @@ public class ScheduleEntryController {
return scheduleEntryWriter.setZenith(id, Double.parseDouble(value)); return scheduleEntryWriter.setZenith(id, Double.parseDouble(value));
} }
@PostMapping("set/{id}/daySecond") @PostMapping("set/{id}/hour")
public ScheduleEntryDto daySecond(@PathVariable final long id, @RequestBody final int daySecond) { public ScheduleEntryDto setHour(@PathVariable final long id, @RequestBody final int value) {
return scheduleEntryWriter.daySecond(id, daySecond); return scheduleEntryWriter.setHour(id, value);
}
@PostMapping("set/{id}/minute")
public ScheduleEntryDto setMinute(@PathVariable final long id, @RequestBody final int value) {
return scheduleEntryWriter.setMinute(id, value);
}
@PostMapping("set/{id}/second")
public ScheduleEntryDto setSecond(@PathVariable final long id, @RequestBody final int value) {
return scheduleEntryWriter.setSecond(id, value);
} }
@PostMapping("set/{id}/fuzzySeconds") @PostMapping("set/{id}/fuzzySeconds")
@ -87,11 +95,6 @@ public class ScheduleEntryController {
return scheduleEntryWriter.setFuzzySeconds(id, value); return scheduleEntryWriter.setFuzzySeconds(id, value);
} }
@PostMapping("set/{id}/skip")
public ScheduleEntryDto setSkip(@PathVariable final long id, @RequestBody final int value) {
return scheduleEntryWriter.setSkip(id, value);
}
@PostMapping("set/{id}/property") @PostMapping("set/{id}/property")
public ScheduleEntryDto setProperty(@PathVariable final long id, @RequestBody(required = false) final Long propertyId) { public ScheduleEntryDto setProperty(@PathVariable final long id, @RequestBody(required = false) final Long propertyId) {
return scheduleEntryWriter.setProperty(id, propertyId); return scheduleEntryWriter.setProperty(id, propertyId);

Some files were not shown because too many files have changed in this diff Show More