UI device editing

This commit is contained in:
Patrick Haßel 2021-11-03 10:02:52 +01:00
parent 4939d9398f
commit 927e8df13a
44 changed files with 504 additions and 156 deletions

View File

@ -1,5 +1,4 @@
import {validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
import {Property} from "../property/property.service";
import {validateBooleanAllowNull, validateNumberAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull, validateStringNullToEmpty} from "../validators";
export abstract class Device {
@ -19,16 +18,18 @@ export abstract class Device {
validateNumberNotNull(json['id']),
validateStringNotEmptyNotNull(json['title']),
type,
Property.fromJsonAllowNull(json['getState']),
Property.fromJsonAllowNull(json['setState']),
validateStringNullToEmpty(json['getState']),
validateStringNullToEmpty(json['setState']),
validateBooleanAllowNull(json['state']),
);
case "DeviceShutter":
return new DeviceShutter(
validateNumberNotNull(json['id']),
validateStringNotEmptyNotNull(json['title']),
type,
Property.fromJsonAllowNull(json['getPercent']),
Property.fromJsonAllowNull(json['setPercent']),
validateStringNullToEmpty(json['getPercent']),
validateStringNullToEmpty(json['setPercent']),
validateNumberAllowNull(json['percent']),
);
}
throw new Error("No such type: " + type);
@ -50,8 +51,9 @@ export class DeviceSwitch extends Device {
id: number,
title: string,
type: string,
readonly getState: Property | null,
readonly setState: Property | null,
public getState: string,
public setState: string,
public state: boolean | null,
) {
super(id, title, type);
}
@ -64,8 +66,9 @@ export class DeviceShutter extends Device {
id: number,
title: string,
type: string,
readonly getPercent: Property | null,
readonly setPercent: Property | null,
public getPercent: string,
public setPercent: string,
public percent: number | null,
) {
super(id, title, type);
}

View File

@ -17,4 +17,28 @@ export class DeviceService {
this.api.getList("device/findAll", Device.fromJson, compare, next, error);
}
set(device: Device, key: string, value: any, next: (item: Device) => void, error: (error: any) => void = NO_OP): void {
this.api.postReturnItem("device/set/" + device.id + "/" + key, value, Device.fromJson, next, error);
}
setDeviceSwitch(device: Device, key: string, value: any, next: (item: Device) => void, error: (error: any) => void = NO_OP): void {
this.api.postReturnItem("device/set/" + device.id + "/DeviceSwitch/" + key, value, Device.fromJson, next, error);
}
setDeviceShutter(device: Device, key: string, value: any, next: (item: Device) => void, error: (error: any) => void = NO_OP): void {
this.api.postReturnItem("device/set/" + device.id + "/DeviceShutter/" + key, value, Device.fromJson, next, error);
}
getById(id: number, next: (item: Device) => void, error: (error: any) => void = NO_OP): void {
this.api.getItem("device/getById/" + id, Device.fromJson, next, error);
}
create(type: string, next: (item: Device) => void, error: (error: any) => void = NO_OP): void {
this.api.postReturnItem("device/create/", type, Device.fromJson, next, error);
}
delete(device: Device, next: () => void, error: (error: any) => void = NO_OP): void {
this.api.getItem("device/delete/" + device.id, _ => _, next, error);
}
}

View File

