ChannelList + Read/Write-Channel in PropertyList

This commit is contained in:
Patrick Haßel 2021-11-09 12:43:39 +01:00
parent 5b769dd8ce
commit 0534cf0bae
43 changed files with 648 additions and 209 deletions

View File

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

View File

@ -1,26 +0,0 @@
import {validateStringNotEmptyNotNull} from "./validators";
export class KeyValuePair {
constructor(
readonly key: string,
readonly value: string,
) {
}
static fromJson(json: any): KeyValuePair {
return new KeyValuePair(
validateStringNotEmptyNotNull(json['key']),
validateStringNotEmptyNotNull(json['value']),
);
}
public static trackBy(index: number, item: KeyValuePair): string {
return item.value;
}
public static compareKey(a: KeyValuePair, b: KeyValuePair): number {
return a.value.localeCompare(b.value);
}
}

View File

@ -0,0 +1,26 @@
import {validateNumberNotNull, validateStringNotEmptyNotNull} from "./validators";
export class SearchResult {
constructor(
readonly id: number,
readonly title: string,
) {
}
static fromJson(json: any): SearchResult {
return new SearchResult(
validateNumberNotNull(json['id']),
validateStringNotEmptyNotNull(json['title']),
);
}
public static trackBy(index: number, item: SearchResult): string {
return item.title;
}
public static compareTitle(a: SearchResult, b: SearchResult): number {
return a.title.localeCompare(b.title);
}
}

View File

@ -0,0 +1,85 @@
import {validateBooleanNotNull, validateDateAllowNull, validateNumberAllowNull, validateNumberNotNull, validateStringEmptyToNull, validateStringNotEmptyNotNull} from "../validators";
import {prefix} from "../../helpers";
export abstract class Channel {
constructor(
readonly id: number,
readonly title: string,
readonly type: string,
) {
// nothing
}
static fromJsonAllowNull(json: any): Channel | null {
if (!json) {
return null;
}
return this.fromJson(json);
}
static fromJson(json: any): Channel {
const type: string = validateStringNotEmptyNotNull(json['type']);
switch (type) {
case "KnxGroup":
return new KnxGroup(
validateNumberNotNull(json['id']),
validateStringNotEmptyNotNull(json['title']),
type,
validateNumberNotNull(json['addressRaw']),
validateStringNotEmptyNotNull(json['addressStr']),
validateNumberNotNull(json['dptMain']),
validateNumberNotNull(json['dptSub']),
validateStringEmptyToNull(json['description']),
validateNumberNotNull(json['puid']),
validateBooleanNotNull(json['ets']),
validateNumberAllowNull(json['value']),
validateDateAllowNull(json['timestamp']),
);
}
throw new Error("No such type: " + type);
}
public static trackBy(index: number, item: Channel): number {
return item.id;
}
public static compareTypeThenTitle(a: Channel, b: Channel): number {
const type: number = -a.type.localeCompare(b.type);
if (type !== 0) {
return type;
}
return a.title.localeCompare(b.title);
}
}
export class KnxGroup extends Channel {
public addresMain: number;
public addresMid: number;
public addresSub: number;
public dpt: string;
constructor(
id: number,
title: string,
type: string,
public addressRaw: number,
public addressStr: string,
public dptMain: number,
public dptSub: number,
public description: string | null,
public puid: number,
public ets: boolean,
public value: number | null,
public timestamp: Date | null,
) {
super(id, title, type);
const addressParts = this.addressStr.split("/");
this.addresMain = parseInt(addressParts[0]);
this.addresMid = parseInt(addressParts[1]);
this.addresSub = parseInt(addressParts[2]);
this.dpt = this.dptMain + "." + prefix(this.dptSub, "0", 3);
}
}

View File

