diff --git a/application.properties b/application.properties index fdb4ed8..d5d609b 100644 --- a/application.properties +++ b/application.properties @@ -4,7 +4,6 @@ spring.datasource.url=jdbc:h2:./Homeautomation;AUTO_SERVER=TRUE spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect #- spring.jpa.hibernate.ddl-auto=create #- diff --git a/src/main/angular/src/app/api/Duration.ts b/src/main/angular/src/app/api/Duration.ts new file mode 100644 index 0000000..dc39d20 --- /dev/null +++ b/src/main/angular/src/app/api/Duration.ts @@ -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 = /(?[+-]*)(?\d+(?:[.,]\d+)?)(?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; + } + +} diff --git a/src/main/angular/src/app/api/schedule/entry/ScheduleEntry.ts b/src/main/angular/src/app/api/schedule/entry/ScheduleEntry.ts index 64670eb..34db59d 100644 --- a/src/main/angular/src/app/api/schedule/entry/ScheduleEntry.ts +++ b/src/main/angular/src/app/api/schedule/entry/ScheduleEntry.ts @@ -2,13 +2,18 @@ import {validateBooleanNotNull, validateDateAllowNull, validateNumberAllowNull, import {Timestamp} from "../../Timestamp"; import {Property} from "../../property/Property"; import {Bulk} from "../../bulk/Bulk"; - -function getDaySeconds(date: Date): number { - return date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds(); -} +import {formatNumber} from "@angular/common"; export class ScheduleEntry { + readonly todo: string; + + readonly dayMinute: number; + + readonly anyWeekday: boolean; + + readonly executable: boolean; + private constructor( readonly id: number, readonly position: number, @@ -35,7 +40,24 @@ export class ScheduleEntry { readonly value: number, 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 { @@ -75,4 +97,8 @@ export class ScheduleEntry { 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'); + } + } diff --git a/src/main/angular/src/app/api/schedule/entry/Zenith.ts b/src/main/angular/src/app/api/schedule/entry/Zenith.ts new file mode 100644 index 0000000..130002c --- /dev/null +++ b/src/main/angular/src/app/api/schedule/entry/Zenith.ts @@ -0,0 +1,12 @@ +export class Zenith { + + constructor( + readonly title: string, + readonly value: number, + readonly sunrise: boolean, + readonly sunset: boolean, + ) { + // - + } + +} diff --git a/src/main/angular/src/app/app.module.ts b/src/main/angular/src/app/app.module.ts index d5379d7..60a5d0b 100644 --- a/src/main/angular/src/app/app.module.ts +++ b/src/main/angular/src/app/app.module.ts @@ -1,4 +1,4 @@ -import {NgModule} from '@angular/core'; +import {LOCALE_ID, NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; 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 {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({ declarations: [ AppComponent, @@ -37,6 +45,8 @@ import {EntryValueComponent} from './shared/entry-value/entry-value.component'; BulkEditorComponent, LeftPadDirective, EntryValueComponent, + BoolComponent, + DurationComponent, ], imports: [ BrowserModule, @@ -45,7 +55,9 @@ import {EntryValueComponent} from './shared/entry-value/entry-value.component'; FormsModule, FontAwesomeModule, ], - providers: [], + providers: [ + {provide: LOCALE_ID, useValue: 'de-DE'} + ], bootstrap: [AppComponent] }) export class AppModule { diff --git a/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.html b/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.html index 4d08705..5572775 100644 --- a/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.html +++ b/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.html @@ -1,151 +1,113 @@ +
+ +
+
+
- - - - - - - -

- -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
- - - - - - +
+
+ + + + + +
+
- +
+
+
+ +
+
+ +
+
+
- - - - - - - - - - +
+
+
+ +/- +
+
+ +
+
+
+
+ Überspringen +
+
+ +
+
+
- - - - - - - - - - - - - - - - - - - - - - - - -
- -
 MoDiMiDoFrSaSoTypSonnenstandUhrzeitUnschärfeNächste AusführungEingeschaft setzenMassenausführung   