@ -6,7 +6,7 @@ export class Schedule {
constructor(
public id: number,
public enabled: boolean,
public name: string,
public title: string,
public propertyName: string,
public propertyType: string,
public entries: ScheduleEntry[],
@ -18,7 +18,7 @@ export class Schedule {
return new Schedule(
validateNumberNotNull(json['id']),
validateBooleanNotNull(json['enabled']),
validateStringNotEmptyNotNull(json['name']),
validateStringNotEmptyNotNull(json['title']),
validateStringNullToEmpty(json['propertyName']),
validateStringNullToEmpty(json['propertyType']),
validateListOrEmpty(json['entries'], ScheduleEntry.fromJson, ScheduleEntry.compare),
@ -34,7 +34,7 @@ export class Schedule {
}
public static compareName(a: Schedule, b: Schedule): number {
return a.name.localeCompare(b.name);
return a.title.localeCompare(b.title);
}
}

View File

@ -21,8 +21,8 @@ export class ScheduleService {
this.api.postReturnItem("schedule/set/" + schedule.id + "/" + key, value, Schedule.fromJson, next, error);
}
findById(id: number, next: (item: Schedule) => void, error: (error: any) => void = NO_OP): void {
this.api.getItem("schedule/findById/" + id, Schedule.fromJson, next, error);
getById(id: number, next: (item: Schedule) => void, error: (error: any) => void = NO_OP): void {
this.api.getItem("schedule/getById/" + id, Schedule.fromJson, next, error);
}
create(next: (item: Schedule) => void, error: (error: any) => void = NO_OP): void {

View File

@ -3,8 +3,10 @@ import {RouterModule, Routes} from '@angular/router';
import {ScheduleListComponent} from "./pages/schedule-list/schedule-list.component";
import {ScheduleComponent} from "./pages/schedule/schedule.component";
import {DeviceListComponent} from "./pages/device-list/device-list.component";
import {DeviceComponent} from "./pages/device/device.component";
const routes: Routes = [
{path: 'Device', component: DeviceComponent},
{path: 'DeviceList', component: DeviceListComponent},
{path: 'Schedule', component: ScheduleComponent},
{path: 'ScheduleList', component: ScheduleListComponent},

View File

@ -12,6 +12,7 @@ import {NumberComponent} from './shared/number/number.component';
import {ScheduleComponent} from "./pages/schedule/schedule.component";
import {SearchComponent} from './shared/search/search.component';
import {DeviceListComponent} from './pages/device-list/device-list.component';
import {DeviceComponent} from './pages/device/device.component';
@NgModule({
declarations: [
@ -22,6 +23,7 @@ import {DeviceListComponent} from './pages/device-list/device-list.component';
NumberComponent,
SearchComponent,
DeviceListComponent,
DeviceComponent,
],
imports: [
BrowserModule,

View File

@ -1,5 +1,6 @@
import {Injectable} from '@angular/core';
import {Schedule} from "./api/schedule/Schedule";
import {Device} from "./api/device/Device";
@Injectable({
providedIn: 'root'
@ -16,6 +17,16 @@ export class DataService {
this._schedule = schedule;
}
private _device?: Device;
get device(): Device | undefined {
return this._device;
}
set device(device: Device | undefined) {
this._device = device;
}
constructor() {
}

View File

@ -1,18 +1,24 @@
<ng-container *ngFor="let device of devices">
<ng-container *ngFor="let device of devices.sort(Device.compareTitle); trackBy: Device.trackBy">
<ng-container [ngSwitch]="device.type">
<div class="device" *ngSwitchCase="'DeviceSwitch'" [class.switchOn]="isSwitchStateOn(device)" [class.switchOff]="isSwitchStateOff(device)" [class.switchUnknown]="isSwitchStateUnknown(device)">
<div class="device" *ngSwitchCase="'DeviceSwitch'" [ngClass]="getSwitchClassList(device)">
<div class="title">
{{device.title}}
</div>
<div class="edit" [routerLink]="['/Device', {id: device.id}]">
Bearbeiten
</div>
<div class="controls">
<img class="control" src="assets/switch-on.svg" (click)="setSwitchState(device, true)"/>
<img class="control" src="assets/switch-off.svg" (click)="setSwitchState(device, false)"/>
<img alt="An" class="control" src="assets/switch-on.svg" (click)="setSwitchState(device, true)"/>
<img alt="Aus" class="control" src="assets/switch-off.svg" (click)="setSwitchState(device, false)"/>
</div>
</div>
<div class="device" *ngSwitchCase="'DeviceShutter'" [class.blindOpen]="isShutterStateOpen(device)" [class.shutterBetween]="isShutterStateBetween(device)" [class.shutterClosed]="isShutterStateClosed(device)" [class.shutterUnknown]="isShutterStateUnknown(device)">
<div class="device" *ngSwitchCase="'DeviceShutter'" [ngClass]="getShutterClassList(device)">
<div class="title">
{{device.title}}
</div>
<div class="edit" [routerLink]="['/Device', {id: device.id}]">
Bearbeiten
</div>
<div class="controls">
<div class="control button" (click)="setShutterPercent(device, 0)">
<span class="center">Auf</span>
@ -33,3 +39,11 @@
</div>
</ng-container>
</ng-container>
<p>
<select [(ngModel)]="createType">
<option ngValue="DeviceSwitch">Schalter</option>
<option ngValue="DeviceShutter">Rollladen</option>
</select>
<button (click)="create()">+ Hinzufügen</button>
</p>

View File

@ -4,10 +4,17 @@
border-radius: 10px;
.title {
float: left;
font-weight: bold;
}
.edit {
float: right;
}
.controls {
clear: both;
.control {
position: relative;
float: left;
@ -49,7 +56,7 @@
background-color: gray;
}
.blindOpen {
.shutterOpen {
background-color: palegreen;
}

View File

@ -10,8 +10,12 @@ import {Device, DeviceShutter, DeviceSwitch} from "../../api/device/Device";
})
export class DeviceListComponent implements OnInit {
readonly Device = Device;
devices: Device[] = [];
createType: string = "DeviceSwitch";
constructor(
readonly deviceService: DeviceService,
readonly propertyService: PropertyService,
@ -36,7 +40,7 @@ export class DeviceListComponent implements OnInit {
if (!device.setState) {
throw new Error("Property 'setState' not set for: " + device);
}
this.propertyService.set(device.setState.name, value ? 1 : 0);
this.propertyService.set(device.setState, value ? 1 : 0);
}
setShutterPercent(d: Device, value: number): void {
@ -44,38 +48,30 @@ export class DeviceListComponent implements OnInit {
if (!device.setPercent) {
throw new Error("Property 'setPercent' not set for: " + device);
}
this.propertyService.set(device.setPercent.name, value);
this.propertyService.set(device.setPercent, value);
}
isSwitchStateOn(device: Device): boolean {
return (device as DeviceSwitch).getState?.booleanValue === true;
create(): void {
this.deviceService.create(this.createType, device => this.devices.push(device));
}
isSwitchStateOff(device: Device): boolean {
return (device as DeviceSwitch).getState?.booleanValue === false;
getSwitchClassList(device: Device): object {
const value: boolean | null | undefined = (device as DeviceSwitch).state;
return {
switchOn: value === true,
switchOff: value === false,
switchUnknown: value === null || value === undefined,
};
}
isSwitchStateUnknown(device: Device): boolean {
const value: boolean | null | undefined = (device as DeviceSwitch).getState?.booleanValue;
return value === null || value === undefined;
}
isShutterStateOpen(device: Device): boolean {
return (device as DeviceShutter).getPercent?.numberValue === 0;
}
isShutterStateBetween(device: Device): boolean {
const value: number | null | undefined = (device as DeviceShutter).getPercent?.numberValue;
return value !== null && value !== undefined && value > 0 && value < 100;
}
isShutterStateClosed(device: Device): boolean {
return (device as DeviceShutter).getPercent?.numberValue === 100;
}
isShutterStateUnknown(device: Device): boolean {
const value: number | null | undefined = (device as DeviceShutter).getPercent?.numberValue;
return value === null || value === undefined;
getShutterClassList(device: Device): object {
const value: number | null | undefined = (device as DeviceShutter).percent;
return {
shutterOpen: value === 0,
shutterBetween: value !== null && value !== undefined && value > 0 && value < 100,
shutterClosed: value === 100,
shutterUnknown: value === null || value === undefined,
};
}
}

View File

@ -0,0 +1,53 @@
<ng-container *ngIf="device">
<h1>{{device.title}}</h1>
</ng-container>
<ng-container *ngIf="deviceSwitch">
<table class="vertical">
<tr>
<th>Name</th>
<td>
<app-edit-field [initial]="deviceSwitch.title" (valueChange)="set('title', $event)"></app-edit-field>
</td>
</tr>
<tr>
<th>Zustand Lesen</th>
<td>
<app-search [searchService]="propertyService" [initial]="deviceSwitch.getState" [showKey]="true" (valueChange)="setDeviceSwitch('getState', $event)"></app-search>
</td>
</tr>
<tr>
<th>Zustand Schreiben</th>
<td>
<app-search [searchService]="propertyService" [initial]="deviceSwitch.setState" [showKey]="true" (valueChange)="setDeviceSwitch('setState', $event)"></app-search>
</td>
</tr>
</table>
</ng-container>
<ng-container *ngIf="deviceShutter">
<table class="vertical">
<tr>
<th>Name</th>
<td>
<app-edit-field [initial]="deviceShutter.title" (valueChange)="set('title', $event)"></app-edit-field>
</td>
</tr>
<tr>
<th>Position Lesen</th>
<td>
<app-search [searchService]="propertyService" [initial]="deviceShutter.getPercent" [showKey]="true" (valueChange)="setDeviceShutter('getPercent', $event)"></app-search>
</td>
</tr>
<tr>
<th>Position Schreiben</th>
<td>
<app-search [searchService]="propertyService" [initial]="deviceShutter.setPercent" [showKey]="true" (valueChange)="setDeviceShutter('setPercent', $event)"></app-search>
</td>
</tr>
</table>
</ng-container>
<p>
<button (click)="delete()">Löschen</button>
</p>

View File

@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {DeviceComponent} from './device.component';
describe('DeviceComponent', () => {
let component: DeviceComponent;
let fixture: ComponentFixture<DeviceComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DeviceComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeviceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,77 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {DataService} from "../../data.service";
import {PropertyService} from "../../api/property/property.service";
import {Device, DeviceShutter, DeviceSwitch} from "../../api/device/Device";
import {DeviceService} from "../../api/device/device.service";
@Component({
selector: 'app-device',
templateUrl: './device.component.html',
styleUrls: ['./device.component.less']
})
export class DeviceComponent implements OnInit {
device!: Device;
deviceSwitch: DeviceSwitch | undefined;
deviceShutter: DeviceShutter | undefined;
constructor(
readonly router: Router,
readonly activatedRoute: ActivatedRoute,
readonly deviceService: DeviceService,
readonly dataService: DataService,
readonly propertyService: PropertyService,
) {
// nothing
}
ngOnInit(): void {
this.dataService.device = undefined;
this.activatedRoute.params.subscribe(params => this.deviceService.getById(params.id, device => this.setDevice(device)));
}
private setDevice(device: Device): void {
this.device = device;
switch (device.type) {
case "DeviceSwitch":
this.deviceSwitch = device as DeviceSwitch;
break;
case "DeviceShutter":
this.deviceShutter = device as DeviceShutter;
break;
}
this.dataService.device = device;
}
set(key: string, value: any): void {
this.deviceService.set(this.device, key, value, device => this.setDevice(device));
}
setDeviceSwitch(key: string, value: any): void {
this.deviceService.setDeviceSwitch(this.device, key, value, device => this.setDevice(device));
}
setDeviceShutter(key: string, value: any): void {
this.deviceService.setDeviceShutter(this.device, key, value, device => this.setDevice(device));
}
delete(): void {
if (confirm(this.getDeviceTypeTitle() + " \"" + this.device.title + "\" wirklich löschen?")) {
this.deviceService.delete(this.device, () => this.router.navigate(["/DeviceList"]));
}
}
private getDeviceTypeTitle(): string {
switch (this.device.type) {
case "DeviceSwitch":
return "Schalter";
case "DeviceShutter":
return "Rollladen";
default:
return "[TYP ???]";
}
}
}

View File

@ -12,7 +12,7 @@
</td>
<td [routerLink]="['/Schedule', {id: schedule.id}]">
{{schedule.name}}
{{schedule.title}}
</td>
<td class="delete" (click)="delete(schedule)">

View File

@ -50,7 +50,7 @@ export class ScheduleListComponent implements OnInit {
}
delete(schedule: Schedule): void {
if (confirm("Zeitplan \"" + schedule.name + "\" wirklich löschen?")) {
if (confirm("Zeitplan \"" + schedule.title + "\" wirklich löschen?")) {
this.scheduleService.delete(schedule, () => this.remove(schedule));
}
}

View File

@ -11,7 +11,7 @@
<tr class="header">
<ng-container *ngTemplateOutlet="boolean;context:{schedule: schedule, value: schedule.enabled, key:'enabled'}"></ng-container>
<td colspan="8">
<app-edit-field [initial]="schedule.name" (valueChange)="set(null, 'name', $event)"></app-edit-field>
<app-edit-field [initial]="schedule.title" (valueChange)="set(null, 'title', $event)"></app-edit-field>
</td>
<td colspan="5">
<app-search [searchService]="propertyService" [initial]="schedule.propertyName" (valueChange)="set(null, 'propertyName', $event)"></app-search>

View File

@ -34,7 +34,7 @@ export class ScheduleComponent implements OnInit {
ngOnInit(): void {
this.dataService.schedule = undefined;
this.activatedRoute.params.subscribe(params => this.scheduleService.findById(params.id, schedule => this.setSchedule(schedule)));
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params.id, schedule => this.setSchedule(schedule)));
}
private setSchedule(schedule: Schedule): void {

View File

@ -1,9 +1,20 @@
<div
*ngIf="!searching"
(click)="startSearch()"
(click)="start()"
[class.empty]="!selected"
>
{{selected?.value ? selected?.value : "-LEER-"}}
<ng-container *ngIf="selected">
{{selected.value}}
<ng-container *ngIf="showKey">
[{{selected.key}}]
</ng-container>
</ng-container>
<ng-container *ngIf="!selected">
-LEER-
</ng-container>
</div>
<input
@ -13,8 +24,23 @@
[(ngModel)]="term"
(ngModelChange)="changed()"
(keydown)="inputKeyPress($event)"
(blur)="blur()"
>
<div #resultList *ngIf="searching" class="resultList">
<div *ngFor="let result of results" class="result" (click)="select(result)">{{result.value}}</div>
<div *ngIf="allowEmpty" class="result" (click)="select(undefined)">
-
</div>
<div *ngIf="selected" class="result selected" (click)="select(undefined)">
{{selected.value}}
<ng-container *ngIf="showKey">
[{{selected.key}}]
</ng-container>
</div>
<div *ngFor="let result of results" class="result" (click)="select(result)">
{{result.value}}
<ng-container *ngIf="showKey">
[{{result.key}}]
</ng-container>
</div>
</div>

View File

@ -1,3 +1,12 @@
.empty {
color: gray;
}
.selected {
font-weight: bold;
border-bottom: 1px solid black;
}
.resultList {
position: absolute;
background-color: lightgray;

View File

@ -26,8 +26,14 @@ export class SearchComponent<T> implements OnInit {
@Input()
initial!: string;
@Input()
showKey: boolean = false;
@Input()
allowEmpty: boolean = true;
@Output()
valueChange: EventEmitter<string> = new EventEmitter<string>();
valueChange: EventEmitter<string | null> = new EventEmitter<string | null>();
term: string = "";
@ -41,7 +47,9 @@ export class SearchComponent<T> implements OnInit {
}
ngOnInit(): void {
this.searchService.get(this.initial, result => this.selected = result, _ => _);
if (this.initial) {
this.searchService.get(this.initial, result => this.selected = result, _ => _);
}
}
changed(): void {
@ -56,7 +64,7 @@ export class SearchComponent<T> implements OnInit {
}
}
startSearch(): void {
start(): void {
this.term = this.initial;
if (this.resultList && this.input) {
this.resultList.style.left = this.input.style.left;
@ -72,7 +80,7 @@ export class SearchComponent<T> implements OnInit {
this.doSearch();
break;
case 'Escape':
this.cancelSearch();
this.cancel();
break;
}
}
@ -86,11 +94,15 @@ export class SearchComponent<T> implements OnInit {
}
}
cancelSearch(): void {
setTimeout(() => this.searching = false, 10);
blur() {
setTimeout(() => this.cancel(), 100);
}
select(result: KeyValuePair): void {
cancel(): void {
this.searching = false;
}
select(result: KeyValuePair | undefined): void {
this.searching = false;
this.selected = result;
this.valueChange.emit(this.selected?.key);

View File

@ -2,6 +2,7 @@ package de.ph87.homeautomation;
import com.luckycatlabs.sunrisesunset.Zenith;
import de.ph87.homeautomation.device.DeviceWriteService;
import de.ph87.homeautomation.device.devices.DeviceDto;
import de.ph87.homeautomation.knx.group.KnxGroupDto;
import de.ph87.homeautomation.knx.group.KnxGroupRepository;
import de.ph87.homeautomation.knx.group.KnxGroupWriteService;
@ -19,6 +20,8 @@ import tuwien.auto.calimero.GroupAddress;
import javax.annotation.PostConstruct;
import java.time.ZonedDateTime;
import static de.ph87.homeautomation.shared.Helpers.mapIfNotNull;
@SuppressWarnings({"unchecked", "UnusedReturnValue", "SameParameterValue", "UnusedAssignment", "RedundantSuppression"})
@Slf4j
@Service
@ -49,8 +52,8 @@ public class DemoDataService {
final KnxGroupDto bad_licht_mitte_status = createKnxGroupIfNotExists("Bad Licht Mitte Status", 0, 3, 30, "1.001", PropertyType.ON_OFF, true, false);
final KnxGroupDto helligkeit = createKnxGroupIfNotExists("Helligkeit", 0, 5, 6, "9.004", PropertyType.LUX, false, true);
deviceWriteService.createDeviceSwitch("Bad Licht Mitte", bad_licht_mitte_status, bad_licht_mitte_schalten);
deviceWriteService.createDeviceShutter("Flur OG Rollladen", null, flur_rollladen_position_anfahren);
createDeviceSwitch("Bad Licht Mitte", bad_licht_mitte_status, bad_licht_mitte_schalten);
createDeviceShutter("Flur OG Rollladen", null, flur_rollladen_position_anfahren);
if (scheduleRepository.count() == 0) {
final Schedule scheduleEgFlurLicht = createSchedule("EG Flur Licht", eg_flur_licht_schalten);
@ -106,10 +109,18 @@ public class DemoDataService {
}
}
private DeviceDto createDeviceSwitch(final String title, final PropertyDto getPercent, final PropertyDto setPercent) {
return deviceWriteService.createDeviceSwitch(title, mapIfNotNull(getPercent, PropertyDto::getName), mapIfNotNull(setPercent, PropertyDto::getName));
}
private DeviceDto createDeviceShutter(final String title, final PropertyDto getPercent, final PropertyDto setPercent) {
return deviceWriteService.createDeviceShutter(title, mapIfNotNull(getPercent, PropertyDto::getName), mapIfNotNull(setPercent, PropertyDto::getName));
}
private Schedule createSchedule(final String s, final PropertyDto propertyDto) {
final Schedule schedule = new Schedule();
schedule.setEnabled(true);
schedule.setName(s);
schedule.setTitle(s);
schedule.setPropertyName(propertyDto.getName());
schedule.setPropertyType(propertyDto.getPropertyType());
return schedule;

View File

@ -1,7 +1,9 @@
package de.ph87.homeautomation.device;
import de.ph87.homeautomation.device.devices.Device;
import de.ph87.homeautomation.device.devices.DeviceDto;
import de.ph87.homeautomation.property.PropertySetException;
import de.ph87.homeautomation.device.devices.DeviceShutter;
import de.ph87.homeautomation.device.devices.DeviceSwitch;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -21,9 +23,49 @@ public class DeviceController {
return deviceReadService.findAll();
}
@PostMapping("set")
public void set(@RequestBody final DeviceSetDto dto) throws PropertySetException {
deviceWriteService.set(dto);
@GetMapping("getById/{id}")
public DeviceDto getById(@PathVariable final long id) {
return deviceReadService.getDtoById(id);
}
@PostMapping("create")
public DeviceDto create(@RequestBody final String typeString) {
return deviceWriteService.create(typeString);
}
@GetMapping("delete/{id}")
public void delete(@PathVariable final long id) {
deviceWriteService.delete(id);
}
@PostMapping("set/{id}/enabled")
public DeviceDto setEnabled(@PathVariable final long id, @RequestBody final boolean enabled) {
return deviceWriteService.set(id, Device::setEnabled, enabled);
}
@PostMapping("set/{id}/title")
public DeviceDto setName(@PathVariable final long id, @RequestBody final String title) {
return deviceWriteService.set(id, Device::setTitle, title);
}
@PostMapping("set/{id}/DeviceSwitch/setState")
public DeviceDto setDeviceSwitchSetState(@PathVariable final long id, @RequestBody(required = false) final String name) {
return deviceWriteService.setDeviceSwitch(id, DeviceSwitch::setSetState, name);
}
@PostMapping("set/{id}/DeviceSwitch/getState")
public DeviceDto setDeviceSwitchGetState(@PathVariable final long id, @RequestBody(required = false) final String name) {
return deviceWriteService.setDeviceSwitch(id, DeviceSwitch::setGetState, name);
}
@PostMapping("set/{id}/DeviceShutter/setPercent")
public DeviceDto setDeviceShutterSetPercent(@PathVariable final long id, @RequestBody(required = false) final String name) {
return deviceWriteService.setDeviceShutter(id, DeviceShutter::setSetPercent, name);
}
@PostMapping("set/{id}/DeviceShutter/getPercent")
public DeviceDto setDeviceShutterGetPercent(@PathVariable final long id, @RequestBody(required = false) final String name) {
return deviceWriteService.setDeviceShutter(id, DeviceShutter::setGetPercent, name);
}
}

View File

@ -1,8 +1,8 @@
package de.ph87.homeautomation.device;
import de.ph87.homeautomation.device.devices.*;
import de.ph87.homeautomation.property.PropertyDto;
import de.ph87.homeautomation.property.PropertyService;
import de.ph87.office.web.NotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -27,19 +27,25 @@ public class DeviceReadService {
return deviceRepository.findAll().stream().map(this::toDto).collect(Collectors.toList());
}
private DeviceDto toDto(final Device device) {
public DeviceDto toDto(final Device device) {
if (device instanceof DeviceSwitch) {
final DeviceSwitch deviceSwitch = (DeviceSwitch) device;
final PropertyDto getState = mapIfNotNull(deviceSwitch.getGetState(), propertyService::getById);
final PropertyDto setState = mapIfNotNull(deviceSwitch.getSetState(), propertyService::getById);
return new DeviceSwitchDto(deviceSwitch, getState, setState);
final Boolean state = mapIfNotNull(deviceSwitch.getGetState(), propertyService::readBoolean);
return new DeviceSwitchDto(deviceSwitch, deviceSwitch.getGetState(), deviceSwitch.getSetState(), state);
} else if (device instanceof DeviceShutter) {
final DeviceShutter deviceShutter = (DeviceShutter) device;
final PropertyDto getPercent = mapIfNotNull(deviceShutter.getGetPercent(), propertyService::getById);
final PropertyDto setPercent = mapIfNotNull(deviceShutter.getSetPercent(), propertyService::getById);
return new DeviceShutterDto(deviceShutter, getPercent, setPercent);
final Double percent = mapIfNotNull(deviceShutter.getGetPercent(), propertyService::readNumber);
return new DeviceShutterDto(deviceShutter, deviceShutter.getGetPercent(), deviceShutter.getSetPercent(), percent);
}
throw new RuntimeException("Not implemented: toDto(" + device + ")");
}
public Device getById(final long id) {
return deviceRepository.findById(id).orElseThrow(() -> new NotFoundException("Device.id=%d", id));
}
public DeviceDto getDtoById(final long id) {
return toDto(getById(id));
}
}

View File

@ -9,4 +9,6 @@ public interface DeviceRepository extends CrudRepository<Device, Long> {
List<Device> findAll();
boolean existsByTitle(String title);
}

View File

@ -1,18 +1,19 @@
package de.ph87.homeautomation.device;
import de.ph87.homeautomation.device.devices.Device;
import de.ph87.homeautomation.device.devices.DeviceDto;
import de.ph87.homeautomation.device.devices.DeviceShutter;
import de.ph87.homeautomation.device.devices.DeviceSwitch;
import de.ph87.homeautomation.property.PropertyDto;
import de.ph87.homeautomation.property.PropertyService;
import de.ph87.homeautomation.property.PropertySetException;
import de.ph87.office.web.NotFoundException;
import de.ph87.homeautomation.schedule.ScheduleWriteService;
import de.ph87.office.web.BadRequestException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static de.ph87.homeautomation.shared.Helpers.mapIfNotNull;
import java.util.function.BiConsumer;
@Slf4j
@Service
@ -24,24 +25,28 @@ public class DeviceWriteService {
private final PropertyService propertyService;
public void createDeviceSwitch(final String title, final PropertyDto getState, final PropertyDto setState) {
private final DeviceReadService deviceReadService;
public DeviceDto createDeviceSwitch(final String title, final String getStatePropertyName, final String setStatePropertyName) {
final DeviceSwitch deviceSwitch = new DeviceSwitch();
deviceSwitch.setTitle(title);
deviceSwitch.setGetState(mapIfNotNull(getState, PropertyDto::getName));
deviceSwitch.setSetState(mapIfNotNull(setState, PropertyDto::getName));
deviceSwitch.setGetState(getStatePropertyName);
deviceSwitch.setSetState(setStatePropertyName);
deviceRepository.save(deviceSwitch);
return deviceReadService.toDto(deviceSwitch);
}
public void createDeviceShutter(final String title, final PropertyDto getPercent, final PropertyDto setPercent) {
public DeviceDto createDeviceShutter(final String title, final String getPercentPropertyName, final String setPercentPropertyName) {
final DeviceShutter deviceShutter = new DeviceShutter();
deviceShutter.setTitle(title);
deviceShutter.setGetPercent(mapIfNotNull(getPercent, PropertyDto::getName));
deviceShutter.setSetPercent(mapIfNotNull(setPercent, PropertyDto::getName));
deviceShutter.setGetPercent(getPercentPropertyName);
deviceShutter.setSetPercent(setPercentPropertyName);
deviceRepository.save(deviceShutter);
return deviceReadService.toDto(deviceShutter);
}
public void set(final DeviceSetDto dto) throws PropertySetException {
final Device device = getById(dto.getId());
final Device device = deviceReadService.getById(dto.getId());
if (device instanceof DeviceSwitch) {
setSwitch((DeviceSwitch) device, dto.getProperty(), dto.getValue());
} else if (device instanceof DeviceShutter) {
@ -63,8 +68,51 @@ public class DeviceWriteService {
}
}
private Device getById(final long id) {
return deviceRepository.findById(id).orElseThrow(() -> new NotFoundException("Device.id=%d", id));
public <T> DeviceDto set(final long id, final BiConsumer<Device, T> setter, final T value) {
final Device device = deviceReadService.getById(id);
setter.accept(device, value);
return deviceReadService.toDto(device);
}
public <T> DeviceDto setDeviceSwitch(final long id, final BiConsumer<DeviceSwitch, T> setter, final T value) {
final Device device = deviceReadService.getById(id);
if (!(device instanceof DeviceSwitch)) {
throw new BadRequestException("Not a DeviceSwitch: %s", device);
}
setter.accept((DeviceSwitch) device, value);
return deviceReadService.toDto(device);
}
public <T> DeviceDto setDeviceShutter(final long id, final BiConsumer<DeviceShutter, T> setter, final T value) {
final Device device = deviceReadService.getById(id);
if (!(device instanceof DeviceShutter)) {
throw new BadRequestException("Not a DeviceShutter: %s", device);
}
setter.accept((DeviceShutter) device, value);
return deviceReadService.toDto(device);
}
public void delete(final long id) {
deviceRepository.deleteById(id);
}
public DeviceDto create(final String type) {
switch (type) {
case "DeviceSwitch":
return createDeviceSwitch(generateUnusedTitle(), null, null);
case "DeviceShutter":
return createDeviceShutter(generateUnusedTitle(), null, null);
}
throw new RuntimeException("Not implemented type: " + type);
}
private String generateUnusedTitle() {
int index = 0;
String title = null;
while (title == null || deviceRepository.existsByTitle(title)) {
title = ScheduleWriteService.NAME_PREFIX + ++index;
}
return title;
}
}

View File

@ -21,4 +21,6 @@ public abstract class Device {
private String title;
private boolean enabled = false;
}

View File

@ -1,19 +1,21 @@
package de.ph87.homeautomation.device.devices;
import de.ph87.homeautomation.property.PropertyDto;
import lombok.Getter;
@Getter
public class DeviceShutterDto extends DeviceDto {
public final PropertyDto getPercent;
public final String getPercent;
public final PropertyDto setPercent;
public final String setPercent;
public DeviceShutterDto(final DeviceShutter device, final PropertyDto getPercent, final PropertyDto setPercent) {
public final Double percent;
public DeviceShutterDto(final DeviceShutter device, final String getPercent, final String setPercent, final Double percent) {
super(device);
this.getPercent = getPercent;
this.setPercent = setPercent;
this.percent = percent;
}
}

View File

@ -1,19 +1,21 @@
package de.ph87.homeautomation.device.devices;
import de.ph87.homeautomation.property.PropertyDto;
import lombok.Getter;
@Getter
public class DeviceSwitchDto extends DeviceDto {
public final PropertyDto getState;
public final String getState;
public final PropertyDto setState;
public final String setState;
public DeviceSwitchDto(final DeviceSwitch device, final PropertyDto getState, final PropertyDto setState) {
public final Boolean state;
public DeviceSwitchDto(final DeviceSwitch device, final String getState, final String setState, final Boolean state) {
super(device);
this.getState = getState;
this.setState = setState;
this.state = state;
}
}

View File

@ -1,6 +1,5 @@
package de.ph87.homeautomation.knx.group;
import de.ph87.homeautomation.property.IProperty;
import de.ph87.homeautomation.property.PropertyType;
import lombok.AccessLevel;
import lombok.Getter;
@ -15,7 +14,7 @@ import java.time.ZonedDateTime;
@Setter
@ToString
@Entity
public class KnxGroup implements IProperty {
public class KnxGroup {
@Id
@GeneratedValue

View File

@ -62,7 +62,7 @@ public class KnxGroupSetService implements IPropertyOwner {
}
@Override
public Number readNumber(final String propertyName) {
public Double readNumber(final String propertyName) {
return knxGroupRepository.findByAddressRaw(parseGroupAddress(propertyName).getRawAddress()).map(KnxGroup::getNumberValue).orElse(null);
}

View File

@ -1,7 +0,0 @@
package de.ph87.homeautomation.property;
public interface IProperty {
String getPropertyName();
}

View File

@ -12,7 +12,7 @@ public interface IPropertyOwner {
Boolean readBoolean(String propertyName);
Number readNumber(String propertyName);
Double readNumber(String propertyName);
List<PropertyDto> findAllProperties();

View File

@ -27,7 +27,7 @@ public class PropertyService {
return getOwnerOrThrow(propertyName).readBoolean(propertyName);
}
public Number readNumber(final String propertyName) {
public Double readNumber(final String propertyName) {
return getOwnerOrThrow(propertyName).readNumber(propertyName);
}

View File

@ -25,7 +25,7 @@ public class Schedule {
private boolean enabled = false;
@Column(nullable = false, unique = true)
private String name;
private String title;
private String propertyName;

View File

@ -45,15 +45,15 @@ public class ScheduleCalculationService {
.filter(entry -> entry.getNextFuzzyTimestamp() != null && entry.getNextFuzzyTimestamp().isAfter(now))
.min(Comparator.comparing(ScheduleEntry::getNextFuzzyTimestamp));
if (nextEntry.isEmpty()) {
log.info("No next schedule for \"{}\"", schedule.getName());
log.info("No next schedule for \"{}\"", schedule.getTitle());
} else {
log.info("Next schedule for \"{}\": {}", schedule.getName(), nextEntry.get().getNextFuzzyTimestamp());
log.info("Next schedule for \"{}\": {}", schedule.getTitle(), nextEntry.get().getNextFuzzyTimestamp());
}
eventPublisher.publishEvent(new ScheduleThreadWakeUpEvent());
}
private void calculateEntry(final Schedule schedule, final ScheduleEntry entry, final ZonedDateTime now) {
log.debug("calculateNext \"{}\", {}:", schedule.getName(), entry);
log.debug("calculateNext \"{}\", {}:", schedule.getTitle(), entry);
if (!schedule.isEnabled() || !entry.isEnabled() || !isAnyWeekdayEnabled(entry)) {
entry.setNextClearTimestamp(null);
return;

View File

@ -1,7 +1,6 @@
package de.ph87.homeautomation.schedule;
import de.ph87.homeautomation.property.PropertyType;
import de.ph87.homeautomation.schedule.entry.ScheduleNextExecutionDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -21,16 +20,11 @@ public class ScheduleController {
return scheduleReadService.findAllDtos();
}
@GetMapping("findById/{id}")
public ScheduleDto findById(@PathVariable final long id) {
@GetMapping("getById/{id}")
public ScheduleDto getById(@PathVariable final long id) {
return scheduleReadService.getDtoById(id);
}
@GetMapping("findAllNext")
public List<ScheduleNextExecutionDto> findAllNext() {
return scheduleReadService.findAllNextExecutionDtos();
}
@GetMapping("create")
public ScheduleDto create() {
return scheduleWriteService.create();
@ -48,7 +42,7 @@ public class ScheduleController {
@PostMapping("set/{id}/name")
public ScheduleDto setName(@PathVariable final long id, @RequestBody final String name) {
return scheduleWriteService.set(id, Schedule::setName, name);
return scheduleWriteService.set(id, Schedule::setTitle, name);
}
@PostMapping("set/{id}/propertyName")

View File

@ -14,7 +14,7 @@ public class ScheduleDto {
public final boolean enabled;
public final String name;
public final String title;
public final String propertyName;
@ -25,7 +25,7 @@ public class ScheduleDto {
public ScheduleDto(final Schedule schedule) {
this.id = schedule.getId();
this.enabled = schedule.isEnabled();
this.name = schedule.getName();
this.title = schedule.getTitle();
this.propertyName = schedule.getPropertyName();
this.propertyType = schedule.getPropertyType();
this.entries = schedule.getEntries().stream().map(ScheduleEntryDto::new).collect(Collectors.toSet());

View File

@ -41,7 +41,7 @@ public class ScheduleExecutionService {
private void executeEntry(final Schedule schedule, final ScheduleEntry entry, final ZonedDateTime now) {
entry.setLastClearTimestamp(entry.getNextClearTimestamp());
log.info("Executing Schedule \"{}\" Entry {}", schedule.getName(), entry);
log.info("Executing Schedule \"{}\" Entry {}", schedule.getTitle(), entry);
try {
propertyService.set(schedule.getPropertyName(), entry.getValue());
} catch (PropertySetException e) {

View File

@ -1,17 +1,13 @@
package de.ph87.homeautomation.schedule;
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
import de.ph87.homeautomation.schedule.entry.ScheduleNextExecutionDto;
import de.ph87.office.web.NotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
@ -36,16 +32,6 @@ public class ScheduleReadService {
return findAll().stream().map(scheduleMapper::toDto).collect(Collectors.toList());
}
public List<ScheduleNextExecutionDto> findAllNextExecutionDtos() {
final ZonedDateTime now = ZonedDateTime.now();
return scheduleRepository.findAll().stream()
.map(schedule -> ScheduleNextExecutionDto.create(schedule, now))
.filter(Optional::isPresent)
.map(Optional::get)
.sorted(Comparator.comparing(ScheduleNextExecutionDto::getNextTimestamp))
.collect(Collectors.toList());
}
public Schedule getByEntry(final ScheduleEntry entry) {
return scheduleRepository.getByEntriesContaining(entry);
}

View File

@ -11,6 +11,6 @@ public interface ScheduleRepository extends CrudRepository<Schedule, Long> {
Schedule getByEntriesContaining(ScheduleEntry entry);
boolean existsByName(String name);
boolean existsByTitle(String title);
}

View File

@ -24,6 +24,21 @@ public class ScheduleWriteService {
private final ScheduleRepository scheduleRepository;
public ScheduleDto create() {
final Schedule entry = new Schedule();
entry.setTitle(generateUnusedName());
return scheduleMapper.toDto(scheduleRepository.save(entry));
}
private String generateUnusedName() {
int index = 0;
String name = null;
while (name == null || scheduleRepository.existsByTitle(name)) {
name = ScheduleWriteService.NAME_PREFIX + ++index;
}
return name;
}
public <T> ScheduleDto set(final long id, final BiConsumer<Schedule, T> setter, final T value) {
final Schedule schedule = scheduleReadService.getById(id);
setter.accept(schedule, value);
@ -31,21 +46,6 @@ public class ScheduleWriteService {
return scheduleMapper.toDto(schedule);
}
public ScheduleDto create() {
final Schedule entry = new Schedule();
entry.setName(generateUnusedName());
return scheduleMapper.toDto(scheduleRepository.save(entry));
}
private String generateUnusedName() {
int index = 0;
String name = null;
while (name == null || scheduleRepository.existsByName(name)) {
name = ScheduleWriteService.NAME_PREFIX + ++index;
}
return name;
}
public void delete(final long id) {
scheduleRepository.deleteById(id);
}

View File

@ -17,7 +17,7 @@ public class ScheduleNextExecutionDto {
public final double numberValue;
private ScheduleNextExecutionDto(final Schedule schedule, final ScheduleEntry entry) {
this.name = schedule.getName();
this.name = schedule.getTitle();
this.nextTimestamp = entry.getNextFuzzyTimestamp();
this.numberValue = entry.getValue();
}