From bb2af445427d4191e9b08e472ac5c595795a60a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Wed, 27 Nov 2024 09:45:19 +0100 Subject: [PATCH] Tunable --- data/G | 44 ++++--- .../angular/src/app/api/Tunable/Tunable.ts | 37 ++++++ .../src/app/api/Tunable/TunableFilter.ts | 5 + .../src/app/api/Tunable/tunable.service.ts | 40 ++++++ src/main/angular/src/app/app.component.html | 1 + src/main/angular/src/app/app.routes.ts | 2 + .../device-list-page.component.html | 2 +- .../knx-group-list-page.component.html | 2 +- .../shutter-list-page.component.html | 2 +- .../tunable-list-page.component.html | 8 ++ .../tunable-list-page.component.less | 5 + .../tunable-list-page.component.ts | 68 +++++++++++ .../tunable-list/tunable-list.component.html | 33 +++++ .../tunable-list/tunable-list.component.less | 65 ++++++++++ .../tunable-list/tunable-list.component.ts | 54 ++++++++ src/main/angular/src/styles.less | 2 +- .../de/ph87/home/common/map/MapHelper.java | 69 +++++++++++ .../java/de/ph87/home/demo/DemoService.java | 95 ++++++++++----- .../java/de/ph87/home/tunable/Tunable.java | 51 ++++++++ .../ph87/home/tunable/TunableController.java | 85 +++++++++++++ .../java/de/ph87/home/tunable/TunableDto.java | 96 +++++++++++++++ .../de/ph87/home/tunable/TunableFilter.java | 41 +++++++ .../ph87/home/tunable/TunableRepository.java | 15 +++ .../de/ph87/home/tunable/TunableService.java | 115 ++++++++++++++++++ 24 files changed, 887 insertions(+), 50 deletions(-) create mode 100644 src/main/angular/src/app/api/Tunable/Tunable.ts create mode 100644 src/main/angular/src/app/api/Tunable/TunableFilter.ts create mode 100644 src/main/angular/src/app/api/Tunable/tunable.service.ts create mode 100644 src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.html create mode 100644 src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.less create mode 100644 src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.ts create mode 100644 src/main/angular/src/app/shared/tunable-list/tunable-list.component.html create mode 100644 src/main/angular/src/app/shared/tunable-list/tunable-list.component.less create mode 100644 src/main/angular/src/app/shared/tunable-list/tunable-list.component.ts create mode 100644 src/main/java/de/ph87/home/common/map/MapHelper.java create mode 100644 src/main/java/de/ph87/home/tunable/Tunable.java create mode 100644 src/main/java/de/ph87/home/tunable/TunableController.java create mode 100644 src/main/java/de/ph87/home/tunable/TunableDto.java create mode 100644 src/main/java/de/ph87/home/tunable/TunableFilter.java create mode 100644 src/main/java/de/ph87/home/tunable/TunableRepository.java create mode 100644 src/main/java/de/ph87/home/tunable/TunableService.java diff --git a/data/G b/data/G index 8aa03a8..0dc2dd6 100644 --- a/data/G +++ b/data/G @@ -8,8 +8,8 @@ - - + + @@ -39,8 +39,8 @@ - - + + @@ -58,8 +58,8 @@ - - + + @@ -95,8 +95,8 @@ - - + + @@ -126,7 +126,7 @@ - + @@ -144,8 +144,8 @@ - - + + @@ -174,7 +174,7 @@ - + @@ -204,13 +204,13 @@ - + - + - + @@ -227,6 +227,20 @@ + + + + + + + + + + + + + + diff --git a/src/main/angular/src/app/api/Tunable/Tunable.ts b/src/main/angular/src/app/api/Tunable/Tunable.ts new file mode 100644 index 0000000..ce3c142 --- /dev/null +++ b/src/main/angular/src/app/api/Tunable/Tunable.ts @@ -0,0 +1,37 @@ +import {Property} from "../Property/Property"; +import {orNull, validateString} from "../common/validators"; + +export class Tunable { + constructor( + readonly uuid: string, + readonly name: string, + readonly slug: string, + readonly statePropertyId: string, + readonly stateProperty: Property | null, + readonly brightnessPropertyId: string, + readonly brightnessProperty: Property | null, + readonly coldnessPropertyId: string, + readonly coldnessProperty: Property | null, + ) { + // + } + + static fromJson(json: any): Tunable { + return new Tunable( + validateString(json.uuid), + validateString(json.name), + validateString(json.slug), + validateString(json.statePropertyId), + orNull(json.stateProperty, Property.fromJson), + validateString(json.brightnessPropertyId), + orNull(json.brightnessProperty, Property.fromJson), + validateString(json.coldnessPropertyId), + orNull(json.coldnessProperty, Property.fromJson), + ); + } + + static trackBy(index: number, tunable: Tunable) { + return tunable.uuid; + } + +} diff --git a/src/main/angular/src/app/api/Tunable/TunableFilter.ts b/src/main/angular/src/app/api/Tunable/TunableFilter.ts new file mode 100644 index 0000000..9e2bc67 --- /dev/null +++ b/src/main/angular/src/app/api/Tunable/TunableFilter.ts @@ -0,0 +1,5 @@ +export class TunableFilter { + + search: string = ""; + +} diff --git a/src/main/angular/src/app/api/Tunable/tunable.service.ts b/src/main/angular/src/app/api/Tunable/tunable.service.ts new file mode 100644 index 0000000..3686a43 --- /dev/null +++ b/src/main/angular/src/app/api/Tunable/tunable.service.ts @@ -0,0 +1,40 @@ +import {Injectable} from '@angular/core'; +import {CrudService} from '../common/CrudService'; +import {Tunable} from './Tunable'; +import {ApiService} from '../common/api.service'; +import {Next} from '../common/types'; + +import {TunableFilter} from './TunableFilter'; + +@Injectable({ + providedIn: 'root' +}) +export class TunableService extends CrudService { + + constructor( + api: ApiService, + ) { + super(api, ['Tunable'], Tunable.fromJson); + } + + getByUuid(uuid: string, next: Next): void { + this.getSingle(['getByUuid', uuid], next); + } + + list(filter: TunableFilter | null, next: Next): void { + this.postList(['list'], filter, next); + } + + setState(tunable: Tunable, state: boolean, next?: Next): void { + this.getNone(['setState', tunable.uuid, state], next); + } + + setBrightness(tunable: Tunable, brightness: number, next?: Next): void { + this.getNone(['setBrightness', tunable.uuid, brightness], next); + } + + setColdness(tunable: Tunable, coldness: number, next?: Next): void { + this.getNone(['setColdness', tunable.uuid, coldness], next); + } + +} diff --git a/src/main/angular/src/app/app.component.html b/src/main/angular/src/app/app.component.html index 923fcbd..f56d361 100644 --- a/src/main/angular/src/app/app.component.html +++ b/src/main/angular/src/app/app.component.html @@ -1,6 +1,7 @@
diff --git a/src/main/angular/src/app/app.routes.ts b/src/main/angular/src/app/app.routes.ts index 369cda4..5466694 100644 --- a/src/main/angular/src/app/app.routes.ts +++ b/src/main/angular/src/app/app.routes.ts @@ -2,9 +2,11 @@ import {Routes} from '@angular/router'; import {KnxGroupListPageComponent} from './pages/knx-group-list-page/knx-group-list-page.component'; import {DeviceListPageComponent} from './pages/device-list-page/device-list-page.component'; import {ShutterListPageComponent} from './pages/shutter-list-page/shutter-list-page.component'; +import {TunableListPageComponent} from './pages/tunable-list-page/tunable-list-page.component'; export const routes: Routes = [ {path: 'DeviceList', component: DeviceListPageComponent}, + {path: 'TunableList', component: TunableListPageComponent}, {path: 'ShutterList', component: ShutterListPageComponent}, {path: 'GroupList', component: KnxGroupListPageComponent}, {path: '**', redirectTo: 'GroupList'}, diff --git a/src/main/angular/src/app/pages/device-list-page/device-list-page.component.html b/src/main/angular/src/app/pages/device-list-page/device-list-page.component.html index 77ac081..103ffac 100644 --- a/src/main/angular/src/app/pages/device-list-page/device-list-page.component.html +++ b/src/main/angular/src/app/pages/device-list-page/device-list-page.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.html b/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.html index a75e98b..1ec261d 100644 --- a/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.html +++ b/src/main/angular/src/app/pages/knx-group-list-page/knx-group-list-page.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html index 3bf82dc..b5cc55c 100644 --- a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html +++ b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.html b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.html new file mode 100644 index 0000000..e256d68 --- /dev/null +++ b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.html @@ -0,0 +1,8 @@ +
+
+ +
+
+ +
+
diff --git a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.less b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.less new file mode 100644 index 0000000..e4e2c79 --- /dev/null +++ b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.less @@ -0,0 +1,5 @@ +@import "../../../config"; + +input { + border-bottom: @border solid lightgray; +} diff --git a/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.ts b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.ts new file mode 100644 index 0000000..c7dca62 --- /dev/null +++ b/src/main/angular/src/app/pages/tunable-list-page/tunable-list-page.component.ts @@ -0,0 +1,68 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {TunableListComponent} from '../../shared/tunable-list/tunable-list.component'; +import {Tunable} from '../../api/Tunable/Tunable'; +import {TunableService} from '../../api/Tunable/tunable.service'; +import {FormsModule} from '@angular/forms'; +import {TunableFilter} from '../../api/Tunable/TunableFilter'; +import {Subscription} from 'rxjs'; +import {ApiService} from '../../api/common/api.service'; + +@Component({ + selector: 'app-tunable-list-page', + standalone: true, + imports: [ + TunableListComponent, + FormsModule + ], + templateUrl: './tunable-list-page.component.html', + styleUrl: './tunable-list-page.component.less' +}) +export class TunableListPageComponent implements OnInit, OnDestroy { + + private readonly subs: Subscription[] = []; + + protected tunableList: Tunable[] = []; + + protected filter: TunableFilter = new TunableFilter(); + + private fetchTimeout: any; + + constructor( + protected readonly tunableService: TunableService, + protected readonly apiService: ApiService, + ) { + // + } + + ngOnInit(): void { + this.fetch(); + this.subs.push(this.tunableService.subscribe(tunable => this.updateTunable(tunable))); + this.apiService.connected(() => this.fetch()); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + + fetchDelayed() { + if (this.fetchTimeout) { + clearTimeout(this.fetchTimeout); + this.fetchTimeout = undefined; + } + this.fetchTimeout = setTimeout(() => this.fetch(), 300) + } + + private fetch() { + this.tunableService.list(this.filter, list => this.tunableList = list) + } + + private updateTunable(tunable: Tunable) { + const index = this.tunableList.findIndex(d => d.uuid === tunable.uuid); + if (index >= 0) { + this.tunableList.splice(index, 1, tunable); + } else { + this.fetch(); + } + } + +} diff --git a/src/main/angular/src/app/shared/tunable-list/tunable-list.component.html b/src/main/angular/src/app/shared/tunable-list/tunable-list.component.html new file mode 100644 index 0000000..996d333 --- /dev/null +++ b/src/main/angular/src/app/shared/tunable-list/tunable-list.component.html @@ -0,0 +1,33 @@ +
+ +
+ +
+ +
+ {{ tunable.name }} +
+ +
+ {{ tunable.stateProperty?.lastValueChange | relative:now }} +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+ +
+ +
diff --git a/src/main/angular/src/app/shared/tunable-list/tunable-list.component.less b/src/main/angular/src/app/shared/tunable-list/tunable-list.component.less new file mode 100644 index 0000000..6a1e25b --- /dev/null +++ b/src/main/angular/src/app/shared/tunable-list/tunable-list.component.less @@ -0,0 +1,65 @@ +@import "../../../config"; + +.tunableList { + overflow-y: auto; + height: 100%; + + .tunable { + + .name { + float: left; + } + + .timestamp { + float: right; + font-size: 80%; + } + + .sliders { + float: left; + clear: left; + width: 60%; + overflow: visible; + padding-top: 0.4em; + + .slider { + float: left; + clear: left; + margin-left: @space; + width: 100%; + overflow: visible; + + input { + width: 100%; + height: 2em; + } + + } + + } + + .actions { + float: right; + + .switch { + float: left; + margin-left: @space; + width: 4em; + aspect-ratio: 1; + } + + .switchOn { + //noinspection CssUnknownTarget + background-image: url("/switchOn.svg"); + } + + .switchOff { + //noinspection CssUnknownTarget + background-image: url("/switchOff.svg"); + } + + } + + } + +} diff --git a/src/main/angular/src/app/shared/tunable-list/tunable-list.component.ts b/src/main/angular/src/app/shared/tunable-list/tunable-list.component.ts new file mode 100644 index 0000000..07b685f --- /dev/null +++ b/src/main/angular/src/app/shared/tunable-list/tunable-list.component.ts @@ -0,0 +1,54 @@ +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {NgClass, NgForOf} from '@angular/common'; +import {Tunable} from '../../api/Tunable/Tunable'; +import {TunableService} from '../../api/Tunable/tunable.service'; +import {RelativePipe} from '../../api/common/relative.pipe'; +import {Subscription, timer} from 'rxjs'; +import {FormsModule} from '@angular/forms'; + +@Component({ + selector: 'app-tunable-list', + standalone: true, + imports: [ + NgForOf, + NgClass, + RelativePipe, + FormsModule + ], + templateUrl: './tunable-list.component.html', + styleUrl: './tunable-list.component.less' +}) +export class TunableListComponent implements OnInit, OnDestroy { + + @Input() + tunableList: Tunable[] = []; + + protected readonly Tunable = Tunable; + + protected now: Date = new Date(); + + private readonly subs: Subscription[] = []; + + constructor( + protected readonly tunableService: TunableService, + ) { + // + } + + ngOnInit(): void { + this.now = new Date(); + this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date())); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + + ngClass(tunable: Tunable) { + return { + "stateOn": tunable.stateProperty?.state?.value === true, + "stateOff": tunable.stateProperty?.state?.value === false, + }; + } + +} diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index c6aada8..f9c2c35 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -17,7 +17,7 @@ div { overflow: hidden; } -input { +input[type=text] { all: unset; width: calc(100% - 2 * 0.2em - @border); padding-left: 0.2em; diff --git a/src/main/java/de/ph87/home/common/map/MapHelper.java b/src/main/java/de/ph87/home/common/map/MapHelper.java new file mode 100644 index 0000000..7117f60 --- /dev/null +++ b/src/main/java/de/ph87/home/common/map/MapHelper.java @@ -0,0 +1,69 @@ +package de.ph87.home.common.map; + +import jakarta.annotation.Nullable; +import lombok.NonNull; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +public class MapHelper { + + public static void apply(@Nullable final T t, @NonNull final Consumer<@NonNull T> apply) { + if (t == null) { + return; + } + apply.accept(t); + } + + public static void apply(@Nullable final T t, @NonNull final U u, @NonNull final BiConsumer<@NonNull T, @NonNull U> apply) { + if (t == null) { + return; + } + apply.accept(t, u); + } + + @Nullable + public static R map(@Nullable final T t, @NonNull final Function<@NonNull T, R> map) { + return map(t, map, null); + } + + @Nullable + public static R map(@Nullable final T t, @NonNull final Function<@NonNull T, @NonNull R> map, @Nullable final R fallback) { + if (t == null) { + return fallback; + } + return map.apply(t); + } + + @NonNull + public static R mapNN(@Nullable final T t, @NonNull final Function<@NonNull T, @NonNull R> map, @NonNull final R fallback) { + if (t == null) { + return fallback; + } + return map.apply(t); + } + + @Nullable + public static R biMap(@Nullable final T t, @NonNull final U u, @NonNull final BiFunction<@NonNull T, @NonNull U, R> map) { + return biMap(t, u, map, null); + } + + @Nullable + public static R biMap(@Nullable final T t, @NonNull final U u, @NonNull final BiFunction<@NonNull T, @NonNull U, @NonNull R> map, @Nullable final R fallback) { + if (t == null) { + return fallback; + } + return map.apply(t, u); + } + + @NonNull + public static R biMapNN(@Nullable final T t, @NonNull final U u, @NonNull final BiFunction<@NonNull T, @NonNull U, @NonNull R> map, @NonNull final R fallback) { + if (t == null) { + return fallback; + } + return map.apply(t, u); + } + +} diff --git a/src/main/java/de/ph87/home/demo/DemoService.java b/src/main/java/de/ph87/home/demo/DemoService.java index 92fd85f..1254d76 100644 --- a/src/main/java/de/ph87/home/demo/DemoService.java +++ b/src/main/java/de/ph87/home/demo/DemoService.java @@ -4,6 +4,9 @@ import de.ph87.home.device.DeviceService; import de.ph87.home.knx.property.KnxPropertyService; import de.ph87.home.knx.property.KnxPropertyType; import de.ph87.home.shutter.ShutterService; +import de.ph87.home.tunable.TunableService; +import jakarta.annotation.Nullable; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationStartedEvent; @@ -24,43 +27,73 @@ public class DemoService { private final ShutterService shutterService; + private final TunableService tunableService; + @EventListener(ApplicationStartedEvent.class) public void startup() { - knxPropertyService.create("eg_ambiente", KnxPropertyType.BOOLEAN, adr(849), adr(848)); - deviceService.create("EG Ambiente", "eg_ambiente", "eg_ambiente"); + device("eg_ambiente", "EG Ambiente", 849, 848); + device("fernseher", "Wohnzimmer Fernseher", 20, 4); + device("verstaerker", "Wohnzimmer Verstärker", 825, 824); + device("fensterdeko", "Wohnzimmer Fenster", 1823, 1822); + device("haengelampe", "Wohnzimmer Hängelampe", 1794, 1799); + device("receiver", "Receiver", 2561, 2560); - knxPropertyService.create("fernseher", KnxPropertyType.BOOLEAN, adr(20), adr(4)); - deviceService.create("Wohnzimmer Fernseher", "fernseher", "fernseher"); + tunable("wohnzimmer_spots", "Wohnzimmer", 28, 828, 2344, 2343, 1825, 1824); + tunable("kueche_spots", "Küche", 2311, 2304, 2342, 2341, 2321, 2317); + tunable("arbeitszimmer_spots", "Arbeitszimmer", 2058, 2057, 2067, 2069, 2049, 2054); - knxPropertyService.create("verstaerker", KnxPropertyType.BOOLEAN, adr(825), adr(824)); - deviceService.create("Wohnzimmer Verstärker", "verstaerker", "verstaerker"); - - knxPropertyService.create("fensterdeko", KnxPropertyType.BOOLEAN, adr(1823), adr(1822)); - deviceService.create("Wohnzimmer Fenster", "fensterdeko", "fensterdeko"); - - knxPropertyService.create("haengelampe", KnxPropertyType.BOOLEAN, adr(1794), adr(1799)); - deviceService.create("Wohnzimmer Hängelampe", "haengelampe", "haengelampe"); - - knxPropertyService.create("receiver", KnxPropertyType.BOOLEAN, adr(2561), adr(2560)); - deviceService.create("Receiver", "receiver", "receiver"); - - knxPropertyService.create("wohnzimmer_links", KnxPropertyType.DOUBLE, adr(1048), adr(1048)); - shutterService.create("Wohnzimmer Links", "wohnzimmer_links", "wohnzimmer_links"); - - knxPropertyService.create("wohnzimmer_rechts", KnxPropertyType.DOUBLE, adr(1811), adr(1811)); - shutterService.create("Wohnzimmer Rechts", "wohnzimmer_rechts", "wohnzimmer_rechts"); - - knxPropertyService.create("kueche_seite", KnxPropertyType.DOUBLE, adr(2316), adr(2316)); - shutterService.create("Küche Seite", "kueche_seite", "kueche_seite"); - - knxPropertyService.create("kueche_theke", KnxPropertyType.DOUBLE, adr(2320), adr(2320)); - shutterService.create("Küche Theke", "kueche_theke", "kueche_theke"); - - knxPropertyService.create("kueche_tuer", KnxPropertyType.DOUBLE, adr(2324), adr(2324)); - shutterService.create("Küche Tür", "kueche_tuer", "kueche_tuer"); + shutter("wohnzimmer_links", "Wohnzimmer Links", 1048); + shutter("wohnzimmer_rechts", "Wohnzimmer Rechts", 1811); + shutter("kueche_seite", "Küche Seite", 2316); + shutter("kueche_theke", "Küche Theke", 2320); + shutter("kueche_tuer", "Küche Tür", 2324); } - private static GroupAddress adr(final int rawGroupAddress) { + private void device( + @NonNull final String slug, + @NonNull final String name, + @Nullable final Integer stateRead, + @Nullable final Integer stateWrite + ) { + knxPropertyService.create(slug, KnxPropertyType.BOOLEAN, adr(stateRead), adr(stateWrite)); + deviceService.create(name, slug, slug); + } + + private void shutter( + @NonNull final String slug, + @NonNull final String name, + @Nullable final Integer positionReadWrite + ) { + knxPropertyService.create(slug, KnxPropertyType.DOUBLE, adr(positionReadWrite), adr(positionReadWrite)); + shutterService.create(name, slug, slug); + } + + private void tunable( + @NonNull final String slug, + @NonNull final String name, + @Nullable final Integer stateRead, + @Nullable final Integer stateWrite, + @Nullable final Integer brightnessRead, + @Nullable final Integer brightnessWrite, + @Nullable final Integer coldnessRead, + @Nullable final Integer coldnessWrite + ) { + final String stateProperty = slug + "_state"; + knxPropertyService.create(stateProperty, KnxPropertyType.BOOLEAN, adr(stateRead), adr(stateWrite)); + + final String brightnessProperty = slug + "_brightness"; + knxPropertyService.create(brightnessProperty, KnxPropertyType.DOUBLE, adr(brightnessRead), adr(brightnessWrite)); + + final String coldnessProperty = slug + "_coldness"; + knxPropertyService.create(coldnessProperty, KnxPropertyType.DOUBLE, adr(coldnessRead), adr(coldnessWrite)); + + tunableService.create(name, slug, stateProperty, brightnessProperty, coldnessProperty); + } + + private static GroupAddress adr(final Integer rawGroupAddress) { + if (rawGroupAddress == null) { + return null; + } return GroupAddress.freeStyle(rawGroupAddress); } diff --git a/src/main/java/de/ph87/home/tunable/Tunable.java b/src/main/java/de/ph87/home/tunable/Tunable.java new file mode 100644 index 0000000..b781cb2 --- /dev/null +++ b/src/main/java/de/ph87/home/tunable/Tunable.java @@ -0,0 +1,51 @@ +package de.ph87.home.tunable; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.*; + +import java.util.UUID; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class Tunable { + + @Id + @NonNull + private String uuid = UUID.randomUUID().toString(); + + @NonNull + @Column(nullable = false) + private String name; + + @NonNull + @Column(nullable = false, unique = true) + private String slug; + + @Setter + @NonNull + @Column(nullable = false) + private String statePropertyId; + + @Setter + @NonNull + @Column(nullable = false) + private String brightnessPropertyId; + + @Setter + @NonNull + @Column(nullable = false) + private String coldnessPropertyId; + + public Tunable(@NonNull final String name, @NonNull final String slug, @NonNull final String statePropertyId, @NonNull final String brightnessPropertyId, @NonNull final String coldnessPropertyId) { + this.name = name; + this.slug = slug; + this.statePropertyId = statePropertyId; + this.brightnessPropertyId = brightnessPropertyId; + this.coldnessPropertyId = coldnessPropertyId; + } + +} diff --git a/src/main/java/de/ph87/home/tunable/TunableController.java b/src/main/java/de/ph87/home/tunable/TunableController.java new file mode 100644 index 0000000..d6d0687 --- /dev/null +++ b/src/main/java/de/ph87/home/tunable/TunableController.java @@ -0,0 +1,85 @@ +package de.ph87.home.tunable; + +import de.ph87.home.property.PropertyNotFound; +import de.ph87.home.property.PropertyNotWritable; +import de.ph87.home.property.PropertyTypeMismatch; +import jakarta.annotation.Nullable; +import jakarta.servlet.http.HttpServletRequest; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tuwien.auto.calimero.KNXFormatException; + +import java.util.List; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("Tunable") +public class TunableController { + + private final TunableService tunableService; + + @NonNull + @GetMapping("getByUuid/{id}") + @ExceptionHandler(KNXFormatException.class) + private TunableDto getByUuid(@PathVariable final String id, @NonNull final HttpServletRequest request) { + log.debug("getByUuid: path={}", request.getServletPath()); + return tunableService.getByUuidDto(id); + } + + @NonNull + @RequestMapping(value = "list", method = {RequestMethod.GET, RequestMethod.POST}) + private List list(@RequestBody(required = false) @Nullable final TunableFilter filter, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch { + log.debug("list: path={} filter={}", request.getServletPath(), filter); + return tunableService.list(filter); + } + + @NonNull + @GetMapping("get/{uuidOrSlug}") + private TunableDto get(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) { + log.debug("get: path={}", request.getServletPath()); + return tunableService.getByUuidOrSlugDto(uuidOrSlug); + } + + @Nullable + @GetMapping("getState/{uuidOrSlug}") + private Boolean getState(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch { + log.debug("getState: path={}", request.getServletPath()); + return tunableService.getByUuidOrSlugDto(uuidOrSlug).getStateValue(); + } + + @GetMapping("setState/{uuidOrSlug}/{state}") + private TunableDto setState(@PathVariable @NonNull final String uuidOrSlug, @PathVariable final boolean state, @NonNull final HttpServletRequest request) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { + log.debug("setState: path={}", request.getServletPath()); + return tunableService.setState(uuidOrSlug, state); + } + + @Nullable + @GetMapping("getBrightness/{uuidOrSlug}") + private Double getBrightness(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch { + log.debug("getBrightness: path={}", request.getServletPath()); + return tunableService.getByUuidOrSlugDto(uuidOrSlug).getBrightnessValue(); + } + + @GetMapping("setBrightness/{uuidOrSlug}/{brightness}") + private TunableDto setBrightness(@PathVariable @NonNull final String uuidOrSlug, @PathVariable final double brightness, @NonNull final HttpServletRequest request) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { + log.debug("setBrightness: path={}", request.getServletPath()); + return tunableService.setBrightness(uuidOrSlug, brightness); + } + + @Nullable + @GetMapping("getColdness/{uuidOrSlug}") + private Double getColdness(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch { + log.debug("getColdness: path={}", request.getServletPath()); + return tunableService.getByUuidOrSlugDto(uuidOrSlug).getColdnessValue(); + } + + @GetMapping("setColdness/{uuidOrSlug}/{coldness}") + private TunableDto setColdness(@PathVariable @NonNull final String uuidOrSlug, @PathVariable final double coldness, @NonNull final HttpServletRequest request) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { + log.debug("setColdness: path={}", request.getServletPath()); + return tunableService.setColdness(uuidOrSlug, coldness); + } + +} diff --git a/src/main/java/de/ph87/home/tunable/TunableDto.java b/src/main/java/de/ph87/home/tunable/TunableDto.java new file mode 100644 index 0000000..6e4c0e0 --- /dev/null +++ b/src/main/java/de/ph87/home/tunable/TunableDto.java @@ -0,0 +1,96 @@ +package de.ph87.home.tunable; + +import de.ph87.home.property.PropertyDto; +import de.ph87.home.property.PropertyTypeMismatch; +import de.ph87.home.web.IWebSocketMessage; +import jakarta.annotation.Nullable; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +public class TunableDto implements IWebSocketMessage { + + @ToString.Exclude + private final List websocketTopic = List.of("Tunable"); + + @NonNull + private final String uuid; + + @NonNull + private final String name; + + @NonNull + private final String slug; + + @NonNull + private final String statePropertyId; + + @NonNull + private final String brightnessPropertyId; + + @NonNull + private final String coldnessPropertyId; + + @Nullable + @ToString.Exclude + private final PropertyDto stateProperty; + + @Nullable + @ToString.Exclude + private final PropertyDto brightnessProperty; + + @Nullable + @ToString.Exclude + private final PropertyDto coldnessProperty; + + public TunableDto(@NonNull final Tunable tunable, @Nullable final PropertyDto stateProperty, @Nullable final PropertyDto brightnessProperty, @Nullable final PropertyDto coldnessProperty) { + this.uuid = tunable.getUuid(); + this.name = tunable.getName(); + this.slug = tunable.getSlug(); + this.statePropertyId = tunable.getStatePropertyId(); + this.brightnessPropertyId = tunable.getBrightnessPropertyId(); + this.coldnessPropertyId = tunable.getColdnessPropertyId(); + this.stateProperty = stateProperty; + this.brightnessProperty = brightnessProperty; + this.coldnessProperty = coldnessProperty; + } + + @Nullable + @ToString.Include + public String state() { + try { + return "" + getStateValue(); + } catch (PropertyTypeMismatch e) { + return "[PropertyTypeMismatch]"; + } + } + + @Nullable + public Boolean getStateValue() throws PropertyTypeMismatch { + if (stateProperty == null) { + return null; + } + return stateProperty.getStateValueAs(Boolean.class); + } + + @Nullable + public Double getBrightnessValue() throws PropertyTypeMismatch { + if (brightnessProperty == null) { + return null; + } + return brightnessProperty.getStateValueAs(Double.class); + } + + @Nullable + public Double getColdnessValue() throws PropertyTypeMismatch { + if (coldnessProperty == null) { + return null; + } + return coldnessProperty.getStateValueAs(Double.class); + } + +} diff --git a/src/main/java/de/ph87/home/tunable/TunableFilter.java b/src/main/java/de/ph87/home/tunable/TunableFilter.java new file mode 100644 index 0000000..1204705 --- /dev/null +++ b/src/main/java/de/ph87/home/tunable/TunableFilter.java @@ -0,0 +1,41 @@ +package de.ph87.home.tunable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.ph87.home.common.crud.AbstractSearchFilter; +import de.ph87.home.property.PropertyTypeMismatch; +import jakarta.annotation.Nullable; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString +public class TunableFilter extends AbstractSearchFilter { + + @Nullable + @JsonProperty + private Boolean stateNull; + + @Nullable + @JsonProperty + private Boolean stateTrue; + + @Nullable + @JsonProperty + private Boolean stateFalse; + + public boolean filter(@NonNull final TunableDto dto) throws PropertyTypeMismatch { + if (stateNull != null && stateNull != (dto.getStateProperty() == null)) { + return false; + } + final Boolean value = dto.getStateValue(); + if (stateTrue != null && (value == null || stateTrue != value)) { + return false; + } + if (stateFalse != null && (value == null || stateFalse == value)) { + return false; + } + return search(dto.getName()); + } + +} diff --git a/src/main/java/de/ph87/home/tunable/TunableRepository.java b/src/main/java/de/ph87/home/tunable/TunableRepository.java new file mode 100644 index 0000000..629bbfc --- /dev/null +++ b/src/main/java/de/ph87/home/tunable/TunableRepository.java @@ -0,0 +1,15 @@ +package de.ph87.home.tunable; + +import lombok.NonNull; +import org.springframework.data.repository.ListCrudRepository; + +import java.util.List; +import java.util.Optional; + +public interface TunableRepository extends ListCrudRepository { + + Optional findByUuidOrSlug(@NonNull String uuid, @NonNull String slug); + + List findDistinctByStatePropertyIdOrBrightnessPropertyIdOrColdnessPropertyId(@NonNull String state, @NonNull String brightness, @NonNull String coldness); + +} diff --git a/src/main/java/de/ph87/home/tunable/TunableService.java b/src/main/java/de/ph87/home/tunable/TunableService.java new file mode 100644 index 0000000..45bdc4f --- /dev/null +++ b/src/main/java/de/ph87/home/tunable/TunableService.java @@ -0,0 +1,115 @@ +package de.ph87.home.tunable; + +import de.ph87.home.common.crud.CrudAction; +import de.ph87.home.common.crud.EntityNotFound; +import de.ph87.home.property.*; +import jakarta.annotation.Nullable; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class TunableService { + + private final PropertyService propertyService; + + private final TunableRepository tunableRepository; + + private final ApplicationEventPublisher applicationEventPublisher; + + @NonNull + public TunableDto create(@NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty, @NonNull final String brightnessProperty, @NonNull final String coldnessProperty) { + return publish(tunableRepository.save(new Tunable(name, slug, stateProperty, brightnessProperty, coldnessProperty)), CrudAction.UPDATED); + } + + @NonNull + public TunableDto setState(@NonNull final String uuidOrSlug, final boolean state) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { + log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state); + final Tunable tunable = getByUuidOrSlug(uuidOrSlug); + propertyService.write(tunable.getStatePropertyId(), state, Boolean.class); + return publish(tunable, CrudAction.UPDATED); + } + + @NonNull + public TunableDto setBrightness(@NonNull final String uuidOrSlug, final double brightness) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { + log.debug("setBrightness: uuidOrSlug={}, brightness={}", uuidOrSlug, brightness); + final Tunable tunable = getByUuidOrSlug(uuidOrSlug); + propertyService.write(tunable.getBrightnessPropertyId(), brightness, Double.class); + return publish(tunable, CrudAction.UPDATED); + } + + @NonNull + public TunableDto setColdness(@NonNull final String uuidOrSlug, final double coldness) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { + log.debug("setColdness: uuidOrSlug={}, coldness={}", uuidOrSlug, coldness); + final Tunable tunable = getByUuidOrSlug(uuidOrSlug); + propertyService.write(tunable.getColdnessPropertyId(), coldness, Double.class); + return publish(tunable, CrudAction.UPDATED); + } + + @NonNull + public TunableDto getByUuidOrSlugDto(final @NonNull String uuidOrSlug) { + return toDto(getByUuidOrSlug(uuidOrSlug)); + } + + @NonNull + private Tunable getByUuidOrSlug(@NonNull final String uuidOrSlug) { + return tunableRepository.findByUuidOrSlug(uuidOrSlug, uuidOrSlug).orElseThrow(() -> new EntityNotFound("uuidOrSlug", uuidOrSlug)); + } + + @NonNull + public TunableDto toDto(@NonNull final Tunable tunable) { + final PropertyDto state = propertyService.dtoByIdAndTypeOrNull(tunable.getStatePropertyId(), Boolean.class); + final PropertyDto brightness = propertyService.dtoByIdAndTypeOrNull(tunable.getBrightnessPropertyId(), Double.class); + final PropertyDto coldness = propertyService.dtoByIdAndTypeOrNull(tunable.getColdnessPropertyId(), Double.class); + return new TunableDto(tunable, state, brightness, coldness); + } + + @NonNull + private Tunable getByUuid(@NonNull final String uuid) { + return tunableRepository.findById(uuid).orElseThrow(() -> new EntityNotFound("uuid", uuid)); + } + + @NonNull + public List list(@Nullable final TunableFilter filter) throws PropertyTypeMismatch { + final List all = tunableRepository.findAll().stream().map(this::toDto).toList(); + if (filter == null) { + return all; + } + final List results = new ArrayList<>(); + for (final TunableDto dto : all) { + if (filter.filter(dto)) { + results.add(dto); + } + } + return results; + } + + @EventListener(PropertyDto.class) + public void onPropertyChange(@NonNull final PropertyDto dto) { + tunableRepository.findDistinctByStatePropertyIdOrBrightnessPropertyIdOrColdnessPropertyId(dto.getId(), dto.getId(), dto.getId()).forEach(tunable -> publish(tunable, CrudAction.CREATED)); + } + + @NonNull + private TunableDto publish(@NonNull final Tunable tunable, @NonNull final CrudAction action) { + final TunableDto dto = toDto(tunable); + log.info("Tunable {}: {}", action, dto); + applicationEventPublisher.publishEvent(dto); + return dto; + } + + @NonNull + public TunableDto getByUuidDto(@NonNull final String uuid) { + return toDto(getByUuid(uuid)); + } + +}