Compare commits
8 Commits
dc3e262010
...
3970a9a142
| Author | SHA1 | Date | |
|---|---|---|---|
| 3970a9a142 | |||
| ea0aa3e00a | |||
| 8133080e9c | |||
| b6f3db79e4 | |||
| 417bf890a0 | |||
| 61ffab50ba | |||
| ad130fc35e | |||
| 120d6fffdc |
@ -1,4 +1,4 @@
|
||||
import {validateString} from "../common/validators";
|
||||
import {validateString} from "../api/validators";
|
||||
|
||||
export class Area {
|
||||
|
||||
30
src/main/angular/src/app/Device/Device.ts
Normal file
30
src/main/angular/src/app/Device/Device.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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 {
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -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) {
|
||||
@ -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>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -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()));
|
||||
@ -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>
|
||||
@ -0,0 +1,8 @@
|
||||
@import "../../../config";
|
||||
|
||||
.group {
|
||||
|
||||
.name {
|
||||
}
|
||||
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
31
src/main/angular/src/app/Shutter/Shutter.ts
Normal file
31
src/main/angular/src/app/Shutter/Shutter.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -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()));
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {validateDate, validateString} from "../common/validators";
|
||||
import {validateDate, validateString} from "../api/validators";
|
||||
|
||||
export class State {
|
||||
|
||||
19
src/main/angular/src/app/Tag/Tag.ts
Normal file
19
src/main/angular/src/app/Tag/Tag.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
17
src/main/angular/src/app/Tag/tag.service.ts
Normal file
17
src/main/angular/src/app/Tag/tag.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
41
src/main/angular/src/app/Thing/Thing.ts
Normal file
41
src/main/angular/src/app/Thing/Thing.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
7
src/main/angular/src/app/Thing/ThingFilter.ts
Normal file
7
src/main/angular/src/app/Thing/ThingFilter.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class ThingFilter {
|
||||
|
||||
tag: string = "";
|
||||
|
||||
search: string = "";
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="tileContainer">
|
||||
|
||||
<app-thing-tile [now]="now" [thing]="thing" *ngFor="let thing of sorted()"></app-thing-tile>
|
||||
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
34
src/main/angular/src/app/Thing/thing.service.ts
Normal file
34
src/main/angular/src/app/Thing/thing.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
18
src/main/angular/src/app/Thing/thingFromJson.ts
Normal file
18
src/main/angular/src/app/Thing/thingFromJson.ts
Normal 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_);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1 @@
|
||||
@import "../../../config";
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {environment} from "../../../environments/environment";
|
||||
import {environment} from "../../environments/environment";
|
||||
|
||||
export type FromJson<T> = (json: any) => T;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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'},
|
||||
];
|
||||
|
||||
11
src/main/angular/src/app/dashboard/dashboard.component.html
Normal file
11
src/main/angular/src/app/dashboard/dashboard.component.html
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
101
src/main/angular/src/app/dashboard/dashboard.component.ts
Normal file
101
src/main/angular/src/app/dashboard/dashboard.component.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -1,5 +0,0 @@
|
||||
@import "../../../config";
|
||||
|
||||
input {
|
||||
border-bottom: @border solid lightgray;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -1,5 +0,0 @@
|
||||
@import "../../../config";
|
||||
|
||||
input {
|
||||
border-bottom: @border solid lightgray;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -1,5 +0,0 @@
|
||||
@import "../../../config";
|
||||
|
||||
input {
|
||||
border-bottom: @border solid lightgray;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -1,13 +0,0 @@
|
||||
@import "../../../config";
|
||||
|
||||
.groupList {
|
||||
|
||||
.group {
|
||||
|
||||
.name {
|
||||
margin-bottom: @space;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<div class="box">
|
||||
<input type="text" [(ngModel)]="search" (ngModelChange)="fetchDelayed()" placeholder="Filter ...">
|
||||
</div>
|
||||
@ -0,0 +1,6 @@
|
||||
@import "../../../config";
|
||||
|
||||
.box {
|
||||
width: 100%;
|
||||
padding: @space @space 0 @space;
|
||||
}
|
||||
35
src/main/angular/src/app/shared/search/search.component.ts
Normal file
35
src/main/angular/src/app/shared/search/search.component.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<div class="window" (click)="activate.emit(position)">
|
||||
<div class="shutter" [style.height]="position + '%'">
|
||||
<!-- -->
|
||||
</div>
|
||||
</div>
|
||||
@ -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
Loading…
Reference in New Issue
Block a user