Compare commits

...

56 Commits

Author SHA1 Message Date
8ded5e6c7d KNXIllegalArgumentException when reading wrong DPT 2 2025-05-27 13:06:36 +02:00
eb99652e76 KNXIllegalArgumentException when reading wrong DPT 2025-05-27 13:00:06 +02:00
ac750e9515 FIX: UI: updateProperty duplicates 2024-11-25 15:11:06 +01:00
0111036617 KnxGroupImportController 2024-11-25 15:03:06 +01:00
2986238dc8 updating (and deleting) KnxGroup from ETS files 2024-11-25 14:25:18 +01:00
bad017e1dd POST->GET property/get/{id}/value 2024-11-18 13:29:51 +01:00
1acd5506c6 property/get/{id}/value 2024-11-18 13:27:40 +01:00
277e321480 AstroCalculator DST change FIX 2024-10-28 10:41:21 +01:00
bef7cc4a6e css generalized 'tile' (ChannelList) 2024-09-12 09:30:01 +02:00
c174a18a4a FIX: nextFuzzyTimestamp in past 2024-09-12 09:29:35 +02:00
1c0ffc7b64 css generalized 'tile' (PropertyList) 2024-09-12 09:11:58 +02:00
e991b23479 ScheduleEntryType EnumType.String 2024-09-11 16:27:56 +02:00
188d13591f Astro entity removed 2024-09-11 16:26:51 +02:00
80e61a5c6c code clean 2024-09-11 15:59:26 +02:00
4ade9787db removed trailing slash from 'create' urls 2024-09-11 15:45:50 +02:00
d5ce06d4d7 postgres 2024-09-11 15:31:36 +02:00
44820f3201 css generalized 'tile' (DeviceList) 2024-09-11 15:00:56 +02:00
073c42002a css generalized 'tile' (BulkList) 2024-09-11 14:04:27 +02:00
8915d9a8d4 css generalized 'tile' (ScheduleList, ScheduleEditor) 2024-09-11 13:47:48 +02:00
0f91c35771 ScheduleEditor responsive 2024-09-11 11:35:41 +02:00
6ded6da0e2 execute Bulk from ScheduleList 2024-09-09 13:36:10 +02:00
bb972283a3 ScheduleEntry.skip 2024-09-09 13:32:40 +02:00
4cdb051e17 spring-boot-starter-parent 3.3.3 (javax -> jakarta) 2024-09-09 13:10:23 +02:00
2f6e4ff7cb ScheduleList desktop 400px 2024-09-09 11:45:38 +02:00
838e2a16fa schedule-list responsive 2024-08-15 08:25:34 +02:00
979c4d0a39 differentNAmeForSunset "Untergang" 2023-07-05 23:55:01 +02:00
f6acfd83d1 Bulk duplicate 2023-07-05 23:45:27 +02:00
449e58b8ec UI sorting 2023-07-05 23:31:07 +02:00
0a126f554b FIX: BulkReader searched for 'title' instead of 'name' 2023-07-05 23:23:43 +02:00
e17d45aa52 UI NPE lastFuzzyTimestamp 2023-06-26 23:33:27 +02:00
83aacd2a8d Better fulltext search 2023-06-19 10:26:01 +02:00
7970c16026 UI: missing header cell 2023-06-19 09:51:27 +02:00
85e2c70645 default page is now: /DeviceList;type=DeviceSwitch 2023-06-19 09:51:16 +02:00
01a6683e78 entry-value using new preset percentages 2023-06-19 09:51:03 +02:00
390c44b7ef manual bulk execution 2023-06-19 09:46:00 +02:00
7738927be4 preset Shutter percentages changed 2023-06-19 09:45:44 +02:00
59309b6a92 UI: grouping by device.type + sorting by name 2023-06-19 09:30:15 +02:00
5078d1e3bf make Property.slug editable 2023-04-04 11:22:28 +02:00
6353dd6f9e Property.slug + writing to channel via slug 2023-04-04 11:13:24 +02:00
fd5ceced45 Provide detailed Sunrise/-set times per Schedule from Server 2023-04-04 09:34:46 +02:00
0b146b5972 #3 FIX Property- & Device updates
+ Devices in Property::usages
+ code clean
2022-10-26 21:04:18 +02:00
818145e38b deleted Logic 2022-10-26 21:01:19 +02:00
223e9baf4a #5 BulkList sorted by name 2022-10-26 20:16:45 +02:00
e7080a058c #1 SearchComponent now reacts to changes from outside 2022-10-26 20:14:40 +02:00
5952871a3e #8 bulk execution didn't respect Bulk::enabled 2022-10-26 20:06:21 +02:00
929fc4d797 #7 ui triggered POST twice (first for ENTER, second for BLUR) 2022-10-26 20:05:45 +02:00
bc6f0c73b2 FIX: Schedule-editor doesn't apply any changes 2022-10-26 19:41:08 +02:00
113614f1e7 FIX: Schedule-list does not really show NEXT execution but any other 2022-10-25 15:47:44 +02:00
9bc0111c01 Showing 'next' in schedule-list 2022-10-25 15:34:59 +02:00
8d6617ece1 FIX: No Schedule notification on delete 2022-10-25 15:05:35 +02:00
233aa2a821 Schedule notification after execution + Code Clean 2022-10-25 14:59:28 +02:00
7bd2e2f93d ScheduleEntry position 2022-10-25 14:04:46 +02:00
db79d17f7d Schedule publish refactor + a lot code clean 2022-10-25 13:51:39 +02:00
2dbb1bc42f code clean 2022-10-25 12:54:48 +02:00
e5a2b36ac2 removed unused *.spec.ts 2022-10-25 12:49:19 +02:00
26ac11e076 git tag in upload.sh 2022-10-25 11:59:21 +02:00
204 changed files with 3054 additions and 2602 deletions

View File

@ -4,7 +4,8 @@ spring.datasource.url=jdbc:h2:./Homeautomation;AUTO_SERVER=TRUE
spring.datasource.driverClassName=org.h2.Driver spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa spring.datasource.username=sa
spring.datasource.password=password spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect #-
spring.jackson.serialization.indent_output=true
#- #-
spring.jpa.hibernate.ddl-auto=create spring.jpa.hibernate.ddl-auto=create
#- #-

18
pom.xml
View File

@ -9,14 +9,14 @@
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
</properties> </properties>
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version> <version>3.3.3</version>
</parent> </parent>
<dependencies> <dependencies>
@ -41,6 +41,10 @@
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
@ -54,7 +58,7 @@
<dependency> <dependency>
<groupId>com.github.calimero</groupId> <groupId>com.github.calimero</groupId>
<artifactId>calimero-core</artifactId> <artifactId>calimero-core</artifactId>
<version>2.5</version> <version>2.5.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.luckycatlabs</groupId> <groupId>com.luckycatlabs</groupId>
@ -62,6 +66,12 @@
<version>1.2</version> <version>1.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

View File

@ -125,5 +125,8 @@
} }
} }
} }
},
"cli": {
"analytics": false
} }
} }

View File

@ -0,0 +1,82 @@
export class Duration {
static readonly ZERO: Duration = new Duration('0');
readonly totalSeconds: number;
readonly days: number;
readonly hours: number;
readonly minutes: number;
readonly seconds: number;
readonly code: string;
readonly zero: boolean;
private constructor(input: string) {
this.totalSeconds = Duration.parse(input);
const sign = Math.sign(this.totalSeconds);
const abs = Math.abs(this.totalSeconds);
this.days = sign * Math.floor(abs / (24 * 60 * 60));
this.hours = sign * Math.floor(abs / (60 * 60)) % 24;
this.minutes = sign * Math.floor(abs / 60) % 60;
this.seconds = sign * Math.floor(abs) % 60;
this.zero = this.totalSeconds === 0;
this.code = this.buildCode();
}
static ofCode(code: string) {
return new Duration(code);
}
private static parse(input: string) {
const regex = /(?<signs>[+-]*)(?<value>\d+(?:[.,]\d+)?)(?<unit>d|h|m|s|ms)/g;
let totalSeconds = 0;
for (const match of input.matchAll(regex)) {
if (!match?.groups) {
continue;
}
const sign = ((match.groups["signs"].replace(/\++/g, '').length % 2) === 1) ? -1 : 1;
const value = sign * parseInt(match.groups["value"]);
switch (match.groups["unit"]) {
case 'd':
totalSeconds += value * 24 * 60 * 60;
break;
case 'h':
totalSeconds += value * 60 * 60;
break;
case 'm':
totalSeconds += value * 60;
break;
case 's':
totalSeconds += value;
break;
}
}
return totalSeconds;
}
private buildCode() {
if (this.zero) {
return '0';
}
let code = "";
if (this.days != 0) {
code += this.days + "d";
}
if (this.hours != 0) {
code += this.hours + "h";
}
if (this.minutes != 0) {
code += this.minutes + "m";
}
if (this.seconds != 0) {
code += this.seconds + "s";
}
return code;
}
}

View File

@ -1,28 +1,9 @@
import {prefix} from "../helpers";
export class Timestamp { export class Timestamp {
public readonly WEEKDAY: string[] = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
public readonly dayName;
public readonly timeString;
public constructor( public constructor(
readonly date: Date, readonly date: Date,
) { ) {
const now = new Date(); // -
const minutes: string = prefix(this.date.getMinutes(), '0', 2);
if (date.getDate() === now.getDate()) {
this.dayName = "Heute";
this.timeString = date.getHours() + ":" + minutes;
} else if (date.getDate() === now.getDate() + 1) {
this.dayName = "Morgen";
this.timeString = date.getHours() + ":" + minutes;
} else {
this.dayName = this.WEEKDAY[date.getDay()];
this.timeString = date.getHours() + ":" + minutes;
}
} }
public static fromDateOrNull(date: Date | null): Timestamp | null { public static fromDateOrNull(date: Date | null): Timestamp | null {

View File

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {NO_OP} from "./api.service"; import {NO_OP} from "./api.service";
import {Page} from "./Page"; import {Page} from "./Page";
import {Error, FromJson, IEntity, Next} from "./types"; import {FromJson, IEntity, Next} from "./types";
import {environment} from "../../environments/environment"; import {environment} from "../../environments/environment";
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {map} from "rxjs/operators"; import {map} from "rxjs/operators";
@ -19,40 +19,40 @@ export class AbstractRepositoryService<Entity extends IEntity, Creator, Filter>
// nothing // nothing
} }
create(dto: Creator, next: Next<Entity> = NO_OP, error: Error = NO_OP) { create(dto: Creator, next: Next<Entity> = NO_OP) {
this._post(['create'], dto, this.fromJson, next, error); this._post(['create'], dto, this.fromJson, next);
} }
set(entity: Entity, key: string, value: any, next: Next<Entity> = NO_OP, error: Error = NO_OP) { set(entity: Entity, key: string, value: any, next: Next<Entity> = NO_OP) {
this._put(['put', entity.id, key], value, this.fromJson, next, error); this._put(['put', entity.id, key], value, this.fromJson, next);
} }
get(id: number, next: Next<Entity> = NO_OP, error: Error = NO_OP) { get(id: number, next: Next<Entity> = NO_OP) {
this._get(['get', id], this.fromJson, next, error); this._get(['get', id], this.fromJson, next);
} }
filter(filter: Filter, next: Next<Page<Entity>> = NO_OP, error: Error = NO_OP) { filter(filter: Filter, next: Next<Page<Entity>> = NO_OP) {
this._post(['filter'], filter, Page.fromJsonGenerator(this.fromJson), next, error); this._post(['filter'], filter, Page.fromJsonGenerator(this.fromJson), next);
} }
delete(id: number, next: Next<Entity> = NO_OP, error: Error = NO_OP) { delete(id: number, next: Next<Entity> = NO_OP) {
this._delete(['delete', id], this.fromJson, next, error); this._delete(['delete', id], this.fromJson, next);
} }
private _get<T>(path: any[], fromJson: FromJson<T>, next: Next<T>, error: Error): void { private _get<T>(path: any[], fromJson: FromJson<T>, next: Next<T>): void {
this.http.get<any>(this._url(path)).pipe(map(fromJson)).subscribe(next, error); this.http.get<any>(this._url(path)).pipe(map(fromJson)).subscribe(next);
} }
private _put<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T>, error: Error): void { private _put<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T>): void {
this.http.put<any>(this._url(path), data).pipe(map(fromJson)).subscribe(next, error); this.http.put<any>(this._url(path), data).pipe(map(fromJson)).subscribe(next);
} }
private _post<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T>, error: Error): void { private _post<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T>): void {
this.http.post<any>(this._url(path), data).pipe(map(fromJson)).subscribe(next, error); this.http.post<any>(this._url(path), data).pipe(map(fromJson)).subscribe(next);
} }
private _delete<T>(path: any[], fromJson: FromJson<T>, next: Next<T>, error: Error): void { private _delete<T>(path: any[], fromJson: FromJson<T>, next: Next<T>): void {
this.http.delete<any>(this._url(path)).pipe(map(fromJson)).subscribe(next, error); this.http.delete<any>(this._url(path)).pipe(map(fromJson)).subscribe(next);
} }
private _url(path: any[]): string { private _url(path: any[]): string {

View File

@ -16,13 +16,6 @@ export function NO_COMPARE<T>(a: T, b: T): number {
return 0; return 0;
} }
function errorInterceptor(errorHandler: (error: any) => void): ((error: any) => void) {
return error => {
console.error(error);
errorHandler(error);
};
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@ -50,36 +43,36 @@ export class ApiService {
this.updateSubject.pipe(filter(update => update.type === type), map(update => Update.convert(update, fromJson))).subscribe(next); this.updateSubject.pipe(filter(update => update.type === type), map(update => Update.convert(update, fromJson))).subscribe(next);
} }
getReturnItem<T>(path: string, fromJson: (json: any) => T, next: (item: T) => void = NO_OP, error: (error: any) => void = NO_OP) { getReturnItem<T>(path: string, fromJson: (json: any) => T, next: (item: T) => void = NO_OP) {
this.http.get<any>(this.restUrl(path)).pipe(map(fromJson)).subscribe(next, errorInterceptor(error)); this.http.get<any>(this.restUrl(path)).pipe(map(fromJson)).subscribe(next);
} }
getList<T>(path: string, fromJson: (json: any) => T, compare: (a: T, b: T) => number = NO_COMPARE, next: (list: T[]) => void = NO_OP, error: (error: any) => void = NO_OP) { getList<T>(path: string, fromJson: (json: any) => T, compare: (a: T, b: T) => number = NO_COMPARE, next: (list: T[]) => void = NO_OP) {
this.http.get<any[]>(this.restUrl(path)).pipe(map(list => list.map(fromJson).sort(compare))).subscribe(next, errorInterceptor(error)); this.http.get<any[]>(this.restUrl(path)).pipe(map(list => list.map(fromJson).sort(compare))).subscribe(next);
} }
postReturnNone<T>(path: string, data: any, next: (_: void) => void = NO_OP, error: (error: any) => void = NO_OP) { postReturnNone<T>(path: string, data: any, next: (_: void) => void = NO_OP) {
this.http.post<any>(this.restUrl(path), data).subscribe(next, errorInterceptor(error)); this.http.post<any>(this.restUrl(path), data).subscribe(next);
} }
postReturnItem<T>(path: string, data: any, fromJson: (json: any) => T, next: (item: T) => void = NO_OP, error: (error: any) => void = NO_OP) { postReturnItem<T>(path: string, data: any, fromJson: (json: any) => T, next: (item: T) => void = NO_OP) {
this.http.post<any>(this.restUrl(path), data).pipe(map(fromJson)).subscribe(next, errorInterceptor(error)); this.http.post<any>(this.restUrl(path), data).pipe(map(fromJson)).subscribe(next);
} }
putReturnItem<T>(path: string, data: any, fromJson: (json: any) => T, next: (item: T) => void = NO_OP, error: (error: any) => void = NO_OP) { putReturnItem<T>(path: string, data: any, fromJson: (json: any) => T, next: (item: T) => void = NO_OP) {
this.http.put<any>(this.restUrl(path), data).pipe(map(fromJson)).subscribe(next, errorInterceptor(error)); this.http.put<any>(this.restUrl(path), data).pipe(map(fromJson)).subscribe(next);
} }
postReturnPage<T>(path: string, data: any, fromJson: (json: any) => T, next: (page: Page<T>) => void = NO_OP, error: (error: any) => void = NO_OP) { postReturnPage<T>(path: string, data: any, fromJson: (json: any) => T, next: (page: Page<T>) => void = NO_OP) {
this.http.post<any>(this.restUrl(path), data).pipe(map(Page.fromJsonGenerator(fromJson))).subscribe(next, errorInterceptor(error)); this.http.post<any>(this.restUrl(path), data).pipe(map(Page.fromJsonGenerator(fromJson))).subscribe(next);
} }
deleteReturnItem<T>(path: string, fromJson: (json: any) => T, next: (item: T) => void = NO_OP, error: (error: any) => void = NO_OP) { deleteReturnItem<T>(path: string, fromJson: (json: any) => T, next: (item: T) => void = NO_OP) {
this.http.delete<any>(this.restUrl(path)).pipe(map(fromJson)).subscribe(next, errorInterceptor(error)); this.http.delete<any>(this.restUrl(path)).pipe(map(fromJson)).subscribe(next);
} }
postReturnList<T>(path: string, data: any, fromJson: (json: any) => T, next: (list: T[]) => void = NO_OP, error: (error: any) => void = NO_OP) { postReturnList<T>(path: string, data: any, fromJson: (json: any) => T, next: (list: T[]) => void = NO_OP) {
this.http.post<any>(this.restUrl(path), data).pipe(map(list => list.map(fromJson))).subscribe(next, errorInterceptor(error)); this.http.post<any>(this.restUrl(path), data).pipe(map(list => list.map(fromJson))).subscribe(next);
} }
delete(path: string, next: Next<void>): void { delete(path: string, next: Next<void>): void {

View File

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

View File

@ -16,8 +16,8 @@ export class AreaService implements ISearchService {
// nothing // nothing
} }
findAll(next: (list: Area[]) => void, compare: (a: Area, b: Area) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void { findAll(next: (list: Area[]) => void, compare: (a: Area, b: Area) => number = NO_COMPARE): void {
this.api.getList("area/findAll", Area.fromJson, compare, next, error); this.api.getList("area/findAll", Area.fromJson, compare, next);
} }
subscribe(next: (area: Update<Area>) => void): void { subscribe(next: (area: Update<Area>) => void): void {
@ -25,15 +25,15 @@ export class AreaService implements ISearchService {
} }
searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void {
this.api.getReturnItem("area/searchById/" + id, SearchResult.fromJson, next, error); this.api.getReturnItem("area/searchById/" + id, SearchResult.fromJson, next);
} }
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { search(term: string, next: (results: SearchResult[]) => void = NO_OP): void {
this.api.postReturnList("area/searchLike", term, SearchResult.fromJson, next, error); this.api.postReturnList("area/searchLike", term, SearchResult.fromJson, next);
} }
set(area: Area, key: string, value: any, next: (item: Area) => void = NO_OP, error: (error: any) => void = NO_OP): void { set(area: Area, key: string, value: any, next: (item: Area) => void = NO_OP): void {
this.api.postReturnItem("area/set/" + area.id + "/" + key, value, Area.fromJson, next, error); this.api.postReturnItem("area/set/" + area.id + "/" + key, value, Area.fromJson, next);
} }
} }

View File

@ -0,0 +1,25 @@
import {validateDateAllowNull, validateNumberNotNull, validateStringNullToEmpty} from "../validators";
export class Astro {
constructor(
readonly zenith: number,
readonly sunrise: Date,
readonly sunset: Date,
readonly sunriseName: string,
readonly sunsetName: string,
) {
// nothing
}
static fromJson(json: any): Astro {
return new Astro(
validateNumberNotNull(json['zenith']),
validateDateAllowNull(json['sunrise']),
validateDateAllowNull(json['sunset']),
validateStringNullToEmpty(json['sunriseName']),
validateStringNullToEmpty(json['sunsetName']),
);
}
}

View File

@ -25,4 +25,14 @@ export class BulkEntry {
); );
} }
static compareName(a: BulkEntry, b: BulkEntry) {
if (!a.property) {
return +1;
}
if (!b.property) {
return -1;
}
return a.property.title.localeCompare(b.property.title);
}
} }

