From b14c8d63d23350dff8e767a5642d9386e3694515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Thu, 21 Nov 2024 15:57:29 +0100 Subject: [PATCH] Shutter --- .../angular/src/app/api/Shutter/Shutter.ts | 30 ++++++ .../src/app/api/Shutter/ShutterFilter.ts | 5 + .../src/app/api/Shutter/shutter.service.ts | 32 +++++++ src/main/angular/src/app/app.component.html | 1 + src/main/angular/src/app/app.routes.ts | 2 + .../shutter-list-page.component.html | 8 ++ .../shutter-list-page.component.less | 5 + .../shutter-list-page.component.ts | 70 ++++++++++++++ .../shutter-icon/shutter-icon.component.html | 5 + .../shutter-icon/shutter-icon.component.less | 14 +++ .../shutter-icon/shutter-icon.component.ts | 18 ++++ .../shutter-list/shutter-list.component.html | 41 ++++++++ .../shutter-list/shutter-list.component.less | 40 ++++++++ .../shutter-list/shutter-list.component.ts | 47 +++++++++ .../java/de/ph87/home/demo/DemoService.java | 34 +++++-- .../java/de/ph87/home/device/DeviceDto.java | 1 + .../java/de/ph87/home/knx/group/GroupDto.java | 1 + .../home/knx/property/KnxPropertyService.java | 10 +- .../java/de/ph87/home/shutter/Shutter.java | 39 ++++++++ .../ph87/home/shutter/ShutterController.java | 59 ++++++++++++ .../java/de/ph87/home/shutter/ShutterDto.java | 62 ++++++++++++ .../de/ph87/home/shutter/ShutterFilter.java | 41 ++++++++ .../ph87/home/shutter/ShutterRepository.java | 15 +++ .../de/ph87/home/shutter/ShutterService.java | 95 +++++++++++++++++++ 24 files changed, 664 insertions(+), 11 deletions(-) create mode 100644 src/main/angular/src/app/api/Shutter/Shutter.ts create mode 100644 src/main/angular/src/app/api/Shutter/ShutterFilter.ts create mode 100644 src/main/angular/src/app/api/Shutter/shutter.service.ts create mode 100644 src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html create mode 100644 src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.less create mode 100644 src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.ts create mode 100644 src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.html create mode 100644 src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.less create mode 100644 src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.ts create mode 100644 src/main/angular/src/app/shared/shutter-list/shutter-list.component.html create mode 100644 src/main/angular/src/app/shared/shutter-list/shutter-list.component.less create mode 100644 src/main/angular/src/app/shared/shutter-list/shutter-list.component.ts create mode 100644 src/main/java/de/ph87/home/shutter/Shutter.java create mode 100644 src/main/java/de/ph87/home/shutter/ShutterController.java create mode 100644 src/main/java/de/ph87/home/shutter/ShutterDto.java create mode 100644 src/main/java/de/ph87/home/shutter/ShutterFilter.java create mode 100644 src/main/java/de/ph87/home/shutter/ShutterRepository.java create mode 100644 src/main/java/de/ph87/home/shutter/ShutterService.java diff --git a/src/main/angular/src/app/api/Shutter/Shutter.ts b/src/main/angular/src/app/api/Shutter/Shutter.ts new file mode 100644 index 0000000..7e7a727 --- /dev/null +++ b/src/main/angular/src/app/api/Shutter/Shutter.ts @@ -0,0 +1,30 @@ +import {Property} from "../Property/Property"; +import {orNull, validateString} from "../common/validators"; + +export class Shutter { + + constructor( + readonly uuid: string, + readonly name: string, + readonly slug: string, + readonly positionPropertyId: string, + readonly positionProperty: Property | null, + ) { + // - + } + + static fromJson(json: any): Shutter { + return new Shutter( + validateString(json.uuid), + validateString(json.name), + validateString(json.slug), + validateString(json.positionPropertyId), + orNull(json.positionProperty, Property.fromJson), + ); + } + + static trackBy(index: number, shutter: Shutter) { + return shutter.uuid; + } + +} diff --git a/src/main/angular/src/app/api/Shutter/ShutterFilter.ts b/src/main/angular/src/app/api/Shutter/ShutterFilter.ts new file mode 100644 index 0000000..b2c8b87 --- /dev/null +++ b/src/main/angular/src/app/api/Shutter/ShutterFilter.ts @@ -0,0 +1,5 @@ +export class ShutterFilter { + + search: string = ""; + +} diff --git a/src/main/angular/src/app/api/Shutter/shutter.service.ts b/src/main/angular/src/app/api/Shutter/shutter.service.ts new file mode 100644 index 0000000..0297eec --- /dev/null +++ b/src/main/angular/src/app/api/Shutter/shutter.service.ts @@ -0,0 +1,32 @@ +import {Injectable} from '@angular/core'; +import {CrudService} from '../common/CrudService'; +import {Shutter} from './Shutter'; +import {ApiService} from '../common/api.service'; +import {Next} from '../common/types'; + +import {ShutterFilter} from './ShutterFilter'; + +@Injectable({ + providedIn: 'root' +}) +export class ShutterService extends CrudService { + + constructor( + api: ApiService, + ) { + super(api, ['Shutter'], Shutter.fromJson); + } + + getByUuid(uuid: string, next: Next): void { + this.getSingle(['getByUuid', uuid], next); + } + + list(filter: ShutterFilter | null, next: Next): void { + this.postList(['list'], filter, next); + } + + setPosition(shutter: Shutter, position: number, next?: Next): void { + this.getNone(['setPosition', shutter.uuid, position], next); + } + +} diff --git a/src/main/angular/src/app/app.component.html b/src/main/angular/src/app/app.component.html index 2763c34..923fcbd 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 7bee5bb..369cda4 100644 --- a/src/main/angular/src/app/app.routes.ts +++ b/src/main/angular/src/app/app.routes.ts @@ -1,9 +1,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'; export const routes: Routes = [ {path: 'DeviceList', component: DeviceListPageComponent}, + {path: 'ShutterList', component: ShutterListPageComponent}, {path: 'GroupList', component: KnxGroupListPageComponent}, {path: '**', redirectTo: 'GroupList'}, ]; 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 new file mode 100644 index 0000000..3bf82dc --- /dev/null +++ b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.html @@ -0,0 +1,8 @@ +
+
+ +
+
+ +
+
diff --git a/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.less b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.less new file mode 100644 index 0000000..e4e2c79 --- /dev/null +++ b/src/main/angular/src/app/pages/shutter-list-page/shutter-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/shutter-list-page/shutter-list-page.component.ts b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.ts new file mode 100644 index 0000000..4aff16b --- /dev/null +++ b/src/main/angular/src/app/pages/shutter-list-page/shutter-list-page.component.ts @@ -0,0 +1,70 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {ShutterListComponent} from '../../shared/shutter-list/shutter-list.component'; +import {Shutter} from '../../api/Shutter/Shutter'; +import {ShutterService} from '../../api/Shutter/shutter.service'; +import {FormsModule} from '@angular/forms'; +import {ShutterFilter} from '../../api/Shutter/ShutterFilter'; +import {Subscription} from 'rxjs'; +import {KnxGroupListComponent} from '../../shared/knx-group-list/knx-group-list.component'; +import {ApiService} from '../../api/common/api.service'; + +@Component({ + selector: 'app-shutter-list-page', + standalone: true, + imports: [ + ShutterListComponent, + FormsModule, + KnxGroupListComponent + ], + templateUrl: './shutter-list-page.component.html', + styleUrl: './shutter-list-page.component.less' +}) +export class ShutterListPageComponent implements OnInit, OnDestroy { + + private readonly subs: Subscription[] = []; + + protected shutterList: Shutter[] = []; + + protected filter: ShutterFilter = new ShutterFilter(); + + private fetchTimeout: any; + + constructor( + protected readonly shutterService: ShutterService, + protected readonly apiService: ApiService, + ) { + // - + } + + ngOnInit(): void { + this.fetch(); + this.subs.push(this.shutterService.subscribe(shutter => this.updateShutter(shutter))); + 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.shutterService.list(this.filter, list => this.shutterList = list) + } + + private updateShutter(shutter: Shutter) { + const index = this.shutterList.findIndex(d => d.uuid === shutter.uuid); + if (index >= 0) { + this.shutterList.splice(index, 1, shutter); + } else { + this.fetch(); + } + } + +} diff --git a/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.html b/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.html new file mode 100644 index 0000000..742af25 --- /dev/null +++ b/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.html @@ -0,0 +1,5 @@ +
+
+ +
+
diff --git a/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.less b/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.less new file mode 100644 index 0000000..c6e1116 --- /dev/null +++ b/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.less @@ -0,0 +1,14 @@ +@import "../../../../config"; + +.window { + width: 100%; + height: 100%; + background-color: lightskyblue; + border: @border solid black; + + .shutter { + background-color: saddlebrown; + border-bottom: @border solid black; + } + +} diff --git a/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.ts b/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.ts new file mode 100644 index 0000000..dd19f77 --- /dev/null +++ b/src/main/angular/src/app/shared/shutter-list/shutter-icon/shutter-icon.component.ts @@ -0,0 +1,18 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; + +@Component({ + selector: 'app-shutter-icon', + standalone: true, + imports: [], + templateUrl: './shutter-icon.component.html', + styleUrl: './shutter-icon.component.less' +}) +export class ShutterIconComponent { + + @Input() + position?: number + + @Output() + activate: EventEmitter = new EventEmitter(); + +} diff --git a/src/main/angular/src/app/shared/shutter-list/shutter-list.component.html b/src/main/angular/src/app/shared/shutter-list/shutter-list.component.html new file mode 100644 index 0000000..c6dc03f --- /dev/null +++ b/src/main/angular/src/app/shared/shutter-list/shutter-list.component.html @@ -0,0 +1,41 @@ +
+ +
+ +
+ +
+ {{ shutter.name }} +
+ +
+ +
+ +
+ {{ shutter.positionProperty?.lastValueChange | relative:now }} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
diff --git a/src/main/angular/src/app/shared/shutter-list/shutter-list.component.less b/src/main/angular/src/app/shared/shutter-list/shutter-list.component.less new file mode 100644 index 0000000..ed2051e --- /dev/null +++ b/src/main/angular/src/app/shared/shutter-list/shutter-list.component.less @@ -0,0 +1,40 @@ +@import "../../../config"; + +.shutterList { + overflow-y: auto; + height: 100%; + + .shutter { + + .name { + float: left; + } + + .icon { + clear: left; + float: left; + width: 4em; + aspect-ratio: 1; + } + + .timestamp { + float: right; + font-size: 80%; + } + + .actions { + clear: right; + float: right; + + div { + float: left; + margin-left: @space; + width: 3em; + aspect-ratio: 1; + } + + } + + } + +} diff --git a/src/main/angular/src/app/shared/shutter-list/shutter-list.component.ts b/src/main/angular/src/app/shared/shutter-list/shutter-list.component.ts new file mode 100644 index 0000000..f4686d0 --- /dev/null +++ b/src/main/angular/src/app/shared/shutter-list/shutter-list.component.ts @@ -0,0 +1,47 @@ +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {NgClass, NgForOf} from '@angular/common'; +import {Shutter} from '../../api/Shutter/Shutter'; +import {ShutterService} from '../../api/Shutter/shutter.service'; +import {RelativePipe} from '../../api/common/relative.pipe'; +import {Subscription, timer} from 'rxjs'; +import {ShutterIconComponent} from './shutter-icon/shutter-icon.component'; + +@Component({ + selector: 'app-shutter-list', + standalone: true, + imports: [ + NgForOf, + NgClass, + RelativePipe, + ShutterIconComponent + ], + templateUrl: './shutter-list.component.html', + styleUrl: './shutter-list.component.less' +}) +export class ShutterListComponent implements OnInit, OnDestroy { + + protected readonly Shutter = Shutter; + + private readonly subs: Subscription[] = []; + + protected now: Date = new Date(); + + @Input() + shutterList: Shutter[] = []; + + constructor( + protected readonly shutterService: ShutterService, + ) { + // - + } + + 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()); + } + +} diff --git a/src/main/java/de/ph87/home/demo/DemoService.java b/src/main/java/de/ph87/home/demo/DemoService.java index 6f4e7b7..92fd85f 100644 --- a/src/main/java/de/ph87/home/demo/DemoService.java +++ b/src/main/java/de/ph87/home/demo/DemoService.java @@ -3,6 +3,7 @@ package de.ph87.home.demo; 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationStartedEvent; @@ -21,21 +22,42 @@ public class DemoService { private final DeviceService deviceService; + private final ShutterService shutterService; + @EventListener(ApplicationStartedEvent.class) public void startup() { knxPropertyService.create("eg_ambiente", KnxPropertyType.BOOLEAN, adr(849), adr(848)); - knxPropertyService.create("fernseher", KnxPropertyType.BOOLEAN, adr(20), adr(4)); - knxPropertyService.create("verstaerker", KnxPropertyType.BOOLEAN, adr(825), adr(824)); - knxPropertyService.create("fensterdeko", KnxPropertyType.BOOLEAN, adr(1823), adr(1822)); - knxPropertyService.create("haengelampe", KnxPropertyType.BOOLEAN, adr(1794), adr(1799)); - knxPropertyService.create("receiver", KnxPropertyType.BOOLEAN, adr(2561), adr(2560)); - deviceService.create("EG Ambiente", "eg_ambiente", "eg_ambiente"); + + knxPropertyService.create("fernseher", KnxPropertyType.BOOLEAN, adr(20), adr(4)); deviceService.create("Wohnzimmer Fernseher", "fernseher", "fernseher"); + + 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"); } private static GroupAddress adr(final int rawGroupAddress) { diff --git a/src/main/java/de/ph87/home/device/DeviceDto.java b/src/main/java/de/ph87/home/device/DeviceDto.java index c3b79c9..1b1c77c 100644 --- a/src/main/java/de/ph87/home/device/DeviceDto.java +++ b/src/main/java/de/ph87/home/device/DeviceDto.java @@ -14,6 +14,7 @@ import java.util.List; @ToString public class DeviceDto implements IWebSocketMessage { + @ToString.Exclude private final List websocketTopic = List.of("Device"); @NonNull diff --git a/src/main/java/de/ph87/home/knx/group/GroupDto.java b/src/main/java/de/ph87/home/knx/group/GroupDto.java index a3ad14a..c11d4d1 100644 --- a/src/main/java/de/ph87/home/knx/group/GroupDto.java +++ b/src/main/java/de/ph87/home/knx/group/GroupDto.java @@ -14,6 +14,7 @@ import java.util.List; @ToString public class GroupDto implements IWebSocketMessage { + @ToString.Exclude private final List websocketTopic = List.of("Knx", "Group"); @NonNull diff --git a/src/main/java/de/ph87/home/knx/property/KnxPropertyService.java b/src/main/java/de/ph87/home/knx/property/KnxPropertyService.java index 8441929..36e9070 100644 --- a/src/main/java/de/ph87/home/knx/property/KnxPropertyService.java +++ b/src/main/java/de/ph87/home/knx/property/KnxPropertyService.java @@ -71,11 +71,6 @@ public class KnxPropertyService { findAllByAddress(event.getDestination()).forEach(knxProperty -> onProcessEvent(knxProperty, event)); } - @NonNull - private List findAllByAddress(@NonNull final GroupAddress address) { - return knxPropertyRepository.findDistinctByReadOrWrite(address, address); - } - private void onProcessEvent(@NonNull final KnxProperty knxProperty, @NonNull final ProcessEvent event) { log.debug("onProcessEvent: knxProperty={}, event={}", knxProperty, event); groupRepository.findByAddress(event.getDestination()).ifPresent(group -> onProcessEvent(knxProperty, event, group)); @@ -138,4 +133,9 @@ public class KnxPropertyService { } } + @NonNull + private List findAllByAddress(@NonNull final GroupAddress address) { + return knxPropertyRepository.findDistinctByReadOrWrite(address, address); + } + } diff --git a/src/main/java/de/ph87/home/shutter/Shutter.java b/src/main/java/de/ph87/home/shutter/Shutter.java new file mode 100644 index 0000000..79d82e9 --- /dev/null +++ b/src/main/java/de/ph87/home/shutter/Shutter.java @@ -0,0 +1,39 @@ +package de.ph87.home.shutter; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.*; + +import java.util.UUID; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class Shutter { + + @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 positionPropertyId; + + public Shutter(@NonNull final String name, @NonNull final String slug, @NonNull final String positionPropertyId) { + this.name = name; + this.slug = slug; + this.positionPropertyId = positionPropertyId; + } + +} diff --git a/src/main/java/de/ph87/home/shutter/ShutterController.java b/src/main/java/de/ph87/home/shutter/ShutterController.java new file mode 100644 index 0000000..ceffeb6 --- /dev/null +++ b/src/main/java/de/ph87/home/shutter/ShutterController.java @@ -0,0 +1,59 @@ +package de.ph87.home.shutter; + +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("Shutter") +public class ShutterController { + + private final ShutterService shutterService; + + @NonNull + @GetMapping("getByUuid/{id}") + @ExceptionHandler(KNXFormatException.class) + private ShutterDto getByUuid(@PathVariable final String id, @NonNull final HttpServletRequest request) { + log.debug("getByUuid: path={}", request.getServletPath()); + return shutterService.getByUuidDto(id); + } + + @NonNull + @RequestMapping(value = "list", method = {RequestMethod.GET, RequestMethod.POST}) + private List list(@RequestBody(required = false) @Nullable final ShutterFilter filter, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch { + log.debug("list: path={} filter={}", request.getServletPath(), filter); + return shutterService.list(filter); + } + + @NonNull + @GetMapping("get/{uuidOrSlug}") + private ShutterDto get(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) { + log.debug("get: path={}", request.getServletPath()); + return shutterService.getByUuidOrSlugDto(uuidOrSlug); + } + + @Nullable + @GetMapping("getPosition/{uuidOrSlug}") + private Double getPosition(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws PropertyTypeMismatch { + log.debug("getPosition: path={}", request.getServletPath()); + return shutterService.getByUuidOrSlugDto(uuidOrSlug).getPositionValue(); + } + + @GetMapping("setPosition/{uuidOrSlug}/{position}") + private void setPosition(@PathVariable @NonNull final String uuidOrSlug, @PathVariable final double position, @NonNull final HttpServletRequest request) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { + log.debug("setPosition: path={}", request.getServletPath()); + shutterService.setPosition(uuidOrSlug, position); + } + +} diff --git a/src/main/java/de/ph87/home/shutter/ShutterDto.java b/src/main/java/de/ph87/home/shutter/ShutterDto.java new file mode 100644 index 0000000..3d18913 --- /dev/null +++ b/src/main/java/de/ph87/home/shutter/ShutterDto.java @@ -0,0 +1,62 @@ +package de.ph87.home.shutter; + +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 ShutterDto implements IWebSocketMessage { + + @ToString.Exclude + private final List websocketTopic = List.of("Shutter"); + + @NonNull + private final String uuid; + + @NonNull + private final String name; + + @NonNull + private final String slug; + + @NonNull + private final String positionPropertyId; + + @Nullable + @ToString.Exclude + private final PropertyDto positionProperty; + + public ShutterDto(@NonNull final Shutter shutter, @Nullable final PropertyDto positionProperty) { + this.uuid = shutter.getUuid(); + this.name = shutter.getName(); + this.slug = shutter.getSlug(); + this.positionPropertyId = shutter.getPositionPropertyId(); + this.positionProperty = positionProperty; + } + + @Nullable + @ToString.Include + public String position() { + try { + return "" + getPositionValue(); + } catch (PropertyTypeMismatch e) { + return "[PropertyTypeMismatch]"; + } + } + + @Nullable + public Double getPositionValue() throws PropertyTypeMismatch { + if (positionProperty == null) { + return null; + } + return positionProperty.getStateValueAs(Double.class); + } + +} diff --git a/src/main/java/de/ph87/home/shutter/ShutterFilter.java b/src/main/java/de/ph87/home/shutter/ShutterFilter.java new file mode 100644 index 0000000..2096d3f --- /dev/null +++ b/src/main/java/de/ph87/home/shutter/ShutterFilter.java @@ -0,0 +1,41 @@ +package de.ph87.home.shutter; + +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 ShutterFilter extends AbstractSearchFilter { + + @Nullable + @JsonProperty + private Boolean positionNull; + + @Nullable + @JsonProperty + private Double positionMin; + + @Nullable + @JsonProperty + private Double positionMax; + + public boolean filter(@NonNull final ShutterDto dto) throws PropertyTypeMismatch { + if (positionNull != null && positionNull != (dto.getPositionProperty() == null)) { + return false; + } + final Double value = dto.getPositionValue(); + if (positionMin != null && value != null && positionMin <= value) { + return false; + } + if (positionMax != null && value != null && positionMax >= value) { + return false; + } + return search(dto.getName()); + } + +} diff --git a/src/main/java/de/ph87/home/shutter/ShutterRepository.java b/src/main/java/de/ph87/home/shutter/ShutterRepository.java new file mode 100644 index 0000000..1616dc1 --- /dev/null +++ b/src/main/java/de/ph87/home/shutter/ShutterRepository.java @@ -0,0 +1,15 @@ +package de.ph87.home.shutter; + +import lombok.NonNull; +import org.springframework.data.repository.ListCrudRepository; + +import java.util.List; +import java.util.Optional; + +public interface ShutterRepository extends ListCrudRepository { + + Optional findByUuidOrSlug(@NonNull String uuid, @NonNull String slug); + + List findAllByPositionPropertyId(@NonNull String propertyId); + +} diff --git a/src/main/java/de/ph87/home/shutter/ShutterService.java b/src/main/java/de/ph87/home/shutter/ShutterService.java new file mode 100644 index 0000000..1b9f49e --- /dev/null +++ b/src/main/java/de/ph87/home/shutter/ShutterService.java @@ -0,0 +1,95 @@ +package de.ph87.home.shutter; + +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 ShutterService { + + private final PropertyService propertyService; + + private final ShutterRepository shutterRepository; + + private final ApplicationEventPublisher applicationEventPublisher; + + @NonNull + public ShutterDto create(@NonNull final String name, @NonNull final String slug, @NonNull final String positionProperty) { + return publish(shutterRepository.save(new Shutter(name, slug, positionProperty)), CrudAction.CREATED); + } + + public void setPosition(@NonNull final String uuidOrSlug, final double position) throws PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch { + log.debug("setPosition: uuidOrSlug={}, position={}", uuidOrSlug, position); + final Shutter shutter = getByUuidOrSlug(uuidOrSlug); + propertyService.write(shutter.getPositionPropertyId(), position, Double.class); + } + + @NonNull + public ShutterDto getByUuidOrSlugDto(final @NonNull String uuidOrSlug) { + return toDto(getByUuidOrSlug(uuidOrSlug)); + } + + @NonNull + private Shutter getByUuidOrSlug(@NonNull final String uuidOrSlug) { + return shutterRepository.findByUuidOrSlug(uuidOrSlug, uuidOrSlug).orElseThrow(() -> new EntityNotFound("uuidOrSlug", uuidOrSlug)); + } + + @NonNull + public ShutterDto toDto(@NonNull final Shutter shutter) { + final PropertyDto position = propertyService.dtoByIdAndTypeOrNull(shutter.getPositionPropertyId(), Double.class); + return new ShutterDto(shutter, position); + } + + @NonNull + private Shutter getByUuid(@NonNull final String uuid) { + return shutterRepository.findById(uuid).orElseThrow(() -> new EntityNotFound("uuid", uuid)); + } + + @NonNull + public List list(@Nullable final ShutterFilter filter) throws PropertyTypeMismatch { + final List all = shutterRepository.findAll().stream().map(this::toDto).toList(); + if (filter == null) { + return all; + } + final List results = new ArrayList<>(); + for (final ShutterDto dto : all) { + if (filter.filter(dto)) { + results.add(dto); + } + } + return results; + } + + @EventListener(PropertyDto.class) + public void onPropertyChange(@NonNull final PropertyDto dto) { + shutterRepository.findAllByPositionPropertyId(dto.getId()).forEach(shutter -> publish(shutter, CrudAction.UPDATED)); + } + + @NonNull + private ShutterDto publish(@NonNull final Shutter shutter, @NonNull final CrudAction action) { + final ShutterDto dto = toDto(shutter); + log.info("Shutter {}: {}", action, dto); + applicationEventPublisher.publishEvent(dto); + return dto; + } + + @NonNull + public ShutterDto getByUuidDto(@NonNull final String uuid) { + return toDto(getByUuid(uuid)); + } + +}