ScheduleEditor responsive

This commit is contained in:
Patrick Haßel 2024-09-11 11:34:30 +02:00
parent 6ded6da0e2
commit 0f91c35771
27 changed files with 767 additions and 230 deletions

View File

@ -4,7 +4,6 @@ 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.jpa.hibernate.ddl-auto=create spring.jpa.hibernate.ddl-auto=create
#- #-

View File

@ -0,0 +1,82 @@
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

@ -2,13 +2,18 @@ import {validateBooleanNotNull, validateDateAllowNull, validateNumberAllowNull,
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,
@ -35,7 +40,24 @@ export class ScheduleEntry {
readonly value: number, readonly value: number,
readonly bulk: Bulk | null, readonly bulk: Bulk | null,
) { ) {
// nothing this.dayMinute = hour * 60 + minute;
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 {
@ -75,4 +97,8 @@ 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

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

View File

@ -1,4 +1,4 @@
import {NgModule} from '@angular/core'; import {LOCALE_ID, 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,6 +20,14 @@ 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,
@ -37,6 +45,8 @@ import {EntryValueComponent} from './shared/entry-value/entry-value.component';
BulkEditorComponent, BulkEditorComponent,
LeftPadDirective, LeftPadDirective,
EntryValueComponent, EntryValueComponent,
BoolComponent,
DurationComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -45,7 +55,9 @@ import {EntryValueComponent} from './shared/entry-value/entry-value.component';
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,151 +1,113 @@
<ng-container *ngIf="schedule"> <ng-container *ngIf="schedule">
<div id="title">
<app-text [initial]="schedule.title" (valueChange)="scheduleService.set(schedule, 'title', $event)"></app-text>
</div>
<div id="entries">
<div class="entry" *ngFor="let entry of schedule.entries; trackBy: ScheduleEntry.trackBy">
<ng-template #boolean let-entry="entry" let-value="value" let-key="key"> <div class="section">
<td class="boolean" (click)="set(entry, key, !value)" [class.true]="value" [class.false]="!value"> <div class="enabled">
<fa-icon *ngIf="value" [icon]="faCheckCircle"></fa-icon> <app-bool [value]="entry.enabled" (onChange)="entryService.set(entry, 'enabled', $event)"></app-bool>
<fa-icon *ngIf="!value" [icon]="faCircle"></fa-icon> </div>
</td> <div class="title">
</ng-template> <select [ngModel]="entry.bulk?.id" (ngModelChange)="entryService.set(entry, 'bulk', $event)">
<option [ngValue]="null">-</option>
<p> <option [ngValue]="bulk.id" *ngFor="let bulk of bulks.sort(Bulk.compareName)">{{ bulk.name }}</option>
<button (click)="create()">+ Hinzufügen</button>
</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> </select>
</td> </div>
</div>
<ng-container *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'"> <div class="section">
<td> <div class="weekdays">
<select [ngModel]="entry.zenith" (ngModelChange)="set(entry, 'zenith', $event)"> <div>
<option *ngFor="let event of schedule.astros; let index = index" [value]="event.zenith"> <app-bool label="Mo" [value]="entry.monday" (onChange)="entryService.set(entry, 'monday', $event)"></app-bool>
[{{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}} </div>
</option> <div>
</select> <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>
</td> <div class="section" *ngIf="entry.type === 'TIME'">
</ng-container> <div class="time">
<td *ngIf="entry.type !== 'SUNRISE' && entry.type !== 'SUNSET'" class="empty"></td> <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>
<ng-container *ngIf="entry.type === 'TIME'"> <div class="section" *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
<td class="first"> <div class="sun">
<select [ngModel]="entry.hour" (ngModelChange)="set(entry, 'hour', $event)"> <div *ngFor="let zenith of getZenithEntries(entry.type); trackBy: trackByZenith">
<option *ngFor="let _ of [].constructor(24); let value = index" [ngValue]="value">{{value}}</option> <app-bool [label]="zenith.title" [value]="entry.zenith === zenith.value" (onChange)="entryService.set(entry, 'zenith', zenith.value)"></app-bool>
</select> </div>
</td> <div>
<td class="middle">:</td> <input type="number" min="45" max="120" [ngModel]="entry.zenith" (ngModelChange)="entryService.set(entry, 'zenith', $event || 0)">
<td class="last"> </div>
<select [ngModel]="entry.minute" (ngModelChange)="set(entry, 'minute', $event)"> </div>
<option *ngFor="let _ of [].constructor(12); let value = index" [ngValue]="value * 5">{{value * 5 | number:'2.0'}}</option> </div>
</select>
</td>
</ng-container>
<td *ngIf="entry.type !== 'TIME'" colspan="3" class="empty"></td>
<td> <div class="section">
<select [ngModel]="entry.fuzzySeconds" (ngModelChange)="set(entry, 'fuzzySeconds', $event)"> <div class="flexHalf">
<option [ngValue]="0">Keine</option> <div class="flexIcon">
<option [ngValue]="60">1 Minute</option> <img src="assets/dice.svg" alt="+/-" title="Zufallsabweichung +/-">
<option [ngValue]="300">5 Minuten</option> </div>
<option [ngValue]="600">10 Minuten</option> <div class="flexInput">
<option [ngValue]="1800">30 Minuten</option> <app-duration [duration]="Duration.ofCode(entry.fuzzySeconds + 's')" [bgcolor]="entry.fuzzySeconds ? '#88c0ff' : 'white'" [min]="Duration.ofCode('')" (onChange)="entryService.set(entry, 'fuzzySeconds', $event.totalSeconds)"></app-duration>
<option [ngValue]="3600">1 Stunde</option> </div>
<option [ngValue]="7200">2 Stunden</option> </div>
<option [ngValue]="10800">3 Stunden</option> <div class="flexHalf">
<option [ngValue]="21600">6 Stunden</option> <div class="flexIcon">
<option [ngValue]="43200">12 Stunden</option> <img src="assets/skip.svg" alt="Überspringen">
<option [ngValue]="86400">1 Tag</option> </div>
</select> <div class="flexInput flexInputLast">
</td> <input type="number" min="0" [class.skipBack]="entry.skip" [ngModel]="entry.skip" (ngModelChange)="entryService.set(entry, 'skip', $event || 0)">
</div>
</div>
</div>
<ng-container *ngIf="entry.nextClearTimestamp"> <div class="section">
<td class="number first" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.dayName}}</td> <div class="nextFuzzyTimestamp" [class.skipFont]="entry.skip" *ngIf="entry.executable">
<td class="number middle" [class.empty]="entry.fuzzySeconds > 0">:&nbsp;</td> {{ relativeDate(entry.nextFuzzyTimestamp?.date) }}
<td class="number last" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.timeString}}</td> <span [class.fuzzyFont]="entry.fuzzySeconds" *ngIf="entry.fuzzySeconds">
</ng-container> (eig: {{ entry.nextClearTimestamp.date | date:'HH:mm' }})
<ng-container *ngIf="!entry.nextClearTimestamp"> </span>
<td class="empty first"></td> </div>
<td class="empty middle"></td> <div class="inactive" *ngIf="entry.todo">
<td class="empty last"></td> {{ entry.todo }}
</ng-container> </div>
</div>
<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>
</div>
</div>
</ng-container> </ng-container>

View File

@ -1,19 +1,134 @@
select { #title {
background-color: transparent; margin: 0.5em;
border-width: 0;
width: 100%;
outline: none;
font-family: monospace;
} }
th { #entries {
background-color: lightblue; margin-top: 0.5em;
} margin-left: 0.5em;
tr.header { img {
display: inline;
height: 1em;
vertical-align: top;
}
th:not(:first-child), td:not(:first-child) { .entry {
border: none; margin-bottom: 0.5em;
margin-right: 0.5em;
height: 8.2em;
border-radius: 0.2em;
background-color: #ececec;
@media (min-width: 1001px) {
float: left;
width: 400px;
}
.section {
margin: 0.25em;
.enabled {
float: left;
width: 1.4em;
border-radius: 0.2em;
margin-right: 0.25em;
}
.title {
float: left;
padding: 0.1em;
width: calc(100% - 1.65em);
}
.weekdays {
float: left;
width: 75%;
border-radius: 0.2em;
div {
float: left;
width: 14.2857%;
}
}
.modes {
float: left;
width: 25%;
._inner_ {
margin-left: 0.25em;
border-radius: 0.2em;
div {
float: left;
width: 33.3333%;
}
}
}
.time {
width: 50%;
button {
width: 16.25%;
}
input {
width: 35%;
}
}
.sun {
border-radius: 0.2em;
div {
float: left;
width: 20%;
}
}
.flexHalf {
float: left;
width: 50%;
display: flex;
align-items: center;
}
.flexIcon {
width: 2.5em;
text-align: center;
}
.flexInput {
flex-grow: 1;
}
.nextFuzzyTimestamp {
text-align: center;
}
}
}
.skipBack {
background-color: #ffc059;
}
.skipFont {
color: #ff9a00;
}
.fuzzyFont {
color: #489dff;
}
.inactive {
color: gray;
text-align: center;
} }
} }