View File

@ -30,40 +30,48 @@ export class BulkService implements ISearchService {
// nothing // nothing
} }
findAll(next: (list: Bulk[]) => void, compare: (a: Bulk, b: Bulk) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void { findAll(next: (list: Bulk[]) => void, compare: (a: Bulk, b: Bulk) => number = NO_COMPARE): void {
this.api.getList("bulk/findAll", Bulk.fromJson, compare, next, error); this.api.getList("bulk/findAll", Bulk.fromJson, compare, next);
} }
subscribe(next: (bulk: Update<Bulk>) => void): void { subscribe(next: (bulk: Update<Bulk>) => void): void {
this.api.subscribe("BulkDto", Bulk.fromJson, next); this.api.subscribe("BulkDto", Bulk.fromJson, next);
} }
getById(id: number, next: (results: Bulk) => void, error?: (error: any) => void): void { getById(id: number, next: (results: Bulk) => void): void {
this.api.getReturnItem("bulk/getById/" + id, Bulk.fromJson, next, error); this.api.getReturnItem("bulk/getById/" + id, Bulk.fromJson, next);
} }
searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { searchById(id: number, next: (results: SearchResult) => void): void {
this.api.getReturnItem("bulk/searchById/" + id, SearchResult.fromJson, next, error); this.api.getReturnItem("bulk/searchById/" + id, SearchResult.fromJson, next);
} }
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { search(term: string, next: (results: SearchResult[]) => void = NO_OP): void {
this.api.postReturnList("bulk/searchLike", term, SearchResult.fromJson, next, error); this.api.postReturnList("bulk/searchLike", term, SearchResult.fromJson, next);
} }
set(bulk: Bulk, key: string, value: any, next: (item: Bulk) => void = NO_OP, error: (error: any) => void = NO_OP): void { set(bulk: Bulk, key: string, value: any, next: (item: Bulk) => void = NO_OP): void {
this.api.postReturnItem("bulk/set/" + bulk.id + "/" + key, value, Bulk.fromJson, next, error); this.api.postReturnItem("bulk/set/" + bulk.id + "/" + key, value, Bulk.fromJson, next);
} }
create(next: (item: Bulk) => void, error: (error: any) => void = NO_OP): void { create(next: (item: Bulk) => void): void {
this.api.postReturnItem("bulk/create/", new BulkCreate("Neu", true), Bulk.fromJson, next, error); this.api.postReturnItem("bulk/create", new BulkCreate("Neu", true), Bulk.fromJson, next);
} }
delete(bulk: Bulk, next: () => void, error: (error: any) => void = NO_OP): void { delete(bulk: Bulk, next: () => void): void {
this.api.getReturnItem("bulk/delete/" + bulk.id, _ => _, next, error); this.api.getReturnItem("bulk/delete/" + bulk.id, _ => _, next);
} }
setEntry(entry: BulkEntry, key: string, value: any, next: (result: BulkEntry) => void = NO_OP, error: (error: any) => void = NO_OP): void { run(bulk: Bulk, next?: () => void): void {
this.api.putReturnItem("BulkEntry/" + entry.id + "/set/" + key, value, next, error); this.api.getReturnItem("bulk/run/" + bulk.id, _ => _, next);
}
duplicate(bulk: Bulk, next: (item: Bulk) => void): void {
this.api.getReturnItem("bulk/duplicate/" + bulk.id, _ => _, next);
}
setEntry(entry: BulkEntry, key: string, value: any, next: (result: BulkEntry) => void = NO_OP): void {
this.api.putReturnItem("BulkEntry/" + entry.id + "/set/" + key, value, next);
} }
deleteEntry(entry: BulkEntry, next: Next<void>): void { deleteEntry(entry: BulkEntry, next: Next<void>): void {

View File

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

View File

@ -16,8 +16,8 @@ export class ChannelService implements ISearchService {
// nothing // nothing
} }
findAll(next: (list: Channel[]) => void, compare: (a: Channel, b: Channel) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void { findAll(next: (list: Channel[]) => void, compare: (a: Channel, b: Channel) => number = NO_COMPARE): void {
this.api.getList("channel/findAll", Channel.fromJson, compare, next, error); this.api.getList("channel/findAll", Channel.fromJson, compare, next);
} }
subscribe(next: (channel: Update<Channel>) => void): void { subscribe(next: (channel: Update<Channel>) => void): void {
@ -25,15 +25,15 @@ export class ChannelService implements ISearchService {
} }
searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void {
this.api.getReturnItem("channel/searchById/" + id, SearchResult.fromJson, next, error); this.api.getReturnItem("channel/searchById/" + id, SearchResult.fromJson, next);
} }
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { search(term: string, next: (results: SearchResult[]) => void = NO_OP): void {
this.api.postReturnList("channel/searchLike", term, SearchResult.fromJson, next, error); this.api.postReturnList("channel/searchLike", term, SearchResult.fromJson, next);
} }
set(channel: Channel, key: string, value: any, next: (item: Channel) => void = NO_OP, error: (error: any) => void = NO_OP): void { set(channel: Channel, key: string, value: any, next: (item: Channel) => void = NO_OP): void {
this.api.postReturnItem("channel/set/" + channel.id + "/" + key, value, Channel.fromJson, next, error); this.api.postReturnItem("channel/set/" + channel.id + "/" + key, value, Channel.fromJson, next);
} }
} }

View File

@ -62,12 +62,48 @@ export abstract class Device {
return a.position - b.position; return a.position - b.position;
} }
public static compareTitle(a: Device, b: Device): number {
return a.title.localeCompare(b.title);
}
abstract updateProperty(property: Property): void; abstract updateProperty(property: Property): void;
static filterByAreaIdAndRoomId(areaId: number | null, roomId: number | null): (_: Device) => boolean { static filterByAreaIdAndRoomId(areaId: number | null, roomId: number | null): (_: Device) => boolean {
return d => d.areaId === areaId && d.roomId === roomId; return d => d.areaId === areaId && d.roomId === roomId;
} }
getSwitchClassList(): object {
if (!(this instanceof DeviceSwitch)) {
throw Error();
}
const value: number | null | undefined = (this as DeviceSwitch).stateProperty?.readChannel?.value;
return {
deviceSwitchOnBack: value === 1,
deviceSwitchOffBack: value === 0,
disabledBack: value === null || value === undefined,
};
}
getStateSceneClassList(): object {
if (!(this instanceof DeviceStateScene)) {
throw Error();
}
return this.getSwitchClassList();
}
getShutterClassList(): object {
if (!(this instanceof DeviceShutter)) {
throw Error();
}
const value: number | null | undefined = (this as DeviceShutter).positionProperty?.readChannel?.value;
return {
deviceShutterOpenBack: value === 0,
deviceShutterIntermediateBack: value !== null && value !== undefined && value > 0 && value < 100,
deviceShutterClosedBack: value === 100,
disabledBack: value === null || value === undefined,
};
}
} }
export class DeviceSwitch extends Device { export class DeviceSwitch extends Device {

View File

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

View File

@ -20,24 +20,24 @@ export class DeviceService {
this.api.subscribe("DeviceDto", Device.fromJson, next); this.api.subscribe("DeviceDto", Device.fromJson, next);
} }
findAll(next: (list: Device[]) => void, compare: (a: Device, b: Device) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void { findAll(next: (list: Device[]) => void, compare: (a: Device, b: Device) => number = NO_COMPARE): void {
this.api.getList("device/findAll", Device.fromJson, compare, next, error); this.api.getList("device/findAll", Device.fromJson, compare, next);
} }
set(device: Device, key: string, value: any, next: (item: Device) => void, error: (error: any) => void = NO_OP): void { set(device: Device, key: string, value: any, next?: (item: Device) => void): void {
this.api.postReturnItem("device/set/" + device.id + "/" + key, value, Device.fromJson, next, error); this.api.postReturnItem("device/set/" + device.id + "/" + key, value, Device.fromJson, next);
} }
setDeviceSwitch(device: Device, key: string, value: any, next: (item: Device) => void = NO_OP, error: (error: any) => void = NO_OP): void { setDeviceSwitch(device: Device, key: string, value: any, next: (item: Device) => void = NO_OP): void {
this.api.postReturnItem("device/set/" + device.id + "/DeviceSwitch/" + key, value, Device.fromJson, next, error); this.api.postReturnItem("device/set/" + device.id + "/DeviceSwitch/" + key, value, Device.fromJson, next);
} }
setDeviceStateScene(device: Device, key: string, value: any, next: (item: Device) => void = NO_OP, error: (error: any) => void = NO_OP): void { setDeviceStateScene(device: Device, key: string, value: any, next: (item: Device) => void = NO_OP): void {
this.api.postReturnItem("device/set/" + device.id + "/DeviceStateScene/" + key, value, Device.fromJson, next, error); this.api.postReturnItem("device/set/" + device.id + "/DeviceStateScene/" + key, value, Device.fromJson, next);
} }
setDeviceShutter(device: Device, key: string, value: any, next: (item: Device) => void = NO_OP, error: (error: any) => void = NO_OP): void { setDeviceShutter(device: Device, key: string, value: any, next: (item: Device) => void = NO_OP): void {
this.api.postReturnItem("device/set/" + device.id + "/DeviceShutter/" + key, value, Device.fromJson, next, error); this.api.postReturnItem("device/set/" + device.id + "/DeviceShutter/" + key, value, Device.fromJson, next);
} }
setSwitchState(d: Device, value: boolean): void { setSwitchState(d: Device, value: boolean): void {
@ -64,16 +64,16 @@ export class DeviceService {
this.propertyService.set(device.positionProperty, "value", value); this.propertyService.set(device.positionProperty, "value", value);
} }
getById(id: number, next: (item: Device) => void, error: (error: any) => void = NO_OP): void { getById(id: number, next: (item: Device) => void): void {
this.api.getReturnItem("device/searchById/" + id, Device.fromJson, next, error); this.api.getReturnItem("device/searchById/" + id, Device.fromJson, next);
} }
create(type: string, next: (item: Device) => void, error: (error: any) => void = NO_OP): void { create(type: string, next: (item: Device) => void): void {
this.api.postReturnItem("device/create/", type, Device.fromJson, next, error); this.api.postReturnItem("device/create", type, Device.fromJson, next);
} }
delete(device: Device, next: () => void, error: (error: any) => void = NO_OP): void { delete(device: Device, next: () => void): void {
this.api.getReturnItem("device/delete/" + device.id, _ => _, next, error); this.api.getReturnItem("device/delete/" + device.id, _ => _, next);
} }
} }

View File

@ -1,20 +1,37 @@
import {validateDateAllowNull, validateListOrEmpty, validateNumberAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators"; import {undefinedOrNull, validateListOrEmpty, validateNumberNotNull, validateStringEmptyToNull, validateStringNotEmptyNotNull} from "../validators";
import {Channel} from "../channel/Channel"; import {Channel} from "../channel/Channel";
import {SearchResult} from "../SearchResult"; import {SearchResult} from "../SearchResult";
import {environment} from "../../../environments/environment";
export enum PropertyType {
BOOLEAN = 'BOOLEAN',
SHUTTER = 'SHUTTER',
BRIGHTNESS_PERCENT = 'BRIGHTNESS_PERCENT',
COLOR_TEMPERATURE = 'COLOR_TEMPERATURE',
LUX = 'LUX',
}
export class Property { export class Property {
readonly hrefId: string | null;
readonly hrefSlug: string | null;
constructor( constructor(
public id: number, public id: number,
public type: string, public type: PropertyType,
public title: string, public title: string,
public value: number | null, public slug: string | null,
public timestamp: Date | null,
public readChannel: Channel | null, public readChannel: Channel | null,
public writeChannel: Channel | null, public writeChannel: Channel | null,
public usages: SearchResult[], public usages: SearchResult[],
) { ) {
// nothing if (writeChannel || true) {
this.hrefId = environment.restBase + '/property/write/id/' + id + '/'
if (slug) {
this.hrefSlug = environment.restBase + '/property/write/slug/' + slug + '/';
}
}
} }
static fromJsonAllowNull(json: any): Property | null { static fromJsonAllowNull(json: any): Property | null {
@ -27,10 +44,9 @@ export class Property {
static fromJson(json: any): Property { static fromJson(json: any): Property {
return new Property( return new Property(
validateNumberNotNull(json['id']), validateNumberNotNull(json['id']),
validateStringNotEmptyNotNull(json['type']), validateStringNotEmptyNotNull(json['type']) as PropertyType,
validateStringNotEmptyNotNull(json['title']), validateStringNotEmptyNotNull(json['title']),
validateNumberAllowNull(json['value']), validateStringEmptyToNull(json['slug']),
validateDateAllowNull(json['timestamp']),
Channel.fromJsonAllowNull(json['readChannel']), Channel.fromJsonAllowNull(json['readChannel']),
Channel.fromJsonAllowNull(json['writeChannel']), Channel.fromJsonAllowNull(json['writeChannel']),
validateListOrEmpty(json['usages'], SearchResult.fromJson), validateListOrEmpty(json['usages'], SearchResult.fromJson),
@ -55,4 +71,32 @@ export class Property {
} }
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
} }
getStateClassList() {
const value = this.readChannel?.value;
switch (this.type) {
case PropertyType.BOOLEAN:
return {
propertyStateBooleanUnknown: undefinedOrNull(value),
propertyStateBooleanTrue: value > 0,
propertyStateBooleanFalse: value === 0,
};
case PropertyType.SHUTTER:
return {
propertyStatePercentUnknown: undefinedOrNull(value),
propertyStatePercentActive: value === 0,
propertyStatePercentBetween: value > 0 && value < 100,
propertyStatePercentInactive: value === 100,
};
case PropertyType.BRIGHTNESS_PERCENT:
return {
propertyStatePercentUnknown: undefinedOrNull(value),
propertyStatePercentActive: value === 100,
propertyStatePercentBetween: value > 0 && value < 100,
propertyStatePercentInactive: value === 0,
};
}
return {};
}
} }

View File

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

View File

@ -17,8 +17,8 @@ export class PropertyService implements ISearchService {
// nothing // nothing
} }
findAll(next: (list: Property[]) => void, compare: (a: Property, b: Property) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void { findAll(next: (list: Property[]) => void, compare: (a: Property, b: Property) => number = NO_COMPARE): void {
this.api.getList("property/findAll", Property.fromJson, compare, next, error); this.api.getList("property/findAll", Property.fromJson, compare, next);
} }
subscribe(next: (property: Update<Property>) => void): void { subscribe(next: (property: Update<Property>) => void): void {
@ -26,19 +26,19 @@ export class PropertyService implements ISearchService {
} }
searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void {
this.api.getReturnItem("property/searchById/" + id, SearchResult.fromJson, next, error); this.api.getReturnItem("property/searchById/" + id, SearchResult.fromJson, next);
} }
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { search(term: string, next: (results: SearchResult[]) => void = NO_OP): void {
this.api.postReturnList("property/searchLike", term, SearchResult.fromJson, next, error); this.api.postReturnList("property/searchLike", term, SearchResult.fromJson, next);
} }
set(property: Property, key: string, value: any, next: (item: Property) => void = NO_OP, error: (error: any) => void = NO_OP): void { set(property: Property, key: string, value: any, next: (item: Property) => void = NO_OP): void {
this.api.postReturnItem("property/set/" + property.id + "/" + key, value, Property.fromJson, next, error); this.api.postReturnItem("property/set/" + property.id + "/" + key, value, Property.fromJson, next);
} }
create(next: (item: Property) => void, error: (error: any) => void = NO_OP): void { create(next: (item: Property) => void): void {
this.api.getReturnItem("property/create/", Property.fromJson, next, error); this.api.getReturnItem("property/create", Property.fromJson, next);
} }
delete(property: Property, next: Next<void>): void { delete(property: Property, next: Next<void>): void {

View File

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

View File

@ -16,8 +16,8 @@ export class RoomService implements ISearchService {
// nothing // nothing
} }
findAll(next: (list: Room[]) => void, compare: (a: Room, b: Room) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void { findAll(next: (list: Room[]) => void, compare: (a: Room, b: Room) => number = NO_COMPARE): void {
this.api.getList("room/findAll", Room.fromJson, compare, next, error); this.api.getList("room/findAll", Room.fromJson, compare, next);
} }
subscribe(next: (room: Update<Room>) => void): void { subscribe(next: (room: Update<Room>) => void): void {
@ -25,15 +25,15 @@ export class RoomService implements ISearchService {
} }
searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void {
this.api.getReturnItem("room/searchById/" + id, SearchResult.fromJson, next, error); this.api.getReturnItem("room/searchById/" + id, SearchResult.fromJson, next);
} }
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { search(term: string, next: (results: SearchResult[]) => void = NO_OP): void {
this.api.postReturnList("room/searchLike", term, SearchResult.fromJson, next, error); this.api.postReturnList("room/searchLike", term, SearchResult.fromJson, next);
} }
set(room: Room, key: string, value: any, next: (item: Room) => void = NO_OP, error: (error: any) => void = NO_OP): void { set(room: Room, key: string, value: any, next: (item: Room) => void = NO_OP): void {
this.api.postReturnItem("room/set/" + room.id + "/" + key, value, Room.fromJson, next, error); this.api.postReturnItem("room/set/" + room.id + "/" + key, value, Room.fromJson, next);
} }
} }

View File

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

View File

@ -16,8 +16,8 @@ export class SceneService implements ISearchService {
// nothing // nothing
} }
findAll(next: (list: Scene[]) => void, compare: (a: Scene, b: Scene) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void { findAll(next: (list: Scene[]) => void, compare: (a: Scene, b: Scene) => number = NO_COMPARE): void {
this.api.getList("scene/findAll", Scene.fromJson, compare, next, error); this.api.getList("scene/findAll", Scene.fromJson, compare, next);
} }
subscribe(next: (scene: Update<Scene>) => void): void { subscribe(next: (scene: Update<Scene>) => void): void {
@ -25,15 +25,15 @@ export class SceneService implements ISearchService {
} }
searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { searchById(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void {
this.api.getReturnItem("scene/searchById/" + id, SearchResult.fromJson, next, error); this.api.getReturnItem("scene/searchById/" + id, SearchResult.fromJson, next);
} }
search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { search(term: string, next: (results: SearchResult[]) => void = NO_OP): void {
this.api.postReturnList("scene/searchLike", term, SearchResult.fromJson, next, error); this.api.postReturnList("scene/searchLike", term, SearchResult.fromJson, next);
} }
set(scene: Scene, key: string, value: any, next: (item: Scene) => void = NO_OP, error: (error: any) => void = NO_OP): void { set(scene: Scene, key: string, value: any, next: (item: Scene) => void = NO_OP): void {
this.api.postReturnItem("scene/set/" + scene.id + "/" + key, value, Scene.fromJson, next, error); this.api.postReturnItem("scene/set/" + scene.id + "/" + key, value, Scene.fromJson, next);
} }
} }

View File

@ -3,13 +3,18 @@ import {ScheduleEntry} from "./entry/ScheduleEntry";
export class Schedule { export class Schedule {
readonly next?: ScheduleEntry;
readonly last?: ScheduleEntry;
constructor( constructor(
public id: number, readonly id: number,
public enabled: boolean, readonly enabled: boolean,
public title: string, readonly title: string,
public entries: ScheduleEntry[], readonly entries: ScheduleEntry[],
) { ) {
// nothing this.next = entries.filter(e => e.nextFuzzyTimestamp).sort((a, b) => a.nextFuzzyTimestamp.date.getTime() - b.nextFuzzyTimestamp.date.getTime())[0];
this.last = entries.filter(e => e.lastFuzzyTimestamp).sort((a, b) => b.lastFuzzyTimestamp.date.getTime() - a.lastFuzzyTimestamp.date.getTime())[0];
} }
static fromJson(json: any): Schedule { static fromJson(json: any): Schedule {
@ -17,7 +22,7 @@ export class Schedule {
validateNumberNotNull(json['id']), validateNumberNotNull(json['id']),
validateBooleanNotNull(json['enabled']), validateBooleanNotNull(json['enabled']),
validateStringNotEmptyNotNull(json['title']), validateStringNotEmptyNotNull(json['title']),
validateListOrEmpty(json['entries'], ScheduleEntry.fromJson, ScheduleEntry.compare), validateListOrEmpty(json['entries'], ScheduleEntry.fromJson, ScheduleEntry.comparePosition),
); );
} }
@ -25,10 +30,6 @@ export class Schedule {
return item.id; return item.id;
} }
public static compareId(a: Schedule, b: Schedule): number {
return a.id - b.id;
}
public static compareName(a: Schedule, b: Schedule): number { public static compareName(a: Schedule, b: Schedule): number {
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
} }

View File

@ -1,43 +1,69 @@
import {validateBooleanNotNull, validateDateAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull} from "../../validators"; import {validateBooleanNotNull, validateDateAllowNull, validateNumberAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull} from "../../validators";
import {Timestamp} from "../../Timestamp"; import {Timestamp} from "../../Timestamp";
import {Property} from "../../property/Property"; import {Property} from "../../property/Property";
import {Bulk} from "../../bulk/Bulk"; import {Bulk} from "../../bulk/Bulk";
import {formatNumber} from "@angular/common";
function getDaySeconds(date: Date): number {
return date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
}
export class ScheduleEntry { export class ScheduleEntry {
readonly todo: string;
readonly dayMinute: number;
readonly anyWeekday: boolean;
readonly executable: boolean;
private constructor( private constructor(
public id: number, readonly id: number,
public enabled: boolean, readonly position: number,
public monday: boolean, readonly enabled: boolean,
public tuesday: boolean, readonly monday: boolean,
public wednesday: boolean, readonly tuesday: boolean,
public thursday: boolean, readonly wednesday: boolean,
public friday: boolean, readonly thursday: boolean,
public saturday: boolean, readonly friday: boolean,
public sunday: boolean, readonly saturday: boolean,
public type: string, readonly sunday: boolean,
public zenith: number, readonly type: string,
public hour: number, readonly zenith: number,
public minute: number, readonly hour: number,
public second: number, readonly minute: number,
public fuzzySeconds: number, readonly second: number,
public lastClearTimestamp: Timestamp | null, readonly fuzzySeconds: number,
public nextClearTimestamp: Timestamp | null, readonly skip: number,
public nextFuzzyTimestamp: Timestamp | null, readonly lastClearTimestamp: Timestamp | null,
public property: Property | null, readonly nextClearTimestamp: Timestamp | null,
public value: number, readonly nextFuzzyTimestamp: Timestamp | null,
public bulk: Bulk | null, readonly lastFuzzyTimestamp: Timestamp | null,
readonly property: Property | null,
readonly value: number,
readonly bulk: Bulk | null,
) { ) {
// nothing this.dayMinute = hour * 60 + minute;
this.anyWeekday = this.monday || this.tuesday || this.wednesday || this.thursday || this.friday || this.saturday || this.sunday;
this.executable = this.enabled && this.anyWeekday && !!this.bulk;
this.todo = this.getToDo();
}
getToDo(): string {
let result: string[] = [];
if (!this.enabled) {
result.push("inaktiv");
}
if (!this.bulk) {
result.push("keine Aktion");
}
if (!this.anyWeekday) {
result.push("kein Wochentag");
}
return result.join(", ");
} }
static fromJson(json: any): ScheduleEntry { static fromJson(json: any): ScheduleEntry {
return new ScheduleEntry( return new ScheduleEntry(
validateNumberNotNull(json['id']), validateNumberNotNull(json['id']),
validateNumberNotNull(json['position']),
validateBooleanNotNull(json['enabled']), validateBooleanNotNull(json['enabled']),
validateBooleanNotNull(json['monday']), validateBooleanNotNull(json['monday']),
validateBooleanNotNull(json['tuesday']), validateBooleanNotNull(json['tuesday']),
@ -52,9 +78,11 @@ export class ScheduleEntry {
validateNumberNotNull(json['minute']), validateNumberNotNull(json['minute']),
validateNumberNotNull(json['second']), validateNumberNotNull(json['second']),
validateNumberNotNull(json['fuzzySeconds']), validateNumberNotNull(json['fuzzySeconds']),
validateNumberAllowNull(json['skip']) || 0,
Timestamp.fromDateOrNull(validateDateAllowNull(json['lastClearTimestamp'])), Timestamp.fromDateOrNull(validateDateAllowNull(json['lastClearTimestamp'])),
Timestamp.fromDateOrNull(validateDateAllowNull(json['nextClearTimestamp'])), Timestamp.fromDateOrNull(validateDateAllowNull(json['nextClearTimestamp'])),
Timestamp.fromDateOrNull(validateDateAllowNull(json['nextFuzzyTimestamp'])), Timestamp.fromDateOrNull(validateDateAllowNull(json['nextFuzzyTimestamp'])),
Timestamp.fromDateOrNull(validateDateAllowNull(json['lastFuzzyTimestamp'])),
Property.fromJsonAllowNull(json['property']), Property.fromJsonAllowNull(json['property']),
validateNumberNotNull(json['value']), validateNumberNotNull(json['value']),
Bulk.fromJsonOrNull(json['bulk']), Bulk.fromJsonOrNull(json['bulk']),
@ -65,13 +93,12 @@ export class ScheduleEntry {
return item.id; return item.id;
} }
public static compare(a: ScheduleEntry, b: ScheduleEntry): number { public static comparePosition(a: ScheduleEntry, b: ScheduleEntry): number {
if (a.nextFuzzyTimestamp === null) { return a.position - b.position;
return +1; }
} else if (b.nextFuzzyTimestamp === null) {
return -1; get time(): string {
} return formatNumber(this.hour, 'de-DE', '2.0-0') + ':' + formatNumber(this.minute, 'de-DE', '2.0-0');
return getDaySeconds(a.nextFuzzyTimestamp.date) - getDaySeconds(b.nextFuzzyTimestamp.date);
} }
} }

View File

@ -0,0 +1,12 @@
export class Zenith {
constructor(
readonly title: string,
readonly value: number,
readonly sunrise: boolean,
readonly sunset: boolean,
) {
// -
}
}

View File

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

View File

@ -2,6 +2,7 @@ import {Injectable} from '@angular/core';
import {ScheduleEntry} from "./ScheduleEntry"; import {ScheduleEntry} from "./ScheduleEntry";
import {ApiService, NO_OP} from "../../api.service"; import {ApiService, NO_OP} from "../../api.service";
import {Schedule} from "../Schedule"; import {Schedule} from "../Schedule";
import {Next} from "../../types";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -14,20 +15,16 @@ export class ScheduleEntryService {
// nothing // nothing
} }
findAll(next: (list: ScheduleEntry[]) => void, compare: (a: ScheduleEntry, b: ScheduleEntry) => number, error: (error: any) => void = NO_OP): void { create(schedule: Schedule, next: Next<ScheduleEntry> = NO_OP): void {
this.api.getList("scheduleEntry/findAll", ScheduleEntry.fromJson, compare, next, error); this.api.getReturnItem("schedule/entry/create/" + schedule.id, ScheduleEntry.fromJson, next);
} }
set(entry: ScheduleEntry, key: string, value: any, next: (item: ScheduleEntry) => void, error: (error: any) => void = NO_OP): void { set(entry: ScheduleEntry, key: string, value: any, next: Next<ScheduleEntry> = NO_OP): void {
this.api.postReturnItem("schedule/entry/set/" + entry.id + "/" + key, value, ScheduleEntry.fromJson, next, error); this.api.postReturnItem("schedule/entry/set/" + entry.id + "/" + key, value, ScheduleEntry.fromJson, next);
} }
create(schedule: Schedule, next: (item: ScheduleEntry) => void, error: (error: any) => void = NO_OP): void { delete(entry: ScheduleEntry, next: Next<ScheduleEntry> = NO_OP): void {
this.api.getReturnItem("schedule/entry/create/" + schedule.id, ScheduleEntry.fromJson, next, error); this.api.getReturnItem("schedule/entry/delete/" + entry.id, _ => _, next);
}
delete(entry: ScheduleEntry, next: () => void, error: (error: any) => void = NO_OP): void {
this.api.getReturnItem("schedule/entry/delete/" + entry.id, _ => _, next, error);
} }
} }

View File

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

View File

@ -1,6 +1,8 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ApiService, NO_OP} from "../api.service"; import {ApiService, NO_OP} from "../api.service";
import {Schedule} from "./Schedule"; import {Schedule} from "./Schedule";
import {Next} from "../types";
import {Update} from "../Update";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -13,24 +15,28 @@ export class ScheduleService {
// nothing // nothing
} }
findAll(next: (list: Schedule[]) => void, compare: (a: Schedule, b: Schedule) => number, error: (error: any) => void = NO_OP): void { subscribe(next: Next<Update<Schedule>>): void {
this.api.getList("schedule/findAll", Schedule.fromJson, compare, next, error); this.api.subscribe("ScheduleDto", Schedule.fromJson, next);
} }
set(schedule: Schedule, key: string, value: any, next: (item: Schedule) => void, error: (error: any) => void = NO_OP): void { findAll(next: Next<Schedule[]>, compare?: (a: Schedule, b: Schedule) => number): void {
this.api.postReturnItem("schedule/set/" + schedule.id + "/" + key, value, Schedule.fromJson, next, error); this.api.getList("schedule/findAll", Schedule.fromJson, compare, next);
} }
getById(id: number, next: (item: Schedule) => void, error: (error: any) => void = NO_OP): void { getById(id: number, next: (item: Schedule) => void): void {
this.api.getReturnItem("schedule/searchById/" + id, Schedule.fromJson, next, error); this.api.getReturnItem("schedule/searchById/" + id, Schedule.fromJson, next);
} }
create(next: (item: Schedule) => void, error: (error: any) => void = NO_OP): void { create(next: Next<Schedule> = NO_OP): void {
this.api.getReturnItem("schedule/create/", Schedule.fromJson, next, error); this.api.getReturnItem("schedule/create", Schedule.fromJson, next);
} }
delete(schedule: Schedule, next: () => void, error: (error: any) => void = NO_OP): void { set(schedule: Schedule, key: string, value: any, next: Next<Schedule> = NO_OP): void {
this.api.getReturnItem("schedule/delete/" + schedule.id, _ => _, next, error); this.api.postReturnItem("schedule/set/" + schedule.id + "/" + key, value, Schedule.fromJson, next);
}
delete(schedule: Schedule, next: Next<Schedule> = NO_OP): void {
this.api.getReturnItem("schedule/delete/" + schedule.id, _ => _, next);
} }
} }

View File

@ -0,0 +1,65 @@
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
import {DatePipe} from "@angular/common";
import {Timestamp} from "./Timestamp";
@Injectable({
providedIn: 'root'
})
export class TimeService {
readonly datePipe: DatePipe = new DatePipe(this.locale);
private _now: Date = new Date();
constructor(
@Inject(LOCALE_ID) readonly locale: string,
) {
// -
}
get now(): Date {
return this._now;
}
relativeDate(timestamp: Timestamp | undefined) {
const date: Date = timestamp?.date;
if (date === undefined || date === null) {
return "";
}
const relativeName = this.relativeCalendarDaysName(date);
return relativeName + " " + this.datePipe.transform(date, 'HH:mm');
}
relativeCalendarDaysName(date: Date): string {
const prefix = this.relativeCalendarDaysPrefix(date);
const weekday = date.toLocaleDateString(this.locale, {weekday: 'long'});
return prefix + ", " + weekday;
}
private relativeCalendarDaysPrefix(date: Date): string {
const days = this.calendarDays(date);
if (days < -2) {
return "Vor " + -days + " Tagen";
} else if (days === -2) {
return "Vorgestern";
} else if (days === -1) {
return "Gestern";
} else if (days === 1) {
return "Morgen";
} else if (days === 2) {
return "Übermorgen";
} else if (days > 2) {
return "In " + days + " Tagen";
}
return "Heute";
}
private calendarDays(date: Date) {
const DAY_MS = 1000 * 60 * 60 * 24;
const aMidnight = new Date(this._now.getFullYear(), this._now.getMonth(), this._now.getDate());
const bMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const milliseconds = bMidnight.getTime() - aMidnight.getTime();
return Math.floor(milliseconds / DAY_MS);
}
}

View File

@ -87,3 +87,7 @@ export function validateMap<T>(json: any, valueFromJson: (json: any) => T): Map<
} }
return map; return map;
} }
export function undefinedOrNull(value: any) {
return value === undefined || value === null;
}