- - - + + + + - - - : - - - - {{entry.nextClearTimestamp.dayName}}{{entry.nextClearTimestamp.timeString}}{{entry.nextFuzzyTimestamp.dayName}}{{entry.nextFuzzyTimestamp.timeString}} - - - - - - - - - - - -
+
+
+ {{ relativeDate(entry.nextFuzzyTimestamp?.date) }} + + (eig: {{ entry.nextClearTimestamp.date | date:'HH:mm' }}) + +
+
+ {{ entry.todo }} +
+
+
+
diff --git a/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.less b/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.less index 4f5cc2b..c1539c7 100644 --- a/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.less +++ b/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.less @@ -1,19 +1,134 @@ -select { - background-color: transparent; - border-width: 0; - width: 100%; - outline: none; - font-family: monospace; +#title { + margin: 0.5em; } -th { - background-color: lightblue; -} +#entries { + 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) { - border: none; + .entry { + 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; } } diff --git a/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.ts b/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.ts index 6d7cfee..948b679 100644 --- a/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.ts +++ b/src/main/angular/src/app/pages/schedule/editor/schedule-editor.component.ts @@ -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 {Schedule} from "../../../api/schedule/Schedule"; import {ScheduleEntry} from "../../../api/schedule/entry/ScheduleEntry"; 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 {PropertyService} from "../../../api/property/property.service"; -import {BulkService} from "../../../api/bulk/BulkService"; import {Update} from "../../../api/Update"; 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({ selector: 'app-schedule-editor', @@ -17,36 +22,47 @@ import {NO_OP} from "../../../api/api.service"; }) 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( readonly router: Router, readonly activatedRoute: ActivatedRoute, readonly scheduleService: ScheduleService, - readonly scheduleEntryService: ScheduleEntryService, - readonly propertyService: PropertyService, + readonly entryService: ScheduleEntryService, readonly bulkService: BulkService, + @Inject(LOCALE_ID) private locale: string, ) { - // nothing + // - } ngOnInit(): void { 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): void { @@ -57,25 +73,104 @@ export class ScheduleEditorComponent implements OnInit { this.router.navigate(['/ScheduleList']); 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 { - this.scheduleEntryService.create(this.schedule, NO_OP); + this.entryService.create(this.schedule, NO_OP); } delete(entry: ScheduleEntry): void { 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 { if (entry) { - this.scheduleEntryService.set(entry, key, value, NO_OP); + this.entryService.set(entry, key, value, NO_OP); } else { 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; } diff --git a/src/main/angular/src/app/pages/schedule/list/schedule-list.component.less b/src/main/angular/src/app/pages/schedule/list/schedule-list.component.less index 8131b13..bbf9dc5 100644 --- a/src/main/angular/src/app/pages/schedule/list/schedule-list.component.less +++ b/src/main/angular/src/app/pages/schedule/list/schedule-list.component.less @@ -1,5 +1,4 @@ .schedules { - font-size: 4vw; padding: 0.25em; .scheduleBox { @@ -132,8 +131,4 @@ } - @media (min-width: 1000px) { - font-size: 16px; - } - } diff --git a/src/main/angular/src/app/shared/bool/bool.component.html b/src/main/angular/src/app/shared/bool/bool.component.html new file mode 100644 index 0000000..ac38ccc --- /dev/null +++ b/src/main/angular/src/app/shared/bool/bool.component.html @@ -0,0 +1,18 @@ +
+ + {{ label }} + + + + + + + + + + + + + + +
diff --git a/src/main/angular/src/app/shared/bool/bool.component.less b/src/main/angular/src/app/shared/bool/bool.component.less new file mode 100644 index 0000000..ac41e01 --- /dev/null +++ b/src/main/angular/src/app/shared/bool/bool.component.less @@ -0,0 +1,8 @@ +div { + width: 100%; + height: 100%; + padding-top: 0.1em; + padding-bottom: 0.1em; + text-align: center; + background-color: gray; +} diff --git a/src/main/angular/src/app/shared/bool/bool.component.ts b/src/main/angular/src/app/shared/bool/bool.component.ts new file mode 100644 index 0000000..4c23c24 --- /dev/null +++ b/src/main/angular/src/app/shared/bool/bool.component.ts @@ -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 = new EventEmitter(); + + constructor() { + } + + ngOnInit(): void { + } + + toggle() { + + } + + color(): string { + if (this.value === true) { + return this.colorActive; + } else if (this.value === false) { + return this.colorInactive; + } + return ""; + } + +} diff --git a/src/main/angular/src/app/shared/duration/duration.component.html b/src/main/angular/src/app/shared/duration/duration.component.html new file mode 100644 index 0000000..db609bd --- /dev/null +++ b/src/main/angular/src/app/shared/duration/duration.component.html @@ -0,0 +1 @@ + diff --git a/src/main/angular/src/app/shared/duration/duration.component.less b/src/main/angular/src/app/shared/duration/duration.component.less new file mode 100644 index 0000000..8bd6bef --- /dev/null +++ b/src/main/angular/src/app/shared/duration/duration.component.less @@ -0,0 +1,3 @@ +input { + width: 100%; +} diff --git a/src/main/angular/src/app/shared/duration/duration.component.ts b/src/main/angular/src/app/shared/duration/duration.component.ts new file mode 100644 index 0000000..820d2cc --- /dev/null +++ b/src/main/angular/src/app/shared/duration/duration.component.ts @@ -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 = 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; + } + +} diff --git a/src/main/angular/src/assets/checked.svg b/src/main/angular/src/assets/checked.svg new file mode 100644 index 0000000..e2c2b53 --- /dev/null +++ b/src/main/angular/src/assets/checked.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/angular/src/assets/delete.svg b/src/main/angular/src/assets/delete.svg new file mode 100644 index 0000000..87658ae --- /dev/null +++ b/src/main/angular/src/assets/delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/angular/src/assets/dice.svg b/src/main/angular/src/assets/dice.svg new file mode 100644 index 0000000..b838726 --- /dev/null +++ b/src/main/angular/src/assets/dice.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/main/angular/src/assets/down.svg b/src/main/angular/src/assets/down.svg new file mode 100644 index 0000000..60b9d47 --- /dev/null +++ b/src/main/angular/src/assets/down.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/angular/src/assets/right.svg b/src/main/angular/src/assets/right.svg new file mode 100644 index 0000000..89f0740 --- /dev/null +++ b/src/main/angular/src/assets/right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/angular/src/assets/sun.svg b/src/main/angular/src/assets/sun.svg new file mode 100644 index 0000000..60d3560 --- /dev/null +++ b/src/main/angular/src/assets/sun.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/main/angular/src/assets/time.svg b/src/main/angular/src/assets/time.svg new file mode 100644 index 0000000..30f87e7 --- /dev/null +++ b/src/main/angular/src/assets/time.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/angular/src/assets/unchecked.svg b/src/main/angular/src/assets/unchecked.svg new file mode 100644 index 0000000..0cbcda5 --- /dev/null +++ b/src/main/angular/src/assets/unchecked.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index a5dcad5..bc98c42 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -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 { margin: 0; - font-family: arial, sans-serif; width: 100%; } @@ -9,9 +33,9 @@ a { } div { - box-sizing: border-box; overflow: hidden; white-space: nowrap; + box-sizing: border-box; text-overflow: ellipsis; } @@ -127,3 +151,11 @@ table.vertical { clear: both; margin-bottom: 5px; } + +.buttonPlus { + background-color: #8fbc8f; +} + +.buttonMinus { + background-color: #ef8787; +} diff --git a/src/main/java/de/ph87/homeautomation/DemoDataService.java b/src/main/java/de/ph87/homeautomation/DemoDataService.java index 1f8c2e0..ea8a851 100644 --- a/src/main/java/de/ph87/homeautomation/DemoDataService.java +++ b/src/main/java/de/ph87/homeautomation/DemoDataService.java @@ -7,6 +7,7 @@ import de.ph87.homeautomation.bulk.BulkDto; import de.ph87.homeautomation.bulk.entry.BulkEntryController; import de.ph87.homeautomation.bulk.entry.BulkEntryCreateDto; import de.ph87.homeautomation.channel.Channel; +import de.ph87.homeautomation.device.DeviceController; import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.PropertyRepository; import de.ph87.homeautomation.property.PropertyType; @@ -39,23 +40,32 @@ public class DemoDataService { private final ScheduleEntryController scheduleEntryController; + private final DeviceController deviceController; + public void insertDemoData() { if (!config.isInsertDemoData()) { return; } - final Property propertyDirect = createProperty("propertyDirect", "direct", PropertyType.BOOLEAN, null, null); - final Property propertyBulkBoolean = createProperty("propertyBulkBoolean", null, PropertyType.BOOLEAN, null, null); - final Property propertyBulkShutter = createProperty("propertyBulkShutter", null, PropertyType.SHUTTER, null, null); - final Property propertyBulkBrightness = createProperty("propertyBulkBrightness", null, PropertyType.BRIGHTNESS_PERCENT, null, null); - final Property propertyBulkColorTemperature = createProperty("propertyBulkColorTemperature", null, PropertyType.COLOR_TEMPERATURE, null, null); - final BulkDto bulk = bulkController.create(new BulkCreateDto("bulk", true)); - bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkBoolean.getId(), 1, 0)); - bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkShutter.getId(), 35, 0)); - bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkBrightness.getId(), 40, 0)); - bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkColorTemperature.getId(), 55, 0)); - final long scheduleId = createSchedule(true, "schedule"); + + final long schedule = createSchedule(true, "Schedule"); + + deviceController.create("DeviceSwitch"); + deviceController.create("DeviceSwitch"); + deviceController.create("DeviceSwitch"); + deviceController.create("DeviceShutter"); + deviceController.create("DeviceShutter"); + deviceController.create("DeviceShutter"); + + 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); - 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) { @@ -73,22 +83,23 @@ public class DemoDataService { 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) { - newScheduleEntry(scheduleId, enabled, ScheduleEntryType.TIME, null, hour, minute, second, fuzzySeconds, property, value, 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, 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(); scheduleEntryController.setEnabled(id, enabled); scheduleEntryController.setType(id, type.name()); if (zenith != null) { scheduleEntryController.setZenith(id, zenith.degrees() + ""); } - scheduleEntryController.setHour(id, hour); - scheduleEntryController.setMinute(id, minute); - scheduleEntryController.setSecond(id, second); + scheduleEntryController.daySecond(id, (hour * 60 + minute) * 60 + second); scheduleEntryController.setFuzzySeconds(id, fuzzySeconds); - scheduleEntryController.setProperty(id, property == null ? null : property.getId()); scheduleEntryController.setValue(id, value); scheduleEntryController.setBulk(id, bulk == null ? null : bulk.getId()); scheduleEntryController.setSkip(id, 1); diff --git a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryController.java b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryController.java index 9ac5160..dddfadc 100644 --- a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryController.java +++ b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryController.java @@ -1,8 +1,10 @@ package de.ph87.homeautomation.schedule.entry; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +@Slf4j @RestController @RequestMapping("schedule/entry") @RequiredArgsConstructor @@ -75,19 +77,9 @@ public class ScheduleEntryController { return scheduleEntryWriter.setZenith(id, Double.parseDouble(value)); } - @PostMapping("set/{id}/hour") - public ScheduleEntryDto setHour(@PathVariable final long id, @RequestBody final int value) { - 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}/daySecond") + public ScheduleEntryDto daySecond(@PathVariable final long id, @RequestBody final int daySecond) { + return scheduleEntryWriter.daySecond(id, daySecond); } @PostMapping("set/{id}/fuzzySeconds") diff --git a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryWriter.java b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryWriter.java index 46e516b..b35bbd9 100644 --- a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryWriter.java +++ b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryWriter.java @@ -8,8 +8,10 @@ import de.ph87.homeautomation.schedule.ScheduleWriter; import de.ph87.homeautomation.web.BadRequestException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -24,6 +26,12 @@ import static java.lang.Math.min; @RequiredArgsConstructor 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 ScheduleReader scheduleReader; @@ -114,16 +122,19 @@ public class ScheduleEntryWriter { return modifyValue(id, ScheduleEntry::setZenith, value); } - public ScheduleEntryDto setHour(final long id, final int value) { - return modifyValue(id, ScheduleEntry::setHour, value); - } - - public ScheduleEntryDto setMinute(final long id, final int value) { - return modifyValue(id, ScheduleEntry::setMinute, value); - } - - public ScheduleEntryDto setSecond(final long id, final int value) { - return modifyValue(id, ScheduleEntry::setSecond, value); + public ScheduleEntryDto daySecond(final long id, final int daySecond) { + 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; + final int minute = (daySecond / MINUTE_SECONDS) % 60; + final int second = daySecond % 60; + return modify(id, entry -> { + entry.setHour(hour); + entry.setMinute(minute); + entry.setSecond(second); + }); } public ScheduleEntryDto setFuzzySeconds(final long id, final int value) {