View File

@ -1,14 +1,19 @@
import {Component, OnInit} from '@angular/core'; import {Component, Inject, LOCALE_ID, 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 {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 {DatePipe} from "@angular/common";
import {Duration} from "../../../api/Duration";
import {Bulk} from "../../../api/bulk/Bulk";
import {BulkService} from "../../../api/bulk/BulkService";
import {Zenith} from "../../../api/schedule/entry/Zenith";
const DAY_MINUTES: number = 24 * 60;
@Component({ @Component({
selector: 'app-schedule-editor', selector: 'app-schedule-editor',
@ -17,36 +22,47 @@ import {NO_OP} from "../../../api/api.service";
}) })
export class ScheduleEditorComponent implements OnInit { export class ScheduleEditorComponent implements OnInit {
readonly ScheduleEntry = ScheduleEntry; protected readonly datePipe: DatePipe = new DatePipe(this.locale);
readonly faCheckCircle = faCheckCircle; protected readonly ScheduleEntry = ScheduleEntry;
readonly faCircle = faCircle; protected readonly Schedule = Schedule;
readonly faTimes = faTimesCircle; protected readonly Duration = Duration;
readonly faUp = faArrowAltCircleUp; protected readonly Bulk = Bulk;
readonly faDown = faArrowAltCircleDown; protected readonly expanded: number[] = [];
readonly Schedule = Schedule; protected now: Date = new Date(Date.now());
schedule!: Schedule; protected schedule!: Schedule;
protected bulks: Bulk[] = [];
protected readonly ZENITH_ENTRIES: Zenith[] = [
new Zenith("Astr.", 107, true, true),
new Zenith("Naut.", 102, true, true),
new Zenith("Bürg.", 96, true, true),
new Zenith("Aufg.", 90.8, true, false),
new Zenith("Unterg.", 90.8, false, true),
];
constructor( constructor(
readonly router: Router, readonly router: Router,
readonly activatedRoute: ActivatedRoute, readonly activatedRoute: ActivatedRoute,
readonly scheduleService: ScheduleService, readonly scheduleService: ScheduleService,
readonly scheduleEntryService: ScheduleEntryService, readonly entryService: ScheduleEntryService,
readonly propertyService: PropertyService,
readonly bulkService: BulkService, readonly bulkService: BulkService,
@Inject(LOCALE_ID) private locale: string,
) { ) {
// nothing // -
} }
ngOnInit(): void { ngOnInit(): void {
this.scheduleService.subscribe(update => this.update(update)); this.scheduleService.subscribe(update => this.update(update));
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params['id'], schedule => this.schedule = schedule)); this.bulkService.findAll(list => this.bulks = list);
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 {
@ -57,25 +73,104 @@ export class ScheduleEditorComponent implements OnInit {
this.router.navigate(['/ScheduleList']); this.router.navigate(['/ScheduleList']);
return; return;
} }
this.schedule = update.payload; this.setSchedule(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.scheduleEntryService.create(this.schedule, NO_OP); this.entryService.create(this.schedule, NO_OP);
} }
delete(entry: ScheduleEntry): void { delete(entry: ScheduleEntry): void {
if (confirm("Eintrag \"" + entry.nextClearTimestamp?.timeString + " +/-" + entry.fuzzySeconds + "\" wirklich löschen?")) { if (confirm("Eintrag \"" + entry.nextClearTimestamp?.timeString + " +/-" + entry.fuzzySeconds + "\" wirklich löschen?")) {
this.scheduleEntryService.delete(entry, NO_OP); this.entryService.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.scheduleEntryService.set(entry, key, value, NO_OP); this.entryService.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);
} }
} }
relativeDate(date: Date | undefined) {
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);
}
getZenithEntries(type: string): Zenith[] {
if (type === 'SUNRISE') {
return this.ZENITH_ENTRIES.filter(zenith => zenith.sunrise);
}
return this.ZENITH_ENTRIES.reverse().filter(zenith => zenith.sunset);
}
trackByZenith(index: number, zenith: Zenith) {
return zenith.value;
}
timeFromString(entry: ScheduleEntry, time: string) {
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;
console.log(hour, minute, second, daySecond);
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);
}
protected readonly console = console;
} }