View File

@ -1,20 +1,17 @@
import {NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router'; import {RouterModule, Routes} from '@angular/router';
import {ScheduleListComponent} from "./pages/schedule-list/schedule-list.component"; import {ScheduleListComponent} from "./pages/schedule/list/schedule-list.component";
import {ScheduleComponent} from "./pages/schedule/schedule.component"; import {ScheduleEditorComponent} from "./pages/schedule/editor/schedule-editor.component";
import {DeviceAllListComponent} from "./pages/device-all-list/device-all-list.component"; import {DeviceListComponent} from "./pages/device/list/device-list.component";
import {DeviceComponent} from "./pages/device/device.component"; import {DeviceEditorComponent} from "./pages/device/editor/device-editor.component";
import {PropertyListComponent} from "./pages/property-list/property-list.component"; import {PropertyListComponent} from "./pages/property/list/property-list.component";
import {ChannelListComponent} from "./pages/channel-list/channel-list.component"; import {ChannelListComponent} from "./pages/channel/list/channel-list.component";
import {AreaListComponent} from "./pages/mobile/device-tree/area-list/area-list.component"; import {BulkListComponent} from "./pages/bulk/list/bulk-list.component";
import {RoomListComponent} from "./pages/mobile/device-tree/room-list/room-list.component"; import {BulkEditorComponent} from "./pages/bulk/editor/bulk-editor.component";
import {DeviceListComponent} from "./pages/mobile/device-tree/device-list/device-list.component";
import {BulkListComponent} from "./pages/bulk/bulk-list/bulk-list.component";
import {BulkComponent} from "./pages/bulk/bulk/bulk.component";
const routes: Routes = [ const routes: Routes = [
{path: 'Device', component: DeviceComponent}, {path: 'Device', component: DeviceEditorComponent},
{path: 'DeviceAllList', component: DeviceAllListComponent}, {path: 'DeviceList', component: DeviceListComponent},
// {path: 'Channel', component: ChannelComponent}, // {path: 'Channel', component: ChannelComponent},
{path: 'ChannelList', component: ChannelListComponent}, {path: 'ChannelList', component: ChannelListComponent},
@ -22,17 +19,13 @@ const routes: Routes = [
// {path: 'Property', component: PropertyComponent}, // {path: 'Property', component: PropertyComponent},
{path: 'PropertyList', component: PropertyListComponent}, {path: 'PropertyList', component: PropertyListComponent},
{path: 'Schedule', component: ScheduleComponent}, {path: 'Schedule', component: ScheduleEditorComponent},
{path: 'ScheduleList', component: ScheduleListComponent}, {path: 'ScheduleList', component: ScheduleListComponent},
{path: 'Bulk', component: BulkComponent}, {path: 'Bulk', component: BulkEditorComponent},
{path: 'BulkList', component: BulkListComponent}, {path: 'BulkList', component: BulkListComponent},
{path: 'AreaList', component: AreaListComponent}, {path: '**', redirectTo: '/DeviceList;type=DeviceSwitch'},
{path: 'RoomList', component: RoomListComponent},
{path: 'DeviceList', component: DeviceListComponent},
{path: '**', redirectTo: '/ScheduleList'},
]; ];
@NgModule({ @NgModule({

View File

@ -1,22 +1,27 @@
<div class="menubar"> <div class="menubar">
<div class="item" [routerLink]="['/DeviceList', {'type': 'DeviceSwitch'}]" [class.itemActive]="isRouteActive('/DeviceList', 'DeviceSwitch')">
Geräte
</div>
<div class="item" [routerLink]="['/DeviceList', {'type': 'DeviceShutter'}]" [class.itemActive]="isRouteActive('/DeviceList', 'DeviceShutter')">
Rollläden
</div>
<div class="item" routerLink="/BulkList" routerLinkActive="itemActive">
Stapel
</div>
<div class="item" routerLink="/ScheduleList" routerLinkActive="itemActive"> <div class="item" routerLink="/ScheduleList" routerLinkActive="itemActive">
Zeitpläne Zeitpläne
</div> </div>
<div class="item" routerLink="/DeviceAllList" routerLinkActive="itemActive">
Geräte
</div>
<div class="item" routerLink="/BulkList" routerLinkActive="itemActive">
Massenverarbeitung
</div>
<div class="item itemSecondary" routerLink="/PropertyList" routerLinkActive="itemActive"> <div class="item itemSecondary" routerLink="/PropertyList" routerLinkActive="itemActive">
Eigenschaften P
</div> </div>
<div class="item itemSecondary" routerLink="/ChannelList" routerLinkActive="itemActive"> <div class="item itemSecondary" routerLink="/ChannelList" routerLinkActive="itemActive">
Kanäle C
</div> </div>
</div> </div>

View File

@ -1,15 +1,17 @@
@import "../config";
.menubar { .menubar {
border-bottom: 1px solid black; border-bottom: @border solid black;
.item { .item {
float: left; float: left;
padding: 10px; padding: @padding;
border-right: 1px solid black; border-right: @border solid black;
} }
.itemSecondary { .itemSecondary {
float: right; float: right;
border-left: 1px solid black; border-left: @border solid black;
border-right: none; border-right: none;
} }

View File

@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {DataService} from "./data.service"; import {Params, Router, UrlSegment} from "@angular/router";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -9,10 +9,27 @@ import {DataService} from "./data.service";
export class AppComponent { export class AppComponent {
title = 'angular'; title = 'angular';
private url: UrlSegment[];
private params: Params;
constructor( constructor(
readonly dataService: DataService, readonly router: Router,
) { ) {
// nothing // nothing
} }
isRouteActive(base: string, type: string): boolean {
const parts = this.router.url.split(';');
if (parts[0] === base) {
for (let i = 1; i < parts.length; i++) {
const param = parts[i].split('=');
if (param[0] === 'type' && param[1] === type) {
return true;
}
}
}
return false;
}
} }

View File

@ -1,43 +1,52 @@
import {NgModule} from '@angular/core'; import {LOCALE_ID, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {AppRoutingModule} from './app-routing.module'; import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {HttpClientModule} from "@angular/common/http"; import {HttpClientModule} from "@angular/common/http";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {EditFieldComponent} from './shared/edit-field/edit-field.component'; import {TextComponent} from './shared/text/text.component';
import {ScheduleListComponent} from './pages/schedule-list/schedule-list.component'; import {ScheduleListComponent} from './pages/schedule/list/schedule-list.component';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {NumberComponent} from './shared/number/number.component'; import {NumberComponent} from './shared/number/number.component';
import {ScheduleComponent} from "./pages/schedule/schedule.component"; import {ScheduleEditorComponent} from "./pages/schedule/editor/schedule-editor.component";
import {SearchComponent} from './shared/search/search.component'; import {SearchComponent} from './shared/search/search.component';
import {DeviceAllListComponent} from './pages/device-all-list/device-all-list.component'; import {DeviceListComponent} from './pages/device/list/device-list.component';
import {DeviceComponent} from './pages/device/device.component'; import {DeviceEditorComponent} from './pages/device/editor/device-editor.component';
import {PropertyListComponent} from './pages/property-list/property-list.component'; import {PropertyListComponent} from './pages/property/list/property-list.component';
import {ChannelListComponent} from './pages/channel-list/channel-list.component'; import {ChannelListComponent} from './pages/channel/list/channel-list.component';
import {AreaListComponent} from './pages/mobile/device-tree/area-list/area-list.component'; import {BulkListComponent} from './pages/bulk/list/bulk-list.component';
import {RoomListComponent} from './pages/mobile/device-tree/room-list/room-list.component'; import {BulkEditorComponent} from './pages/bulk/editor/bulk-editor.component';
import {DeviceListComponent} from './pages/mobile/device-tree/device-list/device-list.component'; import {LeftPadDirective} from './pipes/left-pad.directive';
import {BulkListComponent} from './pages/bulk/bulk-list/bulk-list.component'; import {EntryValueComponent} from './shared/entry-value/entry-value.component';
import {BulkComponent} from './pages/bulk/bulk/bulk.component';
import {registerLocaleData} from '@angular/common';
import localeDe from '@angular/common/locales/de';
import localeDeExtra from '@angular/common/locales/extra/de';
import {BoolComponent} from './shared/bool/bool.component';
import {DurationComponent} from './shared/duration/duration.component';
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
EditFieldComponent, TextComponent,
ScheduleComponent, ScheduleEditorComponent,
ScheduleListComponent, ScheduleListComponent,
NumberComponent, NumberComponent,
SearchComponent, SearchComponent,
DeviceAllListComponent, DeviceListComponent,
DeviceComponent, DeviceEditorComponent,
PropertyListComponent, PropertyListComponent,
ChannelListComponent, ChannelListComponent,
AreaListComponent,
RoomListComponent,
DeviceListComponent, DeviceListComponent,
BulkListComponent, BulkListComponent,
BulkComponent, BulkEditorComponent,
LeftPadDirective,
EntryValueComponent,
BoolComponent,
DurationComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -46,7 +55,9 @@ import {BulkComponent} from './pages/bulk/bulk/bulk.component';
FormsModule, FormsModule,
FontAwesomeModule, FontAwesomeModule,
], ],
providers: [], providers: [
{provide: LOCALE_ID, useValue: 'de-DE'}
],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { export class AppModule {

View File

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

View File

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

View File

@ -1,27 +0,0 @@
<table>
<tr>
<th>&nbsp;</th>
<th>Bezeichnung</th>
<th>&nbsp;</th>
</tr>
<tr *ngFor="let bulk of bulks; trackBy: Bulk.trackBy">
<td class="boolean" (click)="set(bulk, 'enabled', !bulk.enabled)" [class.true]="bulk.enabled" [class.false]="!bulk.enabled">
<fa-icon *ngIf="bulk.enabled" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!bulk.enabled" [icon]="faCircle"></fa-icon>
</td>
<td [routerLink]="['/Bulk', {id: bulk.id}]">
{{bulk.name}}
</td>
<td class="delete" (click)="delete(bulk)">
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</td>
</tr>
</table>
<p>
<button (click)="create()">+ Hinzufügen</button>
</p>

View File

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

View File

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

View File

@ -1,7 +1,9 @@
<ng-container *ngIf="bulk"> <ng-container *ngIf="bulk">
<h1> <h1>
<app-edit-field [initial]="bulk.name" (valueChange)="set(bulk, 'name', $event)"></app-edit-field> <app-text [initial]="bulk.name" (valueChange)="set(bulk, 'name', $event)"></app-text>
</h1> </h1>
<table> <table>
<tr> <tr>
<th>Eigenschaft</th> <th>Eigenschaft</th>
@ -9,7 +11,7 @@
<th>&nbsp;</th> <th>&nbsp;</th>
</tr> </tr>
<ng-container *ngIf="bulk"> <ng-container *ngIf="bulk">
<tr *ngFor="let entry of bulk.entries"> <tr *ngFor="let entry of bulk.entries.sort(BulkEntry.compareName)">
<td> <td>
<app-search [searchService]="propertyService" [allowEmpty]="false" [initial]="entry.property?.id" (valueChange)="setEntry(entry, 'property', $event)"></app-search> <app-search [searchService]="propertyService" [allowEmpty]="false" [initial]="entry.property?.id" (valueChange)="setEntry(entry, 'property', $event)"></app-search>
</td> </td>
@ -20,12 +22,7 @@
</td> </td>
<td *ngSwitchCase="'SHUTTER'" [class.true]="entry.value === 0" [class.false]="entry.value === 100" [class.tristate]="0 < entry.value && entry.value < 100"> <td *ngSwitchCase="'SHUTTER'" [class.true]="entry.value === 0" [class.false]="entry.value === 100" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="setEntry(entry, 'value', $event)"> <select [ngModel]="entry.value" (ngModelChange)="setEntry(entry, 'value', $event)">
<option [ngValue]="0">100% Offen</option> <option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
<option [ngValue]="35">&nbsp;50%</option>
<option [ngValue]="55">&nbsp;75%</option>
<option [ngValue]="75">&nbsp;90% Sonnenschutz</option>
<option [ngValue]="85">100% Schlitze</option>
<option [ngValue]="100">100% Geschlossen</option>
</select> </select>
</td> </td>
<td *ngSwitchCase="'BRIGHTNESS_PERCENT'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100"> <td *ngSwitchCase="'BRIGHTNESS_PERCENT'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
@ -56,7 +53,6 @@
</ng-container> </ng-container>
</table> </table>
<p> <button (click)="create()">+ Hinzufügen</button>
<button (click)="create()">+ Hinzufügen</button>
</p>
</ng-container> </ng-container>

View File

@ -7,11 +7,13 @@ import {BulkEntry} from "../../../api/bulk/BulkEntry";
import {faTimesCircle} from "@fortawesome/free-regular-svg-icons"; import {faTimesCircle} from "@fortawesome/free-regular-svg-icons";
@Component({ @Component({
selector: 'app-bulk', selector: 'app-bulk-editor',
templateUrl: './bulk.component.html', templateUrl: './bulk-editor.component.html',
styleUrls: ['./bulk.component.less'] styleUrls: ['./bulk-editor.component.less']
}) })
export class BulkComponent implements OnInit { export class BulkEditorComponent implements OnInit {
protected readonly BulkEntry = BulkEntry;
readonly faTimes = faTimesCircle; readonly faTimes = faTimesCircle;

View File

@ -0,0 +1,32 @@
<div class="tiles">
<div class="tile" *ngFor="let bulk of bulks.sort(Bulk.compareName); trackBy: Bulk.trackBy">
<div class="tileHead disabledBack" [class.enabledBack]="bulk.enabled">
<div (click)="set(bulk, 'enabled', !bulk.enabled)">
<fa-icon *ngIf="bulk.enabled" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!bulk.enabled" [icon]="faCircle"></fa-icon>
</div>
<div class="flexGrow" [routerLink]="['/Bulk', {id: bulk.id}]">
{{ bulk.name }}
</div>
<div class="tileHeadRight" (click)="run(bulk)">
<fa-icon title="Ausführen" [icon]="faPlay"></fa-icon>
</div>
<div class="tileHeadRight" (click)="duplicate(bulk)">
<fa-icon title="Duplizieren" [icon]="faCopy"></fa-icon>
</div>
<div class="tileHeadDelete" (click)="delete(bulk)">
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</div>
</div>
</div>
</div>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>

View File

@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {BulkService} from "../../../api/bulk/BulkService"; import {BulkService} from "../../../api/bulk/BulkService";
import {Bulk} from "../../../api/bulk/Bulk"; import {Bulk} from "../../../api/bulk/Bulk";
import {faCheckCircle, faCircle, faTimesCircle} from "@fortawesome/free-regular-svg-icons"; import {faCheckCircle, faCircle, faCopy, faPlayCircle, faTimesCircle} from "@fortawesome/free-regular-svg-icons";
@Component({ @Component({
selector: 'app-bulk-list', selector: 'app-bulk-list',
@ -16,6 +16,8 @@ export class BulkListComponent implements OnInit {
readonly faTimes = faTimesCircle; readonly faTimes = faTimesCircle;
readonly faPlay = faPlayCircle;
readonly Bulk = Bulk; readonly Bulk = Bulk;
bulks: Bulk[] = []; bulks: Bulk[] = [];
@ -52,10 +54,21 @@ export class BulkListComponent implements OnInit {
this.bulkService.create(bulk => this.addOrReplace(bulk)); this.bulkService.create(bulk => this.addOrReplace(bulk));
} }
run(bulk: Bulk): void {
if (confirm("Zeitplan \"" + bulk.name + "\" wirklich ausführen?")) {
this.bulkService.run(bulk);
}
}
duplicate(bulk: Bulk): void {
this.bulkService.duplicate(bulk, bulk => this.addOrReplace(bulk));
}
delete(bulk: Bulk): void { delete(bulk: Bulk): void {
if (confirm("Zeitplan \"" + bulk.name + "\" wirklich löschen?")) { if (confirm("Zeitplan \"" + bulk.name + "\" wirklich löschen?")) {
this.bulkService.delete(bulk, () => this.remove(bulk)); this.bulkService.delete(bulk, () => this.remove(bulk));
} }
} }
protected readonly faCopy = faCopy;
} }

View File

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

View File

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

View File

@ -0,0 +1,24 @@
<div class="tiles">
<div class="tile" *ngFor="let channel of channels">
<div>
{{ channel.title }}
</div>
<div class="left">
{{ channel.type }}
</div>
<ng-container *ngIf="channel.type === 'KnxGroup'">
<div class="left">
{{ asKnxGroup(channel).addresMain }}&nbsp;/&nbsp;{{ asKnxGroup(channel).addresMid }}&nbsp;/&nbsp;{{ asKnxGroup(channel).addresSub }}
</div>
<div class="right">
DPT {{ asKnxGroup(channel).dpt }}
</div>
</ng-container>
<div class="timestamp">
{{ channel.timestamp | date:'yyyy-MM-dd HH:mm:ss' || '-' }}
</div>
<div class="value">
{{ channel.value || '-' }}
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
@import "../../../../config";
div {
padding: @padding;
}
.left {
float: left;
}
.right {
float: right;
}
.timestamp {
clear: left;
float: left;
}
.value {
clear: right;
float: right;
}

View File

@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ChannelService} from "../../api/channel/channel.service"; import {ChannelService} from "../../../api/channel/channel.service";
import {Channel, KnxGroup, Logic} from "../../api/channel/Channel"; import {Channel, KnxGroup, Logic} from "../../../api/channel/Channel";
import {Update} from "../../api/Update"; import {Update} from "../../../api/Update";
@Component({ @Component({
selector: 'app-channel-list', selector: 'app-channel-list',

View File

@ -1,66 +0,0 @@
<div class="config">
<select [(ngModel)]="createType">
<option ngValue="DeviceSwitch">Schalter</option>
<option ngValue="DeviceShutter">Rollladen</option>
</select>
<button (click)="create()">+ Hinzufügen</button>
</div>
<ng-container *ngFor="let device of devices.sort(Device.comparePosition); trackBy: Device.trackBy">
<ng-container [ngSwitch]="device.type">
<div class="device" *ngSwitchCase="'DeviceSwitch'" [ngClass]="getSwitchClassList(device)">
<div class="title">
{{device.title}}
</div>
<div class="edit" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon>
</div>
<div class="controls">
<img alt="An" class="control" src="assets/switch-on.svg" (click)="deviceService.setSwitchState(device, true)"/>
<img alt="Aus" class="control" src="assets/switch-off.svg" (click)="deviceService.setSwitchState(device, false)"/>
</div>
</div>
<div class="device" *ngSwitchCase="'DeviceStateScene'" [ngClass]="getStateSceneClassList(device)">
<div class="title">
{{device.title}}
</div>
<div class="edit" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon>
</div>
<div class="controls">
<div *ngFor="let scene of getStateScenes(device)" class="control button" (click)="deviceService.setStateScene(device, scene.number)">
<span class="center">{{scene.title}}</span>
</div>
</div>
</div>
<div class="device" *ngSwitchCase="'DeviceShutter'" [ngClass]="getShutterClassList(device)">
<div class="title">
{{device.title}}
</div>
<div class="edit" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon>
</div>
<div class="controls">
<div class="control button" (click)="deviceService.setShutterPosition(device, 0)">
<span class="center">Auf</span>
</div>
<div class="control button" (click)="deviceService.setShutterPosition(device, 40)">
<span class="center">50%</span>
</div>
<div class="control button" (click)="deviceService.setShutterPosition(device, 75)">
<span class="center">90%</span>
</div>
<div class="control button" (click)="deviceService.setShutterPosition(device, 85)">
<span class="center">Schlitze</span>
</div>
<div class="control button" (click)="deviceService.setShutterPosition(device, 100)">
<span class="center">Zu</span>
</div>
</div>
</div>
</ng-container>
</ng-container>

View File

@ -1,71 +0,0 @@
.device {
padding: 5px;
margin-bottom: 5px;
border-radius: 10px;
@media (min-width: 1000px) {
float: left;
width: 400px;
margin-right: 5px;
}
.title {
float: left;
font-weight: bold;
}
.edit {
float: right;
}
.controls {
clear: both;
.control {
position: relative;
float: left;
width: 60px;
height: 60px;
padding: 5px;
margin: 5px;
border-radius: 25%;
}
.button {
background-color: lightblue;
}
.control:hover {
background-color: lightskyblue;
}
}
}
.switchOn {
background-color: palegreen;
}
.switchOff {
background-color: indianred;
}
.switchUnknown {
background-color: gray;
}
.shutterOpen {
background-color: palegreen;
}
.shutterBetween {
background-color: yellow;
}
.shutterClosed {
background-color: indianred;
}
.shutterUnknown {
background-color: gray;
}

View File

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

View File

@ -1,104 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {DeviceService} from "../../api/device/device.service";
import {PropertyService} from "../../api/property/property.service";
import {Device, DeviceShutter, DeviceStateScene, DeviceSwitch} from "../../api/device/Device";
import {faEdit} from '@fortawesome/free-regular-svg-icons';
import {Scene} from "../../api/scene/Scene";
import {SceneService} from "../../api/scene/scene.service";
@Component({
selector: 'app-device-all-list',
templateUrl: './device-all-list.component.html',
styleUrls: ['./device-all-list.component.less']
})
export class DeviceAllListComponent implements OnInit {
readonly Device = Device;
readonly faEdit = faEdit;
devices: Device[] = [];
scenes: Scene[] = [];
createType: string = "DeviceSwitch";
constructor(
readonly deviceService: DeviceService,
readonly sceneService: SceneService,
readonly propertyService: PropertyService,
) {
// nothing
}
ngOnInit(): void {
this.propertyService.subscribe(update => this.devices.forEach(d => d.updateProperty(update.payload)));
this.deviceService.subscribe(update => this.updateDevice(update.payload, update.existing));
this.deviceService.findAll(devices => this.devices = devices);
this.sceneService.subscribe(update => this.updateScene(update.payload, update.existing));
this.sceneService.findAll(scenes => this.scenes = scenes);
}
create(): void {
this.deviceService.create(this.createType, device => this.updateDevice(device, true));
}
private updateDevice(device: Device, existing: boolean): void {
const index: number = this.devices.findIndex(d => d.id === device.id);
if (index >= 0) {
if (existing) {
this.devices[index] = device;
} else {
this.devices.splice(index, 1)
}
} else {
if (existing) {
this.devices.push(device);
}
}
}
private updateScene(scene: Scene, existing: boolean): void {
const index: number = this.scenes.findIndex(p => p.id === scene.id);
if (index >= 0) {
if (existing) {
this.scenes[index] = scene;
} else {
this.scenes.slice(index, 1);
}
} else if (existing) {
this.scenes.push(scene);
}
}
getSwitchClassList(device: Device): object {
const value: number | null | undefined = (device as DeviceSwitch).stateProperty?.value;
return {
switchOn: value === 1,
switchOff: value === 0,
switchUnknown: value === null || value === undefined,
};
}
getStateSceneClassList(device: Device): object {
return this.getSwitchClassList(device);
}
getShutterClassList(device: Device): object {
const value: number | null | undefined = (device as DeviceShutter).positionProperty?.value;
return {
shutterOpen: value === 0,
shutterBetween: value !== null && value !== undefined && value > 0 && value < 100,
shutterClosed: value === 100,
shutterUnknown: value === null || value === undefined,
};
}
getStateScenes(d: Device): Scene[] {
const device: DeviceStateScene = d as DeviceStateScene;
return device.sceneNumbers.map(sceneNumber => this.scenes.find(scene => scene.number === sceneNumber)).filter(scene => scene !== undefined).map(s => s as Scene);
}
}

View File

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

View File

@ -7,7 +7,7 @@
<tr> <tr>
<th>Name</th> <th>Name</th>
<td> <td>
<app-edit-field [initial]="deviceSwitch.title" (valueChange)="set('title', $event)"></app-edit-field> <app-text [initial]="deviceSwitch.title" (valueChange)="set('title', $event)"></app-text>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -24,7 +24,7 @@
<tr> <tr>
<th>Name</th> <th>Name</th>
<td> <td>
<app-edit-field [initial]="deviceStateScene.title" (valueChange)="set('title', $event)"></app-edit-field> <app-text [initial]="deviceStateScene.title" (valueChange)="set('title', $event)"></app-text>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -47,7 +47,7 @@
<tr> <tr>
<th>Name</th> <th>Name</th>
<td> <td>
<app-edit-field [initial]="deviceShutter.title" (valueChange)="set('title', $event)"></app-edit-field> <app-text [initial]="deviceShutter.title" (valueChange)="set('title', $event)"></app-text>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,16 +1,15 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {DataService} from "../../data.service"; import {PropertyService} from "../../../api/property/property.service";
import {PropertyService} from "../../api/property/property.service"; import {Device, DeviceShutter, DeviceStateScene, DeviceSwitch} from "../../../api/device/Device";
import {Device, DeviceShutter, DeviceStateScene, DeviceSwitch} from "../../api/device/Device"; import {DeviceService} from "../../../api/device/device.service";
import {DeviceService} from "../../api/device/device.service";
@Component({ @Component({
selector: 'app-device', selector: 'app-device-editor',
templateUrl: './device.component.html', templateUrl: './device-editor.component.html',
styleUrls: ['./device.component.less'] styleUrls: ['./device-editor.component.less']
}) })
export class DeviceComponent implements OnInit { export class DeviceEditorComponent implements OnInit {
device!: Device; device!: Device;
@ -24,14 +23,12 @@ export class DeviceComponent implements OnInit {
readonly router: Router, readonly router: Router,
readonly activatedRoute: ActivatedRoute, readonly activatedRoute: ActivatedRoute,
readonly deviceService: DeviceService, readonly deviceService: DeviceService,
readonly dataService: DataService,
readonly propertyService: PropertyService, readonly propertyService: PropertyService,
) { ) {
// nothing // nothing
} }
ngOnInit(): void { ngOnInit(): void {
this.dataService.device = undefined;
this.activatedRoute.params.subscribe(params => this.deviceService.getById(params['id'], device => this.setDevice(device))); this.activatedRoute.params.subscribe(params => this.deviceService.getById(params['id'], device => this.setDevice(device)));
} }
@ -48,7 +45,6 @@ export class DeviceComponent implements OnInit {
this.deviceShutter = device as DeviceShutter; this.deviceShutter = device as DeviceShutter;
break; break;
} }
this.dataService.device = device;
} }
set(key: string, value: any): void { set(key: string, value: any): void {
@ -57,7 +53,7 @@ export class DeviceComponent implements OnInit {
delete(): void { delete(): void {
if (confirm(this.getDeviceTypeTitle() + " \"" + this.device.title + "\" wirklich löschen?")) { if (confirm(this.getDeviceTypeTitle() + " \"" + this.device.title + "\" wirklich löschen?")) {
this.deviceService.delete(this.device, () => this.router.navigate(["/DeviceAllList"])); this.deviceService.delete(this.device, () => this.router.navigate(["/DeviceList", {type: this.device.type}]));
} }
} }

View File

@ -0,0 +1,85 @@
<div class="tiles">
<ng-container *ngFor="let device of devices; trackBy: Device.trackBy">
<div class="tile" *ngIf="device.type == 'DeviceSwitch'" [ngClass]="device.getSwitchClassList()">
<div class="tileHead">
<div class="flexGrow">
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
</div>
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon>
</div>
</div>
<div class="tileBody">
<img alt="An" class="control" src="assets/switch-on.svg" (click)="deviceService.setSwitchState(device, true)"/>
<img alt="Aus" class="control" src="assets/switch-off.svg" (click)="deviceService.setSwitchState(device, false)"/>
</div>
</div>
<div class="tile" *ngIf="device.type === 'DeviceStateScene'" [ngClass]="device.getStateSceneClassList()">
<div class="tileHead">
<div class="flexGrow">
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
</div>
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon>
</div>
</div>
<div class="tileBody">
<div class="control" *ngFor="let scene of getStateScenes(device)" (click)="deviceService.setStateScene(device, scene.number)">
{{ scene.title }}
</div>
</div>
</div>
<div class="tile" *ngIf="device.type === 'DeviceShutter'" [ngClass]="device.getShutterClassList()">
<div class="tileHead">
<div class="flexGrow">
<app-text [initial]="device.title" (valueChange)="set(device, 'title', $event)"></app-text>
</div>
<div class="tileHeadRight" [routerLink]="['/Device', {id: device.id}]">
<fa-icon [icon]="faEdit"></fa-icon>
</div>
</div>
<div class="tileBody">
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 0)">
Auf
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 20)">
20%
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 30)">
30%
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 40)">
40%
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 50)">
50%
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 60)">
60%
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 70)">
70%
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 80)">
80%
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 90)">
90%
</div>
<div class="control controlShutter" (click)="deviceService.setShutterPosition(device, 100)">
Zu
</div>
</div>
</div>
</ng-container>
</div>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>

View File

@ -0,0 +1,17 @@
@import "../../../../config";
.control {
float: left;
width: 4em;
aspect-ratio: 1;
margin: @margin;
border-radius: 25%;
}
.controlShutter {
width: 17.3%;
padding-top: 1.1em;
text-align: center;
margin: calc(@margin / 2);
background-color: lightsteelblue;
}

View File

@ -0,0 +1,90 @@
import {Component, OnInit} from '@angular/core';
import {DeviceService} from "../../../api/device/device.service";
import {PropertyService} from "../../../api/property/property.service";
import {Device, DeviceStateScene} from "../../../api/device/Device";
import {faEdit} from '@fortawesome/free-regular-svg-icons';
import {Scene} from "../../../api/scene/Scene";
import {SceneService} from "../../../api/scene/scene.service";
import {ActivatedRoute} from "@angular/router";
@Component({
selector: 'app-device-list',
templateUrl: './device-list.component.html',
styleUrls: ['./device-list.component.less']
})
export class DeviceListComponent implements OnInit {
readonly Device = Device;
readonly faEdit = faEdit;
devices: Device[] = [];
typeFilter: string;
scenes: Scene[] = [];
constructor(
readonly deviceService: DeviceService,
readonly sceneService: SceneService,
readonly propertyService: PropertyService,
readonly activatedRoute: ActivatedRoute,
) {
// nothing
}
ngOnInit(): void {
this.activatedRoute.params.subscribe(params => {
this.typeFilter = params['type'];
this.propertyService.subscribe(update => this.devices.forEach(d => d.updateProperty(update.payload)));
this.deviceService.subscribe(update => this.updateDevice(update.payload, update.existing));
this.deviceService.findAll(devices => this.devices = devices.filter(d => !this.typeFilter || this.typeFilter == d.type).sort(Device.compareTitle));
this.sceneService.subscribe(update => this.updateScene(update.payload, update.existing));
this.sceneService.findAll(scenes => this.scenes = scenes);
});
}
create(): void {
this.deviceService.create(this.typeFilter, device => this.updateDevice(device, true));
}
private updateDevice(device: Device, existing: boolean): void {
const index: number = this.devices.findIndex(d => d.id === device.id);
if (index >= 0) {
if (existing) {
this.devices[index] = device;
} else {
this.devices.splice(index, 1)
}
} else {
if (existing) {
this.devices.push(device);
}
}
}
private updateScene(scene: Scene, existing: boolean): void {
const index: number = this.scenes.findIndex(p => p.id === scene.id);
if (index >= 0) {
if (existing) {
this.scenes[index] = scene;
} else {
this.scenes.slice(index, 1);
}
} else if (existing) {
this.scenes.push(scene);
}
}
getStateScenes(device: Device): Scene[] {
const casted: DeviceStateScene = device as DeviceStateScene;
return casted.sceneNumbers.map(sceneNumber => this.scenes.find(scene => scene.number === sceneNumber)).filter(scene => scene !== undefined).map(s => s as Scene);
}
set(device: Device, key: string, value: any): void {
this.deviceService.set(device, key, value);
}
}

View File

@ -1,17 +0,0 @@
<div class="list">
<div class="entry">
Haus
</div>
<div class="entry area" *ngFor="let area of areas" [routerLink]="['/RoomList', {id: area.id}]">
{{area.title}}
<fa-icon class="icon" [icon]="faArrowAltCircleRight"></fa-icon>
</div>
<div class="entry device" *ngFor="let device of devices">
{{device.title}}
<fa-icon class="icon" [icon]="faPlayCircle"></fa-icon>
</div>
</div>

View File

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

View File

@ -1,28 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {Area} from "../../../../api/area/Area";
import {Device} from "../../../../api/device/Device";
import {faArrowAltCircleRight, faPlayCircle} from '@fortawesome/free-regular-svg-icons';
import {STUB_AREAS, STUB_DEVICES} from "../../../../api/STUB";
@Component({
selector: 'app-area-list',
templateUrl: './area-list.component.html',
styleUrls: ['../device-tree.less']
})
export class AreaListComponent implements OnInit {
readonly faArrowAltCircleRight = faArrowAltCircleRight;
readonly faPlayCircle = faPlayCircle;
areas: Area[] = STUB_AREAS.sort(Area.comparePosition);
devices: Device[] = STUB_DEVICES.filter(d => d.areaId === null && d.roomId === null).sort(Device.comparePosition);
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -1,24 +0,0 @@
<div class="list devices" *ngIf="room !== undefined">
<div class="entry back" [routerLink]="['/RoomList', {id: room.areaId}]">
<fa-icon [icon]="faArrowAltCircleLeft"></fa-icon>
|
{{room.title}}
</div>
<div class="entry device {{device.type}}" *ngFor="let device of devices">
{{device.title}}
<ng-container *ngIf="device.type === 'DeviceSwitch'">
<fa-icon class="icon" [icon]="faTimesCircle"></fa-icon>
<fa-icon class="icon" [icon]="faCheckCircle"></fa-icon>
</ng-container>
<ng-container *ngIf="device.type === 'DeviceStateScene'">
<fa-icon class="icon" [icon]="faPlayCircle"></fa-icon>
</ng-container>
</div>
</div>

View File

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

View File

@ -1,50 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {Device} from "../../../../api/device/Device";
import {faArrowAltCircleLeft, faArrowAltCircleRight, faCheckCircle, faPlayCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
import {STUB_DEVICES, STUB_ROOMS} from "../../../../api/STUB";
import {Room} from "../../../../api/room/Room";
@Component({
selector: 'app-device-list',
templateUrl: './device-list.component.html',
styleUrls: ['../device-tree.less']
})
export class DeviceListComponent implements OnInit {
readonly faArrowAltCircleLeft = faArrowAltCircleLeft;
readonly faArrowAltCircleRight = faArrowAltCircleRight;
readonly faPlayCircle = faPlayCircle;
readonly faCheckCircle = faCheckCircle;
readonly faTimesCircle = faTimesCircle;
room?: Room;
devices: Device[] = [];
constructor(
readonly route: ActivatedRoute,
) {
this.route.paramMap.subscribe(params => {
const roomIdStr: string | null = params.get("roomId");
if (roomIdStr != null) {
const roomId: number = parseInt(roomIdStr);
this.room = STUB_ROOMS.find(Room.filterById(roomId));
if (this.room) {
this.devices = STUB_DEVICES.filter(Device.filterByAreaIdAndRoomId(this.room.areaId, this.room.id)).sort(Device.comparePosition);
} else {
this.devices = [];
}
}
})
}
ngOnInit(): void {
}
}

View File

@ -1,34 +0,0 @@
.list {
font-size: 10vmin;
.entry {
padding: 3vmin;
border-bottom: 1px solid black;
.icon {
float: right;
margin-left: 3vmin;
}
}
.area {
background-color: lightsteelblue;
}
.area:active {
background-color: cornflowerblue;
}
.device {
background-color: lightcyan;
}
.DeviceSwitch {
background-color: gray;
}
.device:active {
background-color: darkcyan;
}
}

View File

@ -1,15 +0,0 @@
<div class="list rooms" *ngIf="area !== undefined">
<div class="entry back" [routerLink]="['/AreaList']">
<fa-icon [icon]="faArrowAltCircleLeft"></fa-icon>
|
{{area.title}}
</div>
<div class="entry room" *ngFor="let room of rooms" [routerLink]="['/DeviceList', {roomId: room.id}]">
{{room.title}}
<fa-icon class="icon" [icon]="faArrowAltCircleRight"></fa-icon>
</div>
<div class="entry device" *ngFor="let device of devices">
{{device.title}}
<fa-icon class="icon" [icon]="faPlayCircle"></fa-icon>
</div>
</div>

View File

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

View File

@ -1,46 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {Room} from "../../../../api/room/Room";
import {Device} from "../../../../api/device/Device";
import {faArrowAltCircleLeft, faArrowAltCircleRight, faPlayCircle} from '@fortawesome/free-regular-svg-icons';
import {STUB_AREAS, STUB_DEVICES, STUB_ROOMS} from "../../../../api/STUB";
import {Area} from "../../../../api/area/Area";
@Component({
selector: 'app-room-list',
templateUrl: './room-list.component.html',
styleUrls: ['../device-tree.less']
})
export class RoomListComponent implements OnInit {
readonly faArrowAltCircleLeft = faArrowAltCircleLeft;
readonly faArrowAltCircleRight = faArrowAltCircleRight;
readonly faPlayCircle = faPlayCircle;
area?: Area;
rooms: Room[] = [];
devices: Device[] = [];
constructor(
readonly route: ActivatedRoute,
) {
this.route.paramMap.subscribe(params => {
const areaIdStr: string | null = params.get("id");
if (areaIdStr != null) {
const areaId: number = parseInt(areaIdStr);
this.area = STUB_AREAS.find(Area.filterById(areaId));
this.rooms = STUB_ROOMS.filter(Room.filterByAreaId(areaId)).sort(Room.comparePosition);
this.devices = STUB_DEVICES.filter(Device.filterByAreaIdAndRoomId(areaId, null)).sort(Device.comparePosition);
}
})
}
ngOnInit(): void {
}
}

View File

@ -1,77 +0,0 @@
<ng-template #empty>
<td class="empty">-</td>
</ng-template>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>
<table>
<tr>
<th>Bezeichnung</th>
<th>Typ</th>
<th>Wert</th>
<th>Zeitstempel</th>
<th>Lesekanal</th>
<th>Schreibkanal</th>
<th>
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</th>
</tr>
<ng-container *ngFor="let property of properties.sort(Property.compareTypeThenTitle)">
<tr>
<td>
<app-edit-field [initial]="property.title" (valueChange)="edit(property, 'title', $event)"></app-edit-field>
</td>
<td>
<select [(ngModel)]="property.type" (ngModelChange)="edit(property, 'type', property.type)">
<option value="BOOLEAN">Schalter</option>
<option value="SHUTTER">Rollladen</option>
<option value="BRIGHTNESS_PERCENT">Helligkeit [%]</option>
<option value="COLOR_TEMPERATURE">Farbtermperatur</option>
<option value="LUX">Helligkeit [lux]</option>
<option value="SCENE">Szene</option>
</select>
</td>
<ng-container *ngIf="property.value !== null else empty">
<td *ngIf="property.type === 'BOOLEAN'" class="boolean" [class.true]="property.value" [class.false]="!property.value" (click)="edit(property, 'value', property.value > 0 ? 0 : 1)">
{{property.value ? "An" : "Aus"}}
</td>
<td *ngIf="property.type === 'SHUTTER'" class="number" [class.true]="property.value === 0" [class.false]="property.value === 100" [class.tristate]="0 < property.value && property.value < 100">
{{property.value}} %
</td>
<td *ngIf="property.type === 'BRIGHTNESS_PERCENT'" class="number">
{{property.value}} %
</td>
<td *ngIf="property.type === 'COLOR_TEMPERATURE'" class="number">
{{property.value}} K
</td>
<td *ngIf="property.type === 'LUX'" class="number">
{{property.value | number:'0.0-0'}} lux
</td>
<td *ngIf="property.type === 'SCENE'">
{{findScene(property)?.title || "Unbekannt: " + property.value}}
</td>
</ng-container>
<td *ngIf="property.timestamp !== null else empty">
{{property.timestamp | date:'yyyy-MM-dd HH:mm:ss'}}
</td>
<td class="full">
<app-search [searchService]="channelService" [initial]="property.readChannel?.id" (valueChange)="set(property, 'readChannel', $event)"></app-search>
</td>
<td class="full">
<app-search [searchService]="channelService" [initial]="property.writeChannel?.id" (valueChange)="set(property, 'writeChannel', $event)"></app-search>
</td>
<td class="delete">
<fa-icon title="Löschen" *ngIf="property.usages.length === 0" [icon]="faTimes" (click)="delete(property)"></fa-icon>
</td>
</tr>
</ng-container>
</table>

View File

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

View File

@ -0,0 +1,42 @@
<ng-template #empty>
<td class="empty">-</td>
</ng-template>
<div class="tiles">
<div *ngFor="let list of listLists()">
<div class="tile" *ngFor="let property of list">
<div class="tileHead" [ngClass]="property.getStateClassList()">
<app-text [initial]="property.title" (valueChange)="edit(property, 'title', $event)"></app-text>
</div>
<div class="tileBody">
<select [(ngModel)]="property.type" (ngModelChange)="edit(property, 'type', property.type)">
<option value="BOOLEAN">Schalter</option>
<option value="SHUTTER">Rollladen</option>
<option value="BRIGHTNESS_PERCENT">Helligkeit [%]</option>
<option value="COLOR_TEMPERATURE">Farbtermperatur</option>
<option value="LUX">Helligkeit [lux]</option>
<option value="SCENE">Szene</option>
</select>
<app-search [searchService]="channelService" [initial]="property.readChannel?.id" (valueChange)="set(property, 'readChannel', $event)"></app-search>
<app-search [searchService]="channelService" [initial]="property.writeChannel?.id" (valueChange)="set(property, 'writeChannel', $event)"></app-search>
<div class="delete">
<fa-icon title="Löschen" *ngIf="property.usages.length === 0" [icon]="faTimes" (click)="delete(property)"></fa-icon>
</div>
</div>
</div>
</div>
</div>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>

View File

@ -0,0 +1,29 @@
@import "../../../../config";
.propertyStateBooleanUnknown {
background-color: @COLOR_UNKNOWN;
}
.propertyStateBooleanTrue {
background-color: @COLOR_ACTIVE;
}
.propertyStateBooleanFalse {
background-color: @COLOR_INACTIVE;
}
.propertyStatePercentUnknown {
background-color: @COLOR_UNKNOWN;
}
.propertyStatePercentActive {
background-color: @COLOR_ACTIVE;
}
.propertyStatePercentBetween {
background-color: @COLOR_BETWEEN;
}
.propertyStatePercentInactive {
background-color: @COLOR_INACTIVE;
}

View File

@ -1,10 +1,11 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Property} from "../../api/property/Property"; import {Property, PropertyType} from "../../../api/property/Property";
import {PropertyService} from "../../api/property/property.service"; import {PropertyService} from "../../../api/property/property.service";
import {Scene} from "../../api/scene/Scene"; import {Scene} from "../../../api/scene/Scene";
import {SceneService} from "../../api/scene/scene.service"; import {SceneService} from "../../../api/scene/scene.service";
import {ChannelService} from "../../api/channel/channel.service"; import {ChannelService} from "../../../api/channel/channel.service";
import {faTimesCircle} from "@fortawesome/free-regular-svg-icons"; import {faTimesCircle} from "@fortawesome/free-regular-svg-icons";
import {environment} from 'src/environments/environment';
@Component({ @Component({
selector: 'app-property-list', selector: 'app-property-list',
@ -13,11 +14,15 @@ import {faTimesCircle} from "@fortawesome/free-regular-svg-icons";
}) })
export class PropertyListComponent implements OnInit { export class PropertyListComponent implements OnInit {
readonly environment = environment;
readonly faTimes = faTimesCircle; readonly faTimes = faTimesCircle;
Property = Property; protected booleans: Property[] = [];
properties: Property[] = []; protected shutters: Property[] = [];
Property = Property;
scenes: Scene[] = []; scenes: Scene[] = [];
@ -30,7 +35,10 @@ export class PropertyListComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.propertyService.findAll(properties => this.properties = properties, Property.compareTypeThenTitle); this.propertyService.findAll(properties => {
this.booleans = properties.filter(p => p.type === PropertyType.BOOLEAN).sort(Property.compareTypeThenTitle);
this.shutters = properties.filter(p => p.type === PropertyType.SHUTTER).sort(Property.compareTypeThenTitle);
}, Property.compareTypeThenTitle);
this.propertyService.subscribe(update => this.updateProperty(update.payload, update.existing)); this.propertyService.subscribe(update => this.updateProperty(update.payload, update.existing));
this.sceneService.findAll(scenes => this.scenes = scenes, Scene.compareNumber); this.sceneService.findAll(scenes => this.scenes = scenes, Scene.compareNumber);
@ -46,15 +54,23 @@ export class PropertyListComponent implements OnInit {
} }
private updateProperty(property: Property, existing: boolean): void { private updateProperty(property: Property, existing: boolean): void {
const index: number = this.properties.findIndex(p => p.id === property.id); if (property.type === PropertyType.BOOLEAN) {
this.updateProperty2(this.booleans, property, existing);
} else if (property.type === PropertyType.SHUTTER) {
this.updateProperty2(this.shutters, property, existing);
}
}
private updateProperty2(properties: Property[], property: Property, existing: boolean) {
const index: number = properties.findIndex(p => p.id === property.id);
if (index >= 0) { if (index >= 0) {
if (existing) { if (existing) {
this.properties[index] = property; properties[index] = property;
} else { } else {
this.properties.slice(index, 1); properties.slice(index, 1);
} }
} else if (existing) { } else if (existing) {
this.properties.push(property); properties.push(property);
} }
} }
@ -72,7 +88,7 @@ export class PropertyListComponent implements OnInit {
} }
findScene(property: Property): Scene | undefined { findScene(property: Property): Scene | undefined {
return this.scenes.find(s => s.id === property.value); return this.scenes.find(s => s.id === property.readChannel?.value);
} }
set(property: Property, key: string, value: any): void { set(property: Property, key: string, value: any): void {
@ -85,8 +101,22 @@ export class PropertyListComponent implements OnInit {
delete(property: Property): void { delete(property: Property): void {
if (confirm(`Eigenschaft "${property.title}" wirklich löschen?`)) { if (confirm(`Eigenschaft "${property.title}" wirklich löschen?`)) {
this.propertyService.delete(property, () => this.properties.splice(this.properties.findIndex(p => p.id === property.id), 1)); this.propertyService.delete(property, () => {
this.delete2(this.booleans, property);
this.delete2(this.shutters, property);
});
} }
} }
private delete2(properties: Property[], property: Property) {
const index = properties.findIndex(p => p.id === property.id);
if (index >= 0) {
properties.splice(index, 1);
}
}
listLists(): Property[][] {
return [this.shutters, this.booleans];
}
} }

View File

@ -1,27 +0,0 @@
<table>
<tr>
<th>&nbsp;</th>
<th>Bezeichnung</th>
<th>&nbsp;</th>
</tr>
<tr *ngFor="let schedule of schedules; trackBy: Schedule.trackBy">
<td class="boolean" (click)="set(schedule, 'enabled', !schedule.enabled)" [class.true]="schedule.enabled" [class.false]="!schedule.enabled">
<fa-icon *ngIf="schedule.enabled" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!schedule.enabled" [icon]="faCircle"></fa-icon>
</td>
<td [routerLink]="['/Schedule', {id: schedule.id}]">
{{schedule.title}}
</td>
<td class="delete" (click)="delete(schedule)">
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</td>
</tr>
</table>
<p>
<button (click)="create()">+ Hinzufügen</button>
</p>

View File

@ -1,11 +0,0 @@
select {
background-color: transparent;
border-width: 0;
width: 100%;
outline: none;
font-family: monospace;
}
th {
background-color: lightblue;
}

View File

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

View File

@ -1,61 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {ScheduleService} from "../../api/schedule/schedule.service";
import {Schedule} from "../../api/schedule/Schedule";
import {faCheckCircle, faCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
@Component({
selector: 'app-schedule-list',
templateUrl: './schedule-list.component.html',
styleUrls: ['./schedule-list.component.less']
})
export class ScheduleListComponent implements OnInit {
readonly faCheckCircle = faCheckCircle;
readonly faCircle = faCircle;
readonly faTimes = faTimesCircle;
readonly Schedule = Schedule;
schedules: Schedule[] = [];
constructor(
readonly scheduleService: ScheduleService,
) {
// nothing
}
ngOnInit(): void {
this.scheduleService.findAll(schedules => this.schedules = schedules, Schedule.compareName);
}
set(schedule: Schedule, key: string, value: any): void {
this.scheduleService.set(schedule, key, value, schedule => this.addOrReplace(schedule));
}
private addOrReplace(schedule: Schedule): void {
const index: number = this.schedules.findIndex(s => s.id === schedule.id);
if (index < 0) {
this.schedules.push(schedule);
} else {
this.schedules[index] = schedule;
}
this.schedules = this.schedules.sort(Schedule.compareName)
}
private remove(schedule: Schedule): void {
this.schedules.splice(this.schedules.findIndex(s => s.id === schedule.id), 1);
}
create(): void {
this.scheduleService.create(schedule => this.addOrReplace(schedule));
}
delete(schedule: Schedule): void {
if (confirm("Zeitplan \"" + schedule.title + "\" wirklich löschen?")) {
this.scheduleService.delete(schedule, () => this.remove(schedule));
}
}
}

View File

@ -0,0 +1,126 @@
<ng-container *ngIf="schedule">
<div id="title">
<app-text [initial]="schedule.title" (valueChange)="scheduleService.set(schedule, 'title', $event)"></app-text>
</div>
<div class="tiles">
<div class="tile" *ngFor="let entry of schedule.entries; trackBy: ScheduleEntry.trackBy">
<div class="tileHead disabledBack" [class.enabledBack]="entry.executable" [class.skipBack]="entry.skip">
<div class="enabled" (click)="set(entry, 'enabled', !entry.enabled)">
<fa-icon *ngIf="entry.enabled" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!entry.enabled" [icon]="faCircle"></fa-icon>
</div>
<div class="flexGrow">
<select [ngModel]="entry.bulk?.id" (ngModelChange)="entryService.set(entry, 'bulk', $event)">
<option [ngValue]="null">-</option>
<option [ngValue]="bulk.id" *ngFor="let bulk of bulks.sort(Bulk.compareName)">{{ bulk.name }}</option>
</select>
</div>
<div class="tileHeadDelete">
<fa-icon [icon]="faTimesCircle" (click)="delete(entry)"></fa-icon>
</div>
</div>
<div class="tileBodyFlex">
<div class="weekdays">
<div>
<app-bool label="Mo" [value]="entry.monday" (onChange)="entryService.set(entry, 'monday', $event)"></app-bool>
</div>
<div>
<app-bool label="Di" [value]="entry.tuesday" (onChange)="entryService.set(entry, 'tuesday', $event)"></app-bool>
</div>
<div>
<app-bool label="Mi" [value]="entry.wednesday" (onChange)="entryService.set(entry, 'wednesday', $event)"></app-bool>
</div>
<div>
<app-bool label="Do" [value]="entry.thursday" (onChange)="entryService.set(entry, 'thursday', $event)"></app-bool>
</div>
<div>
<app-bool label="Fr" [value]="entry.friday" (onChange)="entryService.set(entry, 'friday', $event)"></app-bool>
</div>
<div>
<app-bool label="Sa" [value]="entry.saturday" (onChange)="entryService.set(entry, 'saturday', $event)"></app-bool>
</div>
<div>
<app-bool label="So" [value]="entry.sunday" (onChange)="entryService.set(entry, 'sunday', $event)"></app-bool>
</div>
</div>
<div class="modes">
<div class="_inner_">
<div>
<app-bool icon="faClock" [value]="entry.type === 'TIME'" (onChange)="entryService.set(entry, 'type', 'TIME')"></app-bool>
</div>
<div>
<app-bool icon="faArrowAltCircleUp" [value]="entry.type === 'SUNRISE'" (onChange)="entryService.set(entry, 'type', 'SUNRISE')"></app-bool>
</div>
<div>
<app-bool icon="faArrowAltCircleDown" [value]="entry.type === 'SUNSET'" (onChange)="entryService.set(entry, 'type', 'SUNSET')"></app-bool>
</div>
</div>
</div>
</div>
<div class="tileBodyFlex" *ngIf="entry.type === 'TIME'">
<div class="flexGrow time">
<button class="buttonPlus" (click)="dayMinuteAdd(entry, +60)">+</button>
<button class="buttonMinus" (click)="dayMinuteAdd(entry, -60)">-</button>
<input type="time" [ngModel]="entry.time" (ngModelChange)="timeFromString(entry, $event)">
<button class="buttonPlus" (click)="dayMinuteAdd(entry, +1)">+</button>
<button class="buttonMinus" (click)="dayMinuteAdd(entry, -1)">-</button>
</div>
</div>
<div class="tileBodyFlex" *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
<div class="flexGrow sun">
<div *ngFor="let zenith of getZenithEntries(entry.type)">
<app-bool [label]="zenith.title" [value]="entry.zenith === zenith.value" (onChange)="entryService.set(entry, 'zenith', zenith.value)"></app-bool>
</div>
<div>
<input type="number" min="45" max="120" [ngModel]="entry.zenith" (ngModelChange)="entryService.set(entry, 'zenith', $event || 0)">
</div>
</div>
</div>
<div class="tileBodyFlex">
<div class="flexHalf">
<div class="flexIcon">
<img class="icon" src="assets/dice.svg" alt="+/-" title="Zufallsabweichung +/-">
</div>
<div class="flexIconInput">
<app-duration [duration]="Duration.ofCode(entry.fuzzySeconds + 's')" [inputClass]="entry.fuzzySeconds ? 'fuzzyBack' : ''" [min]="Duration.ofCode('')" (onChange)="entryService.set(entry, 'fuzzySeconds', $event.totalSeconds)"></app-duration>
</div>
</div>
<div class="flexHalf">
<div class="flexIcon">
<img class="icon" src="assets/skip.svg" alt="Überspringen">
</div>
<div class="flexIconInput flexInputLast">
<input type="number" min="0" [class.skipBack]="entry.skip" [ngModel]="entry.skip" (ngModelChange)="entryService.set(entry, 'skip', $event || 0)">
</div>
</div>
</div>
<div class="tileBodyFlex">
<div class="flexGrow timestamp" [class.skipFont]="entry.skip" *ngIf="entry.executable">
{{ timeService.relativeDate(entry.nextFuzzyTimestamp) }}
<span [class.fuzzyFont]="entry.fuzzySeconds" *ngIf="entry.fuzzySeconds">
(eig: {{ entry.nextClearTimestamp.date | date:'HH:mm' }})
</span>
</div>
<div class="flexGrow inactive" *ngIf="entry.todo">
{{ entry.todo }}
</div>
</div>
</div>
</div>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>
</ng-container>

View File

@ -0,0 +1,73 @@
@import "../../../../config";
@time_input_width: 35%;
#title {
margin: @margin;
}
.weekdays {
float: left;
width: 75%;
border-radius: @border-radius;
div {
float: left;
width: 14.2857%;
}
}
.modes {
float: left;
width: 25%;
._inner_ {
margin-left: @margin;
border-radius: @border-radius;
div {
float: left;
width: 33.3333%;
}
}
}
.time {
width: 100%;
input {
width: @time_input_width;
text-align: center;
margin-right: @margin;
}
button {
width: calc(((100% - @time_input_width) - 4 * @margin) / 4);
margin-right: @margin;
}
button:last-child {
margin-right: 0;
}
}
.sun {
div {
float: left;
width: 20%;
}
}
.timestamp {
text-align: center;
}
.inactive {
color: gray;
text-align: center;
}

View File

@ -0,0 +1,139 @@
import {Component, OnInit} from '@angular/core';
import {ScheduleService} from "../../../api/schedule/schedule.service";
import {Schedule} from "../../../api/schedule/Schedule";
import {ScheduleEntry} from "../../../api/schedule/entry/ScheduleEntry";
import {ScheduleEntryService} from "../../../api/schedule/entry/schedule-entry.service";
import {ActivatedRoute, Router} from "@angular/router";
import {Update} from "../../../api/Update";
import {NO_OP} from "../../../api/api.service";
import {Duration} from "../../../api/Duration";
import {Bulk} from "../../../api/bulk/Bulk";
import {BulkService} from "../../../api/bulk/BulkService";
import {Zenith} from "../../../api/schedule/entry/Zenith";
import {TimeService} from "../../../api/time.service";
import {faCheckCircle, faCircle, faTimesCircle} from "@fortawesome/free-regular-svg-icons";
const DAY_MINUTES: number = 24 * 60;
const ZENITH_ENTRIES: Zenith[] = [
new Zenith("Astr.", 107, true, true),
new Zenith("Naut.", 102, true, true),
new Zenith("Bürg.", 96, true, true),
new Zenith("Aufg.", 90.8, true, false),
new Zenith("Unterg.", 90.8, false, true),
];
const ZENITH_SUNRISE = ZENITH_ENTRIES.filter(zenith => zenith.sunrise);
const ZENITH_SUNSET = ZENITH_ENTRIES.filter(zenith => zenith.sunset).reverse();
@Component({
selector: 'app-schedule-editor',
templateUrl: './schedule-editor.component.html',
styleUrls: ['./schedule-editor.component.less']
})
export class ScheduleEditorComponent implements OnInit {
protected readonly faCheckCircle = faCheckCircle;
protected readonly faTimesCircle = faTimesCircle;
protected readonly faCircle = faCircle;
protected readonly ScheduleEntry = ScheduleEntry;
protected readonly Schedule = Schedule;
protected readonly Duration = Duration;
protected readonly Bulk = Bulk;
protected readonly expanded: number[] = [];
protected now: Date = new Date(Date.now());
protected schedule!: Schedule;
protected bulks: Bulk[] = [];
constructor(
readonly router: Router,
readonly activatedRoute: ActivatedRoute,
readonly scheduleService: ScheduleService,
readonly entryService: ScheduleEntryService,
readonly bulkService: BulkService,
readonly timeService: TimeService,
) {
// -
}
ngOnInit(): void {
this.scheduleService.subscribe(update => this.update(update));
this.bulkService.findAll(list => this.bulks = list);
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params['id'], schedule => this.setSchedule(schedule)));
}
private update(update: Update<Schedule>): void {
if (!this.schedule || this.schedule.id !== update.payload.id) {
return;
}
if (!update.existing) {
this.router.navigate(['/ScheduleList']);
return;
}
this.setSchedule(update.payload);
}
private setSchedule(schedule: Schedule) {
this.schedule = schedule;
this.expanded.length = 0;
this.expanded.push(...schedule.entries.map(e => e.id));
}
create(): void {
this.entryService.create(this.schedule, NO_OP);
}
delete(entry: ScheduleEntry): void {
if (confirm("Eintrag \"" + this.timeService.relativeDate(entry.nextClearTimestamp) + " +/-" + entry.fuzzySeconds + "\" wirklich löschen?")) {
this.entryService.delete(entry, NO_OP);
}
}
set(entry: ScheduleEntry | null, key: string, value: any): void {
if (entry) {
this.entryService.set(entry, key, value, NO_OP);
} else {
this.scheduleService.set(this.schedule, key, value, NO_OP);
}
}
timeFromString(entry: ScheduleEntry, time: string) {
const parts = time.split(':');
const hour = parseInt(parts[0]);
const minute = parseInt(parts[1]);
let second = 0;
if (parts.length === 3) {
second = parseInt(parts[2]);
}
const daySecond = (hour * 24 + minute) * 60 + second;
this.entryService.set(entry, 'daySecond', daySecond);
}
dayMinuteAdd(entry: ScheduleEntry, minutes: number) {
let newMinutes = entry.dayMinute + minutes;
while (newMinutes < 0 || newMinutes >= DAY_MINUTES) {
newMinutes = (newMinutes + DAY_MINUTES) % DAY_MINUTES;
}
this.entryService.set(entry, 'daySecond', newMinutes * 60);
}
getZenithEntries(type: string) {
if (type === 'SUNRISE') {
return ZENITH_SUNRISE;
}
return ZENITH_SUNSET;
}
}

View File

@ -0,0 +1,53 @@
<div class="tiles">
<div class="tile" *ngFor="let schedule of schedules; trackBy: Schedule.trackBy">
<div class="tileHead disabledBack" [class.enabledBack]="schedule.next" [class.skipBack]="schedule.next?.skip">
<div class="enabled" (click)="set(schedule, 'enabled', !schedule.enabled)">
<fa-icon *ngIf="schedule.enabled" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!schedule.enabled" [icon]="faCircle"></fa-icon>
</div>
<div class="flexGrow" [routerLink]="['/Schedule', {id: schedule.id}]">
{{ schedule.title }}
</div>
<div class="tileHeadRight" (click)="skip(schedule.next)" *ngIf="schedule.next">
<img class="icon" src="assets/skip.svg" [alt]="'Über.'">{{ schedule.next?.skip }}
</div>
<div class="tileHeadRight" (click)="delete(schedule)">
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</div>
</div>
<div class="tileBody next">
<div class="timestamp">
<ng-container *ngIf="schedule.next">{{ timeService.relativeDate(schedule.next?.nextFuzzyTimestamp) }}</ng-container>
</div>
<div class="bulk" [class.timestampBulkEmpty]="!schedule.next?.bulk" *ngIf="schedule.next?.bulk">
{{ schedule.next?.bulk?.name }}
<fa-icon [icon]="faPlayCircle" (click)="execute(schedule.next?.bulk)"></fa-icon>
</div>
</div>
<div class="tileBody last">
<div class="timestamp">
<ng-container *ngIf="schedule.last">{{ timeService.relativeDate(schedule.last?.lastFuzzyTimestamp) }}</ng-container>
<ng-container *ngIf="!schedule.last">- - -</ng-container>
</div>
<div class="bulk" [class.timestampBulkEmpty]="!schedule.last?.bulk" *ngIf="schedule.last?.bulk">
{{ schedule.last?.bulk?.name }}
<fa-icon [icon]="faPlayCircle" (click)="execute(schedule.last?.bulk)"></fa-icon>
</div>
</div>
</div>
</div>
<div class="config">
<button (click)="create()">+ Hinzufügen</button>
</div>

View File

@ -0,0 +1,15 @@
@import "../../../../config";
.last {
color: gray;
font-size: 60%;
border-top: @border solid gray;
}
.timestamp {
float: left;
}
.bulk {
float: right;
}

View File

@ -0,0 +1,89 @@
import {Component, OnInit} from '@angular/core';
import {ScheduleService} from "../../../api/schedule/schedule.service";
import {Schedule} from "../../../api/schedule/Schedule";
import {faCheckCircle, faCircle, faPlayCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
import {NO_OP} from "../../../api/api.service";
import {Update} from "../../../api/Update";
import {ScheduleEntryService} from "../../../api/schedule/entry/schedule-entry.service";
import {ScheduleEntry} from "../../../api/schedule/entry/ScheduleEntry";
import {Bulk} from "../../../api/bulk/Bulk";
import {BulkService} from "../../../api/bulk/BulkService";
import {TimeService} from "../../../api/time.service";
@Component({
selector: 'app-schedule-list',
templateUrl: './schedule-list.component.html',
styleUrls: ['./schedule-list.component.less']
})
export class ScheduleListComponent implements OnInit {
readonly faCheckCircle = faCheckCircle;
readonly faCircle = faCircle;
readonly faTimes = faTimesCircle;
readonly Schedule = Schedule;
schedules: Schedule[] = [];
constructor(
readonly scheduleService: ScheduleService,
readonly entryService: ScheduleEntryService,
readonly bulkService: BulkService,
readonly timeService: TimeService,
) {
// nothing
}
ngOnInit(): void {
this.scheduleService.subscribe(update => this.update(update));
this.scheduleService.findAll(schedules => this.schedules = schedules, Schedule.compareName);
}
set(schedule: Schedule, key: string, value: any): void {
this.scheduleService.set(schedule, key, value, NO_OP);
}
private update(update: Update<Schedule>): void {
const index: number = this.schedules.findIndex(s => s.id === update.payload.id);
if (index < 0) {
this.schedules.push(update.payload);
} else if (update.existing) {
this.schedules[index] = update.payload;
} else {
this.schedules.splice(index, 1);
}
this.schedules = this.schedules.sort(Schedule.compareName)
}
private remove(schedule: Schedule): void {
this.schedules.splice(this.schedules.findIndex(s => s.id === schedule.id), 1);
}
create(): void {
this.scheduleService.create(NO_OP);
}
delete(schedule: Schedule): void {
if (confirm("Zeitplan \"" + schedule.title + "\" wirklich löschen?")) {
this.scheduleService.delete(schedule, NO_OP);
}
}
skip(entry: ScheduleEntry) {
let skip = entry.skip + 1;
if (skip > 7) {
skip = 0;
}
this.entryService.set(entry, 'skip', skip);
}
execute(bulk: Bulk | undefined) {
if (bulk && confirm("Stapel Ausführen?\n\n" + bulk.name)) {
this.bulkService.run(bulk);
}
}
protected readonly faPlayCircle = faPlayCircle;
}

View File

@ -1,187 +0,0 @@
<ng-container *ngIf="schedule">
<ng-template #boolean let-entry="entry" let-value="value" let-key="key">
<td class="boolean" (click)="set(entry, key, !value)" [class.true]="value" [class.false]="!value">
<fa-icon *ngIf="value" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!value" [icon]="faCircle"></fa-icon>
</td>
</ng-template>
<table>
<tr class="header">
<ng-container *ngTemplateOutlet="boolean;context:{schedule: schedule, value: schedule.enabled, key:'enabled'}"></ng-container>
<td colspan="24">
<app-edit-field [initial]="schedule.title" (valueChange)="set(null, 'title', $event)"></app-edit-field>
</td>
</tr>
<tr [class.disabled]="!schedule.enabled">
<th>&nbsp;</th>
<th>Mo</th>
<th>Di</th>
<th>Mi</th>
<th>Do</th>
<th>Fr</th>
<th>Sa</th>
<th>So</th>
<th>Typ</th>
<th>Sonnenstand</th>
<th colspan="3">Uhrzeit</th>
<th>Unschärfe</th>
<th colspan="6">Nächste Ausführung</th>
<th colspan="2">Eingeschaft setzen</th>
<th>Massenverarbeitung</th>
<th>&nbsp;</th>
</tr>
<tr *ngFor="let entry of schedule.entries" [class.disabled]="entry.nextClearTimestamp === null">
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.enabled, key:'enabled'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.monday, key:'monday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.tuesday, key:'tuesday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.wednesday, key:'wednesday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.thursday, key:'thursday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.friday, key:'friday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.saturday, key:'saturday'}"></ng-container>
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.sunday, key:'sunday'}"></ng-container>
<td>
<select [(ngModel)]="entry.type" (ngModelChange)="set(entry, 'type', entry.type)">
<option value="TIME">Uhrzeit</option>
<option value="SUNRISE">Sonnenaufgang</option>
<option value="SUNSET">Sonnenuntergang</option>
</select>
</td>
<ng-container *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
<td>
<select [(ngModel)]="entry.zenith" (ngModelChange)="set(entry, 'zenith', entry.zenith)">
<option value="87">
[&nbsp;87°]
<ng-container *ngIf="entry.type === 'SUNRISE'">Nach Sonnenaufgang</ng-container>
<ng-container *ngIf="entry.type === 'SUNSET'">Vor Sonnenuntergang</ng-container>
</option>
<option value="90.8333">
[&nbsp;90°]
<ng-container *ngIf="entry.type === 'SUNRISE'">Sonnenaufgang</ng-container>
<ng-container *ngIf="entry.type === 'SUNSET'">Sonnenuntergang</ng-container>
</option>
<option value="93">[&nbsp;93°]</option>
<option value="96">[&nbsp;96°] Bürgerliche Dämmerung</option>
<option value="99">[&nbsp;99°]</option>
<option value="102">[102°] Nautische Dämmerung</option>
<option value="105">[105°]</option>
<option value="108">[108°] Astronomische Dämmerung</option>
</select>
</td>
</ng-container>
<td *ngIf="entry.type !== 'SUNRISE' && entry.type !== 'SUNSET'" class="empty"></td>
<ng-container *ngIf="entry.type === 'TIME'">
<td class="first">
<select [(ngModel)]="entry.hour" (ngModelChange)="set(entry, 'hour', entry.hour)">
<option *ngFor="let _ of [].constructor(24); let value = index" [ngValue]="value">{{value}}</option>
</select>
</td>
<td class="middle">:</td>
<td class="last">
<select [(ngModel)]="entry.minute" (ngModelChange)="set(entry, 'minute', entry.minute)">
<option *ngFor="let _ of [].constructor(60); let value = index" [ngValue]="value">{{value | number:'2.0'}}</option>
</select>
</td>
</ng-container>
<td *ngIf="entry.type !== 'TIME'" colspan="3" class="empty"></td>
<td>
<select [(ngModel)]="entry.fuzzySeconds" (ngModelChange)="set(entry, 'fuzzySeconds', entry.fuzzySeconds)">
<option [ngValue]="0">Keine</option>
<option [ngValue]="60">1 Minute</option>
<option [ngValue]="300">5 Minuten</option>
<option [ngValue]="600">10 Minuten</option>
<option [ngValue]="1800">30 Minuten</option>
<option [ngValue]="3600">1 Stunde</option>
<option [ngValue]="7200">2 Stunden</option>
<option [ngValue]="10800">3 Stunden</option>
<option [ngValue]="21600">6 Stunden</option>
<option [ngValue]="43200">12 Stunden</option>
<option [ngValue]="86400">1 Tag</option>
</select>
</td>
<ng-container *ngIf="entry.nextClearTimestamp">
<td class="number first" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.dayName}}</td>
<td class="number middle" [class.empty]="entry.fuzzySeconds > 0">:&nbsp;</td>
<td class="number last" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.timeString}}</td>
</ng-container>
<ng-container *ngIf="!entry.nextClearTimestamp">
<td class="empty first"></td>
<td class="empty middle"></td>
<td class="empty last"></td>
</ng-container>
<ng-container *ngIf="entry.nextFuzzyTimestamp && entry.fuzzySeconds > 0">
<td class="number first">{{entry.nextFuzzyTimestamp.dayName}}</td>
<td class="number middle">:&nbsp;</td>
<td class="number last">{{entry.nextFuzzyTimestamp.timeString}}</td>
</ng-container>
<ng-container *ngIf="!entry.nextFuzzyTimestamp || entry.fuzzySeconds <= 0">
<td class="empty first"></td>
<td class="empty middle"></td>
<td class="empty last"></td>
</ng-container>
<td>
<app-search [searchService]="propertyService" [initial]="entry.property?.id" (valueChange)="set(entry, 'property', $event)"></app-search>
</td>
<ng-container [ngSwitch]="entry.property?.type">
<td *ngSwitchCase="'BOOLEAN'" [class.true]="entry.value" [class.false]="!entry.value" (click)="set(entry, 'value', entry.value > 0 ? 0 : 1)">
{{entry.value ? "An" : "Aus"}}
</td>
<td *ngSwitchCase="'SHUTTER'" [class.true]="entry.value === 0" [class.false]="entry.value === 100" [class.tristate]="0 < entry.value && entry.value < 100">
<select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)">
<option [ngValue]="0">100% Offen</option>
<option [ngValue]="35">&nbsp;50%</option>
<option [ngValue]="55">&nbsp;75%</option>
<option [ngValue]="75">&nbsp;90% Sonnenschutz</option>
<option [ngValue]="85">100% Schlitze</option>
<option [ngValue]="100">100% Geschlossen</option>
</select>
</td>
<td *ngSwitchCase="'BRIGHTNESS_PERCENT'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</td>
<td *ngSwitchCase="'COLOR_TEMPERATURE'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</td>
<td *ngSwitchCase="'LUX'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</td>
<td *ngSwitchCase="'SCENE'">
<select [(ngModel)]="entry.value" (ngModelChange)="set(entry, 'value', entry.value)">
<option *ngFor="let scene of scenes" [ngValue]="scene.number">#{{scene.number | number:'2.0-0'}} {{scene.title}}</option>
</select>
</td>
<td *ngSwitchDefault class="empty">
&nbsp;
</td>
</ng-container>
<td>
<app-search [searchService]="bulkService" [initial]="entry.bulk?.id" (valueChange)="set(entry, 'bulk', $event)"></app-search>
</td>
<td class="delete" (click)="delete(entry)">
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</td>
</tr>
</table>
<p>
<button (click)="create()">+ Hinzufügen</button>
</p>
</ng-container>

View File

@ -1,19 +0,0 @@
select {
background-color: transparent;
border-width: 0;
width: 100%;
outline: none;
font-family: monospace;
}
th {
background-color: lightblue;
}
tr.header {
th:not(:first-child), td:not(:first-child) {
border: none;
}
}

View File

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

View File

@ -1,87 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {ScheduleService} from "../../api/schedule/schedule.service";
import {Schedule} from "../../api/schedule/Schedule";
import {ScheduleEntry} from "../../api/schedule/entry/ScheduleEntry";
import {ScheduleEntryService} from "../../api/schedule/entry/schedule-entry.service";
import {faCheckCircle, faCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
import {ActivatedRoute} from "@angular/router";
import {DataService} from "../../data.service";
import {PropertyService} from "../../api/property/property.service";
import {Scene} from "../../api/scene/Scene";
import {SceneService} from "../../api/scene/scene.service";
import {BulkService} from "../../api/bulk/BulkService";
@Component({
selector: 'app-schedule',
templateUrl: './schedule.component.html',
styleUrls: ['./schedule.component.less']
})
export class ScheduleComponent implements OnInit {
readonly faCheckCircle = faCheckCircle;
readonly faCircle = faCircle;
readonly faTimes = faTimesCircle;
readonly Schedule = Schedule;
schedule!: Schedule;
scenes: Scene[] = [];
constructor(
readonly activatedRoute: ActivatedRoute,
readonly scheduleService: ScheduleService,
readonly scheduleEntryService: ScheduleEntryService,
readonly propertyService: PropertyService,
readonly bulkService: BulkService,
readonly sceneService: SceneService,
readonly dataService: DataService,
) {
// nothing
}
ngOnInit(): void {
this.dataService.schedule = undefined;
this.sceneService.findAll(scenes => this.scenes = scenes);
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params['id'], schedule => this.setSchedule(schedule)));
}
private setSchedule(schedule: Schedule): void {
this.schedule = schedule;
this.dataService.schedule = schedule;
}
set(entry: ScheduleEntry | null, key: string, value: any): void {
if (entry) {
this.scheduleEntryService.set(entry, key, value, entry => this.addOrReplace(entry));
} else {
this.scheduleService.set(this.schedule, key, value, schedule => this.setSchedule(schedule));
}
}
private addOrReplace(entry: ScheduleEntry): void {
const index: number = this.schedule.entries.findIndex(s => s.id === entry.id);
if (index < 0) {
this.schedule.entries.push(entry);
} else {
this.schedule.entries[index] = entry;
}
}
private remove(entry: ScheduleEntry): void {
this.schedule.entries.splice(this.schedule.entries.findIndex(e => e.id === entry.id), 1);
}
create(): void {
this.scheduleEntryService.create(this.schedule, entry => this.addOrReplace(entry));
}
delete(entry: ScheduleEntry): void {
if (confirm("Eintrag \"" + entry.nextClearTimestamp?.timeString + " +/-" + entry.fuzzySeconds + "\" wirklich löschen?")) {
this.scheduleEntryService.delete(entry, () => this.remove(entry));
}
}
}

View File

@ -0,0 +1,20 @@
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'leftPad',
})
export class LeftPadDirective implements PipeTransform {
constructor() {
}
transform(value: any, count: number): any {
let result = "" + value;
const rest: number = count - result.length;
if (rest > 0) {
result = " ".repeat(rest) + result;
}
return result;
}
}

