Compare commits

...

8 Commits

143 changed files with 1782 additions and 1043 deletions

View File

@ -1,4 +1,4 @@
import {validateString} from "../common/validators";
import {validateString} from "../api/validators";
export class Area {

View File

@ -0,0 +1,30 @@
import {Property} from "../Property/Property";
import {orNull, validateString} from "../api/validators";
import {Area} from '../Area/Area';
import {Thing} from '../Thing/Thing';
export class Device extends Thing {
constructor(
area: Area,
uuid: string,
name: string,
slug: string,
readonly statePropertyId: string,
readonly stateProperty: Property | null,
) {
super(area, uuid, name, slug);
}
static fromJson(json: any): Device {
return new Device(
Area.fromJson(json.area),
validateString(json.uuid),
validateString(json.name),
validateString(json.slug),
validateString(json.statePropertyId),
orNull(json.stateProperty, Property.fromJson),
);
}
}

View File

@ -0,0 +1,5 @@
<div class="tileContainer deviceList">
<app-device-tile [now]="now" [device]="device" *ngFor="let device of sorted(); trackBy: Device.trackBy"></app-device-tile>
</div>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -1,17 +1,15 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgClass, NgForOf} from '@angular/common';
import {Device} from '../../api/Device/Device';
import {DeviceService} from '../../api/Device/device.service';
import {RelativePipe} from '../../api/common/relative.pipe';
import {NgForOf} from '@angular/common';
import {Device} from '../Device';
import {Subscription, timer} from 'rxjs';
import {DeviceTileComponent} from '../device-tile/device-tile.component';
@Component({
selector: 'app-device-list',
standalone: true,
imports: [
NgForOf,
NgClass,
RelativePipe
DeviceTileComponent
],
templateUrl: './device-list.component.html',
styleUrl: './device-list.component.less'
@ -27,12 +25,6 @@ export class DeviceListComponent implements OnInit, OnDestroy {
@Input()
list: Device[] = [];
constructor(
protected readonly deviceService: DeviceService,
) {
//
}
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
@ -42,13 +34,6 @@ export class DeviceListComponent implements OnInit, OnDestroy {
this.subs.forEach(sub => sub.unsubscribe());
}
ngClass(device: Device) {
return {
"stateOn": device.stateProperty?.state?.value === true,
"stateOff": device.stateProperty?.state?.value === false,
};
}
sorted(): Device[] {
return this.list.sort(Device.compareByAreaThenName);
}

View File

@ -0,0 +1,20 @@
<div class="tile">
<div class="tileInner device" [ngClass]="ngClass(device)">
<div class="name">
{{ device.nameWithArea }}
</div>
<div class="actions">
<div class="action switchOn" (click)="deviceService.setState(device, true)"></div>
<div class="action switchOff" (click)="deviceService.setState(device, false)"></div>
</div>
<div class="timestamp details">
{{ device.stateProperty?.lastValueChange | relative:now }}
</div>
</div>
</div>

View File

@ -0,0 +1,37 @@
@import "../../../config";
.device {
.name {
float: left;
}
.timestamp {
clear: left;
float: left;
font-size: 80%;
}
.actions {
float: right;
.action {
float: left;
margin-left: @space;
width: 4em;
aspect-ratio: 1;
}
.switchOn {
//noinspection CssUnknownTarget
background-image: url("/switchOn.svg");
}
.switchOff {
//noinspection CssUnknownTarget
background-image: url("/switchOff.svg");
}
}
}

View File

@ -0,0 +1,38 @@
import {Component, Input} from '@angular/core';
import {RelativePipe} from "../../api/relative.pipe";
import {Device} from "../Device";
import {DeviceService} from '../device.service';
import {NgClass} from '@angular/common';
@Component({
selector: 'app-device-tile',
standalone: true,
imports: [
RelativePipe,
NgClass
],
templateUrl: './device-tile.component.html',
styleUrl: './device-tile.component.less'
})
export class DeviceTileComponent {
@Input()
now!: Date;
@Input()
device!: Device;
constructor(
protected readonly deviceService: DeviceService,
) {
//
}
ngClass(device: Device) {
return {
"stateOn": device.stateProperty?.state?.value === true,
"stateOff": device.stateProperty?.state?.value === false,
};
}
}

View File

@ -1,10 +1,8 @@
import {Injectable} from '@angular/core';
import {CrudService} from '../common/CrudService';
import {CrudService} from '../api/CrudService';
import {Device} from './Device';
import {ApiService} from '../common/api.service';
import {Next} from '../common/types';
import {DeviceFilter} from './DeviceFilter';
import {ApiService} from '../api/api.service';
import {Next} from '../api/types';
@Injectable({
providedIn: 'root'
@ -21,10 +19,6 @@ export class DeviceService extends CrudService<Device> {
this.getSingle(['getByUuid', uuid], next);
}
list(filter: DeviceFilter | null, next: Next<Device[]>): void {
this.postList(['list'], filter, next);
}
setState(device: Device, state: boolean, next?: Next<void>): void {
this.getNone(['setState', device.uuid, state], next);
}

View File

@ -1,4 +1,4 @@
import {orNull, validateDateOrNull, validateString} from '../common/validators';
import {orNull, validateDateOrNull, validateString} from '../api/validators';
import {State} from '../State/State';
export class Group {

View File

@ -1,10 +1,8 @@
import {Injectable} from '@angular/core';
import {CrudService} from '../common/CrudService';
import {CrudService} from '../api/CrudService';
import {Group} from './Group';
import {ApiService} from '../common/api.service';
import {Next} from '../common/types';
import {GroupFilter} from './GroupFilter';
import {ApiService} from '../api/api.service';
import {Next} from '../api/types';
@Injectable({
providedIn: 'root'
@ -21,8 +19,4 @@ export class GroupService extends CrudService<Group> {
this.getSingle(['getByAddress', address], next);
}
list(filter: GroupFilter | null, next: Next<Group[]>): void {
this.postList(['list'], filter, next);
}
}

View File

@ -1,6 +1,6 @@
<div class="flexBox">
<div class="flexBoxFixed">
<input type="text" [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
<app-search [(search)]="filter.search" (doSearch)="fetch()"></app-search>
</div>
<div class="flexBoxRest verticalScroll">
<app-knx-group-list [groupList]="groupList"></app-knx-group-list>

View File

@ -1,18 +1,20 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {KnxGroupListComponent} from '../../shared/knx-group-list/knx-group-list.component';
import {Group} from '../../api/Group/Group';
import {GroupService} from '../../api/Group/group.service';
import {KnxGroupListComponent} from '../knx-group-list/knx-group-list.component';
import {Group} from '../Group';
import {GroupService} from '../group.service';
import {FormsModule} from '@angular/forms';
import {GroupFilter} from '../../api/Group/GroupFilter';
import {GroupFilter} from '../GroupFilter';
import {Subscription} from 'rxjs';
import {ApiService} from '../../api/common/api.service';
import {ApiService} from '../../api/api.service';
import {SearchComponent} from '../../shared/search/search.component';
@Component({
selector: 'app-knx-group-list-page',
standalone: true,
imports: [
KnxGroupListComponent,
FormsModule
FormsModule,
SearchComponent
],
templateUrl: './knx-group-list-page.component.html',
styleUrl: './knx-group-list-page.component.less'
@ -25,8 +27,6 @@ export class KnxGroupListPageComponent implements OnInit, OnDestroy {
protected filter: GroupFilter = new GroupFilter();
private fetchTimeout: any;
constructor(
protected readonly groupService: GroupService,
protected readonly apiService: ApiService,
@ -44,16 +44,8 @@ export class KnxGroupListPageComponent implements OnInit, OnDestroy {
this.subs.forEach(sub => sub.unsubscribe());
}
fetchDelayed() {
if (this.fetchTimeout) {
clearTimeout(this.fetchTimeout);
this.fetchTimeout = undefined;
}
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
}
private fetch() {
this.groupService.list(this.filter, list => this.groupList = list)
protected fetch() {
this.groupService.filter(this.filter, list => this.groupList = list)
}
private updateGroup(group: Group) {

View File

@ -0,0 +1,5 @@
<div class="tileContainer groupList">
<app-knx-group-tile [now]="now" [group]="group" *ngFor="let group of sorted(); trackBy: Group.trackBy"></app-knx-group-tile>
</div>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -1,16 +1,15 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgClass, NgForOf} from '@angular/common';
import {Group} from '../../api/Group/Group';
import {RelativePipe} from '../../api/common/relative.pipe';
import {NgForOf} from '@angular/common';
import {Group} from '../Group';
import {Subscription, timer} from 'rxjs';
import {KnxGroupTileComponent} from '../knx-group-tile/knx-group-tile.component';
@Component({
selector: 'app-knx-group-list',
standalone: true,
imports: [
NgForOf,
NgClass,
RelativePipe
KnxGroupTileComponent
],
templateUrl: './knx-group-list.component.html',
styleUrl: './knx-group-list.component.less'
@ -26,13 +25,6 @@ export class KnxGroupListComponent implements OnInit, OnDestroy {
@Input()
groupList: Group[] = [];
ngClass(group: Group) {
return {
"stateOn": group.state?.value === true,
"stateOff": group.state?.value === false,
};
}
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));

View File

@ -0,0 +1,31 @@
<div class="tile">
<div class="tileInner group" [ngClass]="ngClass(group)">
<div class="name">
{{ group.name }}
</div>
<div class="details">
<div class="stackLeft address">
{{ group.address }}
</div>
<div class="stackLeft dpt">
DPT {{ group.dpt }}
</div>
<div class="stackRight state">
{{ group.state?.string || '-' }}
</div>
<div class="stackRight timestamp">
{{ group.lastValueChange | relative:now }}:
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,8 @@
@import "../../../config";
.group {
.name {
}
}

View File

@ -0,0 +1,33 @@
import {Component, Input} from '@angular/core';
import {NgClass} from '@angular/common';
import {RelativePipe} from '../../api/relative.pipe';
import {Group} from '../Group';
@Component({
selector: 'app-knx-group-tile',
standalone: true,
imports: [
RelativePipe,
NgClass
],
templateUrl: './knx-group-tile.component.html',
styleUrl: './knx-group-tile.component.less'
})
export class KnxGroupTileComponent {
protected readonly Group = Group;
@Input()
now!: Date;
@Input()
group!: Group;
ngClass(group: Group) {
return {
"stateOn": group.state?.value === true,
"stateOff": group.state?.value === false,
};
}
}

View File

@ -1,5 +1,5 @@
import {State} from "../State/State";
import {orNull, validateDateOrNull, validateString} from "../common/validators";
import {orNull, validateDateOrNull, validateString} from "../api/validators";
export class Property {

View File

@ -0,0 +1,31 @@
import {Property} from "../Property/Property";
import {orNull, validateString} from "../api/validators";
import {Area} from '../Area/Area';
import {Thing} from '../Thing/Thing';
export class Shutter extends Thing {
constructor(
area: Area,
uuid: string,
name: string,
slug: string,
readonly positionPropertyId: string,
readonly positionProperty: Property | null,
) {
super(area, uuid, name, slug);
}
static fromJson(json: any): Shutter {
return new Shutter(
Area.fromJson(json.area),
validateString(json.uuid),
validateString(json.name),
validateString(json.slug),
validateString(json.positionPropertyId),
orNull(json.positionProperty, Property.fromJson),
);
}
}

View File

@ -0,0 +1,5 @@
<div class="tileContainer shutterList">
<app-shutter-tile [now]="now" [shutter]="shutter" *ngFor="let shutter of sorted(); trackBy: Shutter.trackBy"></app-shutter-tile>
</div>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -1,18 +1,15 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgForOf} from '@angular/common';
import {Shutter} from '../../api/Shutter/Shutter';
import {ShutterService} from '../../api/Shutter/shutter.service';
import {RelativePipe} from '../../api/common/relative.pipe';
import {Shutter} from '../Shutter';
import {Subscription, timer} from 'rxjs';
import {ShutterIconComponent} from './shutter-icon/shutter-icon.component';
import {ShutterTileComponent} from '../shutter-tile/shutter-tile.component';
@Component({
selector: 'app-shutter-list',
standalone: true,
imports: [
NgForOf,
RelativePipe,
ShutterIconComponent
ShutterTileComponent
],
templateUrl: './shutter-list.component.html',
styleUrl: './shutter-list.component.less'
@ -28,12 +25,6 @@ export class ShutterListComponent implements OnInit, OnDestroy {
@Input()
list: Shutter[] = [];
constructor(
protected readonly shutterService: ShutterService,
) {
//
}
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));

View File

@ -0,0 +1,4 @@
<div class="window" (click)="activate.emit(position)">
<div *ngIf="isSet(position)" class="shutter" [style.height]="position + '%'"></div>
<div *ngIf="isUnset(position)" class="unknown">?</div>
</div>

View File

@ -1,6 +1,7 @@
@import "../../../../config";
.window {
position: relative;
width: 100%;
height: 100%;
background-color: lightskyblue;
@ -11,4 +12,15 @@
border-bottom: @border solid black;
}
.unknown {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1.25em;
color: red;
font-size: 260%;
text-align: center;
}
}

View File

@ -1,14 +1,23 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {NgIf} from '@angular/common';
import {isSet, isUnset} from '../../../api/validators';
@Component({
selector: 'app-shutter-icon',
standalone: true,
imports: [],
imports: [
NgIf
],
templateUrl: './shutter-icon.component.html',
styleUrl: './shutter-icon.component.less'
})
export class ShutterIconComponent {
protected readonly isUnset = isUnset;
protected readonly isSet = isSet;
@Input()
position?: number

View File

@ -0,0 +1,37 @@
<div class="tile">
<div class="tileInner shutter" [class.unknown]="isUnset(shutter.positionProperty?.state?.value)">
<div class="name">
{{ shutter.nameWithArea }}
</div>
<div class="icon">
<app-shutter-icon [position]="shutter.positionProperty?.state?.value"></app-shutter-icon>
</div>
<div class="timestamp details">
{{ shutter.positionProperty?.lastValueChange | relative:now }}
</div>
<div class="actions">
<div class="action">
<app-shutter-icon [position]="0" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="50" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="80" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="90" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="100" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,36 @@
@import "../../../config";
.shutter {
background-color: lightgray;
border: @border solid gray !important;
.name {
float: left;
}
.icon {
clear: left;
float: left;
width: 4em;
aspect-ratio: 1;
}
.timestamp {
float: right;
font-size: 80%;
}
.actions {
clear: right;
float: right;
.action {
float: left;
margin-left: @space;
width: 3em;
aspect-ratio: 1;
}
}
}

View File

@ -0,0 +1,34 @@
import {Component, Input} from '@angular/core';
import {RelativePipe} from "../../api/relative.pipe";
import {Shutter} from "../Shutter";
import {ShutterService} from '../shutter.service';
import {ShutterIconComponent} from './shutter-icon/shutter-icon.component';
import {isUnset} from '../../api/validators';
@Component({
selector: 'app-shutter-tile',
standalone: true,
imports: [
RelativePipe,
ShutterIconComponent
],
templateUrl: './shutter-tile.component.html',
styleUrl: './shutter-tile.component.less'
})
export class ShutterTileComponent {
protected readonly isUnset = isUnset;
@Input()
now!: Date;
@Input()
shutter!: Shutter;
constructor(
protected readonly shutterService: ShutterService,
) {
//
}
}

View File

@ -1,10 +1,8 @@
import {Injectable} from '@angular/core';
import {CrudService} from '../common/CrudService';
import {CrudService} from '../api/CrudService';
import {Shutter} from './Shutter';
import {ApiService} from '../common/api.service';
import {Next} from '../common/types';
import {ShutterFilter} from './ShutterFilter';
import {ApiService} from '../api/api.service';
import {Next} from '../api/types';
@Injectable({
providedIn: 'root'
@ -21,10 +19,6 @@ export class ShutterService extends CrudService<Shutter> {
this.getSingle(['getByUuid', uuid], next);
}
list(filter: ShutterFilter | null, next: Next<Shutter[]>): void {
this.postList(['list'], filter, next);
}
setPosition(shutter: Shutter, position: number, next?: Next<void>): void {
this.getNone(['setPosition', shutter.uuid, position], next);
}

View File

@ -1,4 +1,4 @@
import {validateDate, validateString} from "../common/validators";
import {validateDate, validateString} from "../api/validators";
export class State {

View File

@ -0,0 +1,19 @@
import {validateString} from '../api/validators';
export class Tag {
constructor(
readonly uuid: string,
readonly name: string,
) {
//
}
static fromJson(json: any): Tag {
return new Tag(
validateString(json.uuid),
validateString(json.name),
);
}
}

View File

@ -0,0 +1,17 @@
import {Injectable} from '@angular/core';
import {ApiService} from '../api/api.service';
import {CrudService} from '../api/CrudService';
import {Tag} from './Tag';
@Injectable({
providedIn: 'root'
})
export class TagService extends CrudService<Tag> {
constructor(
apiService: ApiService,
) {
super(apiService, ['Tag'], Tag.fromJson);
}
}

View File

@ -0,0 +1,41 @@
import {Area} from '../Area/Area';
export abstract class Thing {
protected constructor(
readonly area: Area,
readonly uuid: string,
readonly name: string,
readonly slug: string,
) {
//
}
get nameOrArea(): string {
if (this.name === '') {
return this.area.name;
}
return this.name;
}
get nameWithArea(): string {
if (this.name === '') {
return this.area.name;
}
return this.area.name + ' ' + this.name;
}
static trackBy(index: number, thing: Thing) {
return thing.uuid;
}
static compareByAreaThenName(a: Thing, b: Thing): number {
const area = Area.compareByName(a.area, b.area);
if (area !== 0) {
return area;
}
return a.name.localeCompare(b.name);
}
}

View File

@ -0,0 +1,7 @@
export class ThingFilter {
tag: string = "";
search: string = "";
}

View File

@ -0,0 +1,8 @@
<div class="flexBox">
<div class="flexBoxFixed">
<app-search [(search)]="filter.search" (doSearch)="liveList.refresh()"></app-search>
</div>
<div class="flexBoxRest verticalScroll">
<app-thing-list [list]="liveList.list"></app-thing-list>
</div>
</div>

View File

@ -0,0 +1,67 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {ThingListComponent} from '../thing-list/thing-list.component';
import {Thing} from '../Thing';
import {ThingService} from '../thing.service';
import {FormsModule} from '@angular/forms';
import {Subscription} from 'rxjs';
import {ThingFilter} from '../ThingFilter';
import {ActivatedRoute} from '@angular/router';
import {CrudLiveList} from '../../api/CrudLiveList';
import {SearchComponent} from '../../shared/search/search.component';
@Component({
selector: 'app-thing-list-page',
standalone: true,
imports: [
ThingListComponent,
FormsModule,
SearchComponent
],
templateUrl: './thing-list-page.component.html',
styleUrl: './thing-list-page.component.less'
})
export class ThingListPageComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
protected readonly filter: ThingFilter = new ThingFilter();
protected readonly liveList: CrudLiveList<Thing>;
private tagSet: boolean = false;
constructor(
protected readonly thingService: ThingService,
protected readonly activatedRoute: ActivatedRoute,
) {
this.subs.push(this.liveList = new CrudLiveList(
this.thingService,
false,
undefined,
next => {
if (this.tagSet) {
this.thingService.filter(this.filter, next);
} else {
next([]);
}
})
);
}
ngOnInit(): void {
this.subs.push(this.activatedRoute.params.subscribe(params => {
this.tagSet = 'tag' in params;
if (this.tagSet) {
this.filter.tag = params['tag'] || '';
this.liveList.refresh();
} else {
this.liveList.clear();
}
}));
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
}

View File

@ -0,0 +1,5 @@
<div class="tileContainer">
<app-thing-tile [now]="now" [thing]="thing" *ngFor="let thing of sorted()"></app-thing-tile>
</div>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -0,0 +1,39 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgForOf} from '@angular/common';
import {Subscription, timer} from 'rxjs';
import {ThingTileComponent} from '../thing-tile/thing-tile.component';
import {Thing} from '../Thing';
@Component({
selector: 'app-thing-list',
standalone: true,
imports: [
NgForOf,
ThingTileComponent
],
templateUrl: './thing-list.component.html',
styleUrl: './thing-list.component.less'
})
export class ThingListComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
protected now: Date = new Date();
@Input()
list: Thing[] = [];
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
sorted(): Thing[] {
return this.list.sort(Thing.compareByAreaThenName);
}
}

View File

@ -0,0 +1,5 @@
<app-device-tile *ngIf="isDevice()" [now]="now" [device]="asDevice()"></app-device-tile>
<app-shutter-tile *ngIf="isShutter()" [now]="now" [shutter]="asShutter()"></app-shutter-tile>
<app-tunable-tile *ngIf="isTunable()" [now]="now" [tunable]="asTunable()"></app-tunable-tile>

View File

@ -0,0 +1,55 @@
import {Component, Input} from '@angular/core';
import {Device} from '../../Device/Device';
import {Tunable} from '../../Tunable/Tunable';
import {Shutter} from '../../Shutter/Shutter';
import {DeviceTileComponent} from '../../Device/device-tile/device-tile.component';
import {NgIf} from '@angular/common';
import {ShutterTileComponent} from '../../Shutter/shutter-tile/shutter-tile.component';
import {TunableTileComponent} from '../../Tunable/tunable-tile/tunable-tile.component';
import {Thing} from '../Thing';
@Component({
selector: 'app-thing-tile',
standalone: true,
imports: [
DeviceTileComponent,
NgIf,
ShutterTileComponent,
TunableTileComponent
],
templateUrl: './thing-tile.component.html',
styleUrl: './thing-tile.component.less'
})
export class ThingTileComponent {
@Input()
now!: Date;
@Input()
thing!: Thing;
asDevice(): Device {
return this.thing as Device;
}
isDevice(): boolean {
return this.thing instanceof Device;
}
asShutter(): Shutter {
return this.thing as Shutter;
}
isShutter(): boolean {
return this.thing instanceof Shutter;
}
asTunable(): Tunable {
return this.thing as Tunable;
}
isTunable(): boolean {
return this.thing instanceof Tunable;
}
}

View File

@ -0,0 +1,34 @@
import {Injectable} from '@angular/core';
import {ApiService} from '../api/api.service';
import {CrudService} from '../api/CrudService';
import {Thing} from './Thing';
import {Next} from '../api/types';
import {Subject, Subscription} from 'rxjs';
import {DeviceService} from '../Device/device.service';
import {ShutterService} from '../Shutter/shutter.service';
import {TunableService} from '../Tunable/tunable.service';
import {thingFromJson} from './thingFromJson';
@Injectable({
providedIn: 'root'
})
export class ThingService extends CrudService<Thing> {
constructor(
apiService: ApiService,
protected readonly deviceService: DeviceService,
protected readonly shutterService: ShutterService,
protected readonly tunableService: TunableService,
) {
super(apiService, ['Thing'], thingFromJson);
}
override subscribe(next: Next<Thing>): Subscription {
const subject = new Subject<Thing>();
this.deviceService.subscribe(next => subject.next(next));
this.shutterService.subscribe(next => subject.next(next));
this.tunableService.subscribe(next => subject.next(next));
return subject.subscribe(next);
}
}

View File

@ -0,0 +1,18 @@
import {validateAndRemoveDtoSuffix} from "../api/validators";
import {Device} from "../Device/Device";
import {Shutter} from "../Shutter/Shutter";
import {Tunable} from "../Tunable/Tunable";
import {Thing} from "./Thing";
export function thingFromJson(json: any): Thing {
const _type_ = validateAndRemoveDtoSuffix(json._type_);
switch (_type_) {
case 'Device':
return Device.fromJson(json.payload);
case 'Shutter':
return Shutter.fromJson(json.payload);
case 'Tunable':
return Tunable.fromJson(json.payload);
}
throw new Error("Type not implemented: " + _type_);
}

View File

@ -1,14 +1,15 @@
import {Property} from "../Property/Property";
import {orNull, validateString} from "../common/validators";
import {orNull, validateString} from "../api/validators";
import {Area} from '../Area/Area';
import {Thing} from '../Thing/Thing';
export class Tunable {
export class Tunable extends Thing {
constructor(
readonly area: Area,
readonly uuid: string,
readonly name: string,
readonly slug: string,
area: Area,
uuid: string,
name: string,
slug: string,
readonly statePropertyId: string,
readonly stateProperty: Property | null,
readonly brightnessPropertyId: string,
@ -16,7 +17,7 @@ export class Tunable {
readonly coldnessPropertyId: string,
readonly coldnessProperty: Property | null,
) {
//
super(area, uuid, name, slug);
}
static fromJson(json: any): Tunable {
@ -34,34 +35,4 @@ export class Tunable {
);
}
get nameOrArea(): string {
if (this.name === '') {
return this.area.name;
}
return this.name;
}
get nameWithArea(): string {
if (this.name === '') {
return this.area.name;
}
return this.area.name + ' ' + this.name;
}
static trackBy(index: number, tunable: Tunable) {
return tunable.uuid;
}
static equals(a: Tunable, b: Tunable): boolean {
return a.uuid === b.uuid;
}
static compareByAreaThenName(a: Tunable, b: Tunable): number {
const area = Area.compareByName(a.area, b.area);
if (area !== 0) {
return area;
}
return a.name.localeCompare(b.name);
}
}

View File

@ -0,0 +1,5 @@
<div class="tileContainer tunableList">
<app-tunable-tile [now]="now" [tunable]="tunable" *ngFor="let tunable of sorted(); trackBy: Tunable.trackBy"></app-tunable-tile>
</div>

View File

@ -0,0 +1 @@
@import "../../../config";

View File

@ -1,20 +1,17 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgClass, NgForOf, NgIf} from '@angular/common';
import {Tunable} from '../../api/Tunable/Tunable';
import {TunableService} from '../../api/Tunable/tunable.service';
import {RelativePipe} from '../../api/common/relative.pipe';
import {NgForOf} from '@angular/common';
import {Tunable} from '../Tunable';
import {Subscription, timer} from 'rxjs';
import {FormsModule} from '@angular/forms';
import {TunableTileComponent} from '../tunable-tile/tunable-tile.component';
@Component({
selector: 'app-tunable-list',
standalone: true,
imports: [
NgForOf,
NgClass,
RelativePipe,
FormsModule,
NgIf
TunableTileComponent
],
templateUrl: './tunable-list.component.html',
styleUrl: './tunable-list.component.less'
@ -30,12 +27,6 @@ export class TunableListComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
constructor(
protected readonly tunableService: TunableService,
) {
//
}
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
@ -45,13 +36,6 @@ export class TunableListComponent implements OnInit, OnDestroy {
this.subs.forEach(sub => sub.unsubscribe());
}
ngClass(tunable: Tunable) {
return {
"stateOn": tunable.stateProperty?.state?.value === true,
"stateOff": tunable.stateProperty?.state?.value === false,
};
}
sorted(): Tunable[] {
return this.list.sort(Tunable.compareByAreaThenName);
}

View File

@ -0,0 +1,29 @@
<div class="tile">
<div class="tileInner tunable" [ngClass]="ngClass(tunable)">
<div class="name">
{{ tunable.nameWithArea }}
</div>
<div class="timestamp details">
{{ tunable.stateProperty?.lastValueChange | relative:now }}
</div>
<div class="sliders">
<div class="slider" *ngIf="tunable.brightnessPropertyId !== ''">
<input type="range" [ngModel]="tunable.brightnessProperty?.state?.value" (ngModelChange)="tunableService.setBrightness(tunable, $event)">
</div>
<div class="slider" *ngIf="tunable.coldnessPropertyId !== ''">
<input type="range" [ngModel]="tunable.coldnessProperty?.state?.value" (ngModelChange)="tunableService.setColdness(tunable, $event)">
</div>
</div>
<div class="actions">
<div class="switch switchOn" (click)="tunableService.setState(tunable, true)"></div>
<div class="switch switchOff" (click)="tunableService.setState(tunable, false)"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,59 @@
@import "../../../config";
.tunable {
.name {
float: left;
}
.timestamp {
float: right;
font-size: 80%;
}
.sliders {
float: left;
clear: left;
width: 60%;
overflow: visible;
padding-top: 0.4em;
.slider {
float: left;
clear: left;
margin-left: @space;
width: 100%;
overflow: visible;
input {
width: 100%;
height: 2em;
}
}
}
.actions {
float: right;
.switch {
float: left;
margin-left: @space;
width: 4em;
aspect-ratio: 1;
}
.switchOn {
//noinspection CssUnknownTarget
background-image: url("/switchOn.svg");
}
.switchOff {
//noinspection CssUnknownTarget
background-image: url("/switchOff.svg");
}
}
}

View File

@ -0,0 +1,44 @@
import {Component, Input} from '@angular/core';
import {NgClass, NgIf} from "@angular/common";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {RelativePipe} from "../../api/relative.pipe";
import {Tunable} from "../Tunable";
import {TunableService} from '../tunable.service';
@Component({
selector: 'app-tunable-tile',
standalone: true,
imports: [
NgIf,
ReactiveFormsModule,
RelativePipe,
NgClass,
FormsModule
],
templateUrl: './tunable-tile.component.html',
styleUrl: './tunable-tile.component.less'
})
export class TunableTileComponent {
protected readonly Tunable = Tunable;
@Input()
now!: Date;
@Input()
tunable!: Tunable;
constructor(
protected readonly tunableService: TunableService,
) {
//
}
ngClass(tunable: Tunable) {
return {
"stateOn": tunable.stateProperty?.state?.value === true,
"stateOff": tunable.stateProperty?.state?.value === false,
};
}
}

View File

@ -1,10 +1,8 @@
import {Injectable} from '@angular/core';
import {CrudService} from '../common/CrudService';
import {CrudService} from '../api/CrudService';
import {Tunable} from './Tunable';
import {ApiService} from '../common/api.service';
import {Next} from '../common/types';
import {TunableFilter} from './TunableFilter';
import {ApiService} from '../api/api.service';
import {Next} from '../api/types';
@Injectable({
providedIn: 'root'
@ -21,10 +19,6 @@ export class TunableService extends CrudService<Tunable> {
this.getSingle(['getByUuid', uuid], next);
}
list(filter: TunableFilter | null, next: Next<Tunable[]>): void {
this.postList(['list'], filter, next);
}
setState(tunable: Tunable, state: boolean, next?: Next<void>): void {
this.getNone(['setState', tunable.uuid, state], next);
}

View File

@ -1,46 +1,62 @@
import {CrudService} from "./CrudService";
import {Subscription} from "rxjs";
import {Next} from './types';
export class CrudLiveList<ENTITY> extends Subscription {
interface UUID {
uuid: string;
}
export class CrudLiveList<ENTITY extends UUID> extends Subscription {
private readonly subs: Subscription[] = [];
unfiltered: ENTITY[] = [];
private unfiltered: ENTITY[] = [];
filtered: ENTITY[] = [];
list: ENTITY[] = [];
constructor(
readonly crudService: CrudService<ENTITY>,
readonly equals: (a: ENTITY, b: ENTITY) => boolean,
readonly allowAppending: boolean,
readonly filter: (item: ENTITY) => boolean = _ => true,
readonly all: (next: Next<ENTITY[]>) => any = next => this.crudService.list(next),
readonly equals: (a: ENTITY, b: ENTITY) => boolean = (a, b) => a.uuid === b.uuid,
) {
super(() => {
this.subs.forEach(sub => sub.unsubscribe());
});
this.fetchAll();
this.subs.push(crudService.api.connected(_ => this.fetchAll()));
this.subs.push(crudService.api.connected(_ => this.refresh()));
this.subs.push(crudService.subscribe(item => this.update(item)));
}
private fetchAll() {
this.crudService.all(list => {
refresh() {
this.all(list => {
this.unfiltered = list;
this.updateFiltered();
});
}
clear() {
this.unfiltered = [];
this.updateFiltered();
}
private update(item: ENTITY) {
const index = this.unfiltered.findIndex(i => this.equals(i, item));
if (index >= 0) {
this.unfiltered[index] = item;
} else {
if (!this.allowAppending) {
return;
}
this.unfiltered.push(item);
}
this.updateFiltered();
}
private updateFiltered() {
this.filtered = this.unfiltered.filter(this.filter);
this.list = this.unfiltered.filter(this.filter);
}
}

View File

@ -13,10 +13,14 @@ export abstract class CrudService<ENTITY> {
//
}
all(next: Next<ENTITY[]>) {
list(next: Next<ENTITY[]>): void {
this.getList(['list'], next);
}
filter<FILTER>(filter: FILTER, next: Next<ENTITY[]>): void {
this.postList(['list'], filter, next);
}
subscribe(next: Next<ENTITY>): Subscription {
return this.api.subscribe([...this.path], this.fromJson, next);
}

View File

@ -1,59 +0,0 @@
import {Property} from "../Property/Property";
import {orNull, validateString} from "../common/validators";
import {Area} from '../Area/Area';
export class Device {
constructor(
readonly area: Area,
readonly uuid: string,
readonly name: string,
readonly slug: string,
readonly statePropertyId: string,
readonly stateProperty: Property | null,
) {
//
}
static fromJson(json: any): Device {
return new Device(
Area.fromJson(json.area),
validateString(json.uuid),
validateString(json.name),
validateString(json.slug),
validateString(json.statePropertyId),
orNull(json.stateProperty, Property.fromJson),
);
}
get nameOrArea(): string {
if (this.name === '') {
return this.area.name;
}
return this.name;
}
get nameWithArea(): string {
if (this.name === '') {
return this.area.name;
}
return this.area.name + ' ' + this.name;
}
static trackBy(index: number, device: Device) {
return device.uuid;
}
static equals(a: Device, b: Device): boolean {
return a.uuid === b.uuid;
}
static compareByAreaThenName(a: Device, b: Device): number {
const area = Area.compareByName(a.area, b.area);
if (area !== 0) {
return area;
}
return a.name.localeCompare(b.name);
}
}

View File

@ -1,60 +0,0 @@
import {Property} from "../Property/Property";
import {orNull, validateString} from "../common/validators";
import {Area} from '../Area/Area';
export class Shutter {
constructor(
readonly area: Area,
readonly uuid: string,
readonly name: string,
readonly slug: string,
readonly positionPropertyId: string,
readonly positionProperty: Property | null,
) {
//
}
static fromJson(json: any): Shutter {
return new Shutter(
Area.fromJson(json.area),
validateString(json.uuid),
validateString(json.name),
validateString(json.slug),
validateString(json.positionPropertyId),
orNull(json.positionProperty, Property.fromJson),
);
}
get nameOrArea(): string {
if (this.name === '') {
return this.area.name;
}
return this.name;
}
get nameWithArea(): string {
if (this.name === '') {
return this.area.name;
}
return this.area.name + ' ' + this.name;
}
static trackBy(index: number, shutter: Shutter) {
return shutter.uuid;
}
static equals(a: Shutter, b: Shutter): boolean {
return a.uuid === b.uuid;
}
static compareByAreaThenName(a: Shutter, b: Shutter): number {
const area = Area.compareByName(a.area, b.area);
if (area !== 0) {
return area;
}
return a.name.localeCompare(b.name);
}
}

View File

@ -1,4 +1,4 @@
import {environment} from "../../../environments/environment";
import {environment} from "../../environments/environment";
export type FromJson<T> = (json: any) => T;

View File

@ -78,3 +78,19 @@ export function orNull<T, R>(item: T | null | undefined, map: (t: T) => R): R |
}
return map(item);
}
export function validateAndRemoveDtoSuffix(json: any): string {
const type = validateString(json);
if (!type.endsWith('Dto')) {
throw Error("Type name does not end with Dto: " + type);
}
return type.substring(0, type.length - 3);
}
export function isSet(value: any) {
return value !== null && value !== undefined;
}
export function isUnset(value: any) {
return value === null || value === undefined;
}

View File

@ -1,9 +1,9 @@
<div class="flexBox">
<div class="flexBoxFixed menu">
<div class="item itemLeft" routerLink="Dashboard" routerLinkActive="active">Dash</div>
<div class="item itemLeft" routerLink="DeviceList" routerLinkActive="active">Geräte</div>
<div class="item itemLeft" routerLink="TunableList" routerLinkActive="active">Lichter</div>
<div class="item itemLeft" routerLink="ShutterList" routerLinkActive="active">Rollläden</div>
<div class="item itemLeft" routerLink="ThingList/device" routerLinkActive="active">Geräte</div>
<div class="item itemLeft" routerLink="ThingList/light" routerLinkActive="active">Licht</div>
<div class="item itemLeft" routerLink="ThingList/shutter" routerLinkActive="active">Rollladen</div>
<div class="item itemRight" routerLink="GroupList" routerLinkActive="active">KNX</div>
</div>
<div class="flexBoxRest">

View File

@ -1,6 +1,6 @@
import {Component} from '@angular/core';
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {ApiService} from './api/common/api.service';
import {ApiService} from './api/api.service';
import {NgIf} from '@angular/common';
@Component({

View File

@ -7,7 +7,7 @@ import {registerLocaleData} from '@angular/common';
import localeDe from '@angular/common/locales/de';
import localeDeExtra from '@angular/common/locales/extra/de';
import {stompServiceFactory} from './api/common/ws';
import {stompServiceFactory} from './api/ws';
import {StompService} from '@stomp/ng2-stompjs';
registerLocaleData(localeDe, 'de-DE', localeDeExtra);

View File

@ -1,15 +1,12 @@
import {Routes} from '@angular/router';
import {KnxGroupListPageComponent} from './pages/knx-group-list-page/knx-group-list-page.component';
import {DeviceListPageComponent} from './pages/device-list-page/device-list-page.component';
import {ShutterListPageComponent} from './pages/shutter-list-page/shutter-list-page.component';
import {TunableListPageComponent} from './pages/tunable-list-page/tunable-list-page.component';
import {DashboardComponent} from './pages/dashboard/dashboard.component';
import {KnxGroupListPageComponent} from './Group/knx-group-list-page/knx-group-list-page.component';
import {DashboardComponent} from './dashboard/dashboard.component';
import {ThingListPageComponent} from './Thing/thing-list-page/thing-list-page.component';
export const routes: Routes = [
{path: 'Dashboard', component: DashboardComponent},
{path: 'DeviceList', component: DeviceListPageComponent},
{path: 'TunableList', component: TunableListPageComponent},
{path: 'ShutterList', component: ShutterListPageComponent},
{path: 'GroupList', component: KnxGroupListPageComponent},
{path: 'ThingList', component: ThingListPageComponent},
{path: 'ThingList/:tag', component: ThingListPageComponent},
{path: '**', redirectTo: 'Dashboard'},
];

View File

@ -0,0 +1,11 @@
<div class="flexBox">
<div class="flexBoxFixed">
<app-search [(search)]="search" (doSearch)="refresh()"></app-search>
</div>
<div class="flexBoxRest verticalScroll">
<app-device-list [list]="deviceList.list"></app-device-list>
<app-tunable-list [list]="tunableList.list"></app-tunable-list>
<app-shutter-list [list]="shutterList.list"></app-shutter-list>
<div class="emptyBox" *ngIf="deviceList.list.length === 0 && tunableList.list.length === 0 && shutterList.list.length === 0">- Nichts -</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
@import "../../../config";
@import "../../config";
.subheading {
font-size: 65%;
@ -6,4 +6,5 @@
padding-top: calc(@space * 2);
padding-left: calc(@space * 2);
color: gray;
white-space: nowrap;
}

View File

@ -0,0 +1,101 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {DeviceService} from '../Device/device.service';
import {TunableService} from '../Tunable/tunable.service';
import {Device} from '../Device/Device';
import {Tunable} from '../Tunable/Tunable';
import {Shutter} from '../Shutter/Shutter';
import {ShutterService} from '../Shutter/shutter.service';
import {DeviceListComponent} from '../Device/device-list/device-list.component';
import {FormsModule} from '@angular/forms';
import {CrudLiveList} from '../api/CrudLiveList';
import {TunableListComponent} from '../Tunable/tunable-list/tunable-list.component';
import {ShutterListComponent} from '../Shutter/shutter-list/shutter-list.component';
import {Subscription, timer} from 'rxjs';
import {NgIf} from '@angular/common';
import {SearchComponent} from '../shared/search/search.component';
import {DeviceFilter} from '../Device/DeviceFilter';
import {TunableFilter} from '../Tunable/TunableFilter';
import {ShutterFilter} from '../Shutter/ShutterFilter';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
DeviceListComponent,
FormsModule,
TunableListComponent,
ShutterListComponent,
NgIf,
SearchComponent,
],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.less'
})
export class DashboardComponent implements OnInit, OnDestroy {
protected deviceList!: CrudLiveList<Device>;
protected tunableList!: CrudLiveList<Tunable>;
protected shutterList!: CrudLiveList<Shutter>;
protected now: Date = new Date();
private subs: Subscription[] = [];
protected shutterSubheading: string = "";
protected shuttersShouldBeOpen: boolean = false;
protected search: string = '';
private readonly deviceFilter: DeviceFilter = new DeviceFilter();
private readonly shutterFilter: ShutterFilter = new ShutterFilter();
private readonly tunableFilter: TunableFilter = new TunableFilter();
constructor(
protected readonly deviceService: DeviceService,
protected readonly shutterService: ShutterService,
protected readonly tunableService: TunableService,
) {
}
ngOnInit(): void {
this.newDate();
this.subs.push(timer(5000, 5000).subscribe(() => this.newDate()));
this.subs.push(this.deviceList = new CrudLiveList(this.deviceService, true, device => device.stateProperty?.state?.value === true, next => this.deviceService.filter(this.deviceFilter, next)));
this.subs.push(this.shutterList = new CrudLiveList(this.shutterService, true, shutter => this.shutterFilter2(shutter), next => this.shutterService.filter(this.shutterFilter, next)));
this.subs.push(this.tunableList = new CrudLiveList(this.tunableService, true, tunable => tunable.stateProperty?.state?.value === true, next => this.tunableService.filter(this.tunableFilter, next)));
}
private newDate() {
this.now = new Date();
this.shuttersShouldBeOpen = this.now.getHours() >= 7 && this.now.getHours() < 16;
this.shutterSubheading = this.shuttersShouldBeOpen ? "Geschlossene" : "Offene";
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
private shutterFilter2(shutter: Shutter) {
if (this.shuttersShouldBeOpen) {
return shutter.positionProperty?.state?.value !== 0;
} else {
return shutter.positionProperty?.state?.value !== 100;
}
}
refresh() {
this.deviceFilter.search = this.search;
this.deviceList.refresh();
this.tunableFilter.search = this.search;
this.tunableList.refresh();
this.shutterFilter.search = this.search;
this.shutterList.refresh();
}
}

View File

@ -1,21 +0,0 @@
<div class="verticalScroll">
<div class="subheading">
Eingeschaltete Geräte:
{{ deviceList.filtered.length }} / {{ deviceList.unfiltered.length }}
</div>
<app-device-list [list]="deviceList.filtered"></app-device-list>
<div class="subheading">
Eingeschaltete Lichter:
{{ tunableList.filtered.length }} / {{ tunableList.unfiltered.length }}
</div>
<app-tunable-list [list]="tunableList.filtered"></app-tunable-list>
<div class="subheading">
{{ shutterSubheading }} Rollläden:
{{ shutterList.filtered.length }} / {{ shutterList.unfiltered.length }}
</div>
<app-shutter-list [list]="shutterList.filtered"></app-shutter-list>
</div>

View File

@ -1,76 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {DeviceService} from '../../api/Device/device.service';
import {TunableService} from '../../api/Tunable/tunable.service';
import {Device} from '../../api/Device/Device';
import {Tunable} from '../../api/Tunable/Tunable';
import {Shutter} from '../../api/Shutter/Shutter';
import {ShutterService} from '../../api/Shutter/shutter.service';
import {DeviceListComponent} from '../../shared/device-list/device-list.component';
import {FormsModule} from '@angular/forms';
import {CrudLiveList} from '../../api/common/CrudLiveList';
import {TunableListComponent} from '../../shared/tunable-list/tunable-list.component';
import {ShutterListComponent} from '../../shared/shutter-list/shutter-list.component';
import {Subscription, timer} from 'rxjs';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
DeviceListComponent,
FormsModule,
TunableListComponent,
ShutterListComponent,
],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.less'
})
export class DashboardComponent implements OnInit, OnDestroy {
protected deviceList!: CrudLiveList<Device>;
protected tunableList!: CrudLiveList<Tunable>;
protected shutterList!: CrudLiveList<Shutter>;
protected now: Date = new Date();
private subs: Subscription[] = [];
protected shutterSubheading: string = "";
protected shuttersShouldBeOpen: boolean = false;
constructor(
protected readonly deviceService: DeviceService,
protected readonly tunableService: TunableService,
protected readonly shutterService: ShutterService,
) {
}
ngOnInit(): void {
this.newDate();
this.subs.push(timer(5000, 5000).subscribe(() => this.newDate()));
this.subs.push(this.deviceList = new CrudLiveList(this.deviceService, Device.equals, device => device.stateProperty?.state?.value === true));
this.subs.push(this.tunableList = new CrudLiveList(this.tunableService, Tunable.equals, tunable => tunable.stateProperty?.state?.value === true));
this.subs.push(this.shutterList = new CrudLiveList(this.shutterService, Shutter.equals, shutter => this.shutterFilter(shutter)));
}
private newDate() {
this.now = new Date();
this.shuttersShouldBeOpen = this.now.getHours() >= 7 && this.now.getHours() < 16;
this.shutterSubheading = this.shuttersShouldBeOpen ? "Geschlossene" : "Offene";
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
private shutterFilter(shutter: Shutter) {
if (this.shuttersShouldBeOpen) {
return shutter.positionProperty?.state?.value !== 0;
} else {
return shutter.positionProperty?.state?.value !== 100;
}
}
}

View File

@ -1,8 +0,0 @@
<div class="flexBox">
<div class="flexBoxFixed">
<input type="text" [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
</div>
<div class="flexBoxRest verticalScroll">
<app-device-list [list]="deviceList"></app-device-list>
</div>
</div>

View File

@ -1,5 +0,0 @@
@import "../../../config";
input {
border-bottom: @border solid lightgray;
}

View File

@ -1,68 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {DeviceListComponent} from '../../shared/device-list/device-list.component';
import {Device} from '../../api/Device/Device';
import {DeviceService} from '../../api/Device/device.service';
import {FormsModule} from '@angular/forms';
import {DeviceFilter} from '../../api/Device/DeviceFilter';
import {Subscription} from 'rxjs';
import {ApiService} from '../../api/common/api.service';
@Component({
selector: 'app-device-list-page',
standalone: true,
imports: [
DeviceListComponent,
FormsModule
],
templateUrl: './device-list-page.component.html',
styleUrl: './device-list-page.component.less'
})
export class DeviceListPageComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
protected deviceList: Device[] = [];
protected filter: DeviceFilter = new DeviceFilter();
private fetchTimeout: any;
constructor(
protected readonly deviceService: DeviceService,
protected readonly apiService: ApiService,
) {
//
}
ngOnInit(): void {
this.fetch();
this.subs.push(this.deviceService.subscribe(device => this.updateDevice(device)));
this.apiService.connected(() => this.fetch());
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
fetchDelayed() {
if (this.fetchTimeout) {
clearTimeout(this.fetchTimeout);
this.fetchTimeout = undefined;
}
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
}
private fetch() {
this.deviceService.list(this.filter, list => this.deviceList = list)
}
private updateDevice(device: Device) {
const index = this.deviceList.findIndex(d => d.uuid === device.uuid);
if (index >= 0) {
this.deviceList.splice(index, 1, device);
} else {
this.fetch();
}
}
}

View File

@ -1,8 +0,0 @@
<div class="flexBox">
<div class="flexBoxFixed">
<input type="text" [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
</div>
<div class="flexBoxRest verticalScroll">
<app-shutter-list [list]="shutterList"></app-shutter-list>
</div>
</div>

View File

@ -1,5 +0,0 @@
@import "../../../config";
input {
border-bottom: @border solid lightgray;
}

View File

@ -1,68 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {ShutterListComponent} from '../../shared/shutter-list/shutter-list.component';
import {Shutter} from '../../api/Shutter/Shutter';
import {ShutterService} from '../../api/Shutter/shutter.service';
import {FormsModule} from '@angular/forms';
import {ShutterFilter} from '../../api/Shutter/ShutterFilter';
import {Subscription} from 'rxjs';
import {ApiService} from '../../api/common/api.service';
@Component({
selector: 'app-shutter-list-page',
standalone: true,
imports: [
ShutterListComponent,
FormsModule
],
templateUrl: './shutter-list-page.component.html',
styleUrl: './shutter-list-page.component.less'
})
export class ShutterListPageComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
protected shutterList: Shutter[] = [];
protected filter: ShutterFilter = new ShutterFilter();
private fetchTimeout: any;
constructor(
protected readonly shutterService: ShutterService,
protected readonly apiService: ApiService,
) {
//
}
ngOnInit(): void {
this.fetch();
this.subs.push(this.shutterService.subscribe(shutter => this.updateShutter(shutter)));
this.apiService.connected(() => this.fetch());
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
fetchDelayed() {
if (this.fetchTimeout) {
clearTimeout(this.fetchTimeout);
this.fetchTimeout = undefined;
}
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
}
private fetch() {
this.shutterService.list(this.filter, list => this.shutterList = list)
}
private updateShutter(shutter: Shutter) {
const index = this.shutterList.findIndex(d => d.uuid === shutter.uuid);
if (index >= 0) {
this.shutterList.splice(index, 1, shutter);
} else {
this.fetch();
}
}
}

View File

@ -1,8 +0,0 @@
<div class="flexBox">
<div class="flexBoxFixed">
<input type="text" [(ngModel)]="filter.search" (ngModelChange)="fetchDelayed()" placeholder="Suchen...">
</div>
<div class="flexBoxRest verticalScroll">
<app-tunable-list [list]="tunableList"></app-tunable-list>
</div>
</div>

View File

@ -1,5 +0,0 @@
@import "../../../config";
input {
border-bottom: @border solid lightgray;
}

View File

@ -1,68 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {TunableListComponent} from '../../shared/tunable-list/tunable-list.component';
import {Tunable} from '../../api/Tunable/Tunable';
import {TunableService} from '../../api/Tunable/tunable.service';
import {FormsModule} from '@angular/forms';
import {TunableFilter} from '../../api/Tunable/TunableFilter';
import {Subscription} from 'rxjs';
import {ApiService} from '../../api/common/api.service';
@Component({
selector: 'app-tunable-list-page',
standalone: true,
imports: [
TunableListComponent,
FormsModule
],
templateUrl: './tunable-list-page.component.html',
styleUrl: './tunable-list-page.component.less'
})
export class TunableListPageComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
protected tunableList: Tunable[] = [];
protected filter: TunableFilter = new TunableFilter();
private fetchTimeout: any;
constructor(
protected readonly tunableService: TunableService,
protected readonly apiService: ApiService,
) {
//
}
ngOnInit(): void {
this.fetch();
this.apiService.connected(() => this.fetch());
this.subs.push(this.tunableService.subscribe(tunable => this.updateTunable(tunable)));
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
fetchDelayed() {
if (this.fetchTimeout) {
clearTimeout(this.fetchTimeout);
this.fetchTimeout = undefined;
}
this.fetchTimeout = setTimeout(() => this.fetch(), 300)
}
private fetch() {
this.tunableService.list(this.filter, list => this.tunableList = list)
}
private updateTunable(tunable: Tunable) {
const index = this.tunableList.findIndex(d => d.uuid === tunable.uuid);
if (index >= 0) {
this.tunableList.splice(index, 1, tunable);
} else {
this.fetch();
}
}
}

View File

@ -1,24 +0,0 @@
<div class="deviceList tileContainer">
<div class="tile" *ngFor="let device of sorted(); trackBy: Device.trackBy">
<div class="device tileInner" [ngClass]="ngClass(device)">
<div class="name">
{{ device.nameWithArea }}
</div>
<div class="actions">
<div class="action switchOn" (click)="deviceService.setState(device, true)"></div>
<div class="action switchOff" (click)="deviceService.setState(device, false)"></div>
</div>
<div class="timestamp details">
{{ device.stateProperty?.lastValueChange | relative:now }}
</div>
</div>
</div>
</div>

View File

@ -1,41 +0,0 @@
@import "../../../config";
.deviceList {
.device {
.name {
float: left;
}
.timestamp {
clear: left;
float: left;
font-size: 80%;
}
.actions {
float: right;
.action {
float: left;
margin-left: @space;
width: 4em;
aspect-ratio: 1;
}
.switchOn {
//noinspection CssUnknownTarget
background-image: url("/switchOn.svg");
}
.switchOff {
//noinspection CssUnknownTarget
background-image: url("/switchOff.svg");
}
}
}
}

View File

@ -1,35 +0,0 @@
<div class="groupList tileContainer">
<div class="tile" *ngFor="let group of sorted(); trackBy: Group.trackBy">
<div class="group tileInner" [ngClass]="ngClass(group)">
<div class="name">
{{ group.name }}
</div>
<div class="details">
<div class="stackLeft address">
{{ group.address }}
</div>
<div class="stackLeft dpt">
DPT {{ group.dpt }}
</div>
<div class="stackRight state">
{{ group.state?.string || '-' }}
</div>
<div class="stackRight timestamp">
{{ group.lastValueChange | relative:now }}:
</div>
</div>
</div>
</div>
</div>

View File

@ -1,13 +0,0 @@
@import "../../../config";
.groupList {
.group {
.name {
margin-bottom: @space;
}
}
}

View File

@ -0,0 +1,3 @@
<div class="box">
<input type="text" [(ngModel)]="search" (ngModelChange)="fetchDelayed()" placeholder="Filter ...">
</div>

View File

@ -0,0 +1,6 @@
@import "../../../config";
.box {
width: 100%;
padding: @space @space 0 @space;
}

View File

@ -0,0 +1,35 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-search',
standalone: true,
imports: [
FormsModule
],
templateUrl: './search.component.html',
styleUrl: './search.component.less'
})
export class SearchComponent {
@Input()
search: string = '';
@Output()
searchChange: EventEmitter<string> = new EventEmitter();
@Output()
doSearch: EventEmitter<string> = new EventEmitter();
private fetchTimeout: any;
fetchDelayed() {
if (this.fetchTimeout) {
clearTimeout(this.fetchTimeout);
this.fetchTimeout = undefined;
}
this.searchChange.emit(this.search);
this.fetchTimeout = setTimeout(() => this.doSearch.emit(this.search), 300);
}
}

View File

@ -1,5 +0,0 @@
<div class="window" (click)="activate.emit(position)">
<div class="shutter" [style.height]="position + '%'">
<!-- -->
</div>
</div>

View File

@ -1,41 +0,0 @@
<div class="shutterList tileContainer">
<div class="tile" *ngFor="let shutter of sorted(); trackBy: Shutter.trackBy">
<div class="shutter tileInner">
<div class="name">
{{ shutter.nameWithArea }}
</div>
<div class="icon">
<app-shutter-icon [position]="shutter.positionProperty?.state?.value"></app-shutter-icon>
</div>
<div class="timestamp details">
{{ shutter.positionProperty?.lastValueChange | relative:now }}
</div>
<div class="actions">
<div class="action">
<app-shutter-icon [position]="0" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="50" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="80" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="90" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="100" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
</div>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More