View File

@ -1,5 +1,4 @@
.schedules { .schedules {
font-size: 4vw;
padding: 0.25em; padding: 0.25em;
.scheduleBox { .scheduleBox {
@ -132,8 +131,4 @@
} }
@media (min-width: 1000px) {
font-size: 16px;
}
} }

View File

@ -0,0 +1,18 @@
<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

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

View File

@ -0,0 +1,58 @@
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

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

View File

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

View File

@ -0,0 +1,63 @@
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()
bgcolor: 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

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 553 B

View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,9 @@
<?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>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,4 @@
<?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>

After

Width:  |  Height:  |  Size: 292 B

View File

@ -0,0 +1,4 @@
<?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>

After

Width:  |  Height:  |  Size: 292 B

View File

@ -0,0 +1,12 @@
<?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>

After

Width:  |  Height:  |  Size: 684 B

View File

@ -0,0 +1,4 @@
<?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>

After

Width:  |  Height:  |  Size: 349 B

View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 314 B

View File

@ -1,6 +1,30 @@
// 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: 0.1em;
box-sizing: border-box;
background-color: white;
}
select {
margin-top: -0.1em;
}
body { body {
margin: 0; margin: 0;
font-family: arial, sans-serif;
width: 100%; width: 100%;
} }
@ -9,9 +33,9 @@ a {
} }
div { div {
box-sizing: border-box;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
box-sizing: border-box;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -127,3 +151,11 @@ table.vertical {
clear: both; clear: both;
margin-bottom: 5px; margin-bottom: 5px;
} }
.buttonPlus {
background-color: #8fbc8f;
}
.buttonMinus {
background-color: #ef8787;
}

View File

@ -7,6 +7,7 @@ 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;
@ -39,23 +40,32 @@ 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 Property propertyBulkBoolean = createProperty("propertyBulkBoolean", null, PropertyType.BOOLEAN, null, null); final long schedule = createSchedule(true, "Schedule");
final Property propertyBulkShutter = createProperty("propertyBulkShutter", null, PropertyType.SHUTTER, null, null);
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("DeviceSwitch");
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)); deviceController.create("DeviceShutter");
bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkColorTemperature.getId(), 55, 0));
final long scheduleId = createSchedule(true, "schedule"); final Property propWeihnachtsbeleuchtung = createProperty("Weihnachtsbeleuchtung", null, PropertyType.BOOLEAN, null, null);
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(scheduleId, true, now.getHour(), now.getMinute(), now.getSecond(), 0, propertyDirect, 1, bulk); createTime(schedule, true, now.getHour(), now.getMinute(), now.getSecond(), 0, 1, bulkWeihnachtsbeleuchtungAn);
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) {
@ -73,22 +83,23 @@ 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 Property property, 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 double value, final BulkDto bulk) {
newScheduleEntry(scheduleId, enabled, ScheduleEntryType.TIME, null, hour, minute, second, fuzzySeconds, property, value, bulk); newScheduleEntry(scheduleId, enabled, ScheduleEntryType.TIME, null, hour, minute, second, 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 Property property, final double value, final BulkDto bulk) { private void createSunrise(final long scheduleId, final boolean enabled, final Zenith zenith, final int fuzzySeconds, 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.setHour(id, hour); scheduleEntryController.daySecond(id, (hour * 60 + minute) * 60 + second);
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); scheduleEntryController.setSkip(id, 1);

View File

@ -1,8 +1,10 @@
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
@ -75,19 +77,9 @@ public class ScheduleEntryController {
return scheduleEntryWriter.setZenith(id, Double.parseDouble(value)); return scheduleEntryWriter.setZenith(id, Double.parseDouble(value));
} }
@PostMapping("set/{id}/hour") @PostMapping("set/{id}/daySecond")
public ScheduleEntryDto setHour(@PathVariable final long id, @RequestBody final int value) { public ScheduleEntryDto daySecond(@PathVariable final long id, @RequestBody final int daySecond) {
return scheduleEntryWriter.setHour(id, value); return scheduleEntryWriter.daySecond(id, daySecond);
}
@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")

View File

@ -8,8 +8,10 @@ import de.ph87.homeautomation.schedule.ScheduleWriter;
import de.ph87.homeautomation.web.BadRequestException; import de.ph87.homeautomation.web.BadRequestException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -24,6 +26,12 @@ import static java.lang.Math.min;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ScheduleEntryWriter { public class ScheduleEntryWriter {
private static final int MINUTE_SECONDS = 60;
private static final int HOUR_SECONDS = 60 * MINUTE_SECONDS;
private static final int DAY_SECONDS = 24 * HOUR_SECONDS;
private final ScheduleEntryReader scheduleEntryReader; private final ScheduleEntryReader scheduleEntryReader;
private final ScheduleReader scheduleReader; private final ScheduleReader scheduleReader;
@ -114,16 +122,19 @@ public class ScheduleEntryWriter {
return modifyValue(id, ScheduleEntry::setZenith, value); return modifyValue(id, ScheduleEntry::setZenith, value);
} }
public ScheduleEntryDto setHour(final long id, final int value) { public ScheduleEntryDto daySecond(final long id, final int daySecond) {
return modifyValue(id, ScheduleEntry::setHour, value); if (daySecond < 0 || daySecond > DAY_SECONDS) {
log.error("daySecond must be in [0, {}] but is: {}", DAY_SECONDS, daySecond);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
} }
final int hour = daySecond / HOUR_SECONDS;
public ScheduleEntryDto setMinute(final long id, final int value) { final int minute = (daySecond / MINUTE_SECONDS) % 60;
return modifyValue(id, ScheduleEntry::setMinute, value); final int second = daySecond % 60;
} return modify(id, entry -> {
entry.setHour(hour);
public ScheduleEntryDto setSecond(final long id, final int value) { entry.setMinute(minute);
return modifyValue(id, ScheduleEntry::setSecond, value); entry.setSecond(second);
});
} }
public ScheduleEntryDto setFuzzySeconds(final long id, final int value) { public ScheduleEntryDto setFuzzySeconds(final long id, final int value) {