View File

@ -0,0 +1,18 @@
<div type="checkbox" [style.background-color]="color()" (click)="onChange.emit(!value)">
<ng-container *ngIf="label">
{{ label }}
</ng-container>
<ng-container *ngIf="!label">
<ng-container *ngIf="icon">
<fa-icon *ngIf="icon === 'faCircle'" [icon]="faCircle"></fa-icon>
<fa-icon *ngIf="icon === 'faCheckCircle'" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="icon === 'faClock'" [icon]="faClock"></fa-icon>
<fa-icon *ngIf="icon === 'faArrowAltCircleUp'" [icon]="faArrowAltCircleUp"></fa-icon>
<fa-icon *ngIf="icon === 'faArrowAltCircleDown'" [icon]="faArrowAltCircleDown"></fa-icon>
</ng-container>
<ng-container *ngIf="!icon">
<fa-icon *ngIf="value" [icon]="faCheckCircle"></fa-icon>
<fa-icon *ngIf="!value" [icon]="faCircle"></fa-icon>
</ng-container>
</ng-container>
</div>

View File

@ -0,0 +1,8 @@
div {
width: 100%;
height: 100%;
padding-top: 0.1em;
padding-bottom: 0.1em;
text-align: center;
background-color: gray;
}

View File

@ -0,0 +1,58 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCircle, faClock} from "@fortawesome/free-regular-svg-icons";
@Component({
selector: 'app-bool',
templateUrl: './bool.component.html',
styleUrls: ['./bool.component.less']
})
export class BoolComponent implements OnInit {
protected readonly faCheckCircle = faCheckCircle;
protected readonly faCircle = faCircle;
protected readonly faClock = faClock;
protected readonly faArrowAltCircleUp = faArrowAltCircleUp;
protected readonly faArrowAltCircleDown = faArrowAltCircleDown;
@Input()
icon: string | undefined = undefined;
@Input()
colorActive: string = '#8FBC8FFF';
@Input()
colorInactive: string = '#8c8c8c';
@Input()
label: string | undefined = undefined;
@Input()
value: boolean | undefined = undefined;
@Output()
onChange: EventEmitter<boolean> = new EventEmitter();
constructor() {
}
ngOnInit(): void {
}
toggle() {
}
color(): string {
if (this.value === true) {
return this.colorActive;
} else if (this.value === false) {
return this.colorInactive;
}
return "";
}
}

View File

@ -0,0 +1 @@
<input type="text" [class]="inputClass" [(ngModel)]="code" (focus)="focus = true" (blur)="apply()" (keydown.enter)="apply()" (keydown.escape)="cancel()">

View File

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

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