PropertyListComponent + Scenes

This commit is contained in:
Patrick Haßel 2021-11-07 19:32:27 +01:00
parent bc8ba1ef8b
commit b3d5b3cdd2
49 changed files with 649 additions and 247 deletions

@ -1 +1 @@
Subproject commit 65f72ffc2cf56175b3ca2e51ec7d95228907318a Subproject commit b362ae703f89547339ad5762714b1f6c999429fa

View File

@ -2,7 +2,7 @@ import {KeyValuePair} from "./KeyValuePair";
export interface ISearchService { export interface ISearchService {
get(id: string, next: (results: KeyValuePair) => void, error: (error: any) => void): void; get(id: number, next: (results: KeyValuePair) => void, error: (error: any) => void): void;
search(term: string, next: (results: KeyValuePair[]) => void, error: (error: any) => void): void; search(term: string, next: (results: KeyValuePair[]) => void, error: (error: any) => void): void;

View File

@ -1,5 +1,5 @@
import {validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators"; import {validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
import {Property} from "../property/property.service"; import {Property} from "../property/Property";
export abstract class Device { export abstract class Device {
@ -56,7 +56,7 @@ export class DeviceSwitch extends Device {
} }
updateProperty(property: Property): void { updateProperty(property: Property): void {
if (this.stateProperty?.name === property.name) { if (this.stateProperty?.id === property.id) {
this.stateProperty = property; this.stateProperty = property;
} }
} }
@ -75,7 +75,7 @@ export class DeviceShutter extends Device {
} }
updateProperty(property: Property): void { updateProperty(property: Property): void {
if (this.positionProperty?.name === property.name) { if (this.positionProperty?.id === property.id) {
this.positionProperty = property; this.positionProperty = property;
} }
} }

View File

@ -0,0 +1,42 @@
import {validateDateAllowNull, validateNumberAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
export class Property {
constructor(
public id: number,
public name: string,
public type: string,
public title: string,
public value: number | null,
public timestamp: Date | null,
) {
// nothing
}
static fromJsonAllowNull(json: any): Property | null {
if (json === undefined || json === null) {
return null;
}
return this.fromJson(json);
}
static fromJson(json: any): Property {
return new Property(
validateNumberNotNull(json['id']),
validateStringNotEmptyNotNull(json['name']),
validateStringNotEmptyNotNull(json['type']),
validateStringNotEmptyNotNull(json['title']),
validateNumberAllowNull(json['value']),
validateDateAllowNull(json['timestamp']),
);
}
public static trackBy(index: number, item: Property): string {
return item.name;
}
public static compareName(a: Property, b: Property): number {
return a.name.localeCompare(b.name);
}
}

View File

@ -1,48 +1,9 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ApiService, NO_OP} from "../api.service"; import {ApiService, NO_COMPARE, NO_OP} from "../api.service";
import {validateDateAllowNull, validateNumberAllowNull, validateStringNotEmptyNotNull} from "../validators";
import {ISearchService} from "../ISearchService"; import {ISearchService} from "../ISearchService";
import {KeyValuePair} from "../KeyValuePair"; import {KeyValuePair} from "../KeyValuePair";
import {Update} from "../Update"; import {Update} from "../Update";
import {Property} from "./Property";
export class Property {
constructor(
public name: string,
public type: string,
public title: string,
public value: number | null,
public valueTimestamp: Date | null,
) {
// nothing
}
static fromJsonAllowNull(json: any): Property | null {
if (json === undefined || json === null) {
return null;
}
return this.fromJson(json);
}
static fromJson(json: any): Property {
return new Property(
validateStringNotEmptyNotNull(json['name']),
validateStringNotEmptyNotNull(json['type']),
validateStringNotEmptyNotNull(json['title']),
validateNumberAllowNull(json['value']),
validateDateAllowNull(json['valueTimestamp']),
);
}
public static trackBy(index: number, item: Property): string {
return item.name;
}
public static compareName(a: Property, b: Property): number {
return a.name.localeCompare(b.name);
}
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -55,20 +16,24 @@ export class PropertyService implements ISearchService {
// nothing // nothing
} }
subscribe(next: (device: Update<Property>) => void): void { findAll(next: (list: Property[]) => void, compare: (a: Property, b: Property) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void {
this.api.getList("property/findAll", Property.fromJson, compare, next, error);
}
subscribe(next: (property: Update<Property>) => void): void {
this.api.subscribe("PropertyDto", Property.fromJson, next); this.api.subscribe("PropertyDto", Property.fromJson, next);
} }
get(id: string, next: (results: KeyValuePair) => void, error: (error: any) => void): void { get(id: number, next: (results: KeyValuePair) => void, error: (error: any) => void): void {
this.api.postReturnItem("property/getById", id, KeyValuePair.fromJson, next, error); this.api.getItem("property/getById/" + id, KeyValuePair.fromJson, next, error);
} }
search(term: string, next: (results: KeyValuePair[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { search(term: string, next: (results: KeyValuePair[]) => void = NO_OP, error: (error: any) => void = NO_OP): void {
this.api.postReturnList("property/searchLike", term, KeyValuePair.fromJson, next, error); this.api.postReturnList("property/searchLike", term, KeyValuePair.fromJson, next, error);
} }
set(property: Property, value: number, next: () => void = NO_OP, error: (error: any) => void = NO_OP): void { set(property: Property, key: string, value: any, next: (item: Property) => void = NO_OP, error: (error: any) => void = NO_OP): void {
this.api.postReturnNone("property/set", {name: property.name, value: value}, next, error) this.api.postReturnItem("property/set/" + property.id + "/" + key, value, Property.fromJson, next, error);
} }
} }

View File

@ -0,0 +1,36 @@
import {validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
export class Scene {
constructor(
public id: number,
public number: number,
public title: string,
) {
// nothing
}
static fromJsonAllowNull(json: any): Scene | null {
if (json === undefined || json === null) {
return null;
}
return this.fromJson(json);
}
static fromJson(json: any): Scene {
return new Scene(
validateNumberNotNull(json['id']),
validateNumberNotNull(json['number']),
validateStringNotEmptyNotNull(json['title']),
);
}
public static trackBy(index: number, item: Scene): number {
return item.id;
}
public static compareNumber(a: Scene, b: Scene): number {
return a.number - b.number;
}
}

View File

@ -0,0 +1,16 @@
import {TestBed} from '@angular/core/testing';
import {SceneService} from './scene.service';
describe('SceneService', () => {
let service: SceneService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SceneService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import {Injectable} from '@angular/core';
import {ApiService, NO_COMPARE, NO_OP} from "../api.service";
import {ISearchService} from "../ISearchService";
import {KeyValuePair} from "../KeyValuePair";
import {Update} from "../Update";
import {Scene} from "./Scene";
@Injectable({
providedIn: 'root'
})
export class SceneService implements ISearchService {
constructor(
readonly api: ApiService,
) {
// nothing
}
findAll(next: (list: Scene[]) => void, compare: (a: Scene, b: Scene) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void {
this.api.getList("scene/findAll", Scene.fromJson, compare, next, error);
}
subscribe(next: (scene: Update<Scene>) => void): void {
this.api.subscribe("SceneDto", Scene.fromJson, next);
}
get(id: number, next: (results: KeyValuePair) => void, error: (error: any) => void): void {
this.api.getItem("scene/getById/" + id, KeyValuePair.fromJson, next, error);
}
search(term: string, next: (results: KeyValuePair[]) => void = NO_OP, error: (error: any) => void = NO_OP): void {
this.api.postReturnList("scene/searchLike", term, KeyValuePair.fromJson, next, error);
}
set(scene: Scene, key: string, value: any, next: (item: Scene) => void = NO_OP, error: (error: any) => void = NO_OP): void {
this.api.postReturnItem("scene/set/" + scene.id + "/" + key, value, Scene.fromJson, next, error);
}
}

View File

@ -1,6 +1,6 @@
import {validateBooleanNotNull, validateListOrEmpty, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators"; import {validateBooleanNotNull, validateListOrEmpty, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
import {ScheduleEntry} from "./entry/ScheduleEntry"; import {ScheduleEntry} from "./entry/ScheduleEntry";
import {Property} from "../property/property.service"; import {Property} from "../property/Property";
export class Schedule { export class Schedule {

View File

@ -4,10 +4,13 @@ import {ScheduleListComponent} from "./pages/schedule-list/schedule-list.compone
import {ScheduleComponent} from "./pages/schedule/schedule.component"; import {ScheduleComponent} from "./pages/schedule/schedule.component";
import {DeviceListComponent} from "./pages/device-list/device-list.component"; import {DeviceListComponent} from "./pages/device-list/device-list.component";
import {DeviceComponent} from "./pages/device/device.component"; import {DeviceComponent} from "./pages/device/device.component";
import {PropertyListComponent} from "./pages/property-list/property-list.component";
const routes: Routes = [ const routes: Routes = [
{path: 'Device', component: DeviceComponent}, {path: 'Device', component: DeviceComponent},
{path: 'DeviceList', component: DeviceListComponent}, {path: 'DeviceList', component: DeviceListComponent},
// {path: 'Property', component: PropertyComponent},
{path: 'PropertyList', component: PropertyListComponent},
{path: 'Schedule', component: ScheduleComponent}, {path: 'Schedule', component: ScheduleComponent},
{path: 'ScheduleList', component: ScheduleListComponent}, {path: 'ScheduleList', component: ScheduleListComponent},
{path: '**', redirectTo: '/ScheduleList'}, {path: '**', redirectTo: '/ScheduleList'},

View File

@ -4,6 +4,10 @@
Zeitpläne Zeitpläne
</div> </div>
<div class="item" routerLink="/PropertyList" routerLinkActive="itemActive">
Eigenschaften
</div>
<div class="item" routerLink="/DeviceList" routerLinkActive="itemActive"> <div class="item" routerLink="/DeviceList" routerLinkActive="itemActive">
Geräte Geräte
</div> </div>

View File

@ -13,6 +13,7 @@ import {ScheduleComponent} from "./pages/schedule/schedule.component";
import {SearchComponent} from './shared/search/search.component'; import {SearchComponent} from './shared/search/search.component';
import {DeviceListComponent} from './pages/device-list/device-list.component'; import {DeviceListComponent} from './pages/device-list/device-list.component';
import {DeviceComponent} from './pages/device/device.component'; import {DeviceComponent} from './pages/device/device.component';
import {PropertyListComponent} from './pages/property-list/property-list.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -24,6 +25,7 @@ import {DeviceComponent} from './pages/device/device.component';
SearchComponent, SearchComponent,
DeviceListComponent, DeviceListComponent,
DeviceComponent, DeviceComponent,
PropertyListComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -31,28 +31,20 @@ export class DeviceListComponent implements OnInit {
this.deviceService.findAll(devices => this.devices = devices); this.deviceService.findAll(devices => this.devices = devices);
} }
asDeviceSwitch(device: Device): DeviceSwitch {
return device as DeviceSwitch;
}
asDeviceShutter(device: Device): DeviceShutter {
return device as DeviceShutter;
}
setSwitchState(d: Device, value: boolean): void { setSwitchState(d: Device, value: boolean): void {
const device: DeviceSwitch = d as DeviceSwitch; const device: DeviceSwitch = d as DeviceSwitch;
if (!device.stateProperty) { if (!device.stateProperty) {
throw new Error("Property 'setState' not set for: " + device); throw new Error("Property 'stateProperty' not set for: " + device);
} }
this.propertyService.set(device.stateProperty, value ? 1 : 0); this.propertyService.set(device.stateProperty, "value", value ? 1 : 0);
} }
setShutterPosition(d: Device, value: number): void { setShutterPosition(d: Device, value: number): void {
const device: DeviceShutter = d as DeviceShutter; const device: DeviceShutter = d as DeviceShutter;
if (!device.positionProperty) { if (!device.positionProperty) {
throw new Error("Property 'setPosition' not set for: " + device); throw new Error("Property 'positionProperty' not set for: " + device);
} }
this.propertyService.set(device.positionProperty, value); this.propertyService.set(device.positionProperty, "value", value);
} }
create(): void { create(): void {

View File

@ -13,7 +13,7 @@
<tr> <tr>
<th>Eigenschaft</th> <th>Eigenschaft</th>
<td> <td>
<app-search [searchService]="propertyService" [initial]="deviceSwitch.stateProperty?.name" [showKey]="true" (valueChange)="setDeviceSwitch('stateProperty', $event)"></app-search> <app-search [searchService]="propertyService" [initial]="deviceSwitch.stateProperty?.id" [showKey]="true" (valueChange)="setDeviceSwitch('stateProperty', $event)"></app-search>
</td> </td>
</tr> </tr>
</table> </table>
@ -30,7 +30,7 @@
<tr> <tr>
<th>Eigenschaft</th> <th>Eigenschaft</th>
<td> <td>
<app-search [searchService]="propertyService" [initial]="deviceShutter.positionProperty?.name" [showKey]="true" (valueChange)="setDeviceShutter('positionProperty', $event)"></app-search> <app-search [searchService]="propertyService" [initial]="deviceShutter.positionProperty?.id" [showKey]="true" (valueChange)="setDeviceShutter('positionProperty', $event)"></app-search>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -0,0 +1,53 @@
<table>
<tr>
<th>Bezeichnung</th>
<th>Name</th>
<th>Typ</th>
<th>Wert</th>
<th>Zeitstempel</th>
</tr>
<tr *ngFor="let property of properties">
<td>
<app-edit-field [initial]="property.title" (valueChange)="setProperty(property, 'title', $event)"></app-edit-field>
</td>
<td>
<app-edit-field [initial]="property.name" (valueChange)="setProperty(property, 'name', $event)"></app-edit-field>
</td>
<td>
<select [(ngModel)]="property.type" (ngModelChange)="setProperty(property, 'type',property.type)">
<option value="SWITCH">Schalter</option>
<option value="SHUTTER">Rollladen</option>
<option value="BRIGHTNESS_PERCENT">Helligkeit [%]</option>
<option value="COLOR_TEMPERATURE">Farbtermperatur</option>
<option value="LUX">Helligkeit [lux]</option>
<option value="SCENE">Szene</option>
</select>
</td>
<ng-container *ngIf="property.value !== null">
<td *ngIf="property.type === 'SWITCH'" class="boolean" [class.true]="property.value" [class.false]="!property.value" (click)="setProperty(property, 'value', property.value > 0 ? 0 : 1)">
{{property.value ? "An" : "Aus"}}
</td>
<td *ngIf="property.type === 'SHUTTER'" class="number" [class.true]="property.value === 0" [class.false]="property.value === 100" [class.tristate]="0 < property.value && property.value < 100">
{{property.value}} %
</td>
<td *ngIf="property.type === 'BRIGHTNESS_PERCENT'" class="number">
{{property.value}} %
</td>
<td *ngIf="property.type === 'COLOR_TEMPERATURE'" class="number">
{{property.value}} K
</td>
<td *ngIf="property.type === 'LUX'" class="number">
{{property.value | number:'0.0-0'}} lux
</td>
<td *ngIf="property.type === 'SCENE'">
{{findScene(property)?.title || "Unbekannt: " + property.value}}
</td>
</ng-container>
<ng-container *ngIf="property.value === null">
<td class="empty">
-LEER-
</td>
</ng-container>
<td>{{property.timestamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
</tr>
</table>

View File

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

View File

@ -0,0 +1,71 @@
import {Component, OnInit} from '@angular/core';
import {Property} from "../../api/property/Property";
import {PropertyService} from "../../api/property/property.service";
import {Scene} from "../../api/scene/Scene";
import {SceneService} from "../../api/scene/scene.service";
@Component({
selector: 'app-property-list',
templateUrl: './property-list.component.html',
styleUrls: ['./property-list.component.less']
})
export class PropertyListComponent implements OnInit {
properties: Property[] = [];
scenes: Scene[] = [];
constructor(
readonly propertyService: PropertyService,
readonly sceneService: SceneService,
) {
// nothing
}
ngOnInit(): void {
this.propertyService.findAll(properties => this.properties = properties, Property.compareName);
this.propertyService.subscribe(update => this.updateProperty(update.payload, update.existing));
this.sceneService.findAll(scenes => this.scenes = scenes, Scene.compareNumber);
this.sceneService.subscribe(update => this.updateScene(update.payload, update.existing));
}
setProperty(property: Property, key: string, value: any): void {
this.propertyService.set(property, key, value, property => this.updateProperty(property, true));
}
setScene(scene: Scene, key: string, value: any): void {
this.sceneService.set(scene, key, value, scene => this.updateScene(scene, true));
}
private updateProperty(property: Property, existing: boolean): void {
const index: number = this.properties.findIndex(p => p.id === property.id);
if (index >= 0) {
if (existing) {
this.properties[index] = property;
} else {
this.properties.slice(index, 1);
}
} else if (existing) {
this.properties.push(property);
}
}
private updateScene(scene: Scene, existing: boolean): void {
const index: number = this.scenes.findIndex(p => p.id === scene.id);
if (index >= 0) {
if (existing) {
this.scenes[index] = scene;
} else {
this.scenes.slice(index, 1);
}
} else if (existing) {
this.scenes.push(scene);
}
}
findScene(property: Property): Scene | undefined {
return this.scenes.find(s => s.id === property.value);
}
}

View File

@ -9,56 +9,3 @@ select {
th { th {
background-color: lightblue; background-color: lightblue;
} }
.empty {
text-align: center;
color: gray;
background-color: #DDDDDD;
}
.boolean {
text-align: center;
}
.true {
background-color: palegreen;
}
.false {
background-color: indianred;
}
.number {
text-align: right;
}
.full {
padding: 0;
}
.first {
border-right-width: 0;
padding-right: 0;
}
.middle {
border-right-width: 0;
border-left-width: 0;
padding-right: 0;
padding-left: 0;
}
.last {
border-left-width: 0;
padding-left: 0;
}
.delete {
color: darkred;
}
.disabled {
* {
background-color: gray;
}
}

View File

@ -14,7 +14,7 @@
<app-edit-field [initial]="schedule.title" (valueChange)="set(null, 'title', $event)"></app-edit-field> <app-edit-field [initial]="schedule.title" (valueChange)="set(null, 'title', $event)"></app-edit-field>
</td> </td>
<td colspan="5"> <td colspan="5">
<app-search [searchService]="propertyService" [initial]="schedule.property?.name" (valueChange)="set(null, 'propertyName', $event)"></app-search> <app-search [searchService]="propertyService" [initial]="schedule.property?.id" (valueChange)="set(null, 'propertyName', $event)"></app-search>
</td> </td>
<td colspan="9"> <td colspan="9">
{{schedule.property?.type}} {{schedule.property?.type}}
@ -144,7 +144,7 @@
<option [ngValue]="100">100% Geschlossen</option> <option [ngValue]="100">100% Geschlossen</option>
</select> </select>
</td> </td>
<td *ngIf="schedule.property?.type === 'BRIGHTNESS'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100"> <td *ngIf="schedule.property?.type === 'BRIGHTNESS_PERCENT'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)"> <select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option> <option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select> </select>
@ -161,7 +161,7 @@
</td> </td>
<td *ngIf="schedule.property?.type === 'SCENE'"> <td *ngIf="schedule.property?.type === 'SCENE'">
<select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)"> <select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)">
<option *ngFor="let _ of [].constructor(64); let value = index" [ngValue]="value + 1">{{value + 1}}</option> <option *ngFor="let scene of scenes" [ngValue]="scene.number">#{{scene.number | number:'2.0-0'}} {{scene.title}}</option>
</select> </select>
</td> </td>

View File

@ -17,60 +17,3 @@ tr.header {
} }
} }
.empty {
text-align: center;
color: gray;
background-color: #DDDDDD;
}
.boolean {
text-align: center;
}
.true {
background-color: palegreen;
}
.tristate {
background-color: yellow;
}
.false {
background-color: indianred;
}
.number {
text-align: right;
}
.full {
padding: 0;
}
.first {
border-right-width: 0;
padding-right: 0;
}
.middle {
border-right-width: 0;
border-left-width: 0;
padding-right: 0;
padding-left: 0;
}
.last {
border-left-width: 0;
padding-left: 0;
}
.delete {
color: darkred;
}
.disabled {
* {
background-color: gray;
}
}

View File

@ -7,6 +7,8 @@ import {faCheckCircle, faCircle, faTimesCircle} from '@fortawesome/free-regular-
import {ActivatedRoute} from "@angular/router"; import {ActivatedRoute} from "@angular/router";
import {DataService} from "../../data.service"; import {DataService} from "../../data.service";
import {PropertyService} from "../../api/property/property.service"; import {PropertyService} from "../../api/property/property.service";
import {Scene} from "../../api/scene/Scene";
import {SceneService} from "../../api/scene/scene.service";
@Component({ @Component({
selector: 'app-schedule', selector: 'app-schedule',
@ -22,18 +24,22 @@ export class ScheduleComponent implements OnInit {
schedule!: Schedule; schedule!: Schedule;
scenes: Scene[] = [];
constructor( constructor(
readonly activatedRoute: ActivatedRoute, readonly activatedRoute: ActivatedRoute,
readonly scheduleService: ScheduleService, readonly scheduleService: ScheduleService,
readonly scheduleEntryService: ScheduleEntryService, readonly scheduleEntryService: ScheduleEntryService,
readonly dataService: DataService,
readonly propertyService: PropertyService, readonly propertyService: PropertyService,
readonly sceneService: SceneService,
readonly dataService: DataService,
) { ) {
// nothing // nothing
} }
ngOnInit(): void { ngOnInit(): void {
this.dataService.schedule = undefined; this.dataService.schedule = undefined;
this.sceneService.findAll(scenes => this.scenes = scenes);
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params.id, schedule => this.setSchedule(schedule))); this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params.id, schedule => this.setSchedule(schedule)));
} }

View File

@ -1,3 +0,0 @@
.empty {
color: gray;
}

View File

@ -1,37 +1,22 @@
<div <div *ngIf="!searching" (click)="start()" [class.empty]="!selected">
*ngIf="!searching"
(click)="start()"
[class.empty]="!selected"
>
<ng-container *ngIf="selected"> <ng-container *ngIf="selected">
{{selected.value}} {{selected.value}}
<ng-container *ngIf="showKey"> <ng-container *ngIf="showKey">
[{{selected.key}}] [{{selected.key}}]
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="!selected"> <ng-container *ngIf="!selected">
-LEER- -LEER-
</ng-container> </ng-container>
</div> </div>
<input <input #input type="text" *ngIf="searching" [(ngModel)]="term" (ngModelChange)="changed()" (keydown)="inputKeyPress($event)" (blur)="blur()">
#input
type="text"
*ngIf="searching"
[(ngModel)]="term"
(ngModelChange)="changed()"
(keydown)="inputKeyPress($event)"
(blur)="blur()"
>
<div #resultList *ngIf="searching" class="resultList"> <div #resultList *ngIf="searching" class="resultList">
<div *ngIf="allowEmpty" class="result" (click)="select(undefined)"> <div *ngIf="allowEmpty" class="result" (click)="select(undefined)">
- -
</div> </div>
<div *ngIf="selected" class="result selected" (click)="select(undefined)"> <div *ngIf="selected" class="result selected" (click)="select(selected)">
{{selected.value}} {{selected.value}}
<ng-container *ngIf="showKey"> <ng-container *ngIf="showKey">
[{{selected.key}}] [{{selected.key}}]

View File

@ -1,7 +1,3 @@
.empty {
color: gray;
}
.selected { .selected {
font-weight: bold; font-weight: bold;
border-bottom: 1px solid black; border-bottom: 1px solid black;

View File

@ -24,7 +24,7 @@ export class SearchComponent<T> implements OnInit {
searchService!: ISearchService; searchService!: ISearchService;
@Input() @Input()
initial?: string; initial?: number;
@Input() @Input()
showKey: boolean = false; showKey: boolean = false;
@ -65,7 +65,7 @@ export class SearchComponent<T> implements OnInit {
} }
start(): void { start(): void {
this.term = this.initial || ""; this.term = this.selected?.value || "";
if (this.resultList && this.input) { if (this.resultList && this.input) {
this.resultList.style.left = this.input.style.left; this.resultList.style.left = this.input.style.left;
} }

View File

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Angular</title> <title>Angular</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=scene-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
</head> </head>
<body> <body>

View File

@ -35,3 +35,61 @@ table.vertical {
text-align: left; text-align: left;
} }
} }
.empty {
text-align: center;
color: gray;
background-color: #DDDDDD;
}
.boolean {
text-align: center;
}
.true {
background-color: palegreen;
}
.tristate {
background-color: yellow;
}
.false {
background-color: indianred;
}
.number {
text-align: right;
}
.full {
padding: 0;
}
.first {
border-right-width: 0;
padding-right: 0;
}
.middle {
border-right-width: 0;
border-left-width: 0;
padding-right: 0;
padding-left: 0;
}
.last {
border-left-width: 0;
padding-left: 0;
}
.delete {
color: darkred;
}
.disabled {
* {
background-color: gray;
}
}

View File

@ -9,6 +9,8 @@ import de.ph87.homeautomation.knx.group.KnxGroupReadService;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import de.ph87.homeautomation.property.PropertyRepository; import de.ph87.homeautomation.property.PropertyRepository;
import de.ph87.homeautomation.property.PropertyType; import de.ph87.homeautomation.property.PropertyType;
import de.ph87.homeautomation.scene.SceneRepository;
import de.ph87.homeautomation.scene.SceneWriteService;
import de.ph87.homeautomation.schedule.Schedule; import de.ph87.homeautomation.schedule.Schedule;
import de.ph87.homeautomation.schedule.ScheduleRepository; import de.ph87.homeautomation.schedule.ScheduleRepository;
import de.ph87.homeautomation.schedule.entry.ScheduleEntry; import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
@ -41,6 +43,10 @@ public class DemoDataService {
private final KnxGroupReadService knxGroupReadService; private final KnxGroupReadService knxGroupReadService;
private final SceneRepository sceneRepository;
private final SceneWriteService sceneWriteService;
public void insertDemoData() { public void insertDemoData() {
final Property ambiente_eg = createProperty("ambiente.eg", PropertyType.SWITCH, "Ambiente EG", knx(0, 3, 81), knx(0, 3, 80)); final Property ambiente_eg = createProperty("ambiente.eg", PropertyType.SWITCH, "Ambiente EG", knx(0, 3, 81), knx(0, 3, 80));
final Property ambiente_og = createProperty("ambiente.og", PropertyType.SWITCH, "Ambiente OG", knx(0, 6, 2), knx(0, 6, 3)); final Property ambiente_og = createProperty("ambiente.og", PropertyType.SWITCH, "Ambiente OG", knx(0, 6, 2), knx(0, 6, 3));
@ -62,6 +68,13 @@ public class DemoDataService {
deviceWriteService.createDeviceShutter("Flur Rollladen", flur_og_rollladen); deviceWriteService.createDeviceShutter("Flur Rollladen", flur_og_rollladen);
} }
if (sceneRepository.count() == 0) {
sceneWriteService.create(1, "Alles AUS");
sceneWriteService.create(2, "Nachtlicht");
sceneWriteService.create(30, "Dekoration AUS");
sceneWriteService.create(31, "Dekoration AN");
}
if (scheduleRepository.count() == 0) { if (scheduleRepository.count() == 0) {
final Schedule scheduleEgFlurLicht = createSchedule(true, "EG Flur Licht", flur_eg_licht); final Schedule scheduleEgFlurLicht = createSchedule(true, "EG Flur Licht", flur_eg_licht);
createTime(scheduleEgFlurLicht, true, 1, 0, 0, MIN30, true); createTime(scheduleEgFlurLicht, true, 1, 0, 0, MIN30, true);

View File

@ -22,6 +22,6 @@ public abstract class Channel {
public abstract Double getValue(); public abstract Double getValue();
public abstract ZonedDateTime getValueTimestamp(); public abstract ZonedDateTime getTimestamp();
} }

View File

@ -1,14 +0,0 @@
package de.ph87.homeautomation.device;
import lombok.Data;
@Data
public class DeviceSetDto {
private long id;
private String property;
private double value;
}

View File

@ -5,7 +5,6 @@ import de.ph87.homeautomation.device.devices.DeviceDto;
import de.ph87.homeautomation.device.devices.DeviceShutter; import de.ph87.homeautomation.device.devices.DeviceShutter;
import de.ph87.homeautomation.device.devices.DeviceSwitch; import de.ph87.homeautomation.device.devices.DeviceSwitch;
import de.ph87.homeautomation.property.Property; import de.ph87.homeautomation.property.Property;
import de.ph87.homeautomation.property.PropertyWriteService;
import de.ph87.homeautomation.schedule.ScheduleWriteService; import de.ph87.homeautomation.schedule.ScheduleWriteService;
import de.ph87.homeautomation.web.BadRequestException; import de.ph87.homeautomation.web.BadRequestException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -23,8 +22,6 @@ public class DeviceWriteService {
private final DeviceRepository deviceRepository; private final DeviceRepository deviceRepository;
private final PropertyWriteService propertyWriteService;
private final DeviceReadService deviceReadService; private final DeviceReadService deviceReadService;
public DeviceDto createDeviceSwitch(final String title, final Property stateProperty) { public DeviceDto createDeviceSwitch(final String title, final Property stateProperty) {

View File

@ -42,7 +42,7 @@ public class KnxGroup extends Channel {
private Double value; private Double value;
private ZonedDateTime valueTimestamp; private ZonedDateTime timestamp;
@Embedded @Embedded
private KnxGroupLinkInfo read = new KnxGroupLinkInfo(); private KnxGroupLinkInfo read = new KnxGroupLinkInfo();

View File

@ -72,7 +72,7 @@ public class KnxGroupWriteService {
// TODO implement all DPTXlator... // TODO implement all DPTXlator...
if (valueOptional.isPresent()) { if (valueOptional.isPresent()) {
knxGroup.setValue(valueOptional.get()); knxGroup.setValue(valueOptional.get());
knxGroup.setValueTimestamp(ZonedDateTime.now()); knxGroup.setTimestamp(ZonedDateTime.now());
log.debug("KnxGroup updated: {}", knxGroup); log.debug("KnxGroup updated: {}", knxGroup);
applicationEventPublisher.publishEvent(new ChannelChangedEvent(knxGroup)); applicationEventPublisher.publishEvent(new ChannelChangedEvent(knxGroup));
} else { } else {

View File

@ -17,15 +17,30 @@ public class PropertyController implements ISearchController {
private final PropertyReadService propertyReadService; private final PropertyReadService propertyReadService;
@PostMapping("set") @GetMapping("findAll")
public void set(@RequestBody final PropertySetDto dto) { public List<PropertyDto> findAll() {
propertyWriteService.write(dto.getName(), dto.getValue()); return propertyReadService.findAllDto();
}
@PostMapping("set/{id}/type")
public PropertyDto setPropertyType(@PathVariable final long id, @RequestBody final String propertyType) {
return propertyWriteService.set(id, Property::setType, PropertyType.valueOf(propertyType));
}
@PostMapping("set/{id}/name")
public PropertyDto setPropertyName(@PathVariable final long id, @RequestBody final String propertyName) {
return propertyWriteService.set(id, Property::setName, propertyName);
}
@PostMapping("set/{id}/value")
public PropertyDto setValue(@PathVariable final long id, @RequestBody final double value) {
return propertyWriteService.set(id, propertyWriteService::write, value);
} }
@Override @Override
@PostMapping("getById") @GetMapping("getById/{id}")
public KeyValuePair getById(@RequestBody final String name) { public KeyValuePair getById(@PathVariable final long id) {
final PropertyDto propertyDto = propertyReadService.getDtoByName(name); final PropertyDto propertyDto = propertyReadService.getDtoById(id);
return toKeyValuePair(propertyDto); return toKeyValuePair(propertyDto);
} }
@ -39,9 +54,4 @@ public class PropertyController implements ISearchController {
return new KeyValuePair(propertyDto.getName(), propertyDto.getTitle()); return new KeyValuePair(propertyDto.getName(), propertyDto.getTitle());
} }
@PostMapping("set/{id}/type")
public PropertyDto setPropertyType(@PathVariable final long id, @RequestBody final String propertyType) {
return propertyWriteService.set(id, Property::setType, PropertyType.valueOf(propertyType));
}
} }

View File

@ -18,7 +18,7 @@ public final class PropertyDto implements Serializable {
private final Double value; private final Double value;
private final ZonedDateTime valueTimestamp; private final ZonedDateTime timestamp;
public PropertyDto(final Property property) { public PropertyDto(final Property property) {
this.id = property.getId(); this.id = property.getId();
@ -26,7 +26,7 @@ public final class PropertyDto implements Serializable {
this.name = property.getName(); this.name = property.getName();
this.title = property.getTitle(); this.title = property.getTitle();
this.value = property.getValue(); this.value = property.getValue();
this.valueTimestamp = property.getTimestamp(); this.timestamp = property.getTimestamp();
} }
} }

View File

@ -30,12 +30,16 @@ public class PropertyReadService {
return propertyRepository.findAllByNameLike(like).stream().map(propertyMapper::toDto).collect(Collectors.toList()); return propertyRepository.findAllByNameLike(like).stream().map(propertyMapper::toDto).collect(Collectors.toList());
} }
public PropertyDto getDtoByName(final String name) { public PropertyDto getDtoById(final long id) {
return propertyMapper.toDto(getByName(name)); return propertyMapper.toDto(getById(id));
} }
public Property getById(final long id) { public Property getById(final long id) {
return propertyRepository.findById(id).orElseThrow(RuntimeException::new); return propertyRepository.findById(id).orElseThrow(RuntimeException::new);
} }
public List<PropertyDto> findAllDto() {
return propertyRepository.findAll().stream().map(propertyMapper::toDto).collect(Collectors.toList());
}
} }

View File

@ -11,6 +11,8 @@ public interface PropertyRepository extends CrudRepository<Property, Long> {
List<Property> findAllByReadChannel_Id(long readChannelId); List<Property> findAllByReadChannel_Id(long readChannelId);
List<Property> findAll();
List<Property> findAllByNameLike(final String like); List<Property> findAllByNameLike(final String like);
boolean existsByName(String name); boolean existsByName(String name);

View File

@ -1,5 +1,5 @@
package de.ph87.homeautomation.property; package de.ph87.homeautomation.property;
public enum PropertyType { public enum PropertyType {
SWITCH, SHUTTER, BRIGHTNESS, COLOR_TEMPERATURE, LUX, SCENE SWITCH, SHUTTER, BRIGHTNESS_PERCENT, COLOR_TEMPERATURE, LUX, SCENE
} }

View File

@ -43,7 +43,7 @@ public class PropertyWriteService {
propertyReadService.findAllByReadChannel_Id(event.getChannelId()) propertyReadService.findAllByReadChannel_Id(event.getChannelId())
.forEach(property -> { .forEach(property -> {
property.setValue(property.getReadChannel().getValue()); property.setValue(property.getReadChannel().getValue());
property.setTimestamp(property.getReadChannel().getValueTimestamp()); property.setTimestamp(property.getReadChannel().getTimestamp());
log.debug("Updated Property from Channel: {}", property); log.debug("Updated Property from Channel: {}", property);
webSocketService.send(propertyMapper.toDto(property), true); webSocketService.send(propertyMapper.toDto(property), true);
} }

View File

@ -0,0 +1,33 @@
package de.ph87.homeautomation.scene;
import lombok.*;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Getter
@Setter
@ToString
@Entity
@NoArgsConstructor
public class Scene {
@Id
@GeneratedValue
@Setter(AccessLevel.NONE)
private long id;
@Column(nullable = false, unique = true)
private int number;
@Column(nullable = false, unique = true)
private String title;
public Scene(final int number, final String title) {
this.number = number;
this.title = title;
}
}

View File

@ -0,0 +1,42 @@
package de.ph87.homeautomation.scene;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("scene")
@RequiredArgsConstructor
public class SceneController {
private final SceneReadService sceneReadService;
private final SceneWriteService sceneWriteService;
@GetMapping("findAll")
public List<SceneDto> findAll() {
return sceneReadService.findAll();
}
@GetMapping("getById/{id}")
public SceneDto getById(@PathVariable final long id) {
return sceneReadService.getDtoById(id);
}
@PostMapping("create")
public SceneDto create(@RequestBody final int number) {
return sceneWriteService.create(number, null);
}
@GetMapping("delete/{id}")
public void delete(@PathVariable final long id) {
sceneWriteService.delete(id);
}
@PostMapping("set/{id}/title")
public SceneDto setName(@PathVariable final long id, @RequestBody final String title) {
return sceneWriteService.set(id, Scene::setTitle, title);
}
}

View File

@ -0,0 +1,20 @@
package de.ph87.homeautomation.scene;
import lombok.Getter;
@Getter
public class SceneDto {
public final long id;
public final int number;
public final String title;
public SceneDto(final Scene scene) {
this.id = scene.getId();
this.number = scene.getNumber();
this.title = scene.getTitle();
}
}

View File

@ -0,0 +1,12 @@
package de.ph87.homeautomation.scene;
import org.springframework.stereotype.Service;
@Service
public class SceneMapper {
public SceneDto toDto(final Scene scene) {
return new SceneDto(scene);
}
}

View File

@ -0,0 +1,34 @@
package de.ph87.homeautomation.scene;
import de.ph87.homeautomation.web.NotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class SceneReadService {
private final SceneRepository sceneRepository;
private final SceneMapper sceneMapper;
public List<SceneDto> findAll() {
return sceneRepository.findAll().stream().map(sceneMapper::toDto).collect(Collectors.toList());
}
public Scene getById(final long id) {
return sceneRepository.findById(id).orElseThrow(() -> new NotFoundException("Scene.id=%d", id));
}
public SceneDto getDtoById(final long id) {
return sceneMapper.toDto(getById(id));
}
}

View File

@ -0,0 +1,13 @@
package de.ph87.homeautomation.scene;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface SceneRepository extends CrudRepository<Scene, Long> {
List<Scene> findAll();
boolean existsByTitle(String title);
}

View File

@ -0,0 +1,48 @@
package de.ph87.homeautomation.scene;
import de.ph87.homeautomation.schedule.ScheduleWriteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.function.BiConsumer;
import static de.ph87.homeautomation.shared.Helpers.orElseGet;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class SceneWriteService {
private final SceneRepository sceneRepository;
private final SceneReadService sceneReadService;
private final SceneMapper sceneMapper;
public <T> SceneDto set(final long id, final BiConsumer<Scene, T> setter, final T value) {
final Scene scene = sceneReadService.getById(id);
setter.accept(scene, value);
return sceneMapper.toDto(scene);
}
public void delete(final long id) {
sceneRepository.deleteById(id);
}
public SceneDto create(final int number, final String title) {
return sceneMapper.toDto(sceneRepository.save(new Scene(number, orElseGet(title, this::generateUnusedTitle))));
}
private String generateUnusedTitle() {
int index = 0;
String title = null;
while (title == null || sceneRepository.existsByTitle(title)) {
title = ScheduleWriteService.NAME_PREFIX + ++index;
}
return title;
}
}

View File

@ -3,9 +3,17 @@ package de.ph87.homeautomation.shared;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
public class Helpers { public class Helpers {
public static <T> T orElseGet(final T value, final Supplier<T> orElseGet) {
if (value == null) {
return orElseGet.get();
}
return value;
}
public static <T, U> U mapIfNotNull(final T value, final Function<T, U> map) { public static <T, U> U mapIfNotNull(final T value, final Function<T, U> map) {
if (value == null) { if (value == null) {
return null; return null;

View File

@ -4,7 +4,7 @@ import java.util.List;
public interface ISearchController { public interface ISearchController {
KeyValuePair getById(final String id); KeyValuePair getById(final long id);
List<KeyValuePair> searchLike(final String term); List<KeyValuePair> searchLike(final String term);