@ -0,0 +1,16 @@
import {TestBed} from '@angular/core/testing';
import {ChannelService} from './channel.service';
describe('ChannelService', () => {
let service: ChannelService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ChannelService);
});
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 {SearchResult} from "../SearchResult";
import {Update} from "../Update";
import {Channel} from "./Channel";
@Injectable({
providedIn: 'root'
})
export class ChannelService implements ISearchService {
constructor(
readonly api: ApiService,
) {
// nothing
}
findAll(next: (list: Channel[]) => void, compare: (a: Channel, b: Channel) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void {
this.api.getList("channel/findAll", Channel.fromJson, compare, next, error);
}
subscribe(next: (channel: Update<Channel>) => void): void {
this.api.subscribe("ChannelDto", Channel.fromJson, next);
}
get(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void {
this.api.getItem("channel/getById/" + id, SearchResult.fromJson, next, error);
}
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void {
this.api.postReturnList("channel/searchLike", term, SearchResult.fromJson, next, error);
}
set(channel: Channel, key: string, value: any, next: (item: Channel) => void = NO_OP, error: (error: any) => void = NO_OP): void {
this.api.postReturnItem("channel/set/" + channel.id + "/" + key, value, Channel.fromJson, next, error);
}
}

View File

@ -1,4 +1,5 @@
import {validateDateAllowNull, validateNumberAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
import {Channel} from "../channel/Channel";
export class Property {
@ -9,6 +10,8 @@ export class Property {
public title: string,
public value: number | null,
public timestamp: Date | null,
public readChannel: Channel | null,
public writeChannel: Channel | null,
) {
// nothing
}
@ -28,11 +31,13 @@ export class Property {
validateStringNotEmptyNotNull(json['title']),
validateNumberAllowNull(json['value']),
validateDateAllowNull(json['timestamp']),
Channel.fromJsonAllowNull(json['readChannel']),
Channel.fromJsonAllowNull(json['writeChannel']),
);
}
public static trackBy(index: number, item: Property): string {
return item.name;
public static trackBy(index: number, item: Property): number {
return item.id;
}
public static compareTypeThenTitle(a: Property, b: Property): number {

View File

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core';
import {ApiService, NO_COMPARE, NO_OP} from "../api.service";
import {ISearchService} from "../ISearchService";
import {KeyValuePair} from "../KeyValuePair";
import {SearchResult} from "../SearchResult";
import {Update} from "../Update";
import {Property} from "./Property";
@ -24,12 +24,12 @@ export class PropertyService implements ISearchService {
this.api.subscribe("PropertyDto", Property.fromJson, next);
}
get(id: number, next: (results: KeyValuePair) => void, error: (error: any) => void): void {
this.api.getItem("property/getById/" + id, KeyValuePair.fromJson, next, error);
get(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void {
this.api.getItem("property/getById/" + id, SearchResult.fromJson, next, error);
}
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);
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void {
this.api.postReturnList("property/searchLike", term, SearchResult.fromJson, next, error);
}
set(property: Property, key: string, value: any, next: (item: Property) => void = NO_OP, error: (error: any) => void = NO_OP): void {

View File

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core';
import {ApiService, NO_COMPARE, NO_OP} from "../api.service";
import {ISearchService} from "../ISearchService";
import {KeyValuePair} from "../KeyValuePair";
import {SearchResult} from "../SearchResult";
import {Update} from "../Update";
import {Scene} from "./Scene";
@ -24,12 +24,12 @@ export class SceneService implements ISearchService {
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);
get(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void {
this.api.getItem("scene/getById/" + id, SearchResult.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);
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void {
this.api.postReturnList("scene/searchLike", term, SearchResult.fromJson, next, error);
}
set(scene: Scene, key: string, value: any, next: (item: Scene) => void = NO_OP, error: (error: any) => void = NO_OP): void {

View File

@ -5,10 +5,13 @@ import {ScheduleComponent} from "./pages/schedule/schedule.component";
import {DeviceListComponent} from "./pages/device-list/device-list.component";
import {DeviceComponent} from "./pages/device/device.component";
import {PropertyListComponent} from "./pages/property-list/property-list.component";
import {ChannelListComponent} from "./pages/channel-list/channel-list.component";
const routes: Routes = [
{path: 'Device', component: DeviceComponent},
{path: 'DeviceList', component: DeviceListComponent},
// {path: 'Channel', component: ChannelComponent},
{path: 'ChannelList', component: ChannelListComponent},
// {path: 'Property', component: PropertyComponent},
{path: 'PropertyList', component: PropertyListComponent},
{path: 'Schedule', component: ScheduleComponent},

View File

@ -3,18 +3,17 @@
<div class="item" routerLink="/ScheduleList" routerLinkActive="itemActive">
Zeitpläne
</div>
<div class="item" routerLink="/PropertyList" routerLinkActive="itemActive">
Eigenschaften
</div>
<div class="item" routerLink="/DeviceList" routerLinkActive="itemActive">
Geräte
</div>
<!-- <div class="item breadcrumb" [routerLink]="['/Schedule', {id: dataService.schedule.id}]" routerLinkActive="itemActive" *ngIf="dataService.schedule">-->
<!-- {{dataService.schedule.name}}-->
<!-- </div>-->
<div class="item itemSecondary" routerLink="/PropertyList" routerLinkActive="itemActive">
Eigenschaften
</div>
<div class="item itemSecondary" routerLink="/ChannelList" routerLinkActive="itemActive">
Kanäle
</div>
</div>

View File

@ -7,6 +7,12 @@
border-right: 1px solid black;
}
.itemSecondary {
float: right;
border-left: 1px solid black;
border-right: none;
}
.item:hover {
background-color: lightskyblue;
}

View File

@ -14,6 +14,7 @@ import {SearchComponent} from './shared/search/search.component';
import {DeviceListComponent} from './pages/device-list/device-list.component';
import {DeviceComponent} from './pages/device/device.component';
import {PropertyListComponent} from './pages/property-list/property-list.component';
import {ChannelListComponent} from './pages/channel-list/channel-list.component';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import {PropertyListComponent} from './pages/property-list/property-list.compone
DeviceListComponent,
DeviceComponent,
PropertyListComponent,
ChannelListComponent,
],
imports: [
BrowserModule,

View File

@ -0,0 +1,21 @@
<table>
<tr>
<th>Titel</th>
<th>Typ</th>
<th colspan="3">Adresse</th>
<th>DPT</th>
<th colspan="2">Wert</th>
</tr>
<tr *ngFor="let channel of channels">
<td>{{channel.title}}</td>
<td>{{channel.type}}</td>
<ng-container *ngIf="channel.type === 'KnxGroup'">
<td class="first number">{{asKnxGroup(channel).addresMain}}&nbsp;/&nbsp;</td>
<td class="middle number">{{asKnxGroup(channel).addresMid}}&nbsp;/&nbsp;</td>
<td class="last number">{{asKnxGroup(channel).addresSub}}</td>
<td class="number">{{asKnxGroup(channel).dpt}}</td>
<td class="number">{{asKnxGroup(channel).value}}</td>
<td>{{asKnxGroup(channel).timestamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
</ng-container>
</tr>
</table>

View File

@ -0,0 +1,3 @@
table {
width: 100%;
}

View File

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

View File

@ -0,0 +1,43 @@
import {Component, OnInit} from '@angular/core';
import {ChannelService} from "../../api/channel/channel.service";
import {Channel, KnxGroup} from "../../api/channel/Channel";
import {Update} from "../../api/Update";
@Component({
selector: 'app-channel-list',
templateUrl: './channel-list.component.html',
styleUrls: ['./channel-list.component.less']
})
export class ChannelListComponent implements OnInit {
channels: Channel[] = [];
constructor(
readonly channelService: ChannelService,
) {
// nothing
}
ngOnInit(): void {
this.channelService.subscribe(update => this.updateChannel(update))
this.channelService.findAll(channels => this.channels = channels);
}
asKnxGroup(channel: Channel): KnxGroup {
return channel as KnxGroup;
}
private updateChannel(update: Update<Channel>): void {
const index: number = this.channels.findIndex(c => c.id === update.payload.id);
if (index >= 0) {
if (update.existing) {
this.channels[index] = update.payload;
} else {
this.channels.slice(index, 1);
}
} else {
this.channels.push(update.payload);
}
}
}

View File

@ -29,14 +29,6 @@
padding: 5px;
margin: 5px;
border-radius: 25%;
.center {
position: absolute;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.button {

View File

@ -1,3 +1,7 @@
<ng-template #empty>
<td class="empty">-</td>
</ng-template>
<table>
<tr>
<th>Bezeichnung</th>
@ -5,8 +9,11 @@
<th>Typ</th>
<th>Wert</th>
<th>Zeitstempel</th>
<th>Lesekanal</th>
<th>Schreibkanal</th>
</tr>
<tr *ngFor="let property of properties.sort(Property.compareTypeThenTitle)">
<ng-container *ngFor="let property of properties.sort(Property.compareTypeThenTitle)">
<tr>
<td>
<app-edit-field [initial]="property.title" (valueChange)="edit(property, 'title', $event)"></app-edit-field>
</td>
@ -23,7 +30,7 @@
<option value="SCENE">Szene</option>
</select>
</td>
<ng-container *ngIf="property.value !== null">
<ng-container *ngIf="property.value !== null else empty">
<td *ngIf="property.type === 'BOOLEAN'" class="boolean" [class.true]="property.value" [class.false]="!property.value" (click)="edit(property, 'value', property.value > 0 ? 0 : 1)">
{{property.value ? "An" : "Aus"}}
</td>
@ -43,11 +50,15 @@
{{findScene(property)?.title || "Unbekannt: " + property.value}}
</td>
</ng-container>
<ng-container *ngIf="property.value === null">
<td class="empty">
-LEER-
<td *ngIf="property.timestamp !== null else empty">
{{property.timestamp | date:'yyyy-MM-dd HH:mm:ss'}}
</td>
<td class="full">
<app-search [searchService]="channelService" [initial]="property.readChannel?.id" (valueChange)="set(property, 'readChannel', $event)"></app-search>
</td>
<td class="full">
<app-search [searchService]="channelService" [initial]="property.writeChannel?.id" (valueChange)="set(property, 'writeChannel', $event)"></app-search>
</td>
</ng-container>
<td>{{property.timestamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
</tr>
</ng-container>
</table>

View File

@ -3,6 +3,7 @@ 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";
import {ChannelService} from "../../api/channel/channel.service";
@Component({
selector: 'app-property-list',
@ -20,6 +21,7 @@ export class PropertyListComponent implements OnInit {
constructor(
readonly propertyService: PropertyService,
readonly sceneService: SceneService,
readonly channelService: ChannelService,
) {
// nothing
}
@ -70,4 +72,8 @@ export class PropertyListComponent implements OnInit {
return this.scenes.find(s => s.id === property.value);
}
set(property: Property, key: string, value: any): void {
}
}

View File

@ -1,5 +1,5 @@
<input #input *ngIf="editing" type="text" [(ngModel)]="value" (keypress)="keypress($event)" (blur)="finish()">
<div *ngIf="!editing" [class.empty]="initial == ''" (click)="start()">
<ng-container *ngIf="initial != ''">{{initial}}</ng-container>
<ng-container *ngIf="initial == ''">- LEER -</ng-container>
<ng-container *ngIf="initial == ''">-</ng-container>
</div>

View File

@ -1,31 +1,33 @@
<div *ngIf="!searching" (click)="start()" [class.empty]="!selected">
<div class="all">
<div class="initial" *ngIf="!searching" (click)="start()" [class.empty]="!selected">
<ng-container *ngIf="selected">
{{selected.value}}
{{selected.title}}
<ng-container *ngIf="showKey">
[{{selected.key}}]
[{{selected.id}}]
</ng-container>
</ng-container>
<ng-container *ngIf="!selected">
-LEER-
</ng-container>
</div>
<ng-container *ngIf="!selected">-</ng-container>
</div>
<input #input type="text" *ngIf="searching" [(ngModel)]="term" (ngModelChange)="changed()" (keydown)="inputKeyPress($event)" (focus)="cancelOnBlur=true" (blur)="blur()">
<input #input type="text" *ngIf="searching" [(ngModel)]="term" (ngModelChange)="changed()" (keydown)="inputKeyPress($event)" (focus)="cancelOnBlur=true" (blur)="blur()">
<div #resultList *ngIf="searching" class="resultList">
<div #resultList *ngIf="searching" class="resultList">
<div *ngIf="allowEmpty" class="result" (mousedown)="dontCancelOnBlur()" (click)="select(undefined)">
-
</div>
<div *ngIf="selected" class="result selected" (mousedown)="dontCancelOnBlur()" (click)="select(selected)">
{{selected.value}}
{{selected.title}}
<ng-container *ngIf="showKey">
[{{selected.key}}]
[{{selected.id}}]
</ng-container>
</div>
<div *ngFor="let result of results" class="result" (mousedown)="dontCancelOnBlur()" (click)="select(result)">
{{result.value}}
{{result.title}}
<ng-container *ngIf="showKey">
[{{result.key}}]
[{{result.id}}]
</ng-container>
</div>
</div>
</div>

View File

@ -1,9 +1,15 @@
.selected {
.all {
.initial {
padding: 5px;
height: 100%;
}
.selected {
font-weight: bold;
border-bottom: 1px solid black;
}
}
.resultList {
.resultList {
position: absolute;
background-color: lightgray;
min-width: 200px;
@ -16,4 +22,6 @@
.result:hover {
background-color: lightyellow;
}
}
}

View File

@ -1,5 +1,5 @@
import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
import {KeyValuePair} from "../../api/KeyValuePair";
import {SearchResult} from "../../api/SearchResult";
import {ISearchService} from "../../api/ISearchService";
@Component({
@ -33,13 +33,13 @@ export class SearchComponent<T> implements OnInit {
allowEmpty: boolean = true;
@Output()
valueChange: EventEmitter<string | null> = new EventEmitter<string | null>();
valueChange: EventEmitter<number | null> = new EventEmitter<number | null>();
term: string = "";
results: KeyValuePair[] = [];
results: SearchResult[] = [];
selected?: KeyValuePair;
selected?: SearchResult;
searching: boolean = false;
@ -67,7 +67,7 @@ export class SearchComponent<T> implements OnInit {
}
start(): void {
this.term = this.selected?.value || "";
this.term = this.selected?.title || "";
if (this.resultList && this.input) {
this.resultList.style.left = this.input.style.left;
}
@ -110,10 +110,10 @@ export class SearchComponent<T> implements OnInit {
this.cancelOnBlur = false;
}
select(result: KeyValuePair | undefined): void {
select(result: SearchResult | undefined): void {
this.searching = false;
this.selected = result;
this.valueChange.emit(this.selected?.key);
this.valueChange.emit(this.selected?.id);
}
}

View File

@ -19,7 +19,12 @@ img {
table {
border-collapse: collapse;
th {
background-color: gray;
}
td, th {
height: 0; // (=> auto growth) enables use of height percent for children
padding: 5px;
border: 1px solid black;
@ -36,6 +41,14 @@ table.vertical {
}
}
.center {
position: absolute;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.empty {
text-align: center;
color: gray;

View File

@ -18,6 +18,8 @@ public abstract class Channel {
@Setter(AccessLevel.NONE)
private Long id;
public abstract String getName();
public abstract Class<? extends IChannelOwner> getChannelOwnerClass();
public abstract Double getValue();

View File

@ -0,0 +1,40 @@
package de.ph87.homeautomation.channel;
import de.ph87.homeautomation.shared.ISearchController;
import de.ph87.homeautomation.shared.SearchResult;
import de.ph87.homeautomation.web.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("channel")
@RequiredArgsConstructor
public class ChannelController implements ISearchController {
private final ChannelService channelService;
@GetMapping("findAll")
public List<ChannelDto> findAll() {
return channelService.findAllDto();
}
@Override
@GetMapping("getById/{id}")
public SearchResult getById(@PathVariable final long id) {
return channelService.findDtoById(id).map(this::toSearchResult).orElseThrow(() -> new NotFoundException("Channel.id=" + id));
}
@Override
@PostMapping("searchLike")
public List<SearchResult> searchLike(@RequestBody final String term) {
return channelService.findAllDtoLike(term).stream().map(this::toSearchResult).collect(Collectors.toList());
}
private SearchResult toSearchResult(final ChannelDto dto) {
return new SearchResult(dto.getId(), dto.getTitle());
}
}

View File

@ -0,0 +1,24 @@
package de.ph87.homeautomation.channel;
import lombok.Getter;
import lombok.ToString;
import java.io.Serializable;
@Getter
@ToString
public abstract class ChannelDto implements Serializable {
private final long id;
private final String title;
private final String type;
protected ChannelDto(final Channel channel) {
this.id = channel.getId();
this.title = channel.getName();
this.type = channel.getClass().getSimpleName();
}
}

View File

@ -0,0 +1,7 @@
package de.ph87.homeautomation.channel;
import org.springframework.data.repository.CrudRepository;
public interface ChannelRepository extends CrudRepository<Channel, Long> {
}

View File

@ -1,14 +1,13 @@
package de.ph87.homeautomation.channel;
import de.ph87.homeautomation.property.Property;
import de.ph87.homeautomation.property.PropertyReadService;
import de.ph87.homeautomation.shared.Helpers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
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;
import java.util.Optional;
@ -20,19 +19,7 @@ public class ChannelService {
private final List<IChannelOwner> channelOwners;
private final PropertyReadService propertyReadService;
@EventListener(ApplicationStartedEvent.class)
public void readAllPropertyChannels() {
propertyReadService.findAllByReadChannelNotNull().forEach(property -> {
final Optional<IChannelOwner> ownerOptional = findByChannel(property.getReadChannel());
if (ownerOptional.isPresent()) {
ownerOptional.get().read(property.getReadChannel());
} else {
log.error("No Owner for Property: {}", property);
}
});
}
private final ChannelRepository channelRepository;
public Optional<IChannelOwner> findByChannel(final Channel channel) {
return channelOwners.stream().filter(owner -> channel.getChannelOwnerClass().isInstance(owner)).findFirst();
@ -50,4 +37,27 @@ public class ChannelService {
getByChannel(channel).write(property.getWriteChannel(), value);
}
public ChannelDto toDtoAllowNull(final Channel channel) {
if (channel == null) {
return null;
}
return toDto(channel);
}
public ChannelDto toDto(final Channel channel) {
return getByChannel(channel).toDto(channel);
}
public List<ChannelDto> findAllDto() {
return channelOwners.stream().map(IChannelOwner::findAllDto).reduce(new ArrayList<>(), Helpers::merge);
}
public List<ChannelDto> findAllDtoLike(final String term) {
return channelOwners.stream().map(owner -> owner.findAllDtoLike(term)).reduce(new ArrayList<>(), Helpers::merge);
}
public Optional<ChannelDto> findDtoById(final long id) {
return channelRepository.findById(id).map(this::toDto);
}
}

View File

@ -1,9 +1,17 @@
package de.ph87.homeautomation.channel;
import java.util.List;
public interface IChannelOwner {
void read(final Channel channel);
void write(final Channel channel, final double value);
ChannelDto toDto(final Channel channel);
List<ChannelDto> findAllDto();
List<ChannelDto> findAllDtoLike(final String like);
}

View File

@ -1,12 +1,16 @@
package de.ph87.homeautomation.knx.group;
import de.ph87.homeautomation.channel.Channel;
import de.ph87.homeautomation.channel.ChannelDto;
import de.ph87.homeautomation.channel.IChannelOwner;
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
@ -15,22 +19,31 @@ public class KnxGroupChannelOwnerService implements IChannelOwner {
private final KnxGroupWriteService knxGroupWriteService;
private final KnxGroupReadService knxGroupReadService;
@Override
public void read(final Channel channel) {
if (!(channel instanceof KnxGroup)) {
throw new RuntimeException();
}
final KnxGroup knxGroup = (KnxGroup) channel;
knxGroupWriteService.requestRead(knxGroup);
knxGroupWriteService.requestRead((KnxGroup) channel);
}
@Override
public void write(final Channel channel, final double value) {
if (!(channel instanceof KnxGroup)) {
throw new RuntimeException();
knxGroupWriteService.requestWrite((KnxGroup) channel, value);
}
final KnxGroup knxGroup = (KnxGroup) channel;
knxGroupWriteService.requestWrite(knxGroup, value);
@Override
public ChannelDto toDto(final Channel channel) {
return new KnxGroupDto((KnxGroup) channel);
}
@Override
public List<ChannelDto> findAllDto() {
return knxGroupReadService.findAll().stream().map(this::toDto).collect(Collectors.toList());
}
@Override
public List<ChannelDto> findAllDtoLike(final String like) {
return knxGroupReadService.findAllLike(like).stream().map(this::toDto).collect(Collectors.toList());
}
}

View File

@ -1,12 +1,14 @@
package de.ph87.homeautomation.knx.group;
import lombok.Data;
import de.ph87.homeautomation.channel.ChannelDto;
import lombok.Getter;
import lombok.ToString;
import java.io.Serializable;
import java.time.ZonedDateTime;
@Data
public class KnxGroupDto implements Serializable {
@Getter
@ToString(callSuper = true)
public class KnxGroupDto extends ChannelDto {
public final int addressRaw;
@ -16,8 +18,6 @@ public class KnxGroupDto implements Serializable {
public final int dptSub;
public final String name;
public final String description;
public final int puid;
@ -37,11 +37,11 @@ public class KnxGroupDto implements Serializable {
public final KnxGroupLinkInfo send;
public KnxGroupDto(final KnxGroup knxGroup) {
super(knxGroup);
this.addressRaw = knxGroup.getAddressRaw();
this.addressStr = knxGroup.getAddressStr();
this.dptMain = knxGroup.getDptMain();
this.dptSub = knxGroup.getDptSub();
this.name = knxGroup.getName();
this.description = knxGroup.getDescription();
this.puid = knxGroup.getPuid();
this.ets = knxGroup.isEts();

View File

@ -6,6 +6,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tuwien.auto.calimero.GroupAddress;
import java.util.List;
@Slf4j
@Service
@Transactional
@ -18,4 +20,12 @@ public class KnxGroupReadService {
return knxGroupRepository.findByAddressRaw(new GroupAddress(main, mid, sub).getRawAddress()).orElseThrow(RuntimeException::new);
}
public List<KnxGroup> findAll() {
return knxGroupRepository.findAll();
}
public List<KnxGroup> findAllLike(final String term) {
return knxGroupRepository.findAllByNameContainsIgnoreCaseOrAddressStrContainsIgnoreCase(term, term);
}
}

View File

@ -6,7 +6,7 @@ import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
public interface KnxGroupRepository extends CrudRepository<KnxGroup, String> {
public interface KnxGroupRepository extends CrudRepository<KnxGroup, Long> {
Optional<KnxGroup> findByAddressRaw(int rawAddress);
@ -18,6 +18,6 @@ public interface KnxGroupRepository extends CrudRepository<KnxGroup, String> {
Optional<KnxGroup> findFirstByRead_NextTimestampNotNullOrderByRead_NextTimestampAsc();
boolean existsByAddressRaw(int rawAddress);
List<KnxGroup> findAllByNameContainsIgnoreCaseOrAddressStrContainsIgnoreCase(String name, String addressStr);
}

View File

@ -1,6 +1,7 @@
package de.ph87.homeautomation.knx.group;
import de.ph87.homeautomation.channel.ChannelChangedEvent;
import de.ph87.homeautomation.channel.ChannelDto;
import de.ph87.homeautomation.web.WebSocketService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -120,7 +121,7 @@ public class KnxGroupWriteService {
private KnxGroupDto publish(final KnxGroup knxGroup) {
final KnxGroupDto dto = knxGroupMapper.toDto(knxGroup);
webSocketService.send(dto, true);
webSocketService.send(ChannelDto.class.getSimpleName(), dto, true);
return dto;
}

View File

@ -0,0 +1,36 @@
package de.ph87.homeautomation.property;
import de.ph87.homeautomation.channel.ChannelService;
import de.ph87.homeautomation.channel.IChannelOwner;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class PropertyChannelService {
private final ChannelService channelService;
private final PropertyReadService propertyReadService;
@EventListener(ApplicationStartedEvent.class)
public void readAllPropertyChannels() {
propertyReadService.findAllByReadChannelNotNull().forEach(property -> {
final Optional<IChannelOwner> ownerOptional = channelService.findByChannel(property.getReadChannel());
if (ownerOptional.isPresent()) {
ownerOptional.get().read(property.getReadChannel());
} else {
log.error("No Owner for Property: {}", property);
}
});
}
}

View File

@ -1,7 +1,7 @@
package de.ph87.homeautomation.property;
import de.ph87.homeautomation.shared.ISearchController;
import de.ph87.homeautomation.shared.KeyValuePair;
import de.ph87.homeautomation.shared.SearchResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -44,19 +44,18 @@ public class PropertyController implements ISearchController {
@Override
@GetMapping("getById/{id}")
public KeyValuePair getById(@PathVariable final long id) {
final PropertyDto propertyDto = propertyReadService.getDtoById(id);
return toKeyValuePair(propertyDto);
public SearchResult getById(@PathVariable final long id) {
return toSearchResult(propertyReadService.getDtoById(id));
}
@Override
@PostMapping("searchLike")
public List<KeyValuePair> searchLike(@RequestBody final String term) {
return propertyReadService.findAllDtoLike("%" + term + "%").stream().map(this::toKeyValuePair).collect(Collectors.toList());
public List<SearchResult> searchLike(@RequestBody final String term) {
return propertyReadService.findAllDtoLike("%" + term + "%").stream().map(this::toSearchResult).collect(Collectors.toList());
}
private KeyValuePair toKeyValuePair(final PropertyDto propertyDto) {
return new KeyValuePair(propertyDto.getName(), propertyDto.getTitle());
private SearchResult toSearchResult(final PropertyDto propertyDto) {
return new SearchResult(propertyDto.getId(), propertyDto.getTitle());
}
}

View File

@ -1,5 +1,6 @@
package de.ph87.homeautomation.property;
import de.ph87.homeautomation.channel.ChannelDto;
import lombok.Data;
import java.io.Serializable;
@ -20,13 +21,19 @@ public final class PropertyDto implements Serializable {
private final ZonedDateTime timestamp;
public PropertyDto(final Property property) {
private final ChannelDto readChannel;
private final ChannelDto writeChannel;
public PropertyDto(final Property property, final ChannelDto readChannel, final ChannelDto writeChannel) {
this.id = property.getId();
this.type = property.getType();
this.name = property.getName();
this.title = property.getTitle();
this.value = property.getValue();
this.timestamp = property.getTimestamp();
this.readChannel = readChannel;
this.writeChannel = writeChannel;
}
}

View File

@ -1,5 +1,6 @@
package de.ph87.homeautomation.property;
import de.ph87.homeautomation.channel.ChannelService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -11,8 +12,10 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class PropertyMapper {
private final ChannelService channelService;
public PropertyDto toDto(final Property property) {
return new PropertyDto(property);
return new PropertyDto(property, channelService.toDtoAllowNull(property.getReadChannel()), channelService.toDtoAllowNull(property.getWriteChannel()));
}
}

View File

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

View File

@ -1,17 +0,0 @@
package de.ph87.homeautomation.shared;
import lombok.Getter;
@Getter
public class KeyValuePair {
public final String key;
public final String value;
public KeyValuePair(final String key, final String value) {
this.key = key;
this.value = value;
}
}

View File

@ -0,0 +1,17 @@
package de.ph87.homeautomation.shared;
import lombok.Getter;
@Getter
public class SearchResult {
public final long id;
public final String title;
public SearchResult(final long id, final String title) {
this.id = id;
this.title = title;
}
}