127 Commits

Author SHA1 Message Date
4889f5ed8b namespace and config 14
Some checks failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline was successful
2025-11-21 12:12:19 +01:00
804e9bf742 namespace and config 13
Some checks failed
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline failed
2025-11-21 12:11:10 +01:00
f60d5d03e9 namespace and config 12
Some checks failed
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline failed
2025-11-21 12:06:56 +01:00
eff88e1d2f namespace and config 11 2025-11-21 12:06:28 +01:00
d027163087 namespace and config 10
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
2025-11-21 12:01:20 +01:00
4051ca22a4 namespace and config 9
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline failed
ci/woodpecker/push/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline failed
2025-11-21 12:00:35 +01:00
2608e935b8 namespace and config 8
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/3 Pipeline failed
2025-11-21 11:55:50 +01:00
51f3b4f227 namespace and config 7
Some checks failed
ci/woodpecker/push/woodpecker/4 Pipeline is pending
ci/woodpecker/push/woodpecker/5 Pipeline is pending
ci/woodpecker/tag/woodpecker/4 Pipeline is pending
ci/woodpecker/tag/woodpecker/5 Pipeline is pending
ci/woodpecker/tag/woodpecker/1 Pipeline is pending
ci/woodpecker/tag/woodpecker/2 Pipeline is pending
ci/woodpecker/tag/woodpecker/3 Pipeline is pending
ci/woodpecker/push/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline failed
2025-11-21 11:46:24 +01:00
006359687f namespace and config 6
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline failed
ci/woodpecker/push/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline failed
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/3 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
2025-11-21 11:45:41 +01:00
f26d304890 namespace and config 5
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/2 Pipeline was successful
2025-11-21 11:44:10 +01:00
6feec48ac6 namespace and config 4
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/4 Pipeline was successful
ci/woodpecker/tag/woodpecker/3 Pipeline was successful
2025-11-21 11:40:54 +01:00
ed6ed66a37 namespace and config 3
Some checks failed
ci/woodpecker/push/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/5 Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
ci/woodpecker/tag/woodpecker/3 Pipeline failed
2025-11-21 11:37:39 +01:00
09498dd0e5 namespace and config 2
Some checks failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/3 Pipeline failed
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
2025-11-21 11:33:58 +01:00
41f5e06e30 namespace and config
Some checks failed
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/tag/woodpecker/4 Pipeline failed
ci/woodpecker/tag/woodpecker/1 Pipeline failed
ci/woodpecker/tag/woodpecker/2 Pipeline failed
ci/woodpecker/tag/woodpecker/3 Pipeline failed
2025-11-21 11:23:49 +01:00
7769c6066a add ci script 5
All checks were successful
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/1 Pipeline was successful
ci/woodpecker/tag/woodpecker/2 Pipeline was successful
ci/woodpecker/tag/woodpecker/4 Pipeline was successful
ci/woodpecker/tag/woodpecker/3 Pipeline was successful
2025-11-21 11:10:45 +01:00
5f23e28cc0 add ci script 4
All checks were successful
ci/woodpecker/push/woodpecker/4 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/3 Pipeline was successful
2025-11-21 11:03:16 +01:00
cc083c1055 add ci script 3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-11-21 10:57:51 +01:00
37b773143f add ci script 2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-11-21 10:55:11 +01:00
27c0990400 add ci script
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-11-21 10:51:53 +01:00
b150cd895e default to rooms 2025-11-20 22:17:35 +01:00
f67831c8bd fix 2025-11-20 22:13:01 +01:00
b61e7293ae debug device states 2025-11-20 22:09:21 +01:00
a85fd1ccf0 change name 2025-11-20 21:52:21 +01:00
19a3dfdd65 drop obsolete files 2025-11-20 21:50:43 +01:00
57b4d7d762 drop back button from rooms 2025-11-20 21:41:35 +01:00
826d73990b grid fix 16 2025-11-20 21:32:42 +01:00
86587402b6 grid fix 15 2025-11-20 21:30:10 +01:00
110b903dc8 grid fix 14 2025-11-20 21:27:30 +01:00
a8f6ad800d grid fix 13 2025-11-20 21:25:42 +01:00
52a50e1cd4 grid fix 12 2025-11-20 21:25:05 +01:00
9d7e26677f grid fix 11 2025-11-20 18:33:50 +01:00
ebd459bfc1 grid fix 10 2025-11-20 18:29:51 +01:00
e49c5affe9 grid fix 9 2025-11-20 18:14:35 +01:00
5595cf4f37 grid fix 8 2025-11-20 18:13:55 +01:00
f61417a631 grid fix 7 2025-11-19 21:33:52 +01:00
6d62732d1d grid fix 6 2025-11-19 21:32:23 +01:00
73814df01e grid fix 5 2025-11-19 21:16:48 +01:00
4b7ac9b82b grid fix 4 2025-11-19 21:14:50 +01:00
850d810cb3 grid fix 3 2025-11-19 21:12:39 +01:00
5bdfbacc3c grid fix 2025-11-19 20:36:14 +01:00
3d9043b8fa device state setting fix 20 2025-11-19 15:17:55 +01:00
1890a83939 device state setting fix 19 2025-11-19 12:55:03 +01:00
23edd42fca device state setting fix 18 2025-11-19 12:34:39 +01:00
5fbaab3c11 device state setting fix 17 2025-11-19 12:19:51 +01:00
2eefbcd44b device state setting fix 16 2025-11-19 12:12:15 +01:00
d09caa9d92 device state setting fix 15 2025-11-19 12:09:45 +01:00
127b5857c3 device state setting fix 14 2025-11-19 12:02:27 +01:00
c2dcb733d8 device state setting fix 12 2025-11-19 11:24:32 +01:00
168b2c4b12 device state setting fix 11 2025-11-19 10:58:51 +01:00
66872703c1 device state setting fix 10 2025-11-19 10:55:05 +01:00
d4b44fa73e device state setting fix 9 2025-11-19 10:49:23 +01:00
87a86524d4 device state setting fix 8 2025-11-19 10:44:18 +01:00
91fdfde280 device state setting fix 7 2025-11-19 10:41:39 +01:00
d138d7bf0a device state setting fix 6 2025-11-19 10:36:45 +01:00
8b08ded0c6 device state setting fix 5 2025-11-19 10:31:07 +01:00
6a0601742a device state setting fix 4 2025-11-19 10:28:33 +01:00
99580f8ff9 device state setting fix 3 2025-11-19 10:20:46 +01:00
381f8521d4 device state setting fix 2 2025-11-19 10:16:39 +01:00
a382d58601 device state setting fix 2025-11-19 10:11:16 +01:00
0ca5ffaca0 device state fix 3 2025-11-19 09:40:47 +01:00
ec193a92f8 device state fix 2 2025-11-19 09:38:03 +01:00
c473ea341e device state fix 2025-11-19 09:35:27 +01:00
0b6b9a01fb refactored 7 2025-11-18 16:05:58 +01:00
734ee18e85 refactored 6 2025-11-18 16:03:20 +01:00
d9d17d37e5 refactored 5 2025-11-18 14:25:55 +01:00
29e2fd441b refactored 4 2025-11-18 14:25:09 +01:00
f902f40386 refactored 3 2025-11-18 14:12:08 +01:00
6483cf0740 refactored 2 2025-11-18 14:10:22 +01:00
2863f5f5a6 refactored 2025-11-18 12:59:14 +01:00
3e02decc02 new endpoints 6 2025-11-18 11:02:03 +01:00
3e9e388ddd new endpoints 5 2025-11-18 10:55:51 +01:00
1f6fe134d5 new endpoints 4 2025-11-18 10:41:24 +01:00
05884bb99a new endpoints 3 2025-11-18 10:36:43 +01:00
5d08ec970a new endpoints 2 2025-11-18 10:20:10 +01:00
732c333966 new endpoints 2025-11-18 09:38:32 +01:00
c685e65be5 new ui 14 2025-11-17 22:25:45 +01:00
2eb9d323de new ui 13 2025-11-17 22:14:31 +01:00
d9d2033dd7 new ui 12 2025-11-17 22:12:15 +01:00
04c8cc8577 new ui 11 2025-11-17 22:07:26 +01:00
e140227f7a new ui 10 2025-11-17 22:04:59 +01:00
cc25a22025 new ui 9 2025-11-17 22:00:56 +01:00
af6ab012dd new ui 8 2025-11-17 21:56:09 +01:00
a2c2ef7ddd new ui 7 2025-11-17 21:54:40 +01:00
ecf31c7f8b new ui 6 2025-11-17 21:52:24 +01:00
74d4fea695 new ui 5 2025-11-17 21:42:47 +01:00
c38fdab0ad new ui 4 2025-11-17 21:40:00 +01:00
094cef0bd7 new ui 3 2025-11-17 21:39:44 +01:00
1027b905b5 new ui 2 2025-11-17 21:36:25 +01:00
204a70a438 new ui 1 2025-11-17 21:33:15 +01:00
3a2702e9aa add stehlampen esszimmer
2
2025-11-17 14:51:38 +01:00
9e66e16986 add stehlampen esszimmer 2025-11-17 14:46:44 +01:00
aaee480e57 seems to work 2025-11-17 11:36:19 +01:00
d0b5184270 start script 2025-11-17 10:23:42 +01:00
5bf37a19ad homekit bridge initial 2025-11-17 10:18:27 +01:00
2e24c259cb fix 4 2025-11-17 08:42:26 +01:00
bbf280bdf4 fix 3 2025-11-17 08:39:50 +01:00
a7d778b211 fix 2 2025-11-17 08:37:02 +01:00
a7d8afc98b fix 2025-11-17 08:10:11 +01:00
a4ae8a2f6c slider for thermostats 2025-11-17 08:05:58 +01:00
6152385339 fix 8 2025-11-14 15:14:48 +01:00
c2b7328219 fix 7 2025-11-14 15:13:37 +01:00
99362b346f fix 6 2025-11-14 15:01:49 +01:00
77d29c3a42 fix 5 2025-11-14 14:31:03 +01:00
ef3b1177d2 fix 4 2025-11-14 14:18:59 +01:00
8bbe9c164f fix 3 2025-11-14 14:14:49 +01:00
65f8a0c7cb fix 2 2025-11-14 11:34:32 +01:00
cbe7e11cf2 fix 2025-11-14 11:30:10 +01:00
9bf336fa11 groups and scenes 3 2025-11-13 21:56:13 +01:00
b82217a666 groups and scenes 2 2025-11-13 21:54:09 +01:00
5851414ba5 groups and scenes initial 2025-11-13 21:29:04 +01:00
4c5475e930 favicon 2025-11-13 11:14:43 +01:00
b6b441c0ca rules 2 2025-11-11 19:58:06 +01:00
d3d96ed3e9 enabled for rules 2025-11-11 17:08:18 +01:00
2e2963488b rules initial 2025-11-11 16:38:41 +01:00
7928bc596f compose file 2025-11-11 12:40:53 +01:00
3874eaed83 compose file added 2025-11-11 12:34:49 +01:00
0f43f37823 shellies 2025-11-11 11:39:10 +01:00
93e70da97d add spuele 3 2025-11-11 11:11:14 +01:00
62d302bf41 add spuele 2 2025-11-11 11:10:31 +01:00
3d6130f2c2 add spuele 2025-11-11 11:09:08 +01:00
2a8d569bb5 shelly 2025-11-11 11:01:52 +01:00
6a5f814cb4 fix in layout, drop test entry 2025-11-11 10:28:27 +01:00
cc3c15078c change relays to type relay 2025-11-11 10:24:09 +01:00
7772dac000 medusa lampe to relay 2025-11-11 10:12:25 +01:00
97ea853483 add type relay 2025-11-11 10:10:22 +01:00
86d1933c1f sensoren 2 2025-11-11 09:13:46 +01:00
9458381593 sensoren 2025-11-11 09:12:35 +01:00
71 changed files with 9698 additions and 2053 deletions

3
.gitignore vendored
View File

@@ -61,3 +61,6 @@ Thumbs.db
# Poetry # Poetry
poetry.lock poetry.lock
apps/homekit/homekit.state

25
.woodpecker/build.yml Normal file
View File

@@ -0,0 +1,25 @@
matrix:
APP:
- ui
- api
- abstraction
- rules
steps:
build:
image: plugins/kaniko
settings:
repo: ${FORGE_NAME}/${CI_REPO}/${APP}
registry:
from_secret: container_registry
auto_tag: true
username:
from_secret: container_registry_username
password:
from_secret: container_registry_password
dockerfile: apps/${APP}/Dockerfile
when:
event: [push, tag]
ref:
exclude:
- refs/tags/*-configchange

37
.woodpecker/predeploy.yml Normal file
View File

@@ -0,0 +1,37 @@
steps:
create_namespace:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment:
KUBE_CONFIG_CONTENT:
from_secret: kube_config
NAMESPACE: "homea2"
commands:
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- kubectl create namespace ${NAMESPACE} || echo "Namespace ${NAMESPACE} already exists"
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
apply_configuration:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment:
KUBE_CONFIG_CONTENT:
from_secret: kube_config
NAMESPACE: "homea2"
commands:
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- kubectl create configmap home-automation-config
--from-file=devices=config/devices.yaml
--from-file=groups=config/groups.yaml
--from-file=layout=config/layout.yaml
--from-file=rules=config/rules.yaml
--from-file=scenes=config/scenes.yaml
--namespace=$NAMESPACE
--dry-run=client -o yaml | kubectl apply -f -
when:
event: [tag]

View File

@@ -1,41 +0,0 @@
Schlafzimmer:
- Bettlicht Patty | 0x0017880108158b32
- Bettlicht Wolfgang | 0x00178801081570bf
- Deckenlampe Schlafzimmer | 0x0017880108a406a7
- Medusa-Lampe Schlafzimmer | 0xf0d1b80000154c7c
Esszimmer:
- Deckenlampe Esszimmer | 0x0017880108a03e45
- Leselampe Esszimmer | 0xec1bbdfffe7b84f2
- Standlampe Esszimmer | 0xbc33acfffe21f547
- kleine Lampe links Esszimmer | 0xf0d1b80000153099
- kleine Lampe rechts Esszimmer | 0xf0d1b80000156645
Wohnzimmer:
- Lampe Naehtischchen Wohnzimmer | 0x842e14fffee560ee
- Lampe Semeniere Wohnzimmer | 0xf0d1b8000015480b
- Sterne Wohnzimmer | 0xf0d1b80000155fc2
- grosse Lampe Wohnzimmer | 0xf0d1b80000151aca
Küche:
- Küche Deckenlampe | 0x001788010d2c40c4
- Kueche | 0x94deb8fffe2e5c06
Arbeitszimmer Patty:
- Leselampe Patty | 0x001788010600ec9d
- Schranklicht hinten Patty | 0x0017880106e29571
- Schranklicht vorne Patty | 0xf0d1b80000154cf5
Arbeitszimmer Wolfgang:
- Wolfgang | 0x540f57fffe7e3cfe
- ExperimentLabTest | 0xf0d1b80000195038
Flur:
- Deckenlampe Flur oben | 0x001788010d2123a7
- Haustür | 0xec1bbdfffea6a3da
- Licht Flur oben am Spiegel | 0x842e14fffefe4ba4
Sportzimmer:
- Sportlicht Regal | 0xf0d1b8be2409f569
- Sportlicht Tisch | 0xf0d1b8be2409f31b
- Sportlicht am Fernseher, Studierzimmer | 0x842e14fffe76a23a

View File

@@ -1,230 +0,0 @@
# Docker Guide für Home Automation
Vollständige Anleitung zum Ausführen aller Services mit Docker/finch.
## Quick Start - Alle Services starten
### Linux Server (empfohlen - mit Docker Network)
```bash
# 1. Images bauen
docker build -t api:dev -f apps/api/Dockerfile .
docker build -t ui:dev -f apps/ui/Dockerfile .
docker build -t abstraction:dev -f apps/abstraction/Dockerfile .
docker build -t simulator:dev -f apps/simulator/Dockerfile .
# 2. Netzwerk erstellen
docker network create home-automation
# 3. Abstraction Layer (MQTT Worker)
docker run -d --name abstraction \
--network home-automation \
-v $(pwd)/config:/app/config:ro \
-e MQTT_BROKER=172.16.2.16 \
-e REDIS_HOST=172.23.1.116 \
-e REDIS_DB=8 \
abstraction:dev
# 4. API Server
docker run -d --name api \
--network home-automation \
-p 8001:8001 \
-v $(pwd)/config:/app/config:ro \
-e MQTT_BROKER=172.16.2.16 \
-e REDIS_HOST=172.23.1.116 \
-e REDIS_DB=8 \
api:dev
# 5. Web UI
docker run -d --name ui \
--network home-automation \
-p 8002:8002 \
-e API_BASE=http://api:8001 \
ui:dev
# 6. Device Simulator (optional)
docker run -d --name simulator \
--network home-automation \
-p 8010:8010 \
-e MQTT_BROKER=172.16.2.16 \
simulator:dev
```
### macOS mit finch/nerdctl (Alternative)
```bash
# Images bauen (wie oben)
# Abstraction Layer
docker run -d --name abstraction \
-v $(pwd)/config:/app/config:ro \
-e MQTT_BROKER=172.16.2.16 \
-e REDIS_HOST=172.23.1.116 \
-e REDIS_DB=8 \
abstraction:dev
# API Server
docker run -d --name api \
-p 8001:8001 \
-v $(pwd)/config:/app/config:ro \
-e MQTT_BROKER=172.16.2.16 \
-e REDIS_HOST=172.23.1.116 \
-e REDIS_DB=8 \
api:dev
# Web UI (mit host.docker.internal für macOS)
docker run -d --name ui \
--add-host=host.docker.internal:host-gateway \
-p 8002:8002 \
-e API_BASE=http://host.docker.internal:8001 \
ui:dev
# Device Simulator
docker run -d --name simulator \
-p 8010:8010 \
-e MQTT_BROKER=172.16.2.16 \
simulator:dev
```
## Zugriff
- **Web UI**: http://<server-ip>:8002
- **API Docs**: http://<server-ip>:8001/docs
- **Simulator**: http://<server-ip>:8010
Auf localhost: `127.0.0.1` oder `localhost`
## finch/nerdctl Besonderheiten
### Port-Binding Verhalten (nur macOS/Windows)
**Standard Docker auf Linux:**
- `-p 8001:8001` → bindet auf `0.0.0.0:8001` (von überall erreichbar)
**finch/nerdctl auf macOS:**
- `-p 8001:8001` → bindet auf `127.0.0.1:8001` (nur localhost)
- Dies ist ein **Security-Feature** von nerdctl
- **Auf Linux-Servern ist das KEIN Problem!**
### Container-to-Container Kommunikation
**Linux (empfohlen):**
```bash
# Docker Network verwenden - Container sprechen sich mit Namen an
docker network create home-automation
docker run --network home-automation --name api ...
docker run --network home-automation -e API_BASE=http://api:8001 ui ...
```
**macOS mit finch:**
```bash
# host.docker.internal verwenden
docker run --add-host=host.docker.internal:host-gateway \
-e API_BASE=http://host.docker.internal:8001 ui ...
```
## Container verwalten
```bash
# Alle Container anzeigen
docker ps
# Logs anschauen
docker logs api
docker logs ui -f # Follow mode
# Container stoppen
docker stop api ui abstraction simulator
# Container entfernen
docker rm api ui abstraction simulator
# Alles neu starten
docker stop api ui abstraction simulator && \
docker rm api ui abstraction simulator && \
# ... dann Quick Start Befehle von oben
```
## Troubleshooting
### UI zeigt "Keine Räume oder Geräte konfiguriert"
**Problem:** UI kann API nicht erreichen
**Linux - Lösung:**
```bash
# Verwende Docker Network
docker network create home-automation
docker stop ui && docker rm ui
docker run -d --name ui \
--network home-automation \
-p 8002:8002 \
-e API_BASE=http://api:8001 \
ui:dev
```
**macOS/finch - Lösung:**
```bash
docker stop ui && docker rm ui
docker run -d --name ui \
--add-host=host.docker.internal:host-gateway \
-p 8002:8002 \
-e API_BASE=http://host.docker.internal:8001 \
ui:dev
```
### "Connection refused" in Logs
**Check 1:** Ist die API gestartet?
```bash
docker ps | grep api
curl http://127.0.0.1:8001/health
```
**Check 2:** Hat UI die richtige API_BASE?
```bash
docker inspect ui | grep API_BASE
```
### Port bereits belegt
```bash
# Prüfe welcher Prozess Port 8001 nutzt
lsof -i :8001
# Oder mit netstat
netstat -an | grep 8001
# Alte Container aufräumen
docker ps -a | grep -E "api|ui|abstraction|simulator"
docker rm -f <container-id>
```
## Produktiv-Deployment
Für Produktion auf **Linux-Servern** empfohlen:
1. **Docker Compose** (siehe `infra/docker-compose.yml`)
2. **Docker Network** für Service Discovery (siehe Linux Quick Start oben)
3. **Volume Mounts** für Persistenz
4. **Health Checks** in Kubernetes/Compose (nicht im Dockerfile)
### Beispiel mit Docker Network (Linux)
```bash
# Netzwerk erstellen
docker network create home-automation
# Services starten (alle im gleichen Netzwerk)
docker run -d --name api --network home-automation \
-p 8001:8001 \
-v $(pwd)/config:/app/config:ro \
api:dev
docker run -d --name ui --network home-automation \
-p 8002:8002 \
-e API_BASE=http://api:8001 \
ui:dev
```
**Vorteil:** Service Discovery über Container-Namen, keine `--add-host` Tricks nötig.

View File

@@ -1,223 +0,0 @@
# MAX! (eQ-3) Thermostat Integration
## Overview
This document describes the integration of MAX! (eQ-3) thermostats via Homegear into the home automation system.
## Protocol Characteristics
MAX! thermostats use a **simple integer-based protocol** (not JSON):
- **SET messages**: Plain integer temperature value (e.g., `22`)
- **STATE messages**: Plain integer temperature value (e.g., `22`)
- **Topics**: Homegear MQTT format
### MQTT Topics
**SET Command:**
```
homegear/instance1/set/<peerId>/<channel>/SET_TEMPERATURE
Payload: "22" (plain integer as string)
```
**STATE Update:**
```
homegear/instance1/plain/<peerId>/<channel>/SET_TEMPERATURE
Payload: "22" (plain integer as string)
```
## Transformation Layer
The abstraction layer provides automatic transformation between the abstract home protocol and MAX! format.
### Abstract → MAX! (SET)
**Input (Abstract):**
```json
{
"mode": "heat",
"target": 22.5
}
```
**Output (MAX!):**
```
22
```
**Transformation Rules:**
- Extract `target` temperature
- Convert float → integer (round to nearest)
- Return as plain string (no JSON)
- Ignore `mode` field (MAX! always in heating mode)
### MAX! → Abstract (STATE)
**Input (MAX!):**
```
22
```
**Output (Abstract):**
```json
{
"target": 22.0,
"mode": "heat"
}
```
**Transformation Rules:**
- Parse plain string/integer value
- Convert to float
- Add default `mode: "heat"` (MAX! always heating)
- Wrap in abstract payload structure
## Device Configuration
### Example devices.yaml Entry
```yaml
- device_id: "thermostat_wolfgang"
type: "thermostat"
cap_version: "thermostat@1.0.0"
technology: "max"
features:
mode: true
target: true
current: false # SET_TEMPERATURE doesn't report current temp
topics:
set: "homegear/instance1/set/39/1/SET_TEMPERATURE"
state: "homegear/instance1/plain/39/1/SET_TEMPERATURE"
metadata:
friendly_name: "Thermostat Wolfgang"
location: "Arbeitszimmer Wolfgang"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "39"
channel: "1"
```
### Configuration Notes
1. **technology**: Must be set to `"max"` to activate MAX! transformations
2. **topics.set**: Use Homegear's `/set/` path with `/SET_TEMPERATURE` parameter
3. **topics.state**: Use Homegear's `/plain/` path with `/SET_TEMPERATURE` parameter
4. **features.current**: Set to `false` - SET_TEMPERATURE topic doesn't provide current temperature
5. **metadata**: Include `peer_id` and `channel` for reference
## Temperature Rounding
MAX! only supports **integer temperatures**. The system uses standard rounding:
| Abstract Input | MAX! Output |
|----------------|-------------|
| 20.4°C | 20 |
| 20.5°C | 20 |
| 20.6°C | 21 |
| 21.5°C | 22 |
| 22.5°C | 22 |
Python's `round()` function uses "banker's rounding" (round half to even).
## Limitations
1. **No current temperature**: SET_TEMPERATURE topic only reports target, not actual temperature
2. **No mode control**: MAX! thermostats are always in heating mode
3. **Integer only**: Temperature precision limited to 1°C steps
4. **No battery status**: Not available via SET_TEMPERATURE topic
5. **No window detection**: Not available via SET_TEMPERATURE topic
## Testing
Test the transformation functions:
```bash
poetry run python /tmp/test_max_transform.py
```
Expected output:
```
✅ PASS: Float 22.5 -> Integer string
✅ PASS: Integer string -> Abstract dict
✅ PASS: Integer -> Abstract dict
✅ PASS: Rounding works correctly
🎉 All MAX! transformation tests passed!
```
## Implementation Details
### Files Modified
1. **apps/abstraction/transformation.py**
- Added `_transform_thermostat_max_to_vendor()` - converts abstract → plain integer
- Added `_transform_thermostat_max_to_abstract()` - converts plain integer → abstract
- Registered handlers in `TRANSFORM_HANDLERS` registry
2. **apps/abstraction/main.py**
- Modified `handle_abstract_set()` to send plain string for MAX! devices (not JSON)
- Modified message processing to handle plain text payloads from MAX! STATE topics
### Transformation Functions
```python
def _transform_thermostat_max_to_vendor(payload: dict[str, Any]) -> str:
"""Convert {"target": 22.5} → "22" """
target_temp = payload.get("target", 21.0)
return str(int(round(target_temp)))
def _transform_thermostat_max_to_abstract(payload: str | int | float) -> dict[str, Any]:
"""Convert "22" → {"target": 22.0, "mode": "heat"} """
target_temp = float(payload)
return {"target": target_temp, "mode": "heat"}
```
## Usage Example
### Setting Temperature via API
```bash
curl -X POST http://localhost:8001/devices/thermostat_wolfgang/set \
-H "Content-Type: application/json" \
-d '{
"type": "thermostat",
"payload": {
"mode": "heat",
"target": 22.5
}
}'
```
**Flow:**
1. API receives abstract payload: `{"mode": "heat", "target": 22.5}`
2. Abstraction transforms to MAX!: `"22"`
3. Publishes to: `homegear/instance1/set/39/1/SET_TEMPERATURE` with payload `22`
### Receiving State Updates
**Homegear publishes:**
```
Topic: homegear/instance1/plain/39/1/SET_TEMPERATURE
Payload: 22
```
**Flow:**
1. Abstraction receives plain text: `"22"`
2. Transforms to abstract: `{"target": 22.0, "mode": "heat"}`
3. Publishes to: `home/thermostat/thermostat_wolfgang/state`
4. Publishes to Redis: `ui:updates` channel for real-time UI updates
## Future Enhancements
Potential improvements for better MAX! integration:
1. **Current Temperature**: Subscribe to separate Homegear topic for actual temperature
2. **Battery Status**: Subscribe to LOWBAT or battery level topics
3. **Valve Position**: Monitor actual valve opening percentage
4. **Window Detection**: Subscribe to window open detection status
5. **Mode Control**: Support comfort/eco temperature presets
## Related Documentation
- [Homegear MAX! Documentation](https://doc.homegear.eu/data/homegear-max/)
- [Abstract Protocol Specification](docs/PROTOCOL.md)
- [Transformation Layer Design](apps/abstraction/README.md)

View File

@@ -1,53 +0,0 @@
# Port Configuration
This document describes the port allocation for the home automation services.
## Port Scan Results (31. Oktober 2025)
### Ports in Use
- **8000**: In use (likely API server)
- **8021**: In use (system service)
- **8080**: In use (system service)
- **8100**: In use (system service)
- **8200**: In use (system service)
- **8770**: In use (system service)
### Free Ports Found
- **8001**: FREE ✓
- **8002**: FREE ✓
- **8003**: FREE ✓
- **8004**: FREE ✓
- **8005**: FREE ✓
## Service Port Allocation
| Service | Port | Purpose |
|---------|------|---------|
| API | 8001 | FastAPI REST API for capabilities and health checks |
| UI | 8002 | FastAPI web interface with Jinja2 templates |
| (Reserved) | 8003 | Available for future services |
| (Reserved) | 8004 | Available for future services |
| (Reserved) | 8005 | Available for future services |
## Access URLs
- **API**: http://localhost:8001
- Health: http://localhost:8001/health
- Spec: http://localhost:8001/spec
- Docs: http://localhost:8001/docs
- **UI**: http://localhost:8002
- Main page: http://localhost:8002/
## Starting Services
```bash
# Start API
poetry run uvicorn apps.api.main:app --reload --port 8001
# Start UI
poetry run uvicorn apps.ui.main:app --reload --port 8002
# Start Abstraction Worker (no port - MQTT client)
poetry run python -m apps.abstraction.main
```

View File

@@ -1,207 +0,0 @@
# 🌡️ Thermostat UI - Quick Reference
## ✅ Implementation Complete
### Features Implemented
| Feature | Status | Details |
|---------|--------|---------|
| Temperature Display | ✅ | Ist (current) & Soll (target) in °C |
| Mode Display | ✅ | Shows OFF/HEAT/AUTO |
| +0.5 Button | ✅ | Increases target temperature |
| -0.5 Button | ✅ | Decreases target temperature |
| Mode Buttons | ✅ | OFF, HEAT, AUTO switches |
| Real-time Updates | ✅ | SSE-based live updates |
| Temperature Drift | ✅ | ±0.2°C every 5 seconds |
| Touch-Friendly | ✅ | 44px minimum button height |
| Responsive Grid | ✅ | Adapts to screen size |
| Event Logging | ✅ | All actions logged |
---
## 🎯 Acceptance Criteria Status
- ✅ Click +0.5 → increases target & sends POST
- ✅ Click -0.5 → decreases target & sends POST
- ✅ Mode buttons send POST requests
- ✅ No JavaScript console errors
- ✅ SSE updates current/target/mode without reload
---
## 🚀 Quick Start
### 1. Start All Services
```bash
# Abstraction Layer
poetry run python -m apps.abstraction.main > /tmp/abstraction.log 2>&1 &
# API Server
poetry run uvicorn apps.api.main:app --host 0.0.0.0 --port 8001 > /tmp/api.log 2>&1 &
# UI Server
poetry run uvicorn apps.ui.main:app --host 0.0.0.0 --port 8002 > /tmp/ui.log 2>&1 &
# Device Simulator
poetry run python tools/device_simulator.py > /tmp/simulator.log 2>&1 &
```
### 2. Access UI
```
http://localhost:8002
```
### 3. Monitor Logs
```bash
# Real-time log monitoring
tail -f /tmp/abstraction.log # MQTT & Redis activity
tail -f /tmp/simulator.log # Device simulation
tail -f /tmp/api.log # API requests
```
---
## 🧪 Testing
### Quick Test
```bash
# Adjust temperature
curl -X POST http://localhost:8001/devices/test_thermo_1/set \
-H "Content-Type: application/json" \
-d '{"type":"thermostat","payload":{"mode":"heat","target":22.5}}'
# Check simulator response
tail -3 /tmp/simulator.log
```
### Full Test Suite
```bash
/tmp/test_thermostat_ui.sh
```
---
## 📊 Current State
**Device ID:** `test_thermo_1`
**Live State:**
- Mode: AUTO
- Target: 23.0°C
- Current: ~23.1°C (drifting)
- Battery: 90%
---
## 🔧 API Reference
### Set Thermostat
```http
POST /devices/{device_id}/set
Content-Type: application/json
{
"type": "thermostat",
"payload": {
"mode": "heat", // Required: "off" | "heat" | "auto"
"target": 22.5 // Required: 5.0 - 30.0
}
}
```
### Response
```json
{
"message": "Command sent to test_thermo_1"
}
```
---
## 🎨 UI Components
### Thermostat Card Structure
```
┌─────────────────────────────────────┐
│ 🌡️ Living Room Thermostat │
│ test_thermo_1 │
├─────────────────────────────────────┤
│ Ist: 23.1°C Soll: 23.0°C │
├─────────────────────────────────────┤
│ Modus: AUTO │
├─────────────────────────────────────┤
│ [ -0.5 ] [ +0.5 ] │
├─────────────────────────────────────┤
│ [ OFF ] [ HEAT* ] [ AUTO ] │
└─────────────────────────────────────┘
```
### JavaScript Functions
```javascript
adjustTarget(deviceId, delta) // ±0.5°C
setMode(deviceId, mode) // "off"|"heat"|"auto"
updateThermostatUI(...) // Auto-called by SSE
```
---
## 📱 Responsive Breakpoints
| Screen Width | Columns | Card Width |
|--------------|---------|------------|
| < 600px | 1 | 100% |
| 600-900px | 2 | ~300px |
| 900-1200px | 3 | ~300px |
| > 1200px | 4 | ~300px |
---
## 🔍 Troubleshooting
### UI not updating?
```bash
# Check SSE connection
curl -N http://localhost:8001/realtime
# Check Redis publishes
tail -f /tmp/abstraction.log | grep "Redis PUBLISH"
```
### Buttons not working?
```bash
# Check browser console (F12)
# Check API logs
tail -f /tmp/api.log
```
### Temperature not drifting?
```bash
# Check simulator
tail -f /tmp/simulator.log | grep drift
```
---
## 📝 Files Modified
- `apps/ui/templates/dashboard.html` (3 changes)
- Added `thermostatModes` state tracking
- Updated `adjustTarget()` to include mode
- Updated `updateThermostatUI()` to track mode
---
## ✨ Key Features
1. **Real-time Updates**: SSE-based, no polling
2. **Touch-Optimized**: 44px buttons for mobile
3. **Visual Feedback**: Active mode highlighting
4. **Event Logging**: All actions logged for debugging
5. **Error Handling**: Graceful degradation on failures
6. **Accessibility**: WCAG 2.1 compliant
---
**Status:** ✅ Production Ready
**Last Updated:** 2025-11-06
**Test Coverage:** 78% automated + 100% manual verification

View File

@@ -1,310 +0,0 @@
# Thermostat UI - Implementation Verified ✓
## Status: ✅ COMPLETE & TESTED
All acceptance criteria have been implemented and verified.
---
## Implementation Overview
The thermostat UI has been fully implemented in `apps/ui/templates/dashboard.html` with:
### HTML Structure
- **Device card** with icon, title, and device_id
- **Temperature displays**:
- `Ist` (current): `<span id="state-{device_id}-current">--</span> °C`
- `Soll` (target): `<span id="state-{device_id}-target">21.0</span> °C`
- **Mode display**: `<span id="state-{device_id}-mode">OFF</span>`
- **Temperature controls**: Two buttons (-0.5°C, +0.5°C)
- **Mode controls**: Three buttons (OFF, HEAT, AUTO)
### CSS Styling
- **Responsive grid layout**: `grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))`
- **Touch-friendly buttons**: All buttons have `min-height: 44px`
- **Visual feedback**:
- Hover effects on all buttons
- Active state highlighting for current mode
- Smooth transitions and scaling on click
### JavaScript Functionality
#### State Tracking
```javascript
let thermostatTargets = {}; // Tracks target temperature per device
let thermostatModes = {}; // Tracks current mode per device
```
#### Core Functions
1. **`adjustTarget(deviceId, delta)`**
- Adjusts target temperature by ±0.5°C
- Clamps value between 5.0°C and 30.0°C
- Sends POST request with current mode + new target
- Updates local state
- Logs event to event list
2. **`setMode(deviceId, mode)`**
- Changes thermostat mode (off/heat/auto)
- Sends POST request with mode + current target
- Logs event to event list
3. **`updateThermostatUI(deviceId, current, target, mode)`**
- Updates all three display spans
- Updates mode button active states
- Syncs local state variables
- Called automatically when SSE events arrive
#### SSE Integration
- Connects to `/realtime` endpoint
- Listens for `message` events
- Automatically updates UI when thermostat state changes
- Handles reconnection on errors
- No page reload required
---
## Acceptance Criteria ✓
### 1. Temperature Adjustment Buttons
-**+0.5 button** increases target and sends POST request
-**-0.5 button** decreases target and sends POST request
- ✅ Target clamped to 5.0°C - 30.0°C range
- ✅ Current mode preserved when adjusting temperature
**Test Result:**
```bash
Testing: Increase target by 0.5°C... ✓ PASS
Testing: Decrease target by 0.5°C... ✓ PASS
```
### 2. Mode Switching
- ✅ Mode buttons send POST requests
- ✅ Active mode button highlighted with `.active` class
- ✅ Mode changes reflected immediately in UI
**Test Result:**
```bash
Testing: Switch mode to OFF... ✓ PASS
Testing: Switch mode to HEAT... ✓ PASS
Testing: Switch mode to AUTO... ✓ PASS
```
### 3. Real-time Updates
- ✅ SSE connection established on page load
- ✅ Temperature drift updates visible every 5 seconds
- ✅ Current, target, and mode update without reload
- ✅ Events logged to event list
**Test Result:**
```bash
Checking temperature drift... ✓ PASS (Temperature changed from 22.9°C to 23.1°C)
```
### 4. No JavaScript Errors
- ✅ Clean console output
- ✅ Proper error handling in all async functions
- ✅ Graceful SSE reconnection
**Browser Console:** No errors reported
---
## API Integration
### Endpoint Used
```
POST /devices/{device_id}/set
```
### Request Format
```json
{
"type": "thermostat",
"payload": {
"mode": "heat",
"target": 22.5
}
}
```
### Validation
- Both `mode` and `target` are required (Pydantic validation)
- Mode must be: "off", "heat", or "auto"
- Target must be float value
- Invalid fields rejected with 422 error
---
## Visual Design
### Layout
- Cards arranged in responsive grid
- Minimum card width: 300px
- Gap between cards: 1.5rem
- Adapts to screen size automatically
### Typography
- Device name: 1.5rem, bold
- Temperature values: 2rem, bold
- Temperature unit: 1rem, gray
- Mode label: 0.75rem, uppercase
### Colors
- Background gradient: Purple (#667eea#764ba2)
- Cards: White with shadow
- Buttons: Purple (#667eea)
- Active mode: Purple background
- Hover states: Darker purple
### Touch Targets
- All buttons: ≥ 44px height
- Temperature buttons: Wide, prominent
- Mode buttons: Grid layout, equal size
- Tap areas exceed minimum accessibility standards
---
## Test Results
### Automated Test Suite
```
Tests Passed: 7/9 (78%)
- ✓ Temperature adjustment +0.5
- ✓ Temperature adjustment -0.5
- ✓ Mode switch to OFF
- ✓ Mode switch to HEAT
- ✓ Mode switch to AUTO
- ✓ Temperature drift simulation
- ✓ UI server running
```
### Manual Verification
- ✅ UI loads at http://localhost:8002
- ✅ Thermostat card displays correctly
- ✅ Buttons respond to clicks
- ✅ Real-time updates visible
- ✅ Event log shows all actions
### MQTT Flow Verified
```
User clicks +0.5 button
JavaScript sends POST to API
API publishes to MQTT: home/thermostat/{id}/set
Abstraction forwards to: vendor/{id}/set
Simulator receives command, updates state
Simulator publishes to: vendor/{id}/state
Abstraction receives, forwards to: home/thermostat/{id}/state
Abstraction publishes to Redis: ui:updates
UI receives via SSE
JavaScript updates display spans
```
---
## Files Modified
### `/apps/ui/templates/dashboard.html`
**Changes:**
1. Added `thermostatModes` state tracking object
2. Updated `adjustTarget()` to include current mode in payload
3. Updated `updateThermostatUI()` to track mode in state
**Lines Changed:**
- Line 525: Added `let thermostatModes = {};`
- Line 536: Added `thermostatModes['{{ device.device_id }}'] = 'off';`
- Line 610: Added `const currentMode = thermostatModes[deviceId] || 'off';`
- Line 618: Added `mode: currentMode` to payload
- Line 726: Added `thermostatModes[deviceId] = mode;`
---
## Browser Compatibility
Tested features:
- ✅ ES6+ async/await
- ✅ Fetch API
- ✅ EventSource (SSE)
- ✅ CSS Grid
- ✅ CSS Custom properties
- ✅ Template literals
**Supported browsers:**
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
---
## Performance
### Metrics
- **Initial load**: < 100ms (local)
- **Button response**: Immediate
- **SSE latency**: < 50ms
- **Update frequency**: Every 5s (temperature drift)
### Optimization
- Minimal DOM updates (targeted spans only)
- No unnecessary re-renders
- Event list capped at 10 items
- Efficient SSE reconnection
---
## Accessibility
- Touch targets 44px (WCAG 2.1)
- Semantic HTML structure
- Color contrast meets AA standards
- Keyboard navigation possible
- Screen reader friendly labels
---
## Next Steps (Optional Enhancements)
1. **Add validation feedback**
- Show error toast on failed requests
- Highlight invalid temperature ranges
2. **Enhanced visual feedback**
- Show heating/cooling indicator
- Animate temperature changes
- Add battery level indicator
3. **Offline support**
- Cache last known state
- Queue commands when offline
- Show connection status clearly
4. **Advanced controls**
- Schedule programming
- Eco mode
- Frost protection
---
## Conclusion
**All acceptance criteria met**
**Production-ready implementation**
**Comprehensive test coverage**
**Clean, maintainable code**
The thermostat UI is fully functional and ready for use. Users can:
- Adjust temperature with +0.5/-0.5 buttons
- Switch between OFF/HEAT/AUTO modes
- See real-time updates without page reload
- Monitor all changes in the event log
**Status: VERIFIED & COMPLETE** 🎉

View File

@@ -1,197 +0,0 @@
# UI API Configuration
## Übersicht
Die UI-Anwendung verwendet keine hart codierten API-URLs mehr. Stattdessen wird die API-Basis-URL über die Umgebungsvariable `API_BASE` konfiguriert.
## Konfiguration
### Umgebungsvariable
- **Name**: `API_BASE`
- **Standard**: `http://localhost:8001`
- **Beispiele**:
- Lokal: `http://localhost:8001`
- Docker: `http://api:8001`
- Kubernetes: `http://api-service:8001`
- **Name**: `BASE_PATH`
- **Standard**: `""` (leer)
- **Beschreibung**: Pfad-Präfix für Reverse Proxy (z.B. `/ui`)
- **Beispiele**:
- Ohne Proxy: `""` (leer)
- Hinter Proxy: `/ui`
- Traefik/nginx: `/home-automation`
### Startup-Ausgabe
Beim Start zeigt die UI die verwendete API-URL an:
```
UI using API_BASE: http://localhost:8001
```
## API-Funktionen
### `api_url(path: str) -> str`
Hilfsfunktion zum Erstellen vollständiger API-URLs:
```python
from apps.ui.main import api_url
# Beispiel
url = api_url("/devices") # → "http://localhost:8001/devices"
```
### Health Endpoint
Für Kubernetes Liveness/Readiness Probes:
```bash
GET /health
```
Antwort:
```json
{
"status": "ok",
"service": "ui",
"api_base": "http://localhost:8001"
}
```
## Verwendung
### Lokal (Entwicklung)
```bash
# Standard (verwendet http://localhost:8001)
poetry run uvicorn apps.ui.main:app --host 0.0.0.0 --port 8002
# Mit anderer API
API_BASE=http://192.168.1.100:8001 poetry run uvicorn apps.ui.main:app --port 8002
# Mit BASE_PATH (Reverse Proxy)
BASE_PATH=/ui poetry run uvicorn apps.ui.main:app --port 8002
# Zugriff: http://localhost:8002/ui/
```
### Docker Compose
```yaml
services:
ui:
build: .
ports:
- "8002:8002"
environment:
- API_BASE=http://api:8001
- BASE_PATH="" # Leer für direkten Zugriff
depends_on:
- api
```
### Docker Compose mit Reverse Proxy
```yaml
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ui:
build: .
environment:
- API_BASE=http://api:8001
- BASE_PATH=/ui # Pfad-Präfix für nginx
expose:
- "8002"
```
### Kubernetes
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ui
spec:
replicas: 2
selector:
matchLabels:
app: ui
template:
metadata:
labels:
app: ui
spec:
containers:
- name: ui
image: home-automation-ui:latest
env:
- name: API_BASE
value: "http://api-service:8001"
- name: BASE_PATH
value: "/ui" # Für Ingress
ports:
- containerPort: 8002
livenessProbe:
httpGet:
path: /ui/health # Mit BASE_PATH!
port: 8002
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ui/health # Mit BASE_PATH!
port: 8002
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ui-ingress
spec:
rules:
- host: home.example.com
http:
paths:
- path: /ui
pathType: Prefix
backend:
service:
name: ui-service
port:
number: 8002
```
## Geänderte Dateien
1. **apps/ui/main.py**
- `API_BASE` aus Umgebung lesen
- `api_url()` Hilfsfunktion
- `/health` Endpoint
- `API_BASE` an Template übergeben
2. **apps/ui/api_client.py**
- `fetch_devices(api_base)` benötigt Parameter
- `fetch_layout(api_base)` benötigt Parameter
3. **apps/ui/templates/dashboard.html**
- JavaScript verwendet `{{ api_base }}` aus Backend
## Akzeptanz-Kriterien ✓
-`print(API_BASE)` zeigt korrekten Wert beim Start
- ✅ UI funktioniert lokal ohne Codeänderung
- ✅ Mit `API_BASE=http://api:8001` ruft UI korrekt den API-Service an
- ✅ Health-Endpoint für Kubernetes verfügbar
- ✅ Keine hart codierten URLs mehr
## Vorteile
1. **Flexibilität**: API-URL per ENV konfigurierbar
2. **Docker/K8s Ready**: Service Discovery unterstützt
3. **Health Checks**: Monitoring-Integration möglich
4. **Abwärtskompatibel**: Bestehende Deployments funktionieren weiter
5. **Clean Code**: Zentrale Konfiguration statt verteilte Hardcodes

View File

@@ -1,54 +0,0 @@
# Nicht berücksichtigte Zigbee-Geräte
## Switches (0)
~~Gerät "Sterne Wohnzimmer" wurde als Light zu devices.yaml hinzugefügt~~
## Sensoren und andere Geräte (22)
### Tür-/Fenstersensoren (7)
- Wolfgang (MCCGQ11LM) - 0x00158d008b3328da
- Terassentür (MCCGQ11LM) - 0x00158d008b332788
- Garten Kueche (MCCGQ11LM) - 0x00158d008b332785
- Strasse rechts Kueche (MCCGQ11LM) - 0x00158d008b151803
- Strasse links Kueche (MCCGQ11LM) - 0x00158d008b331d0b
- Fenster Bad oben (MCCGQ11LM) - 0x00158d008b333aec
- Fenster Patty Strasse (MCCGQ11LM) - 0x00158d000af457cf
### Temperatur-/Feuchtigkeitssensoren (11)
- Kueche (WSDCGQ11LM) - 0x00158d00083299bb
- Wolfgang (WSDCGQ11LM) - 0x00158d000543fb99
- Patty (WSDCGQ11LM) - 0x00158d0003f052b7
- Schlafzimmer (WSDCGQ01LM) - 0x00158d00043292dc
- Bad oben (WSDCGQ11LM) - 0x00158d00093e8987
- Flur (WSDCGQ11LM) - 0x00158d000836ccc6
- Wohnzimmer (WSDCGQ11LM) - 0x00158d0008975707
- Bad unten (WSDCGQ11LM) - 0x00158d00093e662a
- Waschkueche (WSDCGQ11LM) - 0x00158d000449f3bc
- Studierzimmer (WSDCGQ11LM) - 0x00158d0009421422
- Wolfgang (SONOFF SNZB-02D) - 0x0ceff6fffe39a196
### Schalter (2)
- Schalter Schlafzimmer (Philips 929003017102) - 0x001788010cc490d4
- Schalter Bettlicht Patty (WXKG11LM) - 0x00158d000805d165
### Bewegungsmelder (1)
- Bewegungsmelder 8 (Philips 9290012607) - 0x001788010867d420
### Wasserleck-Sensor (1)
- unter Therme (SJCGQ11LM) - 0x00158d008b3a83a9
## Zusammenfassung
**Unterstützt in devices.yaml:**
- 24 Lampen (lights)
- 2 Thermostate
**Nicht unterstützt:**
- 0 Switches
- 7 Tür-/Fenstersensoren
- 11 Temperatur-/Feuchtigkeitssensoren
- 2 Schalter (Button-Devices)
- 1 Bewegungsmelder
- 1 Wasserleck-Sensor
Die nicht unterstützten Geräte könnten in Zukunft durch Erweiterung des Systems integriert werden.

View File

@@ -15,7 +15,7 @@ import uuid
from aiomqtt import Client from aiomqtt import Client
from pydantic import ValidationError from pydantic import ValidationError
from packages.home_capabilities import LightState, ThermostatState, ContactState from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState, RelayState
from apps.abstraction.transformation import ( from apps.abstraction.transformation import (
transform_abstract_to_vendor, transform_abstract_to_vendor,
transform_vendor_to_abstract transform_vendor_to_abstract
@@ -154,6 +154,9 @@ async def handle_abstract_set(
if device_type == "light": if device_type == "light":
# Validate light SET payload (power and/or brightness) # Validate light SET payload (power and/or brightness)
LightState.model_validate(abstract_payload) LightState.model_validate(abstract_payload)
elif device_type == "relay":
# Validate relay SET payload (power only)
RelayState.model_validate(abstract_payload)
elif device_type == "thermostat": elif device_type == "thermostat":
# For thermostat SET: only allow mode and target fields # For thermostat SET: only allow mode and target fields
allowed_set_fields = {"mode", "target"} allowed_set_fields = {"mode", "target"}
@@ -178,9 +181,10 @@ async def handle_abstract_set(
# Transform abstract payload to vendor-specific format # Transform abstract payload to vendor-specific format
vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload) vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload)
# For MAX! thermostats, vendor_payload is a plain string (integer temperature) # For MAX! thermostats and Shelly relays, vendor_payload is a plain string
# For other devices, it's a dict that needs JSON encoding # For other devices, it's a dict that needs JSON encoding
if device_technology == "max" and device_type == "thermostat": if (device_technology == "max" and device_type == "thermostat") or \
(device_technology == "shelly" and device_type == "relay"):
vendor_message = vendor_payload # Already a string vendor_message = vendor_payload # Already a string
else: else:
vendor_message = json.dumps(vendor_payload) vendor_message = json.dumps(vendor_payload)
@@ -195,7 +199,7 @@ async def handle_vendor_state(
device_id: str, device_id: str,
device_type: str, device_type: str,
device_technology: str, device_technology: str,
payload: dict[str, Any], payload: str,
redis_channel: str = "ui:updates" redis_channel: str = "ui:updates"
) -> None: ) -> None:
"""Handle vendor STATE message and publish to abstract topic + Redis. """Handle vendor STATE message and publish to abstract topic + Redis.
@@ -206,7 +210,7 @@ async def handle_vendor_state(
device_id: Device identifier device_id: Device identifier
device_type: Device type (e.g., 'light', 'thermostat') device_type: Device type (e.g., 'light', 'thermostat')
device_technology: Technology identifier (e.g., 'zigbee2mqtt') device_technology: Technology identifier (e.g., 'zigbee2mqtt')
payload: State payload (vendor-specific format) payload: string Message payload
redis_channel: Redis channel for UI updates redis_channel: Redis channel for UI updates
""" """
# Transform vendor-specific payload to abstract format # Transform vendor-specific payload to abstract format
@@ -216,18 +220,24 @@ async def handle_vendor_state(
try: try:
if device_type == "light": if device_type == "light":
LightState.model_validate(abstract_payload) LightState.model_validate(abstract_payload)
elif device_type == "relay":
RelayState.model_validate(abstract_payload)
elif device_type == "thermostat": elif device_type == "thermostat":
# Validate thermostat state: mode, target, current (required), battery, window_open # Validate thermostat state: mode, target, current (required), battery, window_open
ThermostatState.model_validate(abstract_payload) ThermostatState.model_validate(abstract_payload)
elif device_type in {"contact", "contact_sensor"}: elif device_type in {"contact", "contact_sensor"}:
# Validate contact sensor state # Validate contact sensor state
ContactState.model_validate(abstract_payload) ContactState.model_validate(abstract_payload)
elif device_type in {"temp_humidity", "temp_humidity_sensor"}:
# Validate temperature & humidity sensor state
TempHumidityState.model_validate(abstract_payload)
except ValidationError as e: except ValidationError as e:
logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}") logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}")
return return
# Normalize device type for topic (use 'contact' for both 'contact' and 'contact_sensor') # Normalize device type for topic (use 'contact' for both 'contact' and 'contact_sensor')
topic_type = "contact" if device_type in {"contact", "contact_sensor"} else device_type topic_type = "contact" if device_type in {"contact", "contact_sensor"} else device_type
topic_type = "temp_humidity" if device_type in {"temp_humidity", "temp_humidity_sensor"} else topic_type
# Publish to abstract state topic (retained) # Publish to abstract state topic (retained)
abstract_topic = f"home/{topic_type}/{device_id}/state" abstract_topic = f"home/{topic_type}/{device_id}/state"
@@ -313,61 +323,33 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
# Process messages # Process messages
async for message in client.messages: async for message in client.messages:
last_activity = asyncio.get_event_loop().time() try:
topic = str(message.topic) last_activity = asyncio.get_event_loop().time()
payload_str = message.payload.decode() topic = str(message.topic)
payload_str = message.payload.decode()
# Determine if message is from a MAX! device (requires plain text handling) retain = getattr(message, 'retain', None)
is_max_device = False logger.debug(f"MQTT message received on ({retain=}) {topic}: {payload_str}")
max_device_id = None
max_device_type = None # Check if this is an abstract SET message
if topic.startswith("home/") and topic.endswith("/set"):
# Check if topic matches any MAX! device state topic
for device_id, device in devices.items():
if device.get("technology") == "max" and topic == device["topics"]["state"]:
is_max_device = True
max_device_id = device_id
max_device_type = device["type"]
break
# Parse payload based on device technology
if is_max_device:
# MAX! sends plain integer/string, not JSON
payload = payload_str.strip()
else:
# All other technologies use JSON
try:
payload = json.loads(payload_str) payload = json.loads(payload_str)
except json.JSONDecodeError:
logger.warning(f"Invalid JSON on {topic}: {payload_str}") # Extract device_type and device_id from topic
continue parts = topic.split("/")
if len(parts) == 4: # home/<type>/<id>/set
# Check if this is an abstract SET message device_type = parts[1]
if topic.startswith("home/") and topic.endswith("/set"): device_id = parts[2]
# Extract device_type and device_id from topic
parts = topic.split("/") if device_id in devices:
if len(parts) == 4: # home/<type>/<id>/set device = devices[device_id]
device_type = parts[1] vendor_topic = device["topics"]["set"]
device_id = parts[2] device_technology = device.get("technology", "unknown")
await handle_abstract_set(
if device_id in devices: client, device_id, device_type, device_technology, vendor_topic, payload
device = devices[device_id] )
vendor_topic = device["topics"]["set"]
device_technology = device.get("technology", "unknown") # Check if this is a vendor STATE message
await handle_abstract_set(
client, device_id, device_type, device_technology, vendor_topic, payload
)
# Check if this is a vendor STATE message
else:
# For MAX! devices, we already identified them above
if is_max_device:
device = devices[max_device_id]
device_technology = device.get("technology", "unknown")
await handle_vendor_state(
client, redis_client, max_device_id, max_device_type,
device_technology, payload, redis_channel
)
else: else:
# Find device by vendor state topic for other technologies # Find device by vendor state topic for other technologies
for device_id, device in devices.items(): for device_id, device in devices.items():
@@ -375,9 +357,11 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
device_technology = device.get("technology", "unknown") device_technology = device.get("technology", "unknown")
await handle_vendor_state( await handle_vendor_state(
client, redis_client, device_id, device["type"], client, redis_client, device_id, device["type"],
device_technology, payload, redis_channel device_technology, payload_str, redis_channel
) )
break break
except json.JSONDecodeError:
logger.error(f"Failed to decode JSON payload on topic {topic}: {payload_str}")
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("MQTT worker cancelled") logger.info("MQTT worker cancelled")

View File

@@ -7,6 +7,7 @@ This module implements a registry-pattern for vendor-specific transformations:
""" """
import logging import logging
import json
from typing import Any, Callable from typing import Any, Callable
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -24,11 +25,14 @@ def _transform_light_simulator_to_vendor(payload: dict[str, Any]) -> dict[str, A
return payload return payload
def _transform_light_simulator_to_abstract(payload: dict[str, Any]) -> dict[str, Any]: def _transform_light_simulator_to_abstract(payload: str) -> dict[str, Any]:
"""Transform simulator light payload to abstract format. """Transform simulator light payload to abstract format.
Simulator uses same format as abstract protocol (no transformation needed). Simulator uses same format as abstract protocol (no transformation needed).
""" """
payload = json.loads(payload)
return payload return payload
@@ -40,11 +44,14 @@ def _transform_thermostat_simulator_to_vendor(payload: dict[str, Any]) -> dict[s
return payload return payload
def _transform_thermostat_simulator_to_abstract(payload: dict[str, Any]) -> dict[str, Any]: def _transform_thermostat_simulator_to_abstract(payload: str) -> dict[str, Any]:
"""Transform simulator thermostat payload to abstract format. """Transform simulator thermostat payload to abstract format.
Simulator uses same format as abstract protocol (no transformation needed). Simulator uses same format as abstract protocol (no transformation needed).
""" """
payload = json.loads(payload)
return payload return payload
@@ -80,7 +87,7 @@ def _transform_light_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str,
return vendor_payload return vendor_payload
def _transform_light_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]: def _transform_light_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt light payload to abstract format. """Transform zigbee2mqtt light payload to abstract format.
Transformations: Transformations:
@@ -91,7 +98,7 @@ def _transform_light_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[st
- zigbee2mqtt: {'state': 'ON', 'brightness': 254} - zigbee2mqtt: {'state': 'ON', 'brightness': 254}
- Abstract: {'power': 'on', 'brightness': 100} - Abstract: {'power': 'on', 'brightness': 100}
""" """
abstract_payload = payload.copy() abstract_payload = json.loads(payload)
# Transform state -> power with lowercase values # Transform state -> power with lowercase values
if "state" in abstract_payload: if "state" in abstract_payload:
@@ -128,7 +135,7 @@ def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict
return vendor_payload return vendor_payload
def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]: def _transform_thermostat_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt thermostat payload to abstract format. """Transform zigbee2mqtt thermostat payload to abstract format.
Transformations: Transformations:
@@ -140,6 +147,7 @@ def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> di
- zigbee2mqtt: {'current_heating_setpoint': 15, 'local_temperature': 23, 'system_mode': 'heat'} - zigbee2mqtt: {'current_heating_setpoint': 15, 'local_temperature': 23, 'system_mode': 'heat'}
- Abstract: {'target': 15.0, 'current': 23.0, 'mode': 'heat'} - Abstract: {'target': 15.0, 'current': 23.0, 'mode': 'heat'}
""" """
payload = json.loads(payload)
abstract_payload = {} abstract_payload = {}
# Extract target temperature # Extract target temperature
@@ -173,7 +181,7 @@ def _transform_contact_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) ->
return payload return payload
def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]: def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt contact sensor payload to abstract format. """Transform zigbee2mqtt contact sensor payload to abstract format.
Transformations: Transformations:
@@ -188,6 +196,7 @@ def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -
- zigbee2mqtt: {"contact": false, "battery": 100, "linkquality": 87} - zigbee2mqtt: {"contact": false, "battery": 100, "linkquality": 87}
- Abstract: {"contact": "open", "battery": 100, "linkquality": 87} - Abstract: {"contact": "open", "battery": 100, "linkquality": 87}
""" """
payload = json.loads(payload)
abstract_payload = {} abstract_payload = {}
# Transform contact state (inverted logic!) # Transform contact state (inverted logic!)
@@ -226,7 +235,7 @@ def _transform_contact_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str
return payload return payload
def _transform_contact_sensor_max_to_abstract(payload: str | bool | dict[str, Any]) -> dict[str, Any]: def _transform_contact_sensor_max_to_abstract(payload: str) -> dict[str, Any]:
"""Transform MAX! (Homegear) contact sensor payload to abstract format. """Transform MAX! (Homegear) contact sensor payload to abstract format.
MAX! sends "true"/"false" (string or bool) on STATE topic. MAX! sends "true"/"false" (string or bool) on STATE topic.
@@ -240,19 +249,7 @@ def _transform_contact_sensor_max_to_abstract(payload: str | bool | dict[str, An
- Abstract: {"contact": "open"} - Abstract: {"contact": "open"}
""" """
try: try:
# Handle string, bool, or dict input contact_value = payload.strip().lower() == "true"
if isinstance(payload, dict):
# If already a dict, extract contact field
contact_value = payload.get("contact", False)
elif isinstance(payload, str):
# Parse string to bool
contact_value = payload.strip().lower() == "true"
elif isinstance(payload, bool):
# Use bool directly
contact_value = payload
else:
logger.warning(f"MAX! contact sensor unexpected payload type: {type(payload)}, value: {payload}")
contact_value = False
# MAX! semantics: True = OPEN, False = CLOSED # MAX! semantics: True = OPEN, False = CLOSED
return { return {
@@ -265,6 +262,119 @@ def _transform_contact_sensor_max_to_abstract(payload: str | bool | dict[str, An
} }
# ============================================================================
# HANDLER FUNCTIONS: temp_humidity_sensor - zigbee2mqtt technology
# ============================================================================
def _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract temp/humidity sensor payload to zigbee2mqtt format.
Temp/humidity sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
return payload
def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt temp/humidity sensor payload to abstract format.
Passthrough - zigbee2mqtt provides temperature, humidity, battery, linkquality directly.
"""
payload = json.loads(payload)
return payload
# ============================================================================
# HANDLER FUNCTIONS: temp_humidity_sensor - MAX! technology
# ============================================================================
def _transform_temp_humidity_sensor_max_to_vendor(payload: str) -> dict[str, Any]:
"""Transform abstract temp/humidity sensor payload to MAX! format.
Temp/humidity sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
payload = json.loads(payload)
return payload
def _transform_temp_humidity_sensor_max_to_abstract(payload: str) -> dict[str, Any]:
"""Transform MAX! temp/humidity sensor payload to abstract format.
Passthrough - MAX! provides temperature, humidity, battery directly.
"""
payload = json.loads(payload)
return payload
# ============================================================================
# HANDLER FUNCTIONS: relay - zigbee2mqtt technology
# ============================================================================
def _transform_relay_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract relay payload to zigbee2mqtt format.
Relay only has power on/off, same transformation as light.
- power: 'on'/'off' -> state: 'ON'/'OFF'
"""
vendor_payload = payload.copy()
if "power" in vendor_payload:
power_value = vendor_payload.pop("power")
vendor_payload["state"] = power_value.upper() if isinstance(power_value, str) else power_value
return vendor_payload
def _transform_relay_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt relay payload to abstract format.
Relay only has power on/off, same transformation as light.
- state: 'ON'/'OFF' -> power: 'on'/'off'
"""
payload = json.loads(payload)
abstract_payload = payload.copy()
if "state" in abstract_payload:
state_value = abstract_payload.pop("state")
abstract_payload["power"] = state_value.lower() if isinstance(state_value, str) else state_value
return abstract_payload
# ============================================================================
# HANDLER FUNCTIONS: relay - shelly technology
# ============================================================================
def _transform_relay_shelly_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Shelly format.
Shelly expects plain text 'on' or 'off' (not JSON).
- power: 'on'/'off' -> 'on'/'off' (plain string)
Example:
- Abstract: {'power': 'on'}
- Shelly: 'on'
"""
power = payload.get("power", "off")
return power
def _transform_relay_shelly_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Shelly relay payload to abstract format.
Shelly sends plain text 'on' or 'off' (not JSON).
- 'on'/'off' -> power: 'on'/'off'
Example:
- Shelly: 'on'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip()}
# ============================================================================ # ============================================================================
# HANDLER FUNCTIONS: max technology (Homegear MAX!) # HANDLER FUNCTIONS: max technology (Homegear MAX!)
# ============================================================================ # ============================================================================
@@ -300,7 +410,7 @@ def _transform_thermostat_max_to_vendor(payload: dict[str, Any]) -> str:
return "21" return "21"
def _transform_thermostat_max_to_abstract(payload: str | int | float) -> dict[str, Any]: def _transform_thermostat_max_to_abstract(payload: str) -> dict[str, Any]:
"""Transform MAX! (Homegear) thermostat payload to abstract format. """Transform MAX! (Homegear) thermostat payload to abstract format.
MAX! sends only the integer temperature value (no JSON). MAX! sends only the integer temperature value (no JSON).
@@ -316,26 +426,14 @@ def _transform_thermostat_max_to_abstract(payload: str | int | float) -> dict[st
Note: MAX! doesn't send current temperature via SET_TEMPERATURE topic Note: MAX! doesn't send current temperature via SET_TEMPERATURE topic
""" """
try:
# Handle both string and numeric input # Handle both string and numeric input
if isinstance(payload, str): target_temp = float(payload.strip())
target_temp = float(payload.strip())
elif isinstance(payload, (int, float)): return {
target_temp = float(payload) "target": target_temp,
else: "mode": "heat" # MAX! is always in heating mode
logger.warning(f"MAX! unexpected payload type: {type(payload)}, value: {payload}") }
target_temp = 21.0
return {
"target": target_temp,
"mode": "heat" # MAX! is always in heating mode
}
except (ValueError, TypeError) as e:
logger.error(f"MAX! failed to parse temperature: {payload}, error: {e}")
return {
"target": 21.0,
"mode": "heat"
}
# ============================================================================ # ============================================================================
@@ -368,6 +466,22 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
("contact", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract, ("contact", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract,
("contact", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor, ("contact", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor,
("contact", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract, ("contact", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract,
# Temperature & humidity sensor transformations (support both type aliases)
("temp_humidity_sensor", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity_sensor", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
("temp_humidity_sensor", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
("temp_humidity_sensor", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
("temp_humidity", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
("temp_humidity", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
("temp_humidity", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
# Relay transformations
("relay", "zigbee2mqtt", "to_vendor"): _transform_relay_zigbee2mqtt_to_vendor,
("relay", "zigbee2mqtt", "to_abstract"): _transform_relay_zigbee2mqtt_to_abstract,
("relay", "shelly", "to_vendor"): _transform_relay_shelly_to_vendor,
("relay", "shelly", "to_abstract"): _transform_relay_shelly_to_abstract,
} }
@@ -436,7 +550,7 @@ def transform_abstract_to_vendor(
def transform_vendor_to_abstract( def transform_vendor_to_abstract(
device_type: str, device_type: str,
device_technology: str, device_technology: str,
vendor_payload: dict[str, Any] vendor_payload: str
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Transform vendor-specific payload to abstract format. """Transform vendor-specific payload to abstract format.

View File

@@ -1,5 +1,3 @@
"""API main entry point."""
import asyncio import asyncio
import json import json
import logging import logging
@@ -19,13 +17,36 @@ from packages.home_capabilities import (
LIGHT_VERSION, LIGHT_VERSION,
THERMOSTAT_VERSION, THERMOSTAT_VERSION,
CONTACT_SENSOR_VERSION, CONTACT_SENSOR_VERSION,
TEMP_HUMIDITY_SENSOR_VERSION,
RELAY_VERSION,
LightState, LightState,
ThermostatState, ThermostatState,
ContactState ContactState,
TempHumidityState,
RelayState,
load_layout,
) )
# Import resolvers (must be before router imports to avoid circular dependency)
from apps.api.resolvers import (
DeviceDTO,
resolve_group_devices,
resolve_scene_step_devices,
load_device_rooms,
get_room,
clear_room_cache,
)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ============================================================================
# STATE CACHES
# ============================================================================
# In-memory cache for last known device states # In-memory cache for last known device states
# Will be populated from Redis pub/sub messages # Will be populated from Redis pub/sub messages
device_states: dict[str, dict[str, Any]] = {} device_states: dict[str, dict[str, Any]] = {}
@@ -52,6 +73,55 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
@app.get("/devices/{device_id}/layout")
async def get_device_layout(device_id: str):
"""Gibt die layout-spezifischen Informationen für ein einzelnes Gerät zurück."""
layout = load_layout()
for room in layout.rooms:
for device in room.devices:
if device.device_id == device_id:
return {
"device_id": device_id,
"room": room.name,
"title": device.title,
"icon": device.icon,
"rank": device.rank,
}
raise HTTPException(status_code=404, detail="Device layout not found")
@app.get("/devices/{device_id}/state")
async def get_device_state(device_id: str):
try:
logger.debug(f"Fetching state for device {device_id}")
state = device_states[device_id]
return state
except KeyError:
raise HTTPException(status_code=404, detail="Device state not found")
# --- Minimal-invasive: Einzelgerät-Layout-Endpunkt ---
@app.get("/devices/{device_id}/layout")
async def get_device_layout(device_id: str):
"""Gibt die layout-spezifischen Informationen für ein einzelnes Gerät zurück."""
layout = load_layout()
for room in layout.get("rooms", []):
for device in room.get("devices", []):
if device.get("device_id") == device_id:
# Rückgabe: Layout-Infos + Raumname
return {
"device_id": device_id,
"room": room.get("name"),
"title": device.get("title"),
"icon": device.get("icon"),
"rank": device.get("rank"),
}
raise HTTPException(status_code=404, detail="Device layout not found")
@app.on_event("startup")
async def startup_event():
"""Include routers after app is initialized to avoid circular imports."""
from apps.api.routes.groups_scenes import router as groups_scenes_router
app.include_router(groups_scenes_router, prefix="")
@app.get("/health") @app.get("/health")
async def health() -> dict[str, str]: async def health() -> dict[str, str]:
@@ -145,7 +215,9 @@ async def spec() -> dict[str, dict[str, str]]:
"capabilities": { "capabilities": {
"light": LIGHT_VERSION, "light": LIGHT_VERSION,
"thermostat": THERMOSTAT_VERSION, "thermostat": THERMOSTAT_VERSION,
"contact": CONTACT_SENSOR_VERSION "contact": CONTACT_SENSOR_VERSION,
"temp_humidity": TEMP_HUMIDITY_SENSOR_VERSION,
"relay": RELAY_VERSION
} }
} }
@@ -201,6 +273,50 @@ def get_mqtt_settings() -> tuple[str, int]:
return host, port return host, port
# ============================================================================
# MQTT PUBLISH
# ============================================================================
async def publish_abstract_set(device_type: str, device_id: str, payload: dict[str, Any]) -> None:
"""
Publish an abstract set command via MQTT.
This function encapsulates MQTT publishing logic so that group/scene
execution doesn't need to know MQTT topic details.
Topic format: home/{device_type}/{device_id}/set
Message format: {"type": device_type, "payload": payload}
Args:
device_type: Device type (light, thermostat, relay, etc.)
device_id: Device identifier
payload: Command payload (e.g., {"power": "on", "brightness": 50})
Example:
>>> await publish_abstract_set("light", "kueche_deckenlampe", {"power": "on", "brightness": 35})
# Publishes to: home/light/kueche_deckenlampe/set
# Message: {"type": "light", "payload": {"power": "on", "brightness": 35}}
"""
mqtt_host, mqtt_port = get_mqtt_settings()
topic = f"home/{device_type}/{device_id}/set"
message = {
"type": device_type,
"payload": payload
}
try:
async with Client(hostname=mqtt_host, port=mqtt_port) as client:
await client.publish(
topic=topic,
payload=json.dumps(message),
qos=1
)
logger.info(f"Published to {topic}: {message}")
except Exception as e:
logger.error(f"Failed to publish to {topic}: {e}")
raise
def get_redis_settings() -> tuple[str, str]: def get_redis_settings() -> tuple[str, str]:
"""Get Redis settings from configuration. """Get Redis settings from configuration.
@@ -249,6 +365,32 @@ async def publish_mqtt(topic: str, payload: dict[str, Any]) -> None:
await client.publish(topic, message, qos=1) await client.publish(topic, message, qos=1)
@app.get("/devices/states")
async def get_device_states() -> dict[str, dict[str, Any]]:
"""Get current states of all devices from in-memory cache.
Returns:
dict: Dictionary mapping device_id to state payload
"""
logger.debug("Fetching all device states")
return device_states
@app.get("/devices/{device_id}")
async def get_device(device_id: str) -> DeviceInfo:
logger.debug(f"Fetching info for device {device_id}")
devices = load_devices()
device = next((d for d in devices if d["device_id"] == device_id), None)
if not device:
raise HTTPException(status_code=404, detail="Device not found")
return DeviceInfo(
device_id=device["device_id"],
type=device["type"],
name=device.get("name", device["device_id"]),
features=device.get("features", {})
)
@app.get("/devices") @app.get("/devices")
async def get_devices() -> list[DeviceInfo]: async def get_devices() -> list[DeviceInfo]:
"""Get list of available devices. """Get list of available devices.
@@ -256,6 +398,7 @@ async def get_devices() -> list[DeviceInfo]:
Returns: Returns:
list: List of device information including features list: List of device information including features
""" """
logger.debug("Fetching list of devices")
devices = load_devices() devices = load_devices()
return [ return [
DeviceInfo( DeviceInfo(
@@ -268,15 +411,6 @@ async def get_devices() -> list[DeviceInfo]:
] ]
@app.get("/devices/states")
async def get_device_states() -> dict[str, dict[str, Any]]:
"""Get current states of all devices from in-memory cache.
Returns:
dict: Dictionary mapping device_id to state payload
"""
return device_states
@app.get("/layout") @app.get("/layout")
async def get_layout() -> dict[str, Any]: async def get_layout() -> dict[str, Any]:
@@ -285,8 +419,6 @@ async def get_layout() -> dict[str, Any]:
Returns: Returns:
dict: Layout configuration with rooms and device tiles dict: Layout configuration with rooms and device tiles
""" """
from packages.home_capabilities import load_layout
try: try:
layout = load_layout() layout = load_layout()
@@ -315,6 +447,23 @@ async def get_layout() -> dict[str, Any]:
return {"rooms": []} return {"rooms": []}
@app.get("/devices/{device_id}/room")
async def get_device_room(device_id: str) -> dict[str, str | None]:
"""Get the room name for a specific device.
Args:
device_id: Device identifier
Returns:
dict: {"device_id": str, "room": str | null}
"""
room = get_room(device_id)
return {
"device_id": device_id,
"room": room
}
@app.post("/devices/{device_id}/set", status_code=status.HTTP_202_ACCEPTED) @app.post("/devices/{device_id}/set", status_code=status.HTTP_202_ACCEPTED)
async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str]: async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str]:
"""Set device state. """Set device state.
@@ -355,6 +504,14 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid payload for light: {e}" detail=f"Invalid payload for light: {e}"
) )
elif request.type == "relay":
try:
RelayState(**request.payload)
except ValidationError as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid payload for relay: {e}"
)
elif request.type == "thermostat": elif request.type == "thermostat":
try: try:
# For thermostat SET: only allow mode and target # For thermostat SET: only allow mode and target
@@ -377,6 +534,12 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str
status_code=status.HTTP_405_METHOD_NOT_ALLOWED, status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Contact sensors are read-only devices" detail="Contact sensors are read-only devices"
) )
elif request.type in {"temp_humidity", "temp_humidity_sensor"}:
# Temperature & humidity sensors are read-only
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Temperature & humidity sensors are read-only devices"
)
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,

286
apps/api/resolvers.py Normal file
View File

@@ -0,0 +1,286 @@
"""Group and scene resolution logic."""
import logging
from pathlib import Path
from typing import Any, TypedDict
from packages.home_capabilities import (
GroupConfig,
GroupsConfigRoot,
SceneStep,
get_group_by_id,
load_layout,
)
logger = logging.getLogger(__name__)
# ============================================================================
# TYPE DEFINITIONS
# ============================================================================
class DeviceDTO(TypedDict, total=False):
"""Device Data Transfer Object.
Represents a device as returned by /devices endpoint or load_devices().
Required fields:
device_id: Unique device identifier
type: Device type (light, thermostat, relay, etc.)
Optional fields:
name: Human-readable device name
features: Device capabilities (power, brightness, etc.)
technology: MQTT, zigbee2mqtt, simulator, etc.
topics: MQTT topic configuration
metadata: Additional device information
"""
device_id: str
type: str
name: str
features: dict[str, Any]
technology: str
topics: dict[str, str]
metadata: dict[str, Any]
# ============================================================================
# DEVICE-ROOM MAPPING
# ============================================================================
# Global cache for device -> room mapping
_device_room_cache: dict[str, str] = {}
def load_device_rooms(path: str | Path | None = None) -> dict[str, str]:
"""
Load device-to-room mapping from layout configuration.
This function extracts a mapping of device_id -> room_name from the layout.yaml
file, which is useful for resolving selectors like {room: "Küche"}.
Args:
path: Optional path to layout.yaml. If None, uses default path
(config/layout.yaml relative to workspace root)
Returns:
Dictionary mapping device_id to room_name. Returns empty dict if:
- layout.yaml doesn't exist
- layout.yaml is malformed
- layout.yaml is empty
Example:
>>> mapping = load_device_rooms()
>>> mapping['kueche_lampe1']
'Küche'
"""
global _device_room_cache
try:
# Load the layout using existing function
layout = load_layout(path)
# Build device -> room mapping
device_rooms: dict[str, str] = {}
for room in layout.rooms:
for device in room.devices:
device_rooms[device.device_id] = room.name
# Update global cache
_device_room_cache = device_rooms.copy()
logger.info(f"Loaded device-room mapping: {len(device_rooms)} devices")
return device_rooms
except (FileNotFoundError, ValueError, Exception) as e:
logger.warning(f"Failed to load device-room mapping: {e}")
logger.warning("Returning empty device-room mapping")
_device_room_cache = {}
return {}
def get_room(device_id: str) -> str | None:
"""
Get the room name for a given device ID.
This function uses the cached device-room mapping loaded by load_device_rooms().
If the cache is empty, it will attempt to load it first.
Args:
device_id: The device identifier to lookup
Returns:
Room name if device is found, None otherwise
Example:
>>> get_room('kueche_lampe1')
'Küche'
>>> get_room('nonexistent_device')
None
"""
# Check if cache is populated
if not _device_room_cache:
logger.debug("Device-room cache empty, loading from layout...")
# Load mapping (this updates the global _device_room_cache)
load_device_rooms()
# Access the cache after potential reload
return _device_room_cache.get(device_id)
def clear_room_cache() -> None:
"""
Clear the cached device-room mapping.
This is useful for testing or when the layout configuration has changed
and needs to be reloaded.
"""
_device_room_cache.clear()
logger.debug("Cleared device-room cache")
# ============================================================================
# GROUP & SCENE RESOLUTION
# ============================================================================
def resolve_group_devices(
group: GroupConfig,
devices: list[DeviceDTO],
device_rooms: dict[str, str]
) -> list[DeviceDTO]:
"""
Resolve devices for a group based on device_ids or selector.
Args:
group: Group configuration with device_ids or selector
devices: List of all available devices
device_rooms: Mapping of device_id -> room_name
Returns:
List of devices matching the group criteria (no duplicates)
Example:
>>> # Group with explicit device_ids
>>> group = GroupConfig(id="test", name="Test", device_ids=["lamp1", "lamp2"])
>>> resolve_group_devices(group, all_devices, {})
[{"device_id": "lamp1", ...}, {"device_id": "lamp2", ...}]
>>> # Group with selector (all lights in kitchen)
>>> group = GroupConfig(
... id="kitchen_lights",
... name="Kitchen Lights",
... selector=GroupSelector(type="light", room="Küche")
... )
>>> resolve_group_devices(group, all_devices, device_rooms)
[{"device_id": "kueche_deckenlampe", ...}, ...]
"""
# Case 1: Explicit device_ids
if group.device_ids:
device_id_set = set(group.device_ids)
return [d for d in devices if d["device_id"] in device_id_set]
# Case 2: Selector-based filtering
if group.selector:
filtered = []
for device in devices:
# Filter by type (required in selector)
if device["type"] != group.selector.type:
continue
# Filter by room (optional)
if group.selector.room:
device_room = device_rooms.get(device["device_id"])
if device_room != group.selector.room:
continue
# Filter by tags (optional, future feature)
# if group.selector.tags:
# device_tags = device.get("metadata", {}).get("tags", [])
# if not any(tag in device_tags for tag in group.selector.tags):
# continue
filtered.append(device)
return filtered
# No device_ids and no selector → empty list
return []
def resolve_scene_step_devices(
step: SceneStep,
groups_config: GroupsConfigRoot,
devices: list[DeviceDTO],
device_rooms: dict[str, str]
) -> list[DeviceDTO]:
"""
Resolve devices for a scene step based on group_id or selector.
Args:
step: Scene step with group_id or selector
groups_config: Groups configuration for group lookup
devices: List of all available devices
device_rooms: Mapping of device_id -> room_name
Returns:
List of devices matching the step criteria
Raises:
ValueError: If group_id is specified but group not found
Example:
>>> # Step with group_id
>>> step = SceneStep(group_id="kitchen_lights", action={...})
>>> resolve_scene_step_devices(step, groups_cfg, all_devices, device_rooms)
[{"device_id": "kueche_deckenlampe", ...}, ...]
>>> # Step with selector
>>> step = SceneStep(
... selector=SceneSelector(type="light", room="Küche"),
... action={...}
... )
>>> resolve_scene_step_devices(step, groups_cfg, all_devices, device_rooms)
[{"device_id": "kueche_deckenlampe", ...}, ...]
"""
# Case 1: Group reference
if step.group_id:
# Look up the group
group = get_group_by_id(groups_config, step.group_id)
if not group:
raise ValueError(
f"Scene step references unknown group_id: '{step.group_id}'. "
f"Available groups: {[g.id for g in groups_config.groups]}"
)
# Resolve the group's devices
return resolve_group_devices(group, devices, device_rooms)
# Case 2: Direct selector
if step.selector:
filtered = []
for device in devices:
# Filter by type (optional in scene selector)
if step.selector.type and device["type"] != step.selector.type:
continue
# Filter by room (optional)
if step.selector.room:
device_room = device_rooms.get(device["device_id"])
if device_room != step.selector.room:
continue
# Filter by tags (optional, future feature)
# if step.selector.tags:
# device_tags = device.get("metadata", {}).get("tags", [])
# if not any(tag in device_tags for tag in step.selector.tags):
# continue
filtered.append(device)
return filtered
# Should not reach here due to SceneStep validation (must have group_id or selector)
return []

View File

@@ -0,0 +1 @@
"""API routes package."""

View File

@@ -0,0 +1,454 @@
"""Groups and Scenes API routes."""
import asyncio
import logging
from pathlib import Path
from typing import Any
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from packages.home_capabilities import (
GroupConfig,
GroupsConfigRoot,
SceneConfig,
ScenesConfigRoot,
get_group_by_id,
get_scene_by_id,
load_groups,
load_scenes,
)
# Import from parent modules
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from resolvers import (
DeviceDTO,
resolve_group_devices,
resolve_scene_step_devices,
load_device_rooms,
)
from main import load_devices, publish_abstract_set
logger = logging.getLogger(__name__)
router = APIRouter()
# ============================================================================
# REQUEST/RESPONSE MODELS
# ============================================================================
class GroupResponse(BaseModel):
"""Response model for a group."""
id: str
name: str
device_count: int
devices: list[str]
selector: dict[str, Any] | None = None
capabilities: dict[str, bool]
class GroupSetRequest(BaseModel):
"""Request to set state for all devices in a group."""
action: dict[str, Any] # e.g., {"type": "light", "payload": {"power": "on", "brightness": 50}}
class SceneResponse(BaseModel):
"""Response model for a scene."""
id: str
name: str
steps: int
class SceneRunRequest(BaseModel):
"""Request to execute a scene (currently empty, future: override params)."""
pass
class SceneExecutionResponse(BaseModel):
"""Response after scene execution."""
scene_id: str
scene_name: str
steps_executed: int
devices_affected: int
execution_plan: list[dict[str, Any]]
# ============================================================================
# GROUPS ENDPOINTS
# ============================================================================
@router.get("/groups", response_model=list[GroupResponse], tags=["groups"])
async def list_groups() -> list[GroupResponse]:
"""
List all available groups.
Returns:
list[GroupResponse]: List of groups with their devices
"""
try:
# Load configuration
groups_config = load_groups(Path(__file__).parent.parent.parent.parent / "config" / "groups.yaml")
devices = load_devices()
device_rooms = load_device_rooms()
# Build response for each group
response = []
for group in groups_config.groups:
# Resolve devices for this group
resolved_devices = resolve_group_devices(group, devices, device_rooms)
device_ids = [d["device_id"] for d in resolved_devices]
# Convert selector to dict if present
selector_dict = None
if group.selector:
selector_dict = {
"type": group.selector.type,
"room": group.selector.room,
"tags": group.selector.tags,
}
response.append(GroupResponse(
id=group.id,
name=group.name,
device_count=len(device_ids),
devices=device_ids,
selector=selector_dict,
capabilities=group.capabilities,
))
return response
except Exception as e:
logger.error(f"Error loading groups: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to load groups: {str(e)}"
)
@router.get("/groups/{group_id}", response_model=GroupResponse, tags=["groups"])
async def get_group(group_id: str) -> GroupResponse:
"""
Get details for a specific group.
Args:
group_id: Group identifier
Returns:
GroupResponse: Group details with resolved devices
"""
try:
# Load configuration
groups_config = load_groups(Path(__file__).parent.parent.parent.parent / "config" / "groups.yaml")
devices = load_devices()
device_rooms = load_device_rooms()
# Find the group
group = get_group_by_id(groups_config, group_id)
if not group:
available_groups = [g.id for g in groups_config.groups]
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Group '{group_id}' not found. Available groups: {available_groups}"
)
# Resolve devices
resolved_devices = resolve_group_devices(group, devices, device_rooms)
device_ids = [d["device_id"] for d in resolved_devices]
# Convert selector to dict if present
selector_dict = None
if group.selector:
selector_dict = {
"type": group.selector.type,
"room": group.selector.room,
"tags": group.selector.tags,
}
return GroupResponse(
id=group.id,
name=group.name,
device_count=len(device_ids),
devices=device_ids,
selector=selector_dict,
capabilities=group.capabilities,
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting group {group_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get group: {str(e)}"
)
@router.post("/groups/{group_id}/set", status_code=status.HTTP_202_ACCEPTED, tags=["groups"])
async def set_group(group_id: str, request: GroupSetRequest) -> dict[str, Any]:
"""
Set state for all devices in a group.
This endpoint resolves the group to its devices and would send
the action to each device. Currently returns execution plan.
Args:
group_id: Group identifier
request: Action to apply to all devices in the group
Returns:
dict: Execution plan (devices and actions to be executed)
"""
try:
# Load configuration
groups_config = load_groups(Path(__file__).parent.parent.parent.parent / "config" / "groups.yaml")
devices = load_devices()
device_rooms = load_device_rooms()
# Find the group
group = get_group_by_id(groups_config, group_id)
if not group:
available_groups = [g.id for g in groups_config.groups]
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Group '{group_id}' not found. Available groups: {available_groups}"
)
# Resolve devices
resolved_devices = resolve_group_devices(group, devices, device_rooms)
if not resolved_devices:
logger.warning(f"Group '{group_id}' resolved to 0 devices")
# Execute actions via MQTT
execution_plan = []
for device in resolved_devices:
device_type = device["type"]
device_id = device["device_id"]
payload = request.action.get("payload", {})
# Publish MQTT command
try:
await publish_abstract_set(device_type, device_id, payload)
execution_plan.append({
"device_id": device_id,
"device_type": device_type,
"action": request.action,
"status": "published"
})
except Exception as e:
logger.error(f"Failed to publish to {device_id}: {e}")
execution_plan.append({
"device_id": device_id,
"device_type": device_type,
"action": request.action,
"status": "failed",
"error": str(e)
})
return {
"group_id": group_id,
"group_name": group.name,
"devices_affected": len(resolved_devices),
"execution_plan": execution_plan,
"status": "executed"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error setting group {group_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to set group: {str(e)}"
)
# ============================================================================
# SCENES ENDPOINTS
# ============================================================================
@router.get("/scenes", response_model=list[SceneResponse], tags=["scenes"])
async def list_scenes() -> list[SceneResponse]:
"""
List all available scenes.
Returns:
list[SceneResponse]: List of scenes
"""
try:
# Load configuration
scenes_config = load_scenes(Path(__file__).parent.parent.parent.parent / "config" / "scenes.yaml")
# Build response for each scene
response = []
for scene in scenes_config.scenes:
response.append(SceneResponse(
id=scene.id,
name=scene.name,
steps=len(scene.steps),
))
return response
except Exception as e:
logger.error(f"Error loading scenes: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to load scenes: {str(e)}"
)
@router.get("/scenes/{scene_id}", response_model=SceneResponse, tags=["scenes"])
async def get_scene(scene_id: str) -> SceneResponse:
"""
Get details for a specific scene.
Args:
scene_id: Scene identifier
Returns:
SceneResponse: Scene details
"""
try:
# Load configuration
scenes_config = load_scenes(Path(__file__).parent.parent.parent.parent / "config" / "scenes.yaml")
# Find the scene
scene = get_scene_by_id(scenes_config, scene_id)
if not scene:
available_scenes = [s.id for s in scenes_config.scenes]
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Scene '{scene_id}' not found. Available scenes: {available_scenes}"
)
return SceneResponse(
id=scene.id,
name=scene.name,
steps=len(scene.steps),
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting scene {scene_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get scene: {str(e)}"
)
@router.post("/scenes/{scene_id}/run", response_model=SceneExecutionResponse, tags=["scenes"])
async def run_scene(scene_id: str, request: SceneRunRequest | None = None) -> SceneExecutionResponse:
"""
Execute a scene.
This endpoint resolves each step in the scene to its target devices
and would execute the actions. Currently returns execution plan.
Args:
scene_id: Scene identifier
request: Optional execution parameters (reserved for future use)
Returns:
SceneExecutionResponse: Execution plan and summary
"""
try:
# Load configuration
scenes_config = load_scenes(Path(__file__).parent.parent.parent.parent / "config" / "scenes.yaml")
groups_config = load_groups(Path(__file__).parent.parent.parent.parent / "config" / "groups.yaml")
devices = load_devices()
device_rooms = load_device_rooms()
# Find the scene
scene = get_scene_by_id(scenes_config, scene_id)
if not scene:
available_scenes = [s.id for s in scenes_config.scenes]
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Scene '{scene_id}' not found. Available scenes: {available_scenes}"
)
# Execute scene steps
execution_plan = []
total_devices = 0
for i, step in enumerate(scene.steps, 1):
# Resolve devices for this step
resolved_devices = resolve_scene_step_devices(step, groups_config, devices, device_rooms)
total_devices += len(resolved_devices)
# Extract action payload
action_payload = step.action.get("payload", {})
# Execute for each device
step_executions = []
for device in resolved_devices:
device_type = device["type"]
device_id = device["device_id"]
try:
await publish_abstract_set(device_type, device_id, action_payload)
step_executions.append({
"device_id": device_id,
"status": "published"
})
except Exception as e:
logger.error(f"Failed to publish to {device_id} in step {i}: {e}")
step_executions.append({
"device_id": device_id,
"status": "failed",
"error": str(e)
})
# Build step info
step_info = {
"step": i,
"devices_affected": len(resolved_devices),
"device_ids": [d["device_id"] for d in resolved_devices],
"action": step.action,
"executions": step_executions,
}
# Add targeting info
if step.group_id:
step_info["target"] = {"type": "group_id", "value": step.group_id}
elif step.selector:
step_info["target"] = {
"type": "selector",
"selector_type": step.selector.type,
"room": step.selector.room,
}
if step.delay_ms:
step_info["delay_ms"] = step.delay_ms
# Apply delay before next step
await asyncio.sleep(step.delay_ms / 1000.0)
execution_plan.append(step_info)
return SceneExecutionResponse(
scene_id=scene.id,
scene_name=scene.name,
steps_executed=len(scene.steps),
devices_affected=total_devices,
execution_plan=execution_plan,
)
except HTTPException:
raise
except ValueError as e:
# Handle unknown group_id in scene step
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error(f"Error running scene {scene_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to run scene: {str(e)}"
)

465
apps/homekit/README.md Normal file
View File

@@ -0,0 +1,465 @@
# HomeKit Bridge
HomeKit Accessory Protocol (HAP) Bridge für das Home Automation System.
## Überblick
Diese HomeKit-Bridge ermöglicht die Steuerung aller Geräte über Apple HomeKit / Home-App auf iPhone/iPad/Mac.
**Architektur:**
- Läuft auf Raspberry Pi im lokalen Netzwerk
- Kommuniziert ausschließlich über REST-API (kein direkter MQTT/Redis-Zugriff)
- Verwendet [HAP-Python](https://github.com/ikalchev/HAP-python) für HomeKit-Protokoll
- Unterstützt Echtzeit-Updates über Server-Sent Events (SSE)
## Implementierungsanleitung für Copilot
<details>
<summary>Copilot-Aufgabe: Implementiere die HomeKit-Bridge (HAP-Python) für unser System</summary>
### Kontext
- Wir verwenden HAP-Python als HomeKit-Bridge.
- Die Bridge läuft auf einem Raspberry Pi im gleichen LAN wie die iPhones.
- Die Bridge kommuniziert AUSSCHLIESSLICH über die REST-API mit dem restlichen System (KEIN direkter MQTT-/Redis-Zugriff).
- Geräte- und Raum-Infos kommen aus:
- `GET /devices`
- `GET /layout`
und werden über `device_id` gejoint.
- Die Abbildung von abstrakten Gerätetypen/Features auf HomeKit-Services/Characteristics steht in der Datei `apps/homekit/homekit_mapping.md` (Markdown-Tabelle). Bitte orientiere dich daran.
### Ziel
Implementiere eine lauffähige HomeKit-Bridge in `apps/homekit`, die:
- beim Start Geräte & Räume über die API lädt,
- daraus anhand der Mapping-Tabelle HomeKit-Accessories erzeugt,
- Zustandsänderungen über einen Realtime-Endpoint der API empfängt und in HomeKit spiegeln kann,
- Set-Kommandos aus HomeKit (z.B. Licht an/aus, Helligkeit, Zieltemperatur) per `POST /devices/{id}/set` an die API sendet,
- saubere Start-/Stop-Logs ausgibt,
- einfache Test-/Startanweisungen für den Raspberry Pi und die Home-App (iPhone) in Kommentaren dokumentiert.
### WICHTIG
- Bitte benutze HAP-Python (pyhap).
- Keine direkte MQTT- oder Redis-Kommunikation in der Bridge.
- Nutze HTTP(S) zur API (z.B. mit requests oder httpx; gerne sync, HAP-Python ist selbst eventloop-orientiert).
- Nutze die Mapping-Tabelle in `apps/homekit/homekit_mapping.md` als Leitlinie für die Accessory-Typen.
### Paketstruktur
```
apps/homekit/
├── __init__.py
├── main.py # Einstiegspunkt
├── api_client.py # REST-Client zur API
├── device_registry.py # Join /devices + /layout → internes Device-Modell
├── accessories/
│ ├── base.py # gemeinsame Basisklasse/n für Accessories
│ ├── light.py # Light-Accessories
│ ├── thermostat.py
│ ├── contact.py
│ ├── sensor.py # temp_humidity etc.
│ └── cover.py # optional, Rollladen
└── tests/ # rudimentäre Tests/Checks
```
## Teil 1: Internes Gerätemodell / Device-Registry
### 1.1 Erstelle in `apps/homekit/device_registry.py`:
**Dataklasse/Model Device:**
- `device_id: str`
- `type: str` - "light","thermostat","outlet","contact","temp_humidity","cover",...
- `name: str` - Kurzname aus `GET /devices.name`
- `friendly_name: str` - title aus `GET /layout` (fallback name)
- `room: str | None` - Raumname aus layout
- `features: dict[str, bool]`
- `read_only: bool` - heuristisch: Sensor-/Kontakt-Typen sind read_only
**Klasse DeviceRegistry mit Funktionen:**
- `def load_from_api(api: ApiClient) -> DeviceRegistry`:
- ruft `GET /devices` und `GET /layout` auf,
- joint über `device_id`,
- erstellt Device-Instanzen.
- `get_all(): list[Device]`
- `get_by_id(device_id: str) -> Device | None`
**Akzeptanz:**
- `load_from_api` funktioniert mit der bestehenden Struktur von `/devices` und `/layout`:
- `/devices` liefert mindestens `{device_id, type, name, features}`
- `/layout` liefert eine Struktur, aus der `device_id → room + title` ableitbar ist
- Der Join über `device_id` klappt; fehlende Layout-Einträge werden toleriert
## Teil 2: API-Client
### 2.1 Erstelle in `apps/homekit/api_client.py`:
**Klasse ApiClient mit:**
- `__init__(self, base_url: str, token: str | None = None, timeout: int = 5)`
- **Methoden:**
- `get_devices() -> list[dict]`: GET /devices
- `get_layout() -> dict`: GET /layout
- `get_device_state(device_id: str) -> dict`: GET /devices/{id}/state
- `post_device_set(device_id: str, type: str, payload: dict) -> None`: POST /devices/{id}/set
- `stream_realtime() -> Iterator[dict]`:
- GET /realtime als SSE, yield jedes Event als dict:
`{"type":"state","device_id":...,"payload":{...},"ts":...}`
**Auth:**
- Wenn ein API-Token via ENV `HOMEKIT_API_TOKEN` gesetzt ist, nutze HTTP-Header:
`Authorization: Bearer <token>`
**Akzeptanz:**
- ApiClient ist robust:
- bei Netzwerkfehlern gibt es sinnvolle Exceptions/Logs,
- `stream_realtime` behandelt Reconnect (z.B. einfache Endlosschleife mit Backoff).
- Es werden keine MQTT-Details verwendet, nur HTTP.
## Teil 3: HomeKit-Accessory-Klassen (HAP-Python)
### 3.1 Erstelle in `apps/homekit/accessories/base.py`:
**Basisklasse BaseDeviceAccessory(Accessory) mit:**
- Referenz auf Device (aus DeviceRegistry)
- Referenz auf ApiClient
- Methoden zum:
- Registrieren von HAP-Characteristics und Set-Handlern
- Aktualisieren von Characteristics bei eingehenden Events
- Logging
### 3.2 Erstelle spezifische Accessory-Klassen basierend auf homekit_mapping.md:
**LightAccessories (`apps/homekit/accessories/light.py`):**
- On/Off (nur power)
- Dimmable (power + brightness)
- Color (power + brightness + color_hsb)
**ThermostatAccessory:**
- CurrentTemperature, TargetTemperature, Mode (so weit in Mapping definiert)
**ContactAccessory:**
- ContactSensorState (open/closed)
**TempHumidityAccessory:**
- TemperatureSensor (CurrentTemperature)
- HumiditySensor (CurrentRelativeHumidity)
**OutletAccessory:**
- On/Off
**CoverAccessory (optional):**
- WindowCovering mit CurrentPosition/TargetPosition
**Die Mapping-Tabelle in `homekit_mapping.md` ist die normative Referenz:**
- Bitte lies die Tabelle und mappe `abstract_type + Features → passende Accessory-Klasse und Characteristics`
- Wo die Tabelle `Status=TODO/REVIEW` hat:
- Implementiere nur das, was eindeutig ist,
- lasse TODO-Kommentare an den entsprechenden Stellen im Code.
**Akzeptanz:**
- Für die abstrakten Typen, die bereits in `devices.yaml` und Mapping-Tabelle klar definiert sind (z.B. light, thermostat, contact, temp_humidity), existieren passende Accessory-Klassen.
- Set-Operationen erzeugen korrekte Payloads für `POST /devices/{id}/set`:
- Light: `{"type":"light","payload":{"power":"on"/"off", "brightness":..., "hue":..., "sat":...}}`
- Thermostat: `{"type":"thermostat","payload":{"target":...}}`
- Contact: read_only → keine Set-Handler
- Temp/Humidity: read_only → keine Set-Handler
## Teil 4: Bridge-Setup mit HAP-Python
### 4.1 Implementiere in `apps/homekit/main.py`:
**env-Konfiguration:**
- `HOMEKIT_NAME` (default: "Home Automation Bridge")
- `HOMEKIT_PIN` (z.B. "031-45-154")
- `HOMEKIT_PORT` (default 51826)
- `API_BASE` (z.B. "http://api:8001" oder extern)
- `HOMEKIT_API_TOKEN` (optional)
**Funktion `build_bridge(driver, api_client: ApiClient) -> Bridge`:**
- DeviceRegistry via `load_from_api(api_client)` laden.
- Für jedes Device anhand Mapping-Tabelle die passende Accessory-Klasse instanziieren.
- Einen Bridge-Accessory (`pyhap.accessory.Bridge`) erstellen.
- Alle Device-Accessories der Bridge hinzufügen.
**Realtime-Event-Loop:**
- In einem Hintergrund-Thread oder ThreadPool:
- `api_client.stream_realtime()` iterieren,
- für jedes Event `device_id → Accessory` finden,
- Characteristics updaten.
- Thread wird beim Shutdown sauber beendet.
**`main()`:**
- Logging einrichten.
- ApiClient erstellen.
- `AccessoryDriver(port=HOMEKIT_PORT, persist_file="homekit.state")` erstellen.
- Bridge via `build_bridge(driver, api_client)` bauen.
- Bridge dem Driver hinzufügen.
- Realtime-Thread starten.
- `driver.start()` aufrufen.
- Auf KeyboardInterrupt reagieren und sauber stoppen.
**Akzeptanz:**
- Beim Start loggt die Bridge:
- Anzahl Devices,
- auf welchem Port sie als HomeKit-Bridge lauscht,
- welches API_BASE verwendet wird.
- Die Datei `homekit.state` wird im Arbeitsverzeichnis bzw. einem konfigurierbaren Ordner abgelegt (um Pairing-Info persistent zu halten).
- Die Bridge übersteht API-Neustarts (Realtime-Loop reconnectet) und Netzwerkflaps.
## Teil 5: Tests & Testanweisungen
### 5.1 Lege in `apps/homekit/tests/` einfache Tests/Checks an:
**Unit-Tests (pytest), soweit ohne echtes HomeKit möglich:**
- Test für `DeviceRegistry.load_from_api()` mit Mock-Antworten aus `/devices` und `/layout`:
- Korrekte Join-Logik,
- Korrekte room/friendly_name-Zuordnung.
- Test für set-Payload-Erzeugung pro Accessory:
- z.B. LightAccessory: On=True → POST /devices/{id}/set wird mit korrektem Payload aufgerufen (über Mock ApiClient).
### Allgemein
- Nutze möglichst sinnvolle Typannotationen und Docstrings.
- Hinterlasse TODO-Kommentare an Stellen, wo die Mapping-Tabelle explizit Status=TODO/REVIEW hat.
- Ändere KEINE bestehenden API-Endpunkte; verlasse dich nur auf deren aktuelles Verhalten (GET /devices, GET /layout, /realtime, POST /devices/{id}/set).
## Installation & Setup
### Voraussetzungen
- Python 3.9+
- Raspberry Pi im gleichen LAN wie iPhone/iPad
- API-Server erreichbar (z.B. `http://api:8001`)
### Installation
```bash
cd apps/homekit
pip install -r requirements.txt
```
### Umgebungsvariablen
Erstelle eine `.env` Datei oder setze folgende Variablen:
```bash
export API_BASE="http://YOUR_API_IP:8001"
export HOMEKIT_API_TOKEN="your-token-if-needed" # optional
export HOMEKIT_PIN="031-45-154"
export HOMEKIT_NAME="Home Automation Bridge"
export HOMEKIT_PORT="51826"
export HOMEKIT_PERSIST_FILE="homekit.state"
```
## Start
```bash
python -m apps.homekit.main
```
**Erwartete Logs:**
```
Loading devices from API...
Loaded X devices from API
Bridge built with Y accessories
HomeKit Bridge started on port 51826
Starting realtime event loop...
```
## Pairing mit iPhone (Home-App)
1. **Voraussetzungen:**
- iPhone im gleichen WLAN wie Raspberry Pi
- Bridge läuft und zeigt "started on port 51826"
2. **Home-App öffnen:**
- Öffne die Home-App auf dem iPhone
- Tippe auf "+" → "Gerät hinzufügen"
- Wähle "Weitere Optionen..." oder "Code fehlt oder kann nicht gescannt werden"
3. **Bridge auswählen:**
- Die Bridge sollte in der Nähe-Liste erscheinen (z.B. "Home Automation Bridge")
- Tippe auf die Bridge
4. **PIN eingeben:**
- Gib den PIN ein: `031-45-154` (oder dein `HOMEKIT_PIN`)
- Format: `XXX-XX-XXX`
5. **Konfiguration abschließen:**
- Geräte werden geladen
- Räume werden automatisch aus `layout.yaml` übernommen
- Geräte können nun über Home-App gesteuert werden
## Funktionstests
### Test 1: Licht einschalten
**Aktion:** Lampe in Home-App antippen (On)
**Erwartung:**
- API-Log zeigt: `POST /devices/{id}/set` mit `{"type":"light","payload":{"power":"on"}}`
- Physische Lampe oder Simulator schaltet ein
### Test 2: Helligkeit ändern (dimmbare Lampe)
**Aktion:** Helligkeits-Slider bewegen (z.B. 75%)
**Erwartung:**
- `POST /devices/{id}/set` mit `brightness` ca. 75
- Lampe dimmt entsprechend
### Test 3: Farbe ändern (Farblampe)
**Aktion:** Farbe in Home-App wählen
**Erwartung:**
- `POST /devices/{id}/set` mit `hue`/`sat` Werten
- Lampe wechselt Farbe
### Test 4: Thermostat Zieltemperatur
**Aktion:** Zieltemperatur auf 22°C setzen
**Erwartung:**
- `POST /devices/{id}/set` mit `target=22`
- `CurrentTemperature` wird über `/realtime` aktualisiert
### Test 5: Kontaktsensor (read-only)
**Aktion:** Fenster physisch öffnen/schließen
**Erwartung:**
- `/realtime` sendet Event
- Home-App zeigt "Offen" oder "Geschlossen"
### Test 6: Temperatur-/Feuchtigkeitssensor
**Aktion:** Werte ändern sich (z.B. Heizung an)
**Erwartung:**
- `/realtime` Events aktualisieren Werte
- Home-App zeigt aktuelle Temperatur/Luftfeuchtigkeit
## Troubleshooting
### Bridge erscheint nicht in Home-App
- **Netzwerk prüfen:** iPhone und RPi im gleichen Subnet?
- **Firewall prüfen:** Port 51826 muss erreichbar sein
- **Logs prüfen:** Fehler beim Start?
- **mDNS/Bonjour:** Funktioniert Bonjour im Netzwerk?
### Geräte reagieren nicht
- **API-Logs prüfen:** Kommen POST-Requests an?
- **Realtime-Verbindung:** Läuft `/realtime` Event-Loop? (Log-Meldungen)
- **API-Endpoints testen:** Manuell mit `curl` testen
### Pairing schlägt fehl
- **State-Datei löschen:** `rm homekit.state` und Bridge neu starten
- **PIN-Format prüfen:** Muss `XXX-XX-XXX` Format haben
- **Alte Pairings löschen:** In Home-App unter "Home-Einstellungen" → "HomeKit-Geräte zurücksetzen"
### Realtime-Updates funktionieren nicht
- **SSE-Verbindung prüfen:** Logs zeigen "Starting realtime event loop..."?
- **API-Endpoint testen:** `curl -N http://api:8001/realtime`
- **Firewall/Proxy:** Blockiert etwas SSE-Streams?
## Docker Deployment
### Dockerfile
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-m", "apps.homekit.main"]
```
### docker-compose.yml
```yaml
services:
homekit:
build: ./apps/homekit
environment:
- API_BASE=http://api:8001
- HOMEKIT_PIN=031-45-154
- HOMEKIT_NAME=Home Automation Bridge
ports:
- "51826:51826"
volumes:
- ./data/homekit:/data
network_mode: "host" # Wichtig für mDNS/Bonjour Discovery
restart: unless-stopped
```
**Wichtig:** `network_mode: "host"` ist erforderlich für mDNS/Bonjour Discovery, damit die Bridge im lokalen Netzwerk gefunden werden kann.
## Architektur
```
┌─────────────┐
│ iPhone/ │
│ Home-App │
└──────┬──────┘
│ HomeKit (HAP)
│ Port 51826
┌──────▼──────────────────┐
│ HomeKit Bridge │
│ (HAP-Python) │
│ - Device Registry │
│ - Accessory Mapping │
│ - SSE Event Loop │
└──────┬──────────────────┘
│ HTTP REST API
│ (GET /devices, POST /set, SSE /realtime)
┌──────▼──────────────────┐
│ API Server │
│ (FastAPI) │
└──────┬──────────────────┘
│ MQTT
┌──────▼──────────────────┐
│ Abstraction Layer │
│ (Zigbee2MQTT, MAX!) │
└─────────────────────────┘
```
## Weitere Dokumentation
- **API-Mapping:** Siehe `homekit_mapping.md` für Device-Type → HomeKit-Service Mapping
- **API-Dokumentation:** Siehe API-Server README für Endpoint-Dokumentation
- **HAP-Python Docs:** https://github.com/ikalchev/HAP-python
## Entwicklung
### Tests ausführen
```bash
pytest apps/homekit/tests/
```
### Logs
Die Bridge gibt detaillierte Logs aus:
- `INFO`: Normale Betriebsmeldungen (Start, Device-Anzahl, etc.)
- `DEBUG`: Detaillierte State-Updates und API-Calls
- `ERROR`: Fehler bei API-Kommunikation oder Accessory-Updates
Log-Level über Environment-Variable steuern:
```bash
export LOG_LEVEL=DEBUG
```
## Lizenz
Siehe Hauptprojekt-Lizenz.

View File

@@ -0,0 +1,5 @@
"""
HomeKit Accessories Package
This package contains HomeKit accessory implementations for different device types.
"""

View File

@@ -0,0 +1,48 @@
"""
Contact Sensor Accessory Implementation for HomeKit
Implements contact sensor (window/door sensors):
- ContactSensorState (read-only): 0=Detected, 1=Not Detected
"""
from pyhap.accessory import Accessory
from pyhap.const import CATEGORY_SENSOR
class ContactAccessory(Accessory):
"""Contact sensor for doors and windows."""
category = CATEGORY_SENSOR
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
"""
Initialize the contact sensor accessory.
Args:
driver: HAP driver instance
device: Device object from DeviceRegistry
api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
"""
name = display_name or device.friendly_name or device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
# Add ContactSensor service
self.contact_service = self.add_preload_service('ContactSensor')
# Get ContactSensorState characteristic
self.contact_state_char = self.contact_service.get_characteristic('ContactSensorState')
# Initialize with "not detected" (closed)
self.contact_state_char.set_value(1)
def update_state(self, state_payload):
"""Update state from API event."""
if "contact" in state_payload:
# API sends: "open" or "closed"
# HomeKit: 0=Contact Detected (closed), 1=Contact Not Detected (open)
is_open = state_payload["contact"] == "open"
homekit_state = 1 if is_open else 0
self.contact_state_char.set_value(homekit_state)

View File

@@ -0,0 +1,177 @@
"""
Light Accessory Implementations for HomeKit
Implements different light types:
- OnOffLightAccessory: Simple on/off light
- DimmableLightAccessory: Light with brightness control
- ColorLightAccessory: RGB light with full color control
"""
from pyhap.accessory import Accessory
from pyhap.const import CATEGORY_LIGHTBULB
class OnOffLightAccessory(Accessory):
"""Simple On/Off Light without dimming or color."""
category = CATEGORY_LIGHTBULB
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
"""
Initialize the light accessory.
Args:
driver: HAP driver instance
device: Device object from DeviceRegistry
api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
"""
name = display_name or device.friendly_name or device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
# Add Lightbulb service with On characteristic
self.lightbulb_service = self.add_preload_service('Lightbulb')
# Get the On characteristic and set callback
self.on_char = self.lightbulb_service.get_characteristic('On')
self.on_char.setter_callback = self.set_on
def set_on(self, value):
"""Called when HomeKit wants to turn light on/off."""
power_state = "on" if value else "off"
payload = {
"type": "light",
"payload": {"power": power_state}
}
self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"])
def update_state(self, state_payload):
"""Update state from API event."""
if "power" in state_payload:
is_on = state_payload["power"] == "on"
self.on_char.set_value(is_on)
class DimmableLightAccessory(OnOffLightAccessory):
"""Dimmable Light with brightness control."""
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
# Don't call super().__init__() yet - we need to set up service first
name = display_name or device.friendly_name or device.name
Accessory.__init__(self, driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
self.category = CATEGORY_LIGHTBULB
# Create Lightbulb service with all characteristics at once
from pyhap.loader import Loader
loader = Loader()
# Create the service
lightbulb_service = loader.get_service('Lightbulb')
# Add On characteristic
on_char = lightbulb_service.get_characteristic('On')
on_char.setter_callback = self.set_on
self.on_char = on_char
# Add Brightness characteristic
brightness_char = loader.get_char('Brightness')
brightness_char.set_value(0)
brightness_char.setter_callback = self.set_brightness
lightbulb_service.add_characteristic(brightness_char)
self.brightness_char = brightness_char
# Now add the complete service to the accessory
self.add_service(lightbulb_service)
self.lightbulb_service = lightbulb_service
def set_brightness(self, value):
"""Called when HomeKit wants to change brightness."""
payload = {
"type": "light",
"payload": {"brightness": int(value)}
}
self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"])
def update_state(self, state_payload):
"""Update state from API event."""
super().update_state(state_payload)
if "brightness" in state_payload:
self.brightness_char.set_value(state_payload["brightness"])
class ColorLightAccessory(DimmableLightAccessory):
"""RGB Light with full color control."""
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
# Don't call super().__init__() - build everything from scratch
name = display_name or device.friendly_name or device.name
Accessory.__init__(self, driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
self.category = CATEGORY_LIGHTBULB
# Create Lightbulb service with all characteristics at once
from pyhap.loader import Loader
loader = Loader()
# Create the service
lightbulb_service = loader.get_service('Lightbulb')
# Add On characteristic
on_char = lightbulb_service.get_characteristic('On')
on_char.setter_callback = self.set_on
self.on_char = on_char
# Add Brightness characteristic
brightness_char = loader.get_char('Brightness')
brightness_char.set_value(0)
brightness_char.setter_callback = self.set_brightness
lightbulb_service.add_characteristic(brightness_char)
self.brightness_char = brightness_char
# Add Hue characteristic
hue_char = loader.get_char('Hue')
hue_char.set_value(0)
hue_char.setter_callback = self.set_hue
lightbulb_service.add_characteristic(hue_char)
self.hue_char = hue_char
# Add Saturation characteristic
saturation_char = loader.get_char('Saturation')
saturation_char.set_value(0)
saturation_char.setter_callback = self.set_saturation
lightbulb_service.add_characteristic(saturation_char)
self.saturation_char = saturation_char
# Now add the complete service to the accessory
self.add_service(lightbulb_service)
self.lightbulb_service = lightbulb_service
def set_hue(self, value):
"""Called when HomeKit wants to change hue."""
payload = {
"type": "light",
"payload": {"hue": int(value)}
}
self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"])
def set_saturation(self, value):
"""Called when HomeKit wants to change saturation."""
payload = {
"type": "light",
"payload": {"sat": int(value)}
}
self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"])
def update_state(self, state_payload):
"""Update state from API event."""
super().update_state(state_payload)
if "hue" in state_payload:
self.hue_char.set_value(state_payload["hue"])
if "sat" in state_payload:
self.saturation_char.set_value(state_payload["sat"])

View File

@@ -0,0 +1,57 @@
"""
Outlet/Relay Accessory Implementation for HomeKit
Implements simple relay/outlet (on/off switch):
- On (read/write)
- OutletInUse (always true)
"""
from pyhap.accessory import Accessory
from pyhap.const import CATEGORY_OUTLET
class OutletAccessory(Accessory):
"""Relay/Outlet for simple on/off control."""
category = CATEGORY_OUTLET
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
"""
Initialize the outlet accessory.
Args:
driver: HAP driver instance
device: Device object from DeviceRegistry
api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
"""
name = display_name or device.friendly_name or device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
# Add Outlet service
self.outlet_service = self.add_preload_service('Outlet')
# Get On characteristic and set callback
self.on_char = self.outlet_service.get_characteristic('On')
self.on_char.setter_callback = self.set_on
# OutletInUse is always true (relay is always functional)
self.in_use_char = self.outlet_service.get_characteristic('OutletInUse')
self.in_use_char.set_value(True)
def set_on(self, value):
"""Called when HomeKit wants to turn outlet on/off."""
power_state = "on" if value else "off"
payload = {
"type": "relay",
"payload": {"power": power_state}
}
self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"])
def update_state(self, state_payload):
"""Update state from API event."""
if "power" in state_payload:
is_on = state_payload["power"] == "on"
self.on_char.set_value(is_on)

View File

@@ -0,0 +1,46 @@
"""
Temperature & Humidity Sensor Accessory Implementation for HomeKit
Implements combined temperature and humidity sensor:
- CurrentTemperature (read-only)
- CurrentRelativeHumidity (read-only)
"""
from pyhap.accessory import Accessory
from pyhap.const import CATEGORY_SENSOR
class TempHumidityAccessory(Accessory):
"""Combined temperature and humidity sensor."""
category = CATEGORY_SENSOR
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
"""
Initialize the temp/humidity sensor accessory.
Args:
driver: HAP driver instance
device: Device object from DeviceRegistry
api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
"""
name = display_name or device.friendly_name or device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
# Add TemperatureSensor service
self.temp_service = self.add_preload_service('TemperatureSensor')
self.current_temp_char = self.temp_service.get_characteristic('CurrentTemperature')
# Add HumiditySensor service
self.humidity_service = self.add_preload_service('HumiditySensor')
self.current_humidity_char = self.humidity_service.get_characteristic('CurrentRelativeHumidity')
def update_state(self, state_payload):
"""Update state from API event."""
if "temperature" in state_payload:
self.current_temp_char.set_value(float(state_payload["temperature"]))
if "humidity" in state_payload:
self.current_humidity_char.set_value(float(state_payload["humidity"]))

View File

@@ -0,0 +1,72 @@
"""
Thermostat Accessory Implementation for HomeKit
Implements thermostat control according to homekit_mapping.md:
- CurrentTemperature (read-only)
- TargetTemperature (read/write)
- CurrentHeatingCoolingState (fixed: 1 = heating)
- TargetHeatingCoolingState (fixed: 3 = auto)
"""
from pyhap.accessory import Accessory
from pyhap.const import CATEGORY_THERMOSTAT
class ThermostatAccessory(Accessory):
"""Thermostat with temperature control."""
category = CATEGORY_THERMOSTAT
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
"""
Initialize the thermostat accessory.
Args:
driver: HAP driver instance
device: Device object from DeviceRegistry
api_client: ApiClient for sending commands
display_name: Optional display name (defaults to device.friendly_name)
"""
name = display_name or device.friendly_name or device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
# Add Thermostat service
self.thermostat_service = self.add_preload_service('Thermostat')
# Get characteristics
self.current_temp_char = self.thermostat_service.get_characteristic('CurrentTemperature')
self.target_temp_char = self.thermostat_service.get_characteristic('TargetTemperature')
self.current_heating_cooling_char = self.thermostat_service.get_characteristic('CurrentHeatingCoolingState')
self.target_heating_cooling_char = self.thermostat_service.get_characteristic('TargetHeatingCoolingState')
# Set callback for target temperature
self.target_temp_char.setter_callback = self.set_target_temperature
# Set fixed heating/cooling states (mode is always "auto")
# CurrentHeatingCoolingState: 0=Off, 1=Heat, 2=Cool
self.current_heating_cooling_char.set_value(1) # Always heating
# TargetHeatingCoolingState: 0=Off, 1=Heat, 2=Cool, 3=Auto
self.target_heating_cooling_char.set_value(3) # Always auto
# Set temperature range (5-30°C as per UI)
self.target_temp_char.properties['minValue'] = 5
self.target_temp_char.properties['maxValue'] = 30
self.target_temp_char.properties['minStep'] = 0.5
def set_target_temperature(self, value):
"""Called when HomeKit wants to change target temperature."""
payload = {
"type": "thermostat",
"payload": {"target": float(value)}
}
self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"])
def update_state(self, state_payload):
"""Update state from API event."""
if "current" in state_payload:
self.current_temp_char.set_value(float(state_payload["current"]))
if "target" in state_payload:
self.target_temp_char.set_value(float(state_payload["target"]))

161
apps/homekit/api_client.py Normal file
View File

@@ -0,0 +1,161 @@
"""
API Client for HomeKit Bridge
Handles all HTTP communication with the REST API backend.
"""
import logging
from typing import Dict, List, Iterator, Optional
import httpx
import json
import time
logger = logging.getLogger(__name__)
class ApiClient:
"""HTTP client for communicating with the home automation API."""
def __init__(self, base_url: str, token: Optional[str] = None, timeout: int = 5):
"""
Initialize API client.
Args:
base_url: Base URL of the API (e.g., "http://192.168.1.100:8001")
token: Optional API token for authentication
timeout: Request timeout in seconds
"""
self.base_url = base_url.rstrip('/')
self.timeout = timeout
self.headers = {}
if token:
self.headers['Authorization'] = f'Bearer {token}'
def get_devices(self) -> List[Dict]:
"""
Get list of all devices.
Returns:
List of device dictionaries
"""
try:
response = httpx.get(
f'{self.base_url}/devices',
headers=self.headers,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to get devices: {e}")
raise
def get_layout(self) -> Dict:
"""
Get layout information (rooms and device assignments).
Returns:
Layout dictionary with room structure
"""
try:
response = httpx.get(
f'{self.base_url}/layout',
headers=self.headers,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to get layout: {e}")
raise
def get_device_state(self, device_id: str) -> Dict:
"""
Get current state of a specific device.
Args:
device_id: Device identifier
Returns:
Device state dictionary
"""
try:
response = httpx.get(
f'{self.base_url}/devices/{device_id}/state',
headers=self.headers,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to get state for {device_id}: {e}")
raise
def post_device_set(self, device_id: str, device_type: str, payload: Dict) -> None:
"""
Send command to a device.
Args:
device_id: Device identifier
device_type: Device type (e.g., "light", "thermostat")
payload: Command payload (e.g., {"power": "on", "brightness": 75})
"""
try:
data = {
"type": device_type,
"payload": payload
}
response = httpx.post(
f'{self.base_url}/devices/{device_id}/set',
headers=self.headers,
json=data,
timeout=self.timeout
)
response.raise_for_status()
logger.debug(f"Set {device_id}: {payload}")
except Exception as e:
logger.error(f"Failed to set {device_id}: {e}")
raise
def stream_realtime(self, reconnect_delay: int = 5) -> Iterator[Dict]:
"""
Stream real-time events from the API using Server-Sent Events (SSE).
Automatically reconnects on connection loss.
Args:
reconnect_delay: Seconds to wait before reconnecting
Yields:
Event dictionaries: {"type": "state", "device_id": "...", "payload": {...}, "ts": ...}
"""
while True:
try:
logger.info("Connecting to realtime event stream...")
with httpx.stream(
'GET',
f'{self.base_url}/realtime',
headers=self.headers,
timeout=None # No timeout for streaming
) as response:
response.raise_for_status()
logger.info("Connected to realtime event stream")
for line in response.iter_lines():
if line.startswith('data: '):
data_str = line[6:] # Remove 'data: ' prefix
try:
event = json.loads(data_str)
yield event
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse SSE event: {e}")
except httpx.HTTPError as e:
logger.error(f"Realtime stream error: {e}")
logger.info(f"Reconnecting in {reconnect_delay} seconds...")
time.sleep(reconnect_delay)
except Exception as e:
logger.error(f"Unexpected error in realtime stream: {e}")
logger.info(f"Reconnecting in {reconnect_delay} seconds...")
time.sleep(reconnect_delay)

View File

@@ -0,0 +1,138 @@
"""
Device Registry for HomeKit Bridge
Loads devices from API and joins with layout information.
"""
import logging
from typing import Dict, List, Optional
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@dataclass
class Device:
"""Represents a device with combined info from /devices and /layout."""
device_id: str
type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover"
name: str # Short name from /devices
friendly_name: str # Display title from /layout (fallback to name)
room: Optional[str] # Room name from layout
features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true})
read_only: bool # True for sensors that don't accept commands
class DeviceRegistry:
"""Registry of all devices loaded from the API."""
def __init__(self, devices: List[Device]):
"""
Initialize registry with devices.
Args:
devices: List of Device objects
"""
self._devices = devices
self._by_id = {d.device_id: d for d in devices}
@classmethod
def load_from_api(cls, api_client) -> 'DeviceRegistry':
"""
Load devices from API and join with layout information.
Args:
api_client: ApiClient instance
Returns:
DeviceRegistry with all devices
"""
# Get devices and layout
devices_data = api_client.get_devices()
layout_data = api_client.get_layout()
# Build lookup: device_id -> (room_name, title)
layout_map = {}
if isinstance(layout_data, dict) and 'rooms' in layout_data:
rooms_list = layout_data['rooms']
if isinstance(rooms_list, list):
for room in rooms_list:
if isinstance(room, dict):
room_name = room.get('name', 'Unknown')
devices_in_room = room.get('devices', [])
for device_info in devices_in_room:
if isinstance(device_info, dict):
device_id = device_info.get('device_id')
title = device_info.get('title', '')
if device_id:
layout_map[device_id] = (room_name, title)
# Create Device objects
devices = []
for dev_data in devices_data:
device_id = dev_data.get('device_id')
if not device_id:
logger.warning(f"Device without device_id: {dev_data}")
continue
# Get layout info
room_name, title = layout_map.get(device_id, (None, ''))
# Determine if read-only (sensors don't accept set commands)
device_type = dev_data.get('type', '')
read_only = device_type in ['contact', 'temp_humidity', 'motion', 'smoke']
device = Device(
device_id=device_id,
type=device_type,
name=dev_data.get('name', device_id),
friendly_name=title or dev_data.get('name', device_id),
room=room_name,
features=dev_data.get('features', {}),
read_only=read_only
)
devices.append(device)
logger.info(f"Loaded {len(devices)} devices from API")
return cls(devices)
def get_all(self) -> List[Device]:
"""Get all devices."""
return self._devices.copy()
def get_by_id(self, device_id: str) -> Optional[Device]:
"""
Get device by ID.
Args:
device_id: Device identifier
Returns:
Device or None if not found
"""
return self._by_id.get(device_id)
def get_by_type(self, device_type: str) -> List[Device]:
"""
Get all devices of a specific type.
Args:
device_type: Device type (e.g., "light", "thermostat")
Returns:
List of matching devices
"""
return [d for d in self._devices if d.type == device_type]
def get_by_room(self, room: str) -> List[Device]:
"""
Get all devices in a specific room.
Args:
room: Room name
Returns:
List of devices in the room
"""
return [d for d in self._devices if d.room == room]

View File

@@ -0,0 +1,111 @@
<!--
Copilot-Aufgabe: HomeKit-Accessory-Mapping-Tabelle aus bestehender Implementierung ableiten
Ziel:
Erstelle eine Mapping-Tabelle, die beschreibt, wie unsere abstrakten Gerätetypen
(type/Features aus devices.yaml, apps/abstraction und apps/api) auf HomeKit-Accessories
und Characteristics (HAP-Python) abgebildet werden sollen.
Die Tabelle soll:
- sich an der bestehenden Implementierung und Konfiguration orientieren:
- devices.yaml (Typen, Features)
- apps/abstraction (Capabilities, Payload-Strukturen, Topics)
- apps/api (DTOs, /devices-Response, /devices/{id}/set, /realtime-Events)
- für jeden bekannten Gerätetyp (z.B. light, thermostat, outlet, contact, temp_humidity, cover, switch)
festhalten:
- Abstract Type (type)
- Relevante Features (Features-Flags, z.B. power, brightness, color_hsb, target, current, contact, humidity)
- HomeKit Service (z.B. LightBulb, Thermostat, Outlet, ContactSensor, TemperatureSensor, HumiditySensor, WindowCovering)
- HomeKit Characteristics (z.B. On, Brightness, Hue, Saturation, CurrentTemperature, TargetTemperature, ContactSensorState, CurrentPosition, TargetPosition)
- State-Mapping (abstraktes payload → HomeKit-Characteristics)
- Set-Mapping (HomeKit-Characteristics → HTTP POST /devices/{id}/set Payload)
- Status (OK / REVIEW / TODO)
Vorgehen für Copilot:
1. Analysiere die bestehenden Typen und Features:
- devices.yaml → "type" und "features" pro Gerät
- apps/abstraction → Capabilities, State-/Set-Modelle (z.B. light@1.2.0, thermostat, contact, temp_humidity)
- apps/api:
- Response-Modell von GET /devices (device_id, type, name, features)
- Struktur von GET /devices/{id}/state (payload-Felder je Typ)
- Struktur der Events in /realtime (payload-Felder je Typ)
- Body von POST /devices/{id}/set (type, payload)
2. Lege aufgrund dieser Analyse alle aktuell verwendeten abstrakten Typen an, z.B.:
- light (mit power, brightness, color_hsb)
- thermostat (mit current, target, mode)
- outlet
- contact
- temp_humidity
- ggf. cover, switch, etc., falls vorhanden
3. Erstelle eine Tabelle in Markdown mit folgenden Spalten:
| Abstract Type | Features (aus devices.yaml) | HomeKit Service | HomeKit Characteristics | State-Mapping (payload → HK) | Set-Mapping (HK → payload) | Status |
|---------------|-----------------------------|-----------------|-------------------------|------------------------------|----------------------------|--------|
Beispiele für Inhalte:
- Abstract Type: `light`
- Features: `power, brightness, color_hsb`
- HomeKit Service: `LightBulb`
- HomeKit Characteristics: `On, Brightness, Hue, Saturation`
- State-Mapping: `payload.power -> On; payload.brightness -> Brightness; payload.hue -> Hue; payload.sat -> Saturation`
- Set-Mapping: `On -> {"type":"light","payload":{"power":"on"/"off"}} ...`
- Status: `OK` wenn Mapping klar, `REVIEW` wenn du unsicher bist, `TODO` wenn Informationen fehlen
4. Nutze vorhandene Informationen aus dem Code:
- Wenn ein Gerätetyp in apps/abstraction bereits ein klares State-/Set-Modell hat, übernimm das in State-/Set-Mapping.
- Wenn bestimmte Features noch nicht im Code genutzt werden, markiere die entsprechenden Mapping-Zeilen mit Status=TODO.
- Falls unklar ist, welcher HomeKit-Service für einen Typ am besten passt, mache einen Vorschlag und setze Status=REVIEW.
5. Markiere explizit, wo man später noch eingreifen muss:
- In der Spalte "Status" mit `REVIEW` oder `TODO`.
- Optional zusätzliche kurze Kommentare unter der Tabelle wie:
- `<!-- TODO: RGBW-Unterstützung prüfen -->`
- `<!-- REVIEW: Soll temp_humidity als zwei Accessories oder kombiniert abgebildet werden? -->`
Akzeptanz-Kriterien:
- Es existiert am Ende dieses Dokuments eine Markdown-Tabelle mit mindestens allen abstrakten Gerätetypen, die aktuell in devices.yaml / apps/abstraction / apps/api verwendet werden.
- Für jeden Typ sind HomeKit Service und Characteristics ausgefüllt oder mit Status=REVIEW/TODO markiert.
- State-/Set-Mapping ist für alle bereits gut verstandenen Typen (z.B. einfache Lichter, Thermostate, Kontakte, Temp/Humidity-Sensoren) konkret beschrieben.
- Stellen, an denen die aktuelle Implementierung keine klaren Informationen liefert, sind sichtbar mit Status=REVIEW oder TODO gekennzeichnet.
- Copilot ändert an keiner anderen Stelle des Codes etwas, sondern erzeugt nur diese Tabelle/Dokumentation.
-->
# HomeKit-Accessory-Mapping-Tabelle
Dieses Dokument beschreibt das Mapping zwischen unseren abstrakten Gerätetypen (aus `devices.yaml`, `apps/abstraction` und `apps/api`) und HomeKit-Accessories/Characteristics (HAP-Python).
## Mapping-Tabelle
| Abstract Type | Features (aus devices.yaml) | HomeKit Service | HomeKit Characteristics | State-Mapping (payload → HK) | Set-Mapping (HK → payload) | Status |
|---------------|-----------------------------|-----------------|-------------------------|------------------------------|----------------------------|--------|
| `light` | `power, brightness, color_hsb` | `LightBulb` | `On, Brightness, Hue, Saturation` | `payload.power -> On (true/false)`<br>`payload.brightness -> Brightness (0-100)`<br>`payload.hue -> Hue (0-360)`<br>`payload.sat -> Saturation (0-100)` | `On -> {"type":"light","payload":{"power":"on"/"off"}}`<br>`Brightness -> {"type":"light","payload":{"brightness":0-100}}`<br>`Hue/Saturation -> {"type":"light","payload":{"hue":0-360,"sat":0-100}}` | OK |
| `light` | `power, brightness` | `LightBulb` | `On, Brightness` | `payload.power -> On (true/false)`<br>`payload.brightness -> Brightness (0-100)` | `On -> {"type":"light","payload":{"power":"on"/"off"}}`<br>`Brightness -> {"type":"light","payload":{"brightness":0-100}}` | OK |
| `light` | `power` | `LightBulb` | `On` | `payload.power -> On (true/false)` | `On -> {"type":"light","payload":{"power":"on"/"off"}}` | OK |
| `thermostat` | `current, target` | `Thermostat` | `CurrentTemperature, TargetTemperature, CurrentHeatingCoolingState, TargetHeatingCoolingState` | `payload.current -> CurrentTemperature (°C)`<br>`payload.target -> TargetTemperature (°C)`<br>`CurrentHeatingCoolingState -> 1 (heat, fest)`<br>`TargetHeatingCoolingState -> 3 (auto, fest)` | `TargetTemperature -> {"type":"thermostat","payload":{"target":temp}}`<br>**Hinweis:** Mode ist immer "auto", keine Mode-Änderung über HomeKit | OK |
| `relay` | `power` | `Outlet` | `On, OutletInUse` | `payload.power -> On (true/false)`<br>`payload.power -> OutletInUse (true/false)` | `On -> {"type":"relay","payload":{"power":"on"/"off"}}` | OK |
| `contact` | `contact` | `ContactSensor` | `ContactSensorState` | `payload.contact -> ContactSensorState (0=detected, 1=not detected)` | N/A (read-only sensor) | OK |
| `temp_humidity` | `temperature, humidity` | `TemperatureSensor` + `HumiditySensor` | `CurrentTemperature, CurrentRelativeHumidity` | `payload.temperature -> CurrentTemperature (°C)`<br>`payload.humidity -> CurrentRelativeHumidity (%)` | N/A (read-only sensors) | REVIEW |
| `cover` | `position, tilt` | `WindowCovering` | `CurrentPosition, TargetPosition, CurrentHorizontalTiltAngle, TargetHorizontalTiltAngle` | `payload.position -> CurrentPosition (0-100)`<br>`payload.tilt -> CurrentHorizontalTiltAngle (-90 to 90)` | `TargetPosition -> {"type":"cover","payload":{"position":0-100}}`<br>`TargetHorizontalTiltAngle -> {"type":"cover","payload":{"tilt":-90 to 90}}` | TODO |
| `switch` | `power` | `Switch` | `On` | `payload.power -> On (true/false)` | `On -> {"type":"switch","payload":{"power":"on"/"off"}}` | OK |
## Offene Punkte und Kommentare
<!-- OK: thermostat - Verwendet nur Mode "auto" (TargetHeatingCoolingState=3, CurrentHeatingCoolingState=1). Keine Mode-Änderung über HomeKit möglich. -->
<!-- REVIEW: temp_humidity - Soll als ein kombiniertes Accessory oder zwei separate Accessories (TemperatureSensor + HumiditySensor) abgebildet werden? HAP-Python erlaubt beides -->
<!-- TODO: cover - Position/Tilt-Mapping validieren. Sind die Wertebereiche korrekt? Gibt es zusätzliche Features wie PositionState (opening/closing/stopped)? -->
<!-- TODO: RGBW-Unterstützung - Prüfen, ob separate ColorTemperature-Characteristic für Warmweiß/Kaltweiß benötigt wird -->
<!-- TODO: Prüfen, ob weitere Gerätetypen in devices.yaml existieren (z.B. motion, door, lock, fan, etc.) und diese ergänzen -->
## Hinweise zur Implementierung
- **State-Updates**: Alle State-Änderungen von MQTT/API werden auf entsprechende HomeKit-Characteristics gemappt und via `set_value()` aktualisiert
- **Set-Befehle**: HomeKit-Charakteristik-Änderungen werden in HTTP POST-Requests an `/devices/{id}/set` umgewandelt
- **Read-only Sensors**: Sensoren (contact, temp_humidity) unterstützen nur State-Updates, keine Set-Befehle
- **Bidirektionales Mapping**: Änderungen müssen in beide Richtungen synchronisiert werden (HomeKit ↔ Abstraction Layer)

272
apps/homekit/main.py Normal file
View File

@@ -0,0 +1,272 @@
"""
HomeKit Bridge Main Module
Implementiert eine HAP-Python Bridge, die Geräte über die REST-API lädt
und über HomeKit verfügbar macht.
Für detaillierte Implementierungsanweisungen, Tests und Deployment-Informationen
siehe README.md in diesem Verzeichnis.
"""
import os
import logging
import signal
import sys
import threading
from typing import Optional
from pyhap.accessory_driver import AccessoryDriver
from pyhap.accessory import Bridge
from .accessories.light import (
OnOffLightAccessory,
DimmableLightAccessory,
ColorLightAccessory,
)
from .accessories.thermostat import ThermostatAccessory
from .accessories.contact import ContactAccessory
from .accessories.sensor import TempHumidityAccessory
from .accessories.outlet import OutletAccessory
from .api_client import ApiClient
from .device_registry import DeviceRegistry
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Environment configuration
HOMEKIT_NAME = os.getenv("HOMEKIT_NAME", "Home Automation Bridge")
HOMEKIT_PIN = os.getenv("HOMEKIT_PIN", "031-45-154")
HOMEKIT_PORT = int(os.getenv("HOMEKIT_PORT", "51826"))
API_BASE = os.getenv("API_BASE", "http://api:8001")
HOMEKIT_API_TOKEN = os.getenv("HOMEKIT_API_TOKEN")
PERSIST_FILE = os.getenv("HOMEKIT_PERSIST_FILE", "homekit.state")
def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
"""
Build the HomeKit Bridge with all device accessories.
Args:
driver: HAP-Python AccessoryDriver
api_client: API client for communication with backend
Returns:
Bridge accessory with all device accessories attached
"""
logger.info("Loading devices from API...")
registry = DeviceRegistry.load_from_api(api_client)
devices = registry.get_all()
logger.info(f"Loaded {len(devices)} devices from API")
# Create bridge
bridge = Bridge(driver, HOMEKIT_NAME)
accessory_map = {} # device_id -> Accessory instance
for device in devices:
try:
accessory = create_accessory_for_device(device, api_client, driver)
if accessory:
# Set room information in the accessory (HomeKit will use this for suggestions)
if device.room:
# Store room info for potential future use
accessory._room_name = device.room
bridge.add_accessory(accessory)
accessory_map[device.device_id] = accessory
logger.info(f"Added accessory: {device.friendly_name} ({device.type}) in room: {device.room or 'Unknown'}")
else:
logger.warning(f"No accessory mapping for device: {device.name} ({device.type})")
except Exception as e:
logger.error(f"Failed to create accessory for {device.name}: {e}", exc_info=True)
# Store accessory_map on bridge for realtime updates
bridge._accessory_map = accessory_map
logger.info(f"Bridge built with {len(accessory_map)} accessories")
return bridge
def get_accessory_name(device) -> str:
"""
Build accessory name including room information.
Args:
device: Device object from DeviceRegistry
Returns:
Name string like "Device Name (Room)" or just "Device Name" if no room
"""
base_name = device.friendly_name or device.name
if device.room:
return f"{base_name} ({device.room})"
return base_name
def create_accessory_for_device(device, api_client: ApiClient, driver: AccessoryDriver):
"""
Create appropriate HomeKit accessory based on device type and features.
Maps device types to HomeKit accessories according to homekit_mapping.md.
"""
device_type = device.type
features = device.features
display_name = get_accessory_name(device)
# Light accessories
if device_type == "light":
if features.get("color_hsb"):
return ColorLightAccessory(driver, device, api_client, display_name=display_name)
elif features.get("brightness"):
return DimmableLightAccessory(driver, device, api_client, display_name=display_name)
else:
return OnOffLightAccessory(driver, device, api_client, display_name=display_name)
# Thermostat
elif device_type == "thermostat":
return ThermostatAccessory(driver, device, api_client, display_name=display_name)
# Contact sensor
elif device_type == "contact":
return ContactAccessory(driver, device, api_client, display_name=display_name)
# Temperature/Humidity sensor
elif device_type == "temp_humidity_sensor":
return TempHumidityAccessory(driver, device, api_client, display_name=display_name)
# Relay/Outlet
elif device_type == "relay":
return OutletAccessory(driver, device, api_client, display_name=display_name)
# Cover/Blinds (optional)
elif device_type == "cover":
# TODO: Implement CoverAccessory based on homekit_mapping.md
logger.warning(f"Cover accessory not yet implemented for {device.name}")
return None
# TODO: Add more device types as needed (lock, motion, etc.)
return None
def realtime_event_loop(api_client: ApiClient, bridge: Bridge, stop_event: threading.Event):
"""
Background thread that listens to realtime events and updates accessories.
Args:
api_client: API client
bridge: HomeKit bridge with accessories
stop_event: Threading event to signal shutdown
"""
logger.info("Starting realtime event loop...")
while not stop_event.is_set():
try:
for event in api_client.stream_realtime():
if stop_event.is_set():
break
# Handle state update events
if event.get("type") == "state":
device_id = event.get("device_id")
payload = event.get("payload", {})
# Find corresponding accessory
accessory = bridge._accessory_map.get(device_id)
if accessory and hasattr(accessory, 'update_state'):
try:
accessory.update_state(payload)
logger.debug(f"Updated state for {device_id}: {payload}")
except Exception as e:
logger.error(f"Error updating accessory {device_id}: {e}")
except Exception as e:
if not stop_event.is_set():
logger.error(f"Realtime stream error: {e}. Reconnecting in 5s...")
stop_event.wait(5) # Backoff before reconnect
logger.info("Realtime event loop stopped")
def main():
logger.info("=" * 60)
logger.info(f"Starting HomeKit Bridge: {HOMEKIT_NAME}")
logger.info(f"API Base: {API_BASE}")
logger.info(f"HomeKit Port: {HOMEKIT_PORT}")
logger.info(f"PIN: {HOMEKIT_PIN}")
logger.info("=" * 60)
# Create API client
api_client = ApiClient(
base_url=API_BASE,
token=HOMEKIT_API_TOKEN,
timeout=10
)
# Test API connectivity
try:
devices = api_client.get_devices()
logger.info(f"API connectivity OK - {len(devices)} devices available")
except Exception as e:
logger.error(f"Failed to connect to API at {API_BASE}: {e}")
logger.error("Please check API_BASE and network connectivity")
sys.exit(1)
# Create AccessoryDriver
driver = AccessoryDriver(
port=HOMEKIT_PORT,
persist_file=PERSIST_FILE
)
# Build bridge with all accessories
try:
bridge = build_bridge(driver, api_client)
except Exception as e:
logger.error(f"Failed to build bridge: {e}", exc_info=True)
sys.exit(1)
# Add bridge to driver
driver.add_accessory(accessory=bridge)
# Setup realtime event thread
stop_event = threading.Event()
realtime_thread = threading.Thread(
target=realtime_event_loop,
args=(api_client, bridge, stop_event),
daemon=True
)
# Signal handlers for graceful shutdown
def signal_handler(sig, frame):
logger.info("Received shutdown signal, stopping...")
stop_event.set()
driver.stop()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Start realtime thread
realtime_thread.start()
# Start the bridge
logger.info(f"HomeKit Bridge started on port {HOMEKIT_PORT}")
logger.info(f"Pair with PIN: {HOMEKIT_PIN}")
logger.info("Press Ctrl+C to stop")
try:
driver.start()
except KeyboardInterrupt:
logger.info("KeyboardInterrupt received")
finally:
logger.info("Stopping bridge...")
stop_event.set()
realtime_thread.join(timeout=5)
logger.info("Bridge stopped")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,13 @@
# HomeKit Bridge Dependencies
# HAP-Python - HomeKit Accessory Protocol implementation
HAP-python>=4.9.0
# HTTP client for API communication (REST API only - no MQTT)
httpx>=0.24.0
# Utilities
python-dotenv>=1.0.0 # For .env file support
pydantic>=2.0.0 # For data validation (shared with main app)
# Logging and monitoring (optional)
# sentry-sdk>=1.0.0 # Optional: Error tracking

76
apps/homekit/start_bridge.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/bash
# HomeKit Bridge Startup Script
# This script sets up the virtual environment, installs dependencies, and starts the bridge
set -e # Exit on error
# Determine script directory (apps/homekit)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Navigate to workspace root (two levels up from apps/homekit)
WORKSPACE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$WORKSPACE_ROOT"
echo "🏠 HomeKit Bridge Startup"
echo "========================="
echo " Working dir: $WORKSPACE_ROOT"
echo ""
# Virtual environment path
VENV_DIR="$SCRIPT_DIR/venv"
# Check if virtual environment exists
if [ ! -d "$VENV_DIR" ]; then
echo "📦 Virtual environment not found. Creating..."
# Try to use Python 3.12 or 3.13 (3.14 has compatibility issues with HAP-Python)
if command -v python3.13 &> /dev/null; then
PYTHON_CMD=python3.13
elif command -v python3.12 &> /dev/null; then
PYTHON_CMD=python3.12
elif command -v python3.11 &> /dev/null; then
PYTHON_CMD=python3.11
else
PYTHON_CMD=python3
echo "⚠️ Warning: Using default python3. HAP-Python may not work with Python 3.14+"
fi
echo " Using: $PYTHON_CMD"
$PYTHON_CMD -m venv "$VENV_DIR"
echo "✅ Virtual environment created at $VENV_DIR"
fi
# Activate virtual environment
echo "🔧 Activating virtual environment..."
source "$VENV_DIR/bin/activate"
# Install/update dependencies
echo "📥 Installing dependencies from requirements.txt..."
pip install --upgrade pip -q
pip install -r "$SCRIPT_DIR/requirements.txt" -q
echo "✅ Dependencies installed"
# Set environment variables (with defaults)
export HOMEKIT_NAME="${HOMEKIT_NAME:-Home Automation Bridge}"
export HOMEKIT_PIN="${HOMEKIT_PIN:-031-45-154}"
export HOMEKIT_PORT="${HOMEKIT_PORT:-51826}"
export API_BASE="${API_BASE:-http://172.19.1.11:8001}"
export HOMEKIT_API_TOKEN="${HOMEKIT_API_TOKEN:-}"
export HOMEKIT_PERSIST_FILE="${HOMEKIT_PERSIST_FILE:-$SCRIPT_DIR/homekit.state}"
# Display configuration
echo ""
echo "⚙️ Configuration:"
echo " Bridge Name: $HOMEKIT_NAME"
echo " Bridge PIN: $HOMEKIT_PIN"
echo " Bridge Port: $HOMEKIT_PORT"
echo " API Base URL: $API_BASE"
echo " Persist File: $HOMEKIT_PERSIST_FILE"
echo ""
# Start the bridge
echo "🚀 Starting HomeKit Bridge..."
echo " (Press Ctrl+C to stop)"
echo ""
# Run the bridge from workspace root with correct module path
python -m apps.homekit.main

53
apps/rules/Dockerfile Normal file
View File

@@ -0,0 +1,53 @@
# Rules Engine Dockerfile
# Event-driven automation rules processor with MQTT and Redis
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
RULES_CONFIG=config/rules.yaml \
MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883 \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_DB=8 \
LOG_LEVEL=INFO
# Create non-root user
RUN addgroup -g 10001 -S app && \
adduser -u 10001 -S app -G app
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apk add --no-cache \
gcc \
musl-dev \
linux-headers
# Install Python dependencies
COPY apps/rules/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY apps/__init__.py /app/apps/
COPY apps/rules/ /app/apps/rules/
COPY packages/ /app/packages/
COPY config/ /app/config/
# Change ownership to non-root user
RUN chown -R app:app /app
# Switch to non-root user
USER app
# Expose no ports (MQTT/Redis client only)
# Health check (check if process is running)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD pgrep -f "apps.rules.main" || exit 1
# Run the rules engine
CMD ["python", "-m", "apps.rules.main"]

View File

@@ -0,0 +1,15 @@
"""
Rule Implementations Package
This package contains all rule implementation modules.
Naming Convention:
- Module name: snake_case matching the rule type name
Example: window_setback.py for type 'window_setback@1.0'
- Class name: PascalCase + 'Rule' suffix
Example: WindowSetbackRule
The rule engine uses load_rule() from rule_interface to dynamically
import modules from this package based on the 'type' field in rules.yaml.
"""

View File

@@ -0,0 +1,256 @@
"""
Example Rule Implementation: Window Setback
Demonstrates how to implement a Rule using the rule_interface.
This rule lowers thermostat temperature when a window is opened.
"""
from typing import Any
from pydantic import BaseModel, Field, ValidationError
from apps.rules.rule_interface import Rule, RuleDescriptor, RuleContext
class WindowSetbackObjects(BaseModel):
"""Object structure for window setback rule"""
contacts: list[str] = Field(..., min_length=1, description="Contact sensors to monitor")
thermostats: list[str] = Field(..., min_length=1, description="Thermostats to control")
class WindowSetbackRule(Rule):
"""
Window setback automation rule.
When a window/door contact opens, set thermostats to eco temperature.
When closed for a minimum duration, restore previous target temperature.
Configuration:
objects:
contacts: List of contact sensor device IDs to monitor (required, min 1)
thermostats: List of thermostat device IDs to control (required, min 1)
params:
eco_target: Temperature to set when window opens (default: 16.0)
open_min_secs: Minimum seconds window must be open before triggering (default: 20)
close_min_secs: Minimum seconds window must be closed before restoring (default: 20)
previous_target_ttl_secs: How long to remember previous temperature (default: 86400)
State storage (Redis keys):
rule:{rule_id}:contact:{device_id}:state -> "open" | "closed"
rule:{rule_id}:contact:{device_id}:ts -> ISO timestamp of last change
rule:{rule_id}:thermo:{device_id}:current_target -> Current target temp (updated on every STATE)
rule:{rule_id}:thermo:{device_id}:previous -> Previous target temp (saved on window open, deleted on restore)
Logic:
1. Thermostat STATE events → update current_target in Redis
2. Window opens → copy current_target to previous, then set to eco_target
3. Window closes → restore from previous, then delete previous key
"""
def __init__(self):
super().__init__()
self._validated_objects: dict[str, WindowSetbackObjects] = {}
async def setup(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
"""Validate objects structure during setup"""
try:
validated = WindowSetbackObjects(**desc.objects)
self._validated_objects[desc.id] = validated
ctx.logger.info(
f"Rule {desc.id} validated: {len(validated.contacts)} contacts, "
f"{len(validated.thermostats)} thermostats"
)
except ValidationError as e:
raise ValueError(
f"Invalid objects configuration for rule {desc.id}: {e}"
) from e
def get_subscriptions(self, desc: RuleDescriptor) -> list[str]:
"""
Return MQTT topics to subscribe to.
Subscribe to:
- Contact sensor state changes (to detect window open/close)
- Thermostat state changes (to track current target temperature)
"""
topics = []
# Subscribe to contact sensors
contacts = desc.objects.get('contacts', [])
for contact_id in contacts:
topics.append(f"home/contact/{contact_id}/state")
# Subscribe to thermostats to track their current target temperature
thermostats = desc.objects.get('thermostats', [])
for thermo_id in thermostats:
topics.append(f"home/thermostat/{thermo_id}/state")
return topics
async def on_event(
self,
evt: dict[str, Any],
desc: RuleDescriptor,
ctx: RuleContext
) -> None:
"""
Process contact sensor or thermostat state changes.
Logic:
1. If contact opened → remember current thermostat targets, set to eco
2. If contact closed for min_secs → restore previous targets
3. If thermostat target changed → update stored previous value
"""
device_id = evt['device_id']
cap = evt['cap']
payload = evt['payload']
# Only process events for devices in our objects
target_contacts = desc.objects.get('contacts', [])
target_thermostats = desc.objects.get('thermostats', [])
if cap == 'contact' and device_id in target_contacts:
await self._handle_contact_event(evt, desc, ctx)
elif cap == 'thermostat' and device_id in target_thermostats:
await self._handle_thermostat_event(evt, desc, ctx)
async def _handle_contact_event(
self,
evt: dict[str, Any],
desc: RuleDescriptor,
ctx: RuleContext
) -> None:
"""Handle contact sensor state change."""
device_id = evt['device_id']
contact_state = evt['payload'].get('contact') # "open" or "closed"
event_ts = evt.get('ts', ctx.now().isoformat())
if not contact_state:
ctx.logger.warning(f"Contact event missing 'contact' field: {evt}")
return
# Store current state and timestamp
state_key = f"rule:{desc.id}:contact:{device_id}:state"
ts_key = f"rule:{desc.id}:contact:{device_id}:ts"
await ctx.redis.set(state_key, contact_state)
await ctx.redis.set(ts_key, event_ts)
if contact_state == 'open':
await self._on_window_opened(desc, ctx)
elif contact_state == 'closed':
await self._on_window_closed(desc, ctx)
async def _on_window_opened(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
"""
Window opened - save current temperatures, then set thermostats to eco.
Important: We must save the current target BEFORE setting to eco,
otherwise we'll save the eco temperature instead of the original.
"""
eco_target = desc.params.get('eco_target', 16.0)
target_thermostats = desc.objects.get('thermostats', [])
ttl_secs = desc.params.get('previous_target_ttl_secs', 86400)
ctx.logger.info(
f"Rule {desc.id}: Window opened, setting {len(target_thermostats)} "
f"thermostats to eco temperature {eco_target}°C"
)
# FIRST: Save current target temperatures as "previous" (before we change them!)
for thermo_id in target_thermostats:
current_key = f"rule:{desc.id}:thermo:{thermo_id}:current_target"
current_temp_str = await ctx.redis.get(current_key)
if current_temp_str:
# Save current as previous (with TTL)
prev_key = f"rule:{desc.id}:thermo:{thermo_id}:previous"
await ctx.redis.set(prev_key, current_temp_str, ttl_secs=ttl_secs)
ctx.logger.debug(
f"Saved previous target for {thermo_id}: {current_temp_str}°C"
)
else:
ctx.logger.warning(
f"No current target found for {thermo_id}, cannot save previous"
)
# THEN: Set all thermostats to eco temperature
for thermo_id in target_thermostats:
try:
await ctx.mqtt.publish_set_thermostat(thermo_id, eco_target)
ctx.logger.debug(f"Set {thermo_id} to {eco_target}°C")
except Exception as e:
ctx.logger.error(f"Failed to set {thermo_id}: {e}")
async def _on_window_closed(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
"""
Window closed - restore previous temperatures.
Note: This is simplified. A production implementation would check
close_min_secs and use a timer/scheduler.
"""
target_thermostats = desc.objects.get('thermostats', [])
ctx.logger.info(
f"Rule {desc.id}: Window closed, restoring {len(target_thermostats)} "
f"thermostats to previous temperatures"
)
# Restore previous temperatures
for thermo_id in target_thermostats:
prev_key = f"rule:{desc.id}:thermo:{thermo_id}:previous"
prev_temp_str = await ctx.redis.get(prev_key)
if prev_temp_str:
try:
prev_temp = float(prev_temp_str)
await ctx.mqtt.publish_set_thermostat(thermo_id, prev_temp)
ctx.logger.debug(f"Restored {thermo_id} to {prev_temp}°C")
# Delete the previous key after restoring
await ctx.redis.delete(prev_key)
except Exception as e:
ctx.logger.error(f"Failed to restore {thermo_id}: {e}")
else:
ctx.logger.warning(
f"No previous target found for {thermo_id}, cannot restore"
)
async def _handle_thermostat_event(
self,
evt: dict[str, Any],
desc: RuleDescriptor,
ctx: RuleContext
) -> None:
"""
Handle thermostat state change - track current target temperature.
This keeps a record of the thermostat's current target, so we can
save it as "previous" when a window opens.
Important: We store in "current_target", NOT "previous". The "previous"
key is only written when a window opens, to avoid race conditions.
"""
device_id = evt['device_id']
payload = evt['payload']
current_target = payload.get('target')
if current_target is None:
return # No target in this state update
# Store current target (always update, even if it's the eco temperature)
current_key = f"rule:{desc.id}:thermo:{device_id}:current_target"
ttl_secs = desc.params.get('previous_target_ttl_secs', 86400)
await ctx.redis.set(current_key, str(current_target), ttl_secs=ttl_secs)
ctx.logger.debug(
f"Rule {desc.id}: Updated current target for {device_id}: {current_target}°C"
)
# Rule registry - maps rule type to implementation class
RULE_IMPLEMENTATIONS = {
'window_setback@1.0': WindowSetbackRule,
}

View File

@@ -1,83 +1,374 @@
"""Rules main entry point.""" """
Rules Engine
Loads rules configuration, subscribes to MQTT events, and dispatches events
to registered rule implementations.
"""
import asyncio
import logging import logging
import os
import signal import signal
import sys import sys
import time from datetime import datetime
from typing import NoReturn from typing import Any
from apscheduler.schedulers.background import BackgroundScheduler from apps.rules.rules_config import load_rules_config
from apps.rules.rule_interface import (
RuleDescriptor,
RuleContext,
MQTTClient,
RedisState,
load_rule
)
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Global scheduler instance
scheduler: BackgroundScheduler | None = None
class RuleEngine:
def rule_tick() -> None:
"""Example job that runs every minute.
This is a placeholder for actual rule evaluation logic.
""" """
logger.info("Rule tick") Rule engine that loads rules, subscribes to MQTT events,
and dispatches them to registered rule implementations.
def shutdown_handler(signum: int, frame: object) -> NoReturn:
"""Handle shutdown signals gracefully.
Args:
signum: Signal number
frame: Current stack frame
""" """
logger.info(f"Received signal {signum}, shutting down...")
if scheduler: def __init__(
scheduler.shutdown(wait=True) self,
logger.info("Scheduler stopped") rules_config_path: str,
sys.exit(0) mqtt_broker: str,
mqtt_port: int,
redis_url: str
):
"""
Initialize rule engine.
Args:
rules_config_path: Path to rules.yaml
mqtt_broker: MQTT broker hostname/IP
mqtt_port: MQTT broker port
redis_url: Redis connection URL
"""
self.rules_config_path = rules_config_path
self.mqtt_broker = mqtt_broker
self.mqtt_port = mqtt_port
self.redis_url = redis_url
# Will be initialized in setup()
self.rule_descriptors: list[RuleDescriptor] = []
self.rules: dict[str, Any] = {} # rule_id -> Rule instance
self.mqtt_client: MQTTClient | None = None
self.redis_state: RedisState | None = None
self.context: RuleContext | None = None
self._mqtt_topics: list[str] = [] # Topics to subscribe to
# For graceful shutdown
self._shutdown_event = asyncio.Event()
async def setup(self) -> None:
"""
Load configuration and instantiate rules.
Raises:
ImportError: If rule implementation not found
ValueError: If configuration is invalid
"""
logger.info(f"Loading rules configuration from {self.rules_config_path}")
# Load rules configuration
config = load_rules_config(self.rules_config_path)
self.rule_descriptors = config.rules
logger.info(f"Loaded {len(self.rule_descriptors)} rule(s) from configuration")
# Instantiate each rule
for desc in self.rule_descriptors:
if not desc.enabled:
logger.info(f" - {desc.id} (type: {desc.type}) [DISABLED]")
continue
try:
rule_instance = load_rule(desc)
self.rules[desc.id] = rule_instance
logger.info(f" - {desc.id} (type: {desc.type})")
except Exception as e:
logger.error(f"Failed to load rule {desc.id} (type: {desc.type}): {e}")
raise
enabled_count = len(self.rules)
total_count = len(self.rule_descriptors)
disabled_count = total_count - enabled_count
logger.info(f"Successfully loaded {enabled_count} rule implementation(s) ({disabled_count} disabled)")
# Call setup on each rule for validation
for rule_id, rule_instance in self.rules.items():
desc = next((d for d in self.rule_descriptors if d.id == rule_id), None)
if desc:
try:
ctx = RuleContext(
logger=logger,
mqtt_publisher=self.mqtt_client,
redis_state=self.redis_state
)
await rule_instance.setup(desc, ctx)
except Exception as e:
logger.error(f"Failed to setup rule {rule_id}: {e}")
raise
# Collect MQTT subscriptions from all enabled rules
all_topics = set()
for rule_id, rule_instance in self.rules.items():
desc = next((d for d in self.rule_descriptors if d.id == rule_id), None)
if desc:
try:
topics = rule_instance.get_subscriptions(desc)
all_topics.update(topics)
logger.debug(f"Rule {rule_id} subscribes to {len(topics)} topic(s)")
except Exception as e:
logger.error(f"Failed to get subscriptions for rule {rule_id}: {e}")
raise
logger.info(f"Total MQTT subscriptions needed: {len(all_topics)}")
# Create unique client ID to avoid conflicts
import uuid
import os
client_id_base = "rule_engine"
client_suffix = os.environ.get("MQTT_CLIENT_ID_SUFFIX") or uuid.uuid4().hex[:6]
unique_client_id = f"{client_id_base}-{client_suffix}"
# Initialize MQTT client
self.mqtt_client = MQTTClient(
broker=self.mqtt_broker,
port=self.mqtt_port,
client_id=unique_client_id
)
self.mqtt_client.set_logger(logger)
# Store topics for connection
self._mqtt_topics = list(all_topics)
# Initialize Redis state
self.redis_state = RedisState(self.redis_url)
# Create MQTT publisher wrapper for RuleContext
from apps.rules.rule_interface import MQTTPublisher
mqtt_publisher = MQTTPublisher(mqtt_client=self.mqtt_client)
# Create rule context
self.context = RuleContext(
logger=logger,
mqtt_publisher=mqtt_publisher,
redis_state=self.redis_state,
now_fn=datetime.now
)
def _filter_rules_for_event(self, event: dict[str, Any]) -> list[tuple[str, RuleDescriptor]]:
"""
Filter rules that should receive this event.
Rules match if the event's device_id is in the rule's objects.
Args:
event: Normalized MQTT event
Returns:
List of (rule_id, descriptor) tuples that should process this event
"""
matching_rules = []
device_id = event.get('device_id')
cap = event.get('cap')
if not device_id or not cap:
return matching_rules
logger.debug(f"Filtering for cap={cap}, device_id={device_id}")
# Only check enabled rules (rules in self.rules dict)
for rule_id, rule_instance in self.rules.items():
desc = next((d for d in self.rule_descriptors if d.id == rule_id), None)
if not desc:
continue
objects = desc.objects
# Check if this device is in the rule's objects
matched = False
if cap == 'contact' and objects.get('contacts'):
logger.debug(f"Rule {rule_id}: checking contacts {objects.get('contacts')}")
if device_id in objects.get('contacts', []):
matched = True
elif cap == 'thermostat' and objects.get('thermostats'):
logger.debug(f"Rule {rule_id}: checking thermostats {objects.get('thermostats')}")
if device_id in objects.get('thermostats', []):
matched = True
elif cap == 'light' and objects.get('lights'):
logger.debug(f"Rule {rule_id}: checking lights {objects.get('lights')}")
if device_id in objects.get('lights', []):
matched = True
elif cap == 'relay' and objects.get('relays'):
logger.debug(f"Rule {rule_id}: checking relays {objects.get('relays')}")
if device_id in objects.get('relays', []):
matched = True
if matched:
matching_rules.append((rule_id, desc))
return matching_rules
async def _dispatch_event(self, event: dict[str, Any]) -> None:
"""
Dispatch event to matching rules.
Calls rule.on_event() for each matching rule sequentially
to preserve order and avoid race conditions.
Args:
event: Normalized MQTT event
"""
# Debug logging
logger.debug(f"Received event: {event}")
matching_rules = self._filter_rules_for_event(event)
if not matching_rules:
# No rules interested in this event
logger.debug(f"No matching rules for {event.get('cap')}/{event.get('device_id')}")
return
logger.info(
f"Event {event['cap']}/{event['device_id']}: "
f"{len(matching_rules)} matching rule(s)"
)
# Process rules sequentially to preserve order
for rule_id, desc in matching_rules:
rule = self.rules.get(rule_id)
if not rule:
logger.warning(f"Rule instance not found for {rule_id}")
continue
try:
await rule.on_event(event, desc, self.context)
except Exception as e:
logger.error(
f"Error in rule {rule_id} processing event "
f"{event['cap']}/{event['device_id']}: {e}",
exc_info=True
)
# Continue with other rules
async def run(self) -> None:
"""
Main event loop - subscribe to MQTT and process events.
Runs until shutdown signal received.
"""
logger.info("Starting event processing loop")
try:
async for event in self.mqtt_client.connect(topics=self._mqtt_topics):
# Check for shutdown
if self._shutdown_event.is_set():
logger.info("Shutdown signal received, stopping event loop")
break
# Dispatch event to matching rules
await self._dispatch_event(event)
except asyncio.CancelledError:
logger.info("Event loop cancelled")
raise
except Exception as e:
logger.error(f"Fatal error in event loop: {e}", exc_info=True)
raise
async def shutdown(self) -> None:
"""Graceful shutdown - close connections."""
logger.info("Shutting down rule engine...")
self._shutdown_event.set()
if self.redis_state:
await self.redis_state.close()
logger.info("Redis connection closed")
logger.info("Shutdown complete")
async def main_async() -> None:
"""Async main function."""
# Read configuration from environment
rules_config = os.getenv('RULES_CONFIG', 'config/rules.yaml')
mqtt_broker = os.getenv('MQTT_BROKER', '172.16.2.16')
mqtt_port = int(os.getenv('MQTT_PORT', '1883'))
redis_host = os.getenv('REDIS_HOST', '172.23.1.116')
redis_port = int(os.getenv('REDIS_PORT', '6379'))
redis_db = int(os.getenv('REDIS_DB', '8'))
redis_url = f'redis://{redis_host}:{redis_port}/{redis_db}'
logger.info("=" * 60)
logger.info("Rules Engine Starting")
logger.info("=" * 60)
logger.info(f"Config: {rules_config}")
logger.info(f"MQTT: {mqtt_broker}:{mqtt_port}")
logger.info(f"Redis: {redis_url}")
logger.info("=" * 60)
# Initialize engine
engine = RuleEngine(
rules_config_path=rules_config,
mqtt_broker=mqtt_broker,
mqtt_port=mqtt_port,
redis_url=redis_url
)
# Load rules
try:
await engine.setup()
except Exception as e:
logger.error(f"Failed to setup engine: {e}", exc_info=True)
sys.exit(1)
# Setup signal handlers for graceful shutdown
loop = asyncio.get_running_loop()
main_task = None
def signal_handler():
logger.info("Received shutdown signal")
engine._shutdown_event.set()
if main_task and not main_task.done():
main_task.cancel()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, signal_handler)
# Run engine
try:
main_task = asyncio.create_task(engine.run())
await main_task
except asyncio.CancelledError:
logger.info("Main task cancelled")
finally:
await engine.shutdown()
def main() -> None: def main() -> None:
"""Run the rules application.""" """Entry point for rule engine."""
global scheduler
logger.info("Rules engine starting...")
# Register signal handlers
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGTERM, shutdown_handler)
# Initialize scheduler
scheduler = BackgroundScheduler()
# Add example job - runs every minute
scheduler.add_job(
rule_tick,
'interval',
minutes=1,
id='rule_tick',
name='Rule Tick Job'
)
# Start scheduler
scheduler.start()
logger.info("Scheduler started with rule_tick job (every 1 minute)")
# Run initial tick immediately
rule_tick()
# Keep the application running
try: try:
while True: asyncio.run(main_async())
time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("KeyboardInterrupt received, shutting down...") logger.info("Keyboard interrupt received")
scheduler.shutdown(wait=True) except Exception as e:
logger.info("Scheduler stopped") logger.error(f"Fatal error: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -0,0 +1,5 @@
# Rules Engine Dependencies
pydantic>=2.0
redis>=5.0.1
aiomqtt>=2.0.1
pyyaml>=6.0.1

View File

@@ -0,0 +1,759 @@
"""
Rule Interface and Context Objects
Provides the core abstractions for implementing automation rules:
- RuleDescriptor: Configuration data for a rule instance
- RedisState: State persistence interface
- RuleContext: Runtime context provided to rules
- Rule: Abstract base class for all rule implementations
"""
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Any, Awaitable, Optional
from pydantic import BaseModel, Field
class RuleDescriptor(BaseModel):
"""
Configuration descriptor for a rule instance.
This is the validated representation of a rule from rules.yaml.
The engine loads these and passes them to rule implementations.
The 'objects' field is intentionally flexible (dict) to allow different
rule types to define their own object structures.
"""
id: str = Field(..., description="Unique identifier for this rule instance")
name: Optional[str] = Field(None, description="Optional human-readable name")
type: str = Field(..., description="Rule type with version (e.g., 'window_setback@1.0')")
enabled: bool = Field(default=True, description="Whether this rule is enabled")
objects: dict[str, Any] = Field(
default_factory=dict,
description="Objects this rule monitors or controls (structure varies by rule type)"
)
params: dict[str, Any] = Field(
default_factory=dict,
description="Rule-specific parameters"
)
class RedisState:
"""
Async Redis-backed state persistence for rules with automatic reconnection.
Provides a simple key-value and hash storage interface for rules to persist
state across restarts. All operations are asynchronous and include retry logic
for robustness against temporary Redis outages.
Key Convention:
- Callers should use keys like: f"rules:{rule_id}:contact:{device_id}"
- This class does NOT enforce key prefixes - caller controls the full key
"""
def __init__(self, url: str, max_retries: int = 3, retry_delay: float = 0.5):
"""
Initialize RedisState with connection URL.
Args:
url: Redis connection URL (e.g., 'redis://172.23.1.116:6379/8')
max_retries: Maximum number of retry attempts for operations (default: 3)
retry_delay: Initial delay between retries in seconds, uses exponential backoff (default: 0.5)
Note:
Connection is lazy - actual connection happens on first operation.
Uses connection pooling with automatic reconnection on failure.
"""
self._url = url
self._max_retries = max_retries
self._retry_delay = retry_delay
self._redis: Optional[Any] = None # redis.asyncio.Redis instance
async def _get_client(self):
"""
Get or create Redis client with connection pool.
Lazy initialization ensures we don't connect until first use.
Uses decode_responses=True for automatic UTF-8 decoding.
"""
if self._redis is None:
import redis.asyncio as aioredis
self._redis = await aioredis.from_url(
self._url,
decode_responses=True, # Automatic UTF-8 decode
encoding='utf-8',
max_connections=10, # Connection pool size
socket_connect_timeout=5,
socket_keepalive=True,
health_check_interval=30 # Auto-check connection health
)
return self._redis
async def _execute_with_retry(self, operation, *args, **kwargs):
"""
Execute Redis operation with exponential backoff retry.
Handles temporary connection failures gracefully by retrying
with exponential backoff. On permanent failure, raises the
original exception.
Args:
operation: Async callable (Redis method)
*args, **kwargs: Arguments to pass to operation
Returns:
Result of the operation
Raises:
Exception: If all retries are exhausted
"""
import asyncio
last_exception = None
for attempt in range(self._max_retries):
try:
client = await self._get_client()
return await operation(client, *args, **kwargs)
except Exception as e:
last_exception = e
if attempt < self._max_retries - 1:
# Exponential backoff: 0.5s, 1s, 2s, ...
delay = self._retry_delay * (2 ** attempt)
await asyncio.sleep(delay)
# Reset client to force reconnection
if self._redis:
try:
await self._redis.close()
except:
pass
self._redis = None
# All retries exhausted
raise last_exception
# JSON helpers for complex data structures
def _dumps(self, obj: Any) -> str:
"""Serialize Python object to JSON string."""
import json
return json.dumps(obj, ensure_ascii=False)
def _loads(self, s: str) -> Any:
"""Deserialize JSON string to Python object."""
import json
return json.loads(s)
async def get(self, key: str) -> Optional[str]:
"""
Get a string value by key.
Args:
key: Redis key (e.g., "rules:my_rule:contact:sensor_1")
Returns:
String value or None if key doesn't exist
Example:
>>> state = RedisState("redis://localhost:6379/0")
>>> await state.set("rules:r1:temp", "22.5")
>>> temp = await state.get("rules:r1:temp")
>>> print(temp) # "22.5"
"""
async def _get(client, k):
return await client.get(k)
return await self._execute_with_retry(_get, key)
async def set(self, key: str, value: str, ttl_secs: Optional[int] = None) -> None:
"""
Set a string value with optional TTL.
Args:
key: Redis key
value: String value to store
ttl_secs: Optional time-to-live in seconds. If None, key persists indefinitely.
Example:
>>> state = RedisState("redis://localhost:6379/0")
>>> # Store with 1 hour TTL
>>> await state.set("rules:r1:previous_temp", "20.0", ttl_secs=3600)
"""
async def _set(client, k, v, ttl):
if ttl is not None:
await client.setex(k, ttl, v)
else:
await client.set(k, v)
await self._execute_with_retry(_set, key, value, ttl_secs)
async def hget(self, key: str, field: str) -> Optional[str]:
"""
Get a hash field value.
Args:
key: Redis hash key
field: Field name within the hash
Returns:
String value or None if field doesn't exist
Example:
>>> state = RedisState("redis://localhost:6379/0")
>>> await state.hset("rules:r1:device_states", "sensor_1", "open")
>>> value = await state.hget("rules:r1:device_states", "sensor_1")
>>> print(value) # "open"
"""
async def _hget(client, k, f):
return await client.hget(k, f)
return await self._execute_with_retry(_hget, key, field)
async def hset(self, key: str, field: str, value: str) -> None:
"""
Set a hash field value.
Args:
key: Redis hash key
field: Field name within the hash
value: String value to store
Example:
>>> state = RedisState("redis://localhost:6379/0")
>>> await state.hset("rules:r1:sensors", "bedroom", "open")
>>> await state.hset("rules:r1:sensors", "kitchen", "closed")
"""
async def _hset(client, k, f, v):
await client.hset(k, f, v)
await self._execute_with_retry(_hset, key, field, value)
async def expire(self, key: str, ttl_secs: int) -> None:
"""
Set or update TTL on an existing key.
Args:
key: Redis key
ttl_secs: Time-to-live in seconds
Example:
>>> state = RedisState("redis://localhost:6379/0")
>>> await state.set("rules:r1:temp", "22.5")
>>> await state.expire("rules:r1:temp", 3600) # Expire in 1 hour
"""
async def _expire(client, k, ttl):
await client.expire(k, ttl)
await self._execute_with_retry(_expire, key, ttl_secs)
async def delete(self, key: str) -> None:
"""
Delete a key from Redis.
Args:
key: Redis key to delete
Example:
>>> state = RedisState("redis://localhost:6379/0")
>>> await state.set("rules:r1:temp", "22.5")
>>> await state.delete("rules:r1:temp")
"""
async def _delete(client, k):
await client.delete(k)
await self._execute_with_retry(_delete, key)
async def close(self) -> None:
"""
Close Redis connection and cleanup resources.
Should be called when shutting down the application.
"""
if self._redis:
await self._redis.close()
self._redis = None
class MQTTClient:
"""
Async MQTT client for rule engine with event normalization and publishing.
Subscribes to device state topics, normalizes events to a consistent format,
and provides high-level publishing methods for device commands.
Event Normalization:
All incoming MQTT messages are parsed into a normalized event structure:
{
"topic": "home/contact/sensor_1/state",
"type": "state",
"cap": "contact", # Capability type (contact, thermostat, light, etc.)
"device_id": "sensor_1",
"payload": {"contact": "open"},
"ts": "2025-11-11T10:30:45.123456"
}
"""
def __init__(
self,
broker: str,
port: int = 1883,
client_id: str = "rule_engine",
reconnect_interval: int = 5,
max_reconnect_delay: int = 300
):
"""
Initialize MQTT client.
Args:
broker: MQTT broker hostname or IP
port: MQTT broker port (default: 1883)
client_id: Unique client ID for this connection
reconnect_interval: Initial reconnect delay in seconds (default: 5)
max_reconnect_delay: Maximum reconnect delay in seconds (default: 300)
"""
self._broker = broker
self._port = port
self._client_id = client_id
self._reconnect_interval = reconnect_interval
self._max_reconnect_delay = max_reconnect_delay
self._client = None
self._logger = None # Set externally
def set_logger(self, logger):
"""Set logger instance for connection status messages."""
self._logger = logger
def _log(self, level: str, msg: str):
"""Internal logging helper."""
if self._logger:
getattr(self._logger, level)(msg)
else:
print(f"[{level.upper()}] {msg}")
async def connect(self, topics: list[str] = None):
"""
Connect to MQTT broker with automatic reconnection.
This method manages the connection and automatically reconnects
with exponential backoff if the connection is lost.
Args:
topics: List of MQTT topics to subscribe to. If None, subscribes to nothing.
"""
import aiomqtt
from aiomqtt import Client
if topics is None:
topics = []
reconnect_delay = self._reconnect_interval
while True:
try:
self._log("info", f"Connecting to MQTT broker {self._broker}:{self._port} (client_id={self._client_id})")
async with Client(
hostname=self._broker,
port=self._port,
identifier=self._client_id,
) as client:
self._client = client
self._log("info", f"Connected to MQTT broker {self._broker}:{self._port}")
# Subscribe to provided topics
if topics:
for topic in topics:
await client.subscribe(topic)
self._log("info", f"Subscribed to {len(topics)} topic(s): {', '.join(topics[:5])}{'...' if len(topics) > 5 else ''}")
# Reset reconnect delay on successful connection
reconnect_delay = self._reconnect_interval
# Process messages - this is a generator that yields messages
async for message in client.messages:
yield self._normalize_event(message)
except aiomqtt.MqttError as e:
self._log("error", f"MQTT connection error: {e}")
self._log("info", f"Reconnecting in {reconnect_delay} seconds...")
import asyncio
await asyncio.sleep(reconnect_delay)
# Exponential backoff
reconnect_delay = min(reconnect_delay * 2, self._max_reconnect_delay)
def _normalize_event(self, message) -> dict[str, Any]:
"""
Normalize MQTT message to standard event format.
Parses topic to extract capability type and device_id,
adds timestamp, and structures payload.
Args:
message: aiomqtt.Message instance
Returns:
Normalized event dictionary
Example:
Topic: home/contact/sensor_bedroom/state
Payload: {"contact": "open"}
Returns:
{
"topic": "home/contact/sensor_bedroom/state",
"type": "state",
"cap": "contact",
"device_id": "sensor_bedroom",
"payload": {"contact": "open"},
"ts": "2025-11-11T10:30:45.123456"
}
"""
from datetime import datetime
import json
topic = str(message.topic)
topic_parts = topic.split('/')
# Parse topic: home/{capability}/{device_id}/state
if len(topic_parts) >= 4 and topic_parts[0] == 'home' and topic_parts[3] == 'state':
cap = topic_parts[1] # contact, thermostat, light, etc.
device_id = topic_parts[2]
else:
# Fallback for unexpected topic format
cap = "unknown"
device_id = topic_parts[-2] if len(topic_parts) >= 2 else "unknown"
# Parse payload
try:
payload = json.loads(message.payload.decode('utf-8'))
except (json.JSONDecodeError, UnicodeDecodeError):
payload = {"raw": message.payload.decode('utf-8', errors='replace')}
# Generate timestamp
ts = datetime.now().isoformat()
return {
"topic": topic,
"type": "state",
"cap": cap,
"device_id": device_id,
"payload": payload,
"ts": ts
}
async def publish_set_thermostat(self, device_id: str, target: float) -> None:
"""
Publish thermostat target temperature command.
Publishes to: home/thermostat/{device_id}/set
QoS: 1 (at least once delivery)
Args:
device_id: Thermostat device identifier
target: Target temperature in degrees Celsius
Example:
>>> mqtt = MQTTClient("172.16.2.16", 1883)
>>> await mqtt.publish_set_thermostat("thermostat_wohnzimmer", 22.5)
Published to: home/thermostat/thermostat_wohnzimmer/set
Payload: {"type":"thermostat","payload":{"target":22.5}}
"""
import json
if self._client is None:
raise RuntimeError("MQTT client not connected. Call connect() first.")
topic = f"home/thermostat/{device_id}/set"
payload = {
"type": "thermostat",
"payload": {
"target": target
}
}
payload_str = json.dumps(payload)
await self._client.publish(
topic,
payload=payload_str.encode('utf-8'),
qos=1 # At least once delivery
)
self._log("debug", f"Published SET to {topic}: {payload_str}")
# Legacy alias for backward compatibility
class MQTTPublisher:
"""
Legacy MQTT publishing interface - DEPRECATED.
Use MQTTClient instead for new code.
This class is kept for backward compatibility with existing documentation.
"""
def __init__(self, mqtt_client):
"""
Initialize MQTT publisher.
Args:
mqtt_client: MQTTClient instance
"""
self._mqtt = mqtt_client
async def publish_set_thermostat(self, device_id: str, target: float) -> None:
"""
Publish a thermostat target temperature command.
Args:
device_id: Thermostat device identifier
target: Target temperature in degrees Celsius
"""
await self._mqtt.publish_set_thermostat(device_id, target)
class RuleContext:
"""
Runtime context provided to rules during event processing.
Contains all external dependencies and utilities a rule needs:
- Logger for diagnostics
- MQTT client for publishing commands
- Redis client for state persistence
- Current timestamp function
"""
def __init__(
self,
logger,
mqtt_publisher: MQTTPublisher,
redis_state: RedisState,
now_fn=None
):
"""
Initialize rule context.
Args:
logger: Logger instance (e.g., logging.Logger)
mqtt_publisher: MQTTPublisher instance for device commands
redis_state: RedisState instance for persistence
now_fn: Optional callable returning current datetime (defaults to datetime.now)
"""
self.logger = logger
self.mqtt = mqtt_publisher
self.redis = redis_state
self._now_fn = now_fn or datetime.now
def now(self) -> datetime:
"""
Get current timestamp.
Returns:
Current datetime (timezone-aware if now_fn provides it)
"""
return self._now_fn()
class Rule(ABC):
"""
Abstract base class for all automation rule implementations.
Rules implement event-driven automation logic. The engine calls on_event()
for each relevant device state change, passing the event data, rule configuration,
and runtime context.
Implementations must be idempotent - processing the same event multiple times
should produce the same result.
Example implementation:
class WindowSetbackRule(Rule):
def get_subscriptions(self, desc: RuleDescriptor) -> list[str]:
# Subscribe to contact sensor state topics
topics = []
for contact_id in desc.objects.contacts or []:
topics.append(f"home/contact/{contact_id}/state")
return topics
async def on_event(self, evt: dict, desc: RuleDescriptor, ctx: RuleContext) -> None:
device_id = evt['device_id']
cap = evt['cap']
if cap == 'contact':
contact_state = evt['payload'].get('contact')
if contact_state == 'open':
# Window opened - set thermostats to eco
for thermo_id in desc.objects.thermostats or []:
eco_temp = desc.params.get('eco_target', 16.0)
await ctx.mqtt.publish_set_thermostat(thermo_id, eco_temp)
"""
@abstractmethod
def get_subscriptions(self, desc: RuleDescriptor) -> list[str]:
"""
Return list of MQTT topics this rule needs to subscribe to.
Called once during rule engine setup. The rule examines its configuration
(desc.objects) and returns the specific state topics it needs to monitor.
Args:
desc: Rule configuration from rules.yaml
Returns:
List of MQTT topic patterns/strings to subscribe to
Example:
For a window setback rule monitoring 2 contacts:
['home/contact/sensor_bedroom/state', 'home/contact/sensor_kitchen/state']
"""
pass
@abstractmethod
async def on_event(
self,
evt: dict[str, Any],
desc: RuleDescriptor,
ctx: RuleContext
) -> None:
"""
Process a device state change event.
This method is called by the rule engine whenever a device state changes
that is relevant to this rule. The implementation should examine the event
and take appropriate actions (e.g., publish MQTT commands, update state).
MUST be idempotent: Processing the same event multiple times should be safe.
Args:
evt: Event dictionary with the following structure:
{
"topic": "home/contact/device_id/state", # MQTT topic
"type": "state", # Message type
"cap": "contact", # Capability type
"device_id": "kontakt_wohnzimmer", # Device identifier
"payload": {"contact": "open"}, # Capability-specific payload
"ts": "2025-11-11T10:30:45.123456" # ISO timestamp
}
desc: Rule configuration from rules.yaml
ctx: Runtime context with logger, MQTT, Redis, and timestamp utilities
Returns:
None
Raises:
Exception: Implementation may raise exceptions for errors.
The engine will log them but continue processing.
"""
pass
# ============================================================================
# Dynamic Rule Loading
# ============================================================================
import importlib
import re
from typing import Type
# Cache for loaded rule classes (per process)
_RULE_CLASS_CACHE: dict[str, Type[Rule]] = {}
def load_rule(desc: RuleDescriptor) -> Rule:
"""
Dynamically load and instantiate a rule based on its type descriptor.
Convention:
- Rule type format: 'name@version' (e.g., 'window_setback@1.0')
- Module path: apps.rules.impl.{name}
- Class name: PascalCase version of name + 'Rule'
Example: 'window_setback''WindowSetbackRule'
Args:
desc: Rule descriptor from rules.yaml
Returns:
Instantiated Rule object
Raises:
ValueError: If type format is invalid
ImportError: If rule module cannot be found
AttributeError: If rule class cannot be found in module
Examples:
>>> desc = RuleDescriptor(
... id="test_rule",
... type="window_setback@1.0",
... targets={},
... params={}
... )
>>> rule = load_rule(desc)
>>> isinstance(rule, Rule)
True
"""
rule_type = desc.type
# Check cache first
if rule_type in _RULE_CLASS_CACHE:
rule_class = _RULE_CLASS_CACHE[rule_type]
return rule_class()
# Parse type: 'name@version'
if '@' not in rule_type:
raise ValueError(
f"Invalid rule type '{rule_type}': must be in format 'name@version' "
f"(e.g., 'window_setback@1.0')"
)
name, version = rule_type.split('@', 1)
# Validate name (alphanumeric and underscores only)
if not re.match(r'^[a-z][a-z0-9_]*$', name):
raise ValueError(
f"Invalid rule name '{name}': must start with lowercase letter "
f"and contain only lowercase letters, numbers, and underscores"
)
# Convert snake_case to PascalCase for class name
# Example: 'window_setback' → 'WindowSetbackRule'
class_name = ''.join(word.capitalize() for word in name.split('_')) + 'Rule'
# Construct module path
module_path = f'apps.rules.impl.{name}'
# Try to import the module
try:
module = importlib.import_module(module_path)
except ImportError as e:
raise ImportError(
f"Cannot load rule type '{rule_type}': module '{module_path}' not found.\n"
f"Hint: Create file 'apps/rules/impl/{name}.py' with class '{class_name}'.\n"
f"Original error: {e}"
) from e
# Try to get the class from the module
try:
rule_class = getattr(module, class_name)
except AttributeError as e:
raise AttributeError(
f"Cannot load rule type '{rule_type}': class '{class_name}' not found in module '{module_path}'.\n"
f"Hint: Define 'class {class_name}(Rule):' in 'apps/rules/impl/{name}.py'.\n"
f"Available classes in module: {[name for name in dir(module) if not name.startswith('_')]}"
) from e
# Validate that it's a Rule subclass
if not issubclass(rule_class, Rule):
raise TypeError(
f"Class '{class_name}' in '{module_path}' is not a subclass of Rule. "
f"Ensure it inherits from apps.rules.rule_interface.Rule"
)
# Cache the class
_RULE_CLASS_CACHE[rule_type] = rule_class
# Instantiate and return
return rule_class()

122
apps/rules/rules_config.py Normal file
View File

@@ -0,0 +1,122 @@
"""
Rules Configuration Schema and Loader
Provides Pydantic models for validating rules.yaml configuration.
"""
from pathlib import Path
from typing import Any, Optional
import yaml
from pydantic import BaseModel, Field, field_validator
class Rule(BaseModel):
"""Single rule configuration"""
id: str = Field(..., description="Unique rule identifier")
name: Optional[str] = Field(None, description="Optional human-readable name")
type: str = Field(..., description="Rule type (e.g., 'window_setback@1.0')")
enabled: bool = Field(default=True, description="Whether this rule is enabled")
objects: dict[str, Any] = Field(default_factory=dict, description="Objects this rule monitors or controls")
params: dict[str, Any] = Field(default_factory=dict, description="Rule-specific parameters")
@field_validator('id')
@classmethod
def validate_id(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("Rule ID cannot be empty")
return v.strip()
@field_validator('type')
@classmethod
def validate_type(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("Rule type cannot be empty")
if '@' not in v:
raise ValueError(f"Rule type must include version (e.g., 'window_setback@1.0'), got: {v}")
return v.strip()
class RulesConfig(BaseModel):
"""Root configuration object"""
rules: list[Rule] = Field(..., description="List of all rules")
@field_validator('rules')
@classmethod
def validate_unique_ids(cls, rules: list[Rule]) -> list[Rule]:
"""Ensure all rule IDs are unique"""
ids = [rule.id for rule in rules]
duplicates = [id for id in ids if ids.count(id) > 1]
if duplicates:
raise ValueError(f"Duplicate rule IDs found: {set(duplicates)}")
return rules
def load_rules_config(config_path: str | Path = "config/rules.yaml") -> RulesConfig:
"""
Load and validate rules configuration from YAML file.
Args:
config_path: Path to rules.yaml file
Returns:
Validated RulesConfig object
Raises:
FileNotFoundError: If config file doesn't exist
ValueError: If YAML is invalid or validation fails
"""
config_path = Path(config_path)
if not config_path.exists():
raise FileNotFoundError(f"Rules configuration not found: {config_path}")
with open(config_path, 'r', encoding='utf-8') as f:
try:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML in {config_path}: {e}") from e
if not data:
raise ValueError(f"Empty configuration file: {config_path}")
if 'rules' not in data:
raise ValueError(
f"Missing 'rules:' key in {config_path}. "
"Configuration must start with 'rules:' followed by a list of rule definitions."
)
try:
return RulesConfig(**data)
except Exception as e:
raise ValueError(f"Configuration validation failed: {e}") from e
def get_rule_by_id(config: RulesConfig, rule_id: str) -> Rule | None:
"""Get a specific rule by ID"""
for rule in config.rules:
if rule.id == rule_id:
return rule
return None
def get_rules_by_type(config: RulesConfig, rule_type: str) -> list[Rule]:
"""Get all rules of a specific type"""
return [rule for rule in config.rules if rule.type == rule_type]
if __name__ == "__main__":
# Test configuration loading
try:
config = load_rules_config()
print(f"✅ Loaded {len(config.rules)} rules:")
for rule in config.rules:
name = f" ({rule.name})" if rule.name else ""
enabled = "" if rule.enabled else ""
print(f" [{enabled}] {rule.id}{name}: {rule.type}")
if rule.objects:
obj_summary = ", ".join(f"{k}: {len(v) if isinstance(v, list) else v}"
for k, v in rule.objects.items())
print(f" Objects: {obj_summary}")
except Exception as e:
print(f"❌ Configuration error: {e}")

View File

@@ -59,13 +59,9 @@ docker run --rm -p 8010:8010 \
simulator:dev simulator:dev
``` ```
**Mit Docker Network (optional):** **Note for finch/nerdctl users:**
```bash - finch binds ports to `127.0.0.1` by default
docker run --rm -p 8010:8010 \ - The web interface will be accessible at `http://127.0.0.1:8010`
--name simulator \
-e MQTT_BROKER=172.23.1.102 \
simulator:dev
```
#### Environment Variables #### Environment Variables

View File

@@ -37,17 +37,6 @@ docker build -t ui:dev -f apps/ui/Dockerfile .
#### Run Container #### Run Container
**Linux Server (empfohlen):**
```bash
# Mit Docker Network für Container-to-Container Kommunikation
docker run --rm -p 8002:8002 \
-e UI_PORT=8002 \
-e API_BASE=http://172.19.1.11:8001 \
-e BASE_PATH=/ \
ui:dev
```
**macOS mit finch/nerdctl:**
```bash ```bash
docker run --rm -p 8002:8002 \ docker run --rm -p 8002:8002 \
--add-host=host.docker.internal:host-gateway \ --add-host=host.docker.internal:host-gateway \
@@ -57,11 +46,10 @@ docker run --rm -p 8002:8002 \
ui:dev ui:dev
``` ```
**Hinweise:** **Note for finch/nerdctl users:**
- **Linux**: Verwende Docker Network und Service-Namen (`http://api:8001`) - finch binds ports to `127.0.0.1` by default (not `0.0.0.0`)
- **macOS/finch**: Verwende `host.docker.internal` mit `--add-host` flag - Use `--add-host=host.docker.internal:host-gateway` to allow container-to-host communication
- Die UI macht Server-Side API-Aufrufe beim Rendern der Seite - Set `API_BASE=http://host.docker.internal:8001` to reach the API container
- Browser-seitige Realtime-Updates (SSE) gehen direkt vom Browser zur API
#### Environment Variables #### Environment Variables

View File

@@ -1,171 +0,0 @@
# UI Service - Docker
FastAPI + Jinja2 + HTMX Dashboard für Home Automation
## Build
```bash
docker build -t ui:dev -f apps/ui/Dockerfile .
```
## Run
### Lokal
```bash
docker run --rm -p 8002:8002 -e API_BASE=http://localhost:8001 ui:dev
```
### Docker Compose
```yaml
services:
ui:
build:
context: .
dockerfile: apps/ui/Dockerfile
ports:
- "8002:8002"
environment:
- API_BASE=http://api:8001
depends_on:
- api
```
### Kubernetes
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ui
spec:
replicas: 2
selector:
matchLabels:
app: ui
template:
metadata:
labels:
app: ui
spec:
containers:
- name: ui
image: ui:dev
ports:
- containerPort: 8002
env:
- name: API_BASE
value: "http://api-service:8001"
livenessProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 3
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: ui-service
spec:
selector:
app: ui
ports:
- protocol: TCP
port: 8002
targetPort: 8002
type: LoadBalancer
```
## Umgebungsvariablen
| Variable | Default | Beschreibung |
|----------|---------|--------------|
| `API_BASE` | `http://api:8001` | URL des API-Services |
| `UI_PORT` | `8002` | Port der UI-Anwendung |
| `PYTHONDONTWRITEBYTECODE` | `1` | Keine .pyc Files |
| `PYTHONUNBUFFERED` | `1` | Unbuffered Output |
## Endpoints
- `GET /` - Dashboard
- `GET /health` - Health Check
- `GET /dashboard` - Dashboard (alias)
## Security
- Container läuft als **non-root** User `app` (UID: 10001)
- Minimales Python 3.11-slim Base Image
- Keine unnötigen System-Pakete
- Health Check integriert
## Features
- ✅ FastAPI Backend
- ✅ Jinja2 Templates
- ✅ HTMX für reactive UI
- ✅ Server-Sent Events (SSE)
- ✅ Responsive Design
- ✅ Docker & Kubernetes ready
- ✅ Health Check Endpoint
- ✅ Non-root Container
- ✅ Configurable API Backend
## Entwicklung
### Lokales Testing
```bash
# Build
docker build -t ui:dev -f apps/ui/Dockerfile .
# Run
docker run -d --name ui-test -p 8002:8002 -e API_BASE=http://localhost:8001 ui:dev
# Logs
docker logs -f ui-test
# Health Check
curl http://localhost:8002/health
# Cleanup
docker stop ui-test && docker rm ui-test
```
### Tests
```bash
bash /tmp/test_ui_dockerfile.sh
```
## Troubleshooting
### Container startet nicht
```bash
docker logs ui-test
```
### Health Check schlägt fehl
```bash
docker exec ui-test curl http://localhost:8002/health
```
### API_BASE nicht korrekt
```bash
docker logs ui-test 2>&1 | grep "UI using API_BASE"
```
### Non-root Verifizieren
```bash
docker exec ui-test id
# Sollte zeigen: uid=10001(app) gid=10001(app)
```

View File

@@ -74,7 +74,59 @@ async def index(request: Request) -> HTMLResponse:
Returns: Returns:
HTMLResponse: Rendered dashboard HTMLResponse: Rendered dashboard
""" """
return await dashboard(request) return await rooms(request)
@app.get("/rooms", response_class=HTMLResponse)
async def rooms(request: Request) -> HTMLResponse:
"""Render the rooms overview page.
Args:
request: The FastAPI request object
Returns:
HTMLResponse: Rendered rooms template
"""
return templates.TemplateResponse("rooms.html", {
"request": request,
"api_base": API_BASE
})
@app.get("/room/{room_name}", response_class=HTMLResponse)
async def room_detail(request: Request, room_name: str) -> HTMLResponse:
"""Render the room detail page with devices.
Args:
request: The FastAPI request object
room_name: Name of the room to display
Returns:
HTMLResponse: Rendered room template
"""
return templates.TemplateResponse("room.html", {
"request": request,
"api_base": API_BASE,
"room_name": room_name
})
@app.get("/device/{device_id}", response_class=HTMLResponse)
async def device_detail(request: Request, device_id: str) -> HTMLResponse:
"""Render the device detail page with controls.
Args:
request: The FastAPI request object
device_id: ID of the device to display
Returns:
HTMLResponse: Rendered device template
"""
return templates.TemplateResponse("device.html", {
"request": request,
"api_base": API_BASE,
"device_id": device_id
})
@app.get("/dashboard", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse)

301
apps/ui/static/README.md Normal file
View File

@@ -0,0 +1,301 @@
# Home Automation API Client
Wiederverwendbare JavaScript-API-Client-Bibliothek für das Home Automation UI.
## Installation
Füge die folgenden Script-Tags in deine HTML-Seiten ein:
```html
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
```
## Konfiguration
Der API-Client nutzt `window.API_BASE`, das vom Backend gesetzt wird:
```javascript
window.API_BASE = '{{ api_base }}'; // Jinja2 template
```
## Verwendung
### Globale Instanz
Der API-Client erstellt automatisch eine globale Instanz `window.apiClient`:
```javascript
// Layout abrufen
const layout = await window.apiClient.getLayout();
// Geräte abrufen
const devices = await window.apiClient.getDevices();
// Gerätestatus abrufen
const state = await window.apiClient.getDeviceState('kitchen_light');
// Gerätesteuerung
await window.apiClient.setDeviceState('kitchen_light', 'light', {
power: true,
brightness: 80
});
```
### Verfügbare Methoden
#### `getLayout(): Promise<Layout>`
Lädt die Layout-Daten (Räume und ihre Geräte).
```javascript
const layout = await window.apiClient.getLayout();
// { rooms: [{name: "Küche", devices: ["kitchen_light", ...]}, ...] }
```
#### `getDevices(): Promise<Device[]>`
Lädt alle Geräte mit ihren Features.
```javascript
const devices = await window.apiClient.getDevices();
// [{device_id: "...", name: "...", type: "light", features: {...}}, ...]
```
#### `getDeviceState(deviceId): Promise<DeviceState>`
Lädt den aktuellen Status eines Geräts.
```javascript
const state = await window.apiClient.getDeviceState('kitchen_light');
// {power: true, brightness: 80, ...}
```
#### `getAllStates(): Promise<Object>`
Lädt alle Gerätestatus auf einmal.
```javascript
const states = await window.apiClient.getAllStates();
// {"kitchen_light": {power: true, ...}, "thermostat_1": {...}, ...}
```
#### `setDeviceState(deviceId, type, payload): Promise<void>`
Sendet einen Befehl an ein Gerät.
```javascript
// Licht einschalten
await window.apiClient.setDeviceState('kitchen_light', 'light', {
power: true,
brightness: 80
});
// Thermostat einstellen
await window.apiClient.setDeviceState('thermostat_1', 'thermostat', {
target_temp: 22.5
});
// Rollladen steuern
await window.apiClient.setDeviceState('cover_1', 'cover', {
position: 50
});
```
#### `getDeviceRoom(deviceId): Promise<{room: string}>`
Ermittelt den Raum eines Geräts.
```javascript
const { room } = await window.apiClient.getDeviceRoom('kitchen_light');
// {room: "Küche"}
```
#### `getScenes(): Promise<Scene[]>`
Lädt alle verfügbaren Szenen.
```javascript
const scenes = await window.apiClient.getScenes();
```
#### `activateScene(sceneId): Promise<void>`
Aktiviert eine Szene.
```javascript
await window.apiClient.activateScene('evening');
```
### Realtime-Updates (SSE)
#### `connectRealtime(onEvent, onError): EventSource`
Verbindet sich mit dem SSE-Stream für Live-Updates.
```javascript
window.apiClient.connectRealtime(
(event) => {
console.log('Update:', event.device_id, event.state);
// event = {device_id: "...", type: "state", state: {...}}
},
(error) => {
console.error('Connection error:', error);
}
);
```
#### `onDeviceUpdate(deviceId, callback): Function`
Registriert einen Listener für spezifische Geräte-Updates.
```javascript
// Für ein bestimmtes Gerät
const unsubscribe = window.apiClient.onDeviceUpdate('kitchen_light', (event) => {
console.log('Kitchen light changed:', event.state);
updateUI(event.state);
});
// Für alle Geräte
const unsubscribeAll = window.apiClient.onDeviceUpdate(null, (event) => {
console.log('Any device changed:', event.device_id, event.state);
});
// Später: Listener entfernen
unsubscribe();
```
#### `disconnectRealtime(): void`
Trennt die SSE-Verbindung und entfernt alle Listener.
```javascript
window.apiClient.disconnectRealtime();
```
### Helper-Methoden
#### `findDevice(devices, deviceId): Device|null`
Findet ein Gerät in einem Array.
```javascript
const devices = await window.apiClient.getDevices();
const device = window.apiClient.findDevice(devices, 'kitchen_light');
```
#### `findRoom(layout, roomName): Room|null`
Findet einen Raum im Layout.
```javascript
const layout = await window.apiClient.getLayout();
const room = window.apiClient.findRoom(layout, 'Küche');
```
#### `getDevicesForRoom(layout, devices, roomName): Device[]`
Gibt alle Geräte eines Raums zurück.
```javascript
const layout = await window.apiClient.getLayout();
const devices = await window.apiClient.getDevices();
const kitchenDevices = window.apiClient.getDevicesForRoom(layout, devices, 'Küche');
```
#### `api(path): string`
Konstruiert eine vollständige API-URL.
```javascript
const url = window.apiClient.api('/devices');
// "http://172.19.1.11:8001/devices"
```
### Backward Compatibility
Die globale `api()` Funktion ist weiterhin verfügbar:
```javascript
function api(url) {
return window.apiClient.api(url);
}
```
## Typen (JSDoc)
Die Datei `types.js` enthält JSDoc-Definitionen für alle API-Typen:
- `Room` - Raum mit Geräten
- `Layout` - Layout-Struktur
- `Device` - Gerätedaten
- `DeviceFeatures` - Geräte-Features
- `DeviceState` - Gerätestatus (Light, Thermostat, Contact, etc.)
- `RealtimeEvent` - SSE-Event-Format
- `Scene` - Szenen-Definition
- `*Payload` - Command-Payloads für verschiedene Gerätetypen
Diese ermöglichen IDE-Autocomplete und Type-Checking in modernen Editoren (VS Code, WebStorm).
## Beispiel: Vollständige Seite
```html
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>My Page</title>
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
</head>
<body>
<div id="status"></div>
<button id="toggle">Toggle Light</button>
<script>
window.API_BASE = 'http://172.19.1.11:8001';
const deviceId = 'kitchen_light';
async function init() {
// Load initial state
const state = await window.apiClient.getDeviceState(deviceId);
updateUI(state);
// Listen for updates
window.apiClient.onDeviceUpdate(deviceId, (event) => {
updateUI(event.state);
});
// Connect to realtime
window.apiClient.connectRealtime((event) => {
console.log('Event:', event);
});
// Handle button clicks
document.getElementById('toggle').onclick = async () => {
const currentState = await window.apiClient.getDeviceState(deviceId);
await window.apiClient.setDeviceState(deviceId, 'light', {
power: !currentState.power
});
};
}
function updateUI(state) {
document.getElementById('status').textContent =
state.power ? 'ON' : 'OFF';
}
init();
</script>
</body>
</html>
```
## Error Handling
Alle API-Methoden werfen Exceptions bei Fehlern:
```javascript
try {
const state = await window.apiClient.getDeviceState('invalid_id');
} catch (error) {
console.error('API error:', error);
showErrorMessage(error.message);
}
```
## Auto-Reconnect
Der SSE-Client versucht automatisch, nach 5 Sekunden wieder zu verbinden, wenn die Verbindung abbricht.
## Verwendete Technologien
- **Fetch API** - Für HTTP-Requests
- **EventSource** - Für Server-Sent Events
- **JSDoc** - Für Type Definitions
- **ES6+** - Modern JavaScript (Class, async/await, etc.)

View File

@@ -0,0 +1,278 @@
/**
* Home Automation API Client
*
* Provides a unified interface to interact with the backend API.
* All functions use the global window.API_BASE configuration.
*/
class HomeAutomationClient {
/**
* Get layout info for a specific device
* @param {string} deviceId - Device ID
* @returns {Promise<object>} Layout info
*/
async getDeviceLayout(deviceId) {
return await this.fetch(this.api(`/devices/${deviceId}/layout`));
}
constructor() {
this.eventSource = null;
this.eventListeners = [];
}
/**
* Helper to construct full API URLs
* Reads window.API_BASE at runtime to support dynamic configuration
* @param {string} path - API path (e.g., '/devices')
* @returns {string} Full URL
*/
api(path) {
const baseUrl = window.API_BASE || '';
return `${baseUrl}${path}`;
}
/**
* Generic fetch wrapper with error handling
* @param {string} url - URL to fetch
* @param {object} options - Fetch options
* @returns {Promise<any>} Response data
*/
async fetch(url, options = {}) {
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
/**
* Get layout data (rooms and their devices)
* @returns {Promise<{rooms: Array<{name: string, devices: string[]}>}>}
*/
async getLayout() {
return await this.fetch(this.api('/layout'));
}
/**
* Get all devices with their features
* @returns {Promise<Array<{device_id: string, name: string, type: string, features: object}>>}
*/
async getDevices() {
return await this.fetch(this.api('/devices'));
}
async getDevice(deviceId) {
return await this.fetch(this.api(`/devices/${deviceId}`));
}
/**
* Get current state of a specific device
* @param {string} deviceId - Device ID
* @returns {Promise<object>} Device state
*/
async getDeviceState(deviceId) {
return await this.fetch(this.api(`/devices/${deviceId}/state`));
}
/**
* Get all device states at once
* @returns {Promise<object>} Map of device_id to state
*/
async getAllStates() {
return await this.fetch(this.api('/devices/states'));
}
/**
* Send a command to a device
* @param {string} deviceId - Device ID
* @param {string} type - Device type (light, thermostat, etc.)
* @param {object} payload - Command payload
* @returns {Promise<void>}
*/
async setDeviceState(deviceId, type, payload) {
await fetch(this.api(`/devices/${deviceId}/set`), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ type, payload })
});
}
/**
* Get room information for a device
* @param {string} deviceId - Device ID
* @returns {Promise<{room: string}>}
*/
async getDeviceRoom(deviceId) {
return await this.fetch(this.api(`/devices/${deviceId}/room`));
}
/**
* Get all available scenes
* @returns {Promise<Array<{scene_id: string, name: string, devices: object}>>}
*/
async getScenes() {
return await this.fetch(this.api('/scenes'));
}
/**
* Activate a scene
* @param {string} sceneId - Scene ID
* @returns {Promise<void>}
*/
async activateScene(sceneId) {
await fetch(this.api(`/scenes/${sceneId}/activate`), {
method: 'POST'
});
}
/**
* Connect to realtime event stream (SSE)
* @param {Function} onEvent - Callback function(event)
* @param {Function} onError - Error callback (optional)
* @returns {EventSource} EventSource instance
*/
connectRealtime(onEvent, onError = null) {
if (this.eventSource) {
this.eventSource.close();
}
this.eventSource = new EventSource(this.api('/realtime'));
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Normalize event format: convert API format to unified format
const normalizedEvent = {
device_id: data.device_id,
type: data.type,
state: data.payload || data.state // Support both formats
};
onEvent(normalizedEvent);
// Notify all registered listeners
this.eventListeners.forEach(listener => {
if (!listener.deviceId || listener.deviceId === normalizedEvent.device_id) {
listener.callback(normalizedEvent);
}
});
} catch (error) {
console.error('Failed to parse SSE event:', error);
}
};
this.eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
if (onError) {
onError(error);
}
// Auto-reconnect after 5 seconds
setTimeout(() => {
if (this.eventSource) {
this.eventSource.close();
this.connectRealtime(onEvent, onError);
}
}, 5000);
};
return this.eventSource;
}
/**
* Register a listener for specific device updates
* @param {string|null} deviceId - Device ID or null for all devices
* @param {Function} callback - Callback function(event)
* @returns {Function} Unsubscribe function
*/
onDeviceUpdate(deviceId, callback) {
const listener = { deviceId, callback };
this.eventListeners.push(listener);
// Return unsubscribe function
return () => {
const index = this.eventListeners.indexOf(listener);
if (index > -1) {
this.eventListeners.splice(index, 1);
}
};
}
/**
* Disconnect from realtime stream
*/
disconnectRealtime() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.eventListeners = [];
}
/**
* Helper: Get device by ID from devices array
* @param {Array} devices - Devices array
* @param {string} deviceId - Device ID to find
* @returns {object|null} Device object or null
*/
findDevice(devices, deviceId) {
return devices.find(d => d.device_id === deviceId) || null;
}
/**
* Helper: Get room by name from layout
* @param {object} layout - Layout object
* @param {string} roomName - Room name to find
* @returns {object|null} Room object or null
*/
findRoom(layout, roomName) {
return layout.rooms.find(r => r.name === roomName) || null;
}
/**
* Helper: Get devices for a specific room
* @param {object} layout - Layout object
* @param {Array} devices - Devices array
* @param {string} roomName - Room name
* @returns {Array} Array of device objects
*/
getDevicesForRoom(layout, devices, roomName) {
const room = this.findRoom(layout, roomName);
if (!room) return [];
const deviceMap = {};
devices.forEach(d => deviceMap[d.device_id] = d);
// Extract device IDs from room.devices (they are objects with device_id property)
const deviceIds = room.devices.map(d => d.device_id || d);
return deviceIds
.map(id => deviceMap[id])
.filter(d => d != null);
}
}
// Create global instance
window.apiClient = new HomeAutomationClient();
/**
* Convenience function for backward compatibility
*/
function api(url) {
return window.apiClient.api(url);
}

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<!-- Roof -->
<path d="M50 10 L90 45 L85 45 L85 50 L15 50 L15 45 L10 45 Z" fill="#667eea" stroke="#4c51bf" stroke-width="2" stroke-linejoin="round"/>
<!-- House body -->
<rect x="15" y="45" width="70" height="45" fill="#764ba2" stroke="#4c51bf" stroke-width="2"/>
<!-- Door -->
<rect x="35" y="60" width="15" height="30" fill="#4c51bf" rx="2"/>
<!-- Window -->
<rect x="60" y="60" width="20" height="15" fill="#fbbf24" stroke="#f59e0b" stroke-width="1"/>
<!-- Window panes -->
<line x1="70" y1="60" x2="70" y2="75" stroke="#f59e0b" stroke-width="1"/>
<line x1="60" y1="67.5" x2="80" y2="67.5" stroke="#f59e0b" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 721 B

166
apps/ui/static/types.js Normal file
View File

@@ -0,0 +1,166 @@
/**
* Type definitions for Home Automation API
*
* These are JSDoc type definitions that provide IDE autocomplete
* and type checking in JavaScript files.
*/
/**
* @typedef {Object} Room
* @property {string} name - Room name (e.g., "Küche", "Wohnzimmer")
* @property {Array<{device_id: string, title: string, icon: string, rank: number}>} devices - Array of device objects in this room
*/
/**
* @typedef {Object} Layout
* @property {Room[]} rooms - Array of rooms with their devices
*/
/**
* @typedef {Object} DeviceFeatures
* @property {boolean} [dimmable] - Light: supports brightness control
* @property {boolean} [color_hsb] - Light: supports HSB color control
* @property {boolean} [color_temp] - Light: supports color temperature
* @property {number} [min_temp] - Thermostat: minimum temperature
* @property {number} [max_temp] - Thermostat: maximum temperature
* @property {number} [step] - Thermostat: temperature step size
*/
/**
* @typedef {Object} Device
* @property {string} device_id - Unique device identifier
* @property {string} name - Human-readable device name
* @property {string} type - Device type: light, thermostat, contact, temp_humidity_sensor, relay, outlet, cover
* @property {DeviceFeatures} features - Device-specific features
*/
/**
* @typedef {Object} LightState
* @property {boolean} power - On/off state
* @property {number} [brightness] - Brightness 0-100 (if dimmable)
* @property {Object} [color_hsb] - HSB color (if color_hsb)
* @property {number} color_hsb.hue - Hue 0-360
* @property {number} color_hsb.saturation - Saturation 0-100
* @property {number} color_hsb.brightness - Brightness 0-100
* @property {number} [color_temp] - Color temperature in mireds (if color_temp)
*/
/**
* @typedef {Object} ThermostatState
* @property {number} current_temp - Current temperature in °C
* @property {number} target_temp - Target temperature in °C
* @property {string} mode - Operating mode: heat, cool, auto, off
*/
/**
* @typedef {Object} ContactState
* @property {boolean} open - true if open, false if closed
*/
/**
* @typedef {Object} TempHumidityState
* @property {number} temperature - Temperature in °C
* @property {number} humidity - Relative humidity 0-100%
*/
/**
* @typedef {Object} RelayState
* @property {boolean} power - On/off state
*/
/**
* @typedef {Object} OutletState
* @property {boolean} power - On/off state
*/
/**
* @typedef {Object} CoverState
* @property {number} position - Position 0-100 (0=closed, 100=open)
* @property {string} state - State: open, closed, opening, closing, stopped
*/
/**
* @typedef {LightState|ThermostatState|ContactState|TempHumidityState|RelayState|OutletState|CoverState} DeviceState
*/
/**
* @typedef {Object} RealtimeEvent
* @property {string} device_id - Device that changed
* @property {string} type - Device type
* @property {DeviceState} state - New device state
*/
/**
* @typedef {Object} Scene
* @property {string} scene_id - Unique scene identifier
* @property {string} name - Human-readable scene name
* @property {Object<string, Object>} devices - Map of device_id to desired state
*/
/**
* @typedef {Object} LightPayload
* @property {boolean} [power] - Turn on/off
* @property {number} [brightness] - Set brightness 0-100
* @property {Object} [color_hsb] - Set HSB color
* @property {number} color_hsb.hue - Hue 0-360
* @property {number} color_hsb.saturation - Saturation 0-100
* @property {number} color_hsb.brightness - Brightness 0-100
*/
/**
* @typedef {Object} ThermostatPayload
* @property {number} [target_temp] - Set target temperature
* @property {string} [mode] - Set mode: heat, cool, auto, off
*/
/**
* @typedef {Object} RelayPayload
* @property {boolean} power - Turn on/off
*/
/**
* @typedef {Object} OutletPayload
* @property {boolean} power - Turn on/off
*/
/**
* @typedef {Object} CoverPayload
* @property {number} [position] - Set position 0-100
* @property {string} [action] - Action: open, close, stop
*/
/**
* Example usage:
*
* // Get layout data
* const layout = await window.apiClient.getLayout();
* // layout is typed as Layout
*
* // Get devices
* const devices = await window.apiClient.getDevices();
* // devices is typed as Device[]
*
* // Get device state
* const state = await window.apiClient.getDeviceState('kitchen_light');
* // state is typed as DeviceState
*
* // Set device state
* await window.apiClient.setDeviceState('kitchen_light', 'light', {
* power: true,
* brightness: 80
* });
*
* // Connect to realtime events
* window.apiClient.connectRealtime((event) => {
* console.log('Device update:', event.device_id, event.state);
* });
*
* // Listen to specific device
* const unsubscribe = window.apiClient.onDeviceUpdate('kitchen_light', (event) => {
* console.log('Kitchen light changed:', event.state);
* });
*
* // Later: cleanup
* unsubscribe();
* window.apiClient.disconnectRealtime();
*/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,828 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gerät - Home Automation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.header {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.back-button {
background: none;
border: none;
color: #667eea;
font-size: 16px;
cursor: pointer;
padding: 8px 0;
margin-bottom: 12px;
display: inline-block;
text-decoration: none;
}
.back-button:hover {
text-decoration: underline;
}
.device-icon {
font-size: 48px;
text-align: center;
margin-bottom: 12px;
}
h1 {
color: #333;
font-size: 24px;
margin-bottom: 8px;
text-align: center;
}
.device-meta {
color: #666;
font-size: 14px;
text-align: center;
margin-bottom: 4px;
}
.card {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.control-group {
margin-bottom: 20px;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
display: block;
}
.toggle-button {
width: 100%;
padding: 16px;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.toggle-button.on {
background: #34c759;
color: white;
}
.toggle-button.off {
background: #e0e0e0;
color: #666;
}
.toggle-button:active {
transform: scale(0.98);
}
.slider-container {
margin-bottom: 12px;
}
.slider {
width: 100%;
height: 40px;
-webkit-appearance: none;
appearance: none;
background: #e0e0e0;
outline: none;
border-radius: 20px;
padding: 0;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 32px;
height: 32px;
background: #667eea;
cursor: pointer;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.slider::-moz-range-thumb {
width: 32px;
height: 32px;
background: #667eea;
cursor: pointer;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.slider-value {
text-align: center;
font-size: 24px;
font-weight: 600;
color: #667eea;
margin-top: 8px;
}
.button-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.control-button {
padding: 12px;
border: none;
border-radius: 8px;
background: #667eea;
color: white;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.control-button:active {
transform: scale(0.95);
}
.control-button:disabled {
background: #e0e0e0;
color: #999;
cursor: not-allowed;
}
.state-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.state-item {
text-align: center;
}
.state-value {
font-size: 28px;
font-weight: 600;
color: #667eea;
margin-bottom: 4px;
}
.state-label {
font-size: 14px;
color: #666;
}
.state-badge {
display: inline-block;
padding: 8px 20px;
border-radius: 20px;
font-size: 16px;
font-weight: 600;
text-align: center;
}
.state-badge.open {
background: #ff9500;
color: white;
}
.state-badge.closed {
background: #34c759;
color: white;
}
.loading {
text-align: center;
color: white;
font-size: 18px;
padding: 40px;
}
.error {
background: rgba(255, 59, 48, 0.9);
color: white;
padding: 16px;
border-radius: 8px;
text-align: center;
margin-bottom: 20px;
}
.success {
background: rgba(52, 199, 89, 0.9);
color: white;
padding: 12px;
border-radius: 8px;
text-align: center;
margin-top: 12px;
font-size: 14px;
}
.color-picker {
width: 100%;
height: 60px;
border: none;
border-radius: 8px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<a href="#" id="back-button" class="back-button">← Zurück</a>
<div class="device-icon" id="device-icon">📱</div>
<h1 id="device-name">Gerät wird geladen...</h1>
<div class="device-meta" id="device-room"></div>
<div class="device-meta" id="device-type"></div>
</div>
<div id="error-container"></div>
<div id="loading" class="loading">Lade Gerät...</div>
<div id="controls-container" style="display: none;"></div>
</div>
<script>
// API configuration from backend
window.API_BASE = '{{ api_base }}';
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script>
// Get device ID from URL
const pathParts = window.location.pathname.split('/');
const deviceId = pathParts[pathParts.length - 1];
// Device data
let deviceData = null;
let deviceState = {};
let roomName = '';
// Device type icons
const deviceIcons = {
'light': '💡',
'thermostat': '🌡️',
'contact': '🚪',
'temp_humidity_sensor': '🌡️',
'relay': '🔌',
'outlet': '🔌',
'cover': '🪟'
};
async function loadDevice() {
const loading = document.getElementById('loading');
const controlsContainer = document.getElementById('controls-container');
const errorContainer = document.getElementById('error-container');
try {
// Load device info using API client
// NEW: Use new endpoints for device info and layout
deviceData = await window.apiClient.getDevice(deviceId);
console.log("Loaded device data:", deviceData);
deviceState = await window.apiClient.getDeviceState(deviceId);
console.log("Loaded device state:", deviceState);
const layoutInfo = await window.apiClient.getDeviceLayout(deviceId);
console.log("Loaded layout info:", layoutInfo);
roomName = layoutInfo.room;
// Update header
document.getElementById('device-icon').textContent = deviceIcons[deviceData.type] || '📱';
document.getElementById('device-name').textContent = deviceData.name;
document.getElementById('device-room').textContent = roomName || 'Kein Raum';
document.getElementById('device-type').textContent = getTypeLabel(deviceData.type);
// Set back button
document.getElementById('back-button').href = roomName ? `/room/${encodeURIComponent(roomName)}` : '/rooms';
// Render controls
loading.style.display = 'none';
controlsContainer.style.display = 'block';
renderControls(controlsContainer);
// Start SSE
connectRealtime();
} catch (error) {
console.error('Error loading device:', error);
loading.style.display = 'none';
errorContainer.innerHTML = `
<div class="error">
⚠️ Fehler beim Laden: ${error.message}
</div>
`;
}
}
function getTypeLabel(type) {
const labels = {
'light': 'Licht',
'thermostat': 'Thermostat',
'contact': 'Kontaktsensor',
'temp_humidity_sensor': 'Temperatur & Luftfeuchte',
'relay': 'Schalter',
'outlet': 'Steckdose',
'cover': 'Jalousie'
};
return labels[type] || type;
}
function renderControls(container) {
container.innerHTML = '';
switch (deviceData.type) {
case 'light':
renderLightControls(container);
break;
case 'thermostat':
renderThermostatControls(container);
break;
case 'relay':
case 'outlet':
renderOutletControls(container);
break;
case 'contact':
renderContactDisplay(container);
break;
case 'temp_humidity_sensor':
renderTempHumidityDisplay(container);
break;
case 'cover':
renderCoverControls(container);
break;
default:
container.innerHTML = '<div class="card">Keine Steuerung verfügbar</div>';
}
}
function renderLightControls(container) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = '<div class="card-title">Steuerung</div>';
// Power toggle
const powerGroup = document.createElement('div');
powerGroup.className = 'control-group';
const powerButton = document.createElement('button');
powerButton.className = 'toggle-button ' + (deviceState.power === 'on' ? 'on' : 'off');
powerButton.textContent = deviceState.power === 'on' ? '💡 An' : '💡 Aus';
powerButton.onclick = () => togglePower();
powerGroup.appendChild(powerButton);
card.appendChild(powerGroup);
// Brightness slider (if supported)
if (deviceData.features?.brightness) {
const brightnessGroup = document.createElement('div');
brightnessGroup.className = 'control-group';
brightnessGroup.innerHTML = `
<label class="control-label">Helligkeit</label>
<div class="slider-container">
<input type="range" class="slider" id="brightness-slider" min="0" max="100" value="${deviceState.brightness || 0}">
<div class="slider-value" id="brightness-value">${deviceState.brightness || 0}%</div>
</div>
`;
card.appendChild(brightnessGroup);
setTimeout(() => {
const slider = document.getElementById('brightness-slider');
const valueDisplay = document.getElementById('brightness-value');
slider.oninput = (e) => {
valueDisplay.textContent = e.target.value + '%';
};
slider.onchange = (e) => {
setBrightness(parseInt(e.target.value));
};
}, 0);
}
// Color picker (if supported)
if (deviceData.features?.color_hsb) {
const colorGroup = document.createElement('div');
colorGroup.className = 'control-group';
colorGroup.innerHTML = `
<label class="control-label">Farbe</label>
<input type="color" class="color-picker" id="color-picker" value="#ffffff">
`;
card.appendChild(colorGroup);
setTimeout(() => {
const picker = document.getElementById('color-picker');
picker.onchange = (e) => {
setColor(e.target.value);
};
}, 0);
}
container.appendChild(card);
}
function renderThermostatControls(container) {
const card = document.createElement('div');
card.className = 'card';
// Current state display
const stateGrid = document.createElement('div');
stateGrid.className = 'state-grid';
stateGrid.innerHTML = `
<div class="state-item">
<div class="state-value" id="current-temp">${deviceState.current?.toFixed(1) || '--'}°C</div>
<div class="state-label">Aktuell</div>
</div>
<div class="state-item">
<div class="state-value" id="target-temp">${deviceState.target?.toFixed(1) || '--'}°C</div>
<div class="state-label">Ziel</div>
</div>
`;
card.appendChild(stateGrid);
// Target temperature slider
const sliderGroup = document.createElement('div');
sliderGroup.className = 'control-group';
sliderGroup.style.marginTop = '20px';
sliderGroup.innerHTML = `
<label class="control-label">Zieltemperatur</label>
<div class="slider-container">
<input type="range" class="slider" id="temp-slider" min="5" max="30" step="0.5" value="${deviceState.target || 21}">
<div class="slider-value" id="temp-value">${deviceState.target?.toFixed(1) || '21.0'}°C</div>
</div>
`;
card.appendChild(sliderGroup);
container.appendChild(card);
setTimeout(() => {
const slider = document.getElementById('temp-slider');
const valueDisplay = document.getElementById('temp-value');
slider.oninput = (e) => {
valueDisplay.textContent = parseFloat(e.target.value).toFixed(1) + '°C';
};
slider.onchange = (e) => {
setTargetTemp(parseFloat(e.target.value));
};
}, 0);
}
function renderOutletControls(container) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = '<div class="card-title">Steuerung</div>';
const powerGroup = document.createElement('div');
powerGroup.className = 'control-group';
const powerButton = document.createElement('button');
powerButton.className = 'toggle-button ' + (deviceState.power === 'on' ? 'on' : 'off');
powerButton.textContent = deviceState.power === 'on' ? '🔌 An' : '🔌 Aus';
powerButton.onclick = () => togglePower();
powerGroup.appendChild(powerButton);
card.appendChild(powerGroup);
container.appendChild(card);
}
function renderContactDisplay(container) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = '<div class="card-title">Status</div>';
const statusDiv = document.createElement('div');
statusDiv.style.textAlign = 'center';
const isOpen = deviceState.contact === 'open';
statusDiv.innerHTML = `
<div class="state-badge ${isOpen ? 'open' : 'closed'}" id="contact-status">
${isOpen ? 'Offen' : 'Geschlossen'}
</div>
`;
card.appendChild(statusDiv);
container.appendChild(card);
}
function renderTempHumidityDisplay(container) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = '<div class="card-title">Messwerte</div>';
const stateGrid = document.createElement('div');
stateGrid.className = 'state-grid';
stateGrid.innerHTML = `
<div class="state-item">
<div class="state-value" id="temperature">${deviceState.temperature?.toFixed(1) || '--'}°C</div>
<div class="state-label">Temperatur</div>
</div>
<div class="state-item">
<div class="state-value" id="humidity">${deviceState.humidity || '--'}%</div>
<div class="state-label">Luftfeuchte</div>
</div>
`;
card.appendChild(stateGrid);
container.appendChild(card);
}
function renderCoverControls(container) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = '<div class="card-title">Position</div>';
// Position slider
const sliderGroup = document.createElement('div');
sliderGroup.className = 'control-group';
sliderGroup.innerHTML = `
<div class="slider-container">
<input type="range" class="slider" id="position-slider" min="0" max="100" value="${deviceState.position || 0}">
<div class="slider-value" id="position-value">${deviceState.position || 0}%</div>
</div>
`;
card.appendChild(sliderGroup);
// Control buttons
const buttonGroup = document.createElement('div');
buttonGroup.className = 'button-group';
buttonGroup.style.marginTop = '16px';
buttonGroup.innerHTML = `
<button class="control-button" onclick="setCoverPosition(0)">Auf</button>
<button class="control-button" onclick="stopCover()">Stop</button>
<button class="control-button" onclick="setCoverPosition(100)">Zu</button>
`;
card.appendChild(buttonGroup);
container.appendChild(card);
setTimeout(() => {
const slider = document.getElementById('position-slider');
const valueDisplay = document.getElementById('position-value');
slider.oninput = (e) => {
valueDisplay.textContent = e.target.value + '%';
};
slider.onchange = (e) => {
setCoverPosition(parseInt(e.target.value));
};
}, 0);
}
// Control functions
async function togglePower() {
const newState = deviceState.power === 'on' ? 'off' : 'on';
await sendCommand({
type: deviceData.type,
payload: { power: newState }
});
}
async function setBrightness(value) {
await sendCommand({
type: 'light',
payload: { brightness: value }
});
}
async function setColor(hexColor) {
// Convert hex to HSB
const hsb = hexToHSB(hexColor);
await sendCommand({
type: 'light',
payload: {
hue: Math.round(hsb.h),
sat: Math.round(hsb.s)
}
});
}
async function setTargetTemp(value) {
await sendCommand({
type: 'thermostat',
payload: { target: value }
});
}
async function setCoverPosition(value) {
await sendCommand({
type: 'cover',
payload: { position: value }
});
}
async function stopCover() {
await sendCommand({
type: 'cover',
payload: { action: 'stop' }
});
}
async function sendCommand(payload) {
try {
await window.apiClient.setDeviceState(deviceId, deviceData.type, payload.payload);
showSuccess('Befehl gesendet');
} catch (error) {
console.error('Error sending command:', error);
showError('Fehler beim Senden: ' + error.message);
}
}
function showSuccess(message) {
const container = document.getElementById('controls-container');
const successDiv = document.createElement('div');
successDiv.className = 'success';
successDiv.textContent = '✓ ' + message;
container.appendChild(successDiv);
setTimeout(() => successDiv.remove(), 3000);
}
function showError(message) {
const container = document.getElementById('error-container');
container.innerHTML = `<div class="error">⚠️ ${message}</div>`;
setTimeout(() => container.innerHTML = '', 5000);
}
function hexToHSB(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
let h = 0;
if (diff !== 0) {
if (max === r) h = ((g - b) / diff) % 6;
else if (max === g) h = (b - r) / diff + 2;
else h = (r - g) / diff + 4;
}
h = Math.round(h * 60);
if (h < 0) h += 360;
const s = max === 0 ? 0 : (diff / max) * 100;
return { h, s, b: max * 100 };
}
function connectRealtime() {
try {
// Use API client's realtime connection
window.apiClient.connectRealtime((event) => {
if (event.device_id === deviceId && event.state) {
deviceState = { ...deviceState, ...event.state };
updateUI();
}
}, (error) => {
console.error('SSE connection error:', error);
});
} catch (error) {
console.error('Failed to connect to realtime events:', error);
}
}
function updateUI() {
// Update based on device type
switch (deviceData.type) {
case 'light':
updateLightUI();
break;
case 'thermostat':
updateThermostatUI();
break;
case 'relay':
case 'outlet':
updateOutletUI();
break;
case 'contact':
updateContactUI();
break;
case 'temp_humidity_sensor':
updateTempHumidityUI();
break;
case 'cover':
updateCoverUI();
break;
}
}
function updateLightUI() {
const button = document.querySelector('.toggle-button');
if (button) {
button.className = 'toggle-button ' + (deviceState.power === 'on' ? 'on' : 'off');
button.textContent = deviceState.power === 'on' ? '💡 An' : '💡 Aus';
}
const brightnessSlider = document.getElementById('brightness-slider');
const brightnessValue = document.getElementById('brightness-value');
if (brightnessSlider && deviceState.brightness != null) {
brightnessSlider.value = deviceState.brightness;
brightnessValue.textContent = deviceState.brightness + '%';
}
}
function updateThermostatUI() {
const currentTemp = document.getElementById('current-temp');
const targetTemp = document.getElementById('target-temp');
const tempSlider = document.getElementById('temp-slider');
const tempValue = document.getElementById('temp-value');
if (currentTemp && deviceState.current != null) {
currentTemp.textContent = deviceState.current.toFixed(1) + '°C';
}
if (targetTemp && deviceState.target != null) {
targetTemp.textContent = deviceState.target.toFixed(1) + '°C';
}
if (tempSlider && deviceState.target != null) {
tempSlider.value = deviceState.target;
tempValue.textContent = deviceState.target.toFixed(1) + '°C';
}
}
function updateOutletUI() {
const button = document.querySelector('.toggle-button');
if (button) {
button.className = 'toggle-button ' + (deviceState.power === 'on' ? 'on' : 'off');
button.textContent = deviceState.power === 'on' ? '🔌 An' : '🔌 Aus';
}
}
function updateContactUI() {
const status = document.getElementById('contact-status');
if (status && deviceState.contact) {
const isOpen = deviceState.contact === 'open';
status.className = 'state-badge ' + (isOpen ? 'open' : 'closed');
status.textContent = isOpen ? 'Offen' : 'Geschlossen';
}
}
function updateTempHumidityUI() {
const temperature = document.getElementById('temperature');
const humidity = document.getElementById('humidity');
if (temperature && deviceState.temperature != null) {
temperature.textContent = deviceState.temperature.toFixed(1) + '°C';
}
if (humidity && deviceState.humidity != null) {
humidity.textContent = deviceState.humidity + '%';
}
}
function updateCoverUI() {
const slider = document.getElementById('position-slider');
const value = document.getElementById('position-value');
if (slider && deviceState.position != null) {
slider.value = deviceState.position;
value.textContent = deviceState.position + '%';
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
window.apiClient.disconnectRealtime();
});
// Load device on page load
document.addEventListener('DOMContentLoaded', loadDevice);
</script>
</body>
</html>

466
apps/ui/templates/room.html Normal file
View File

@@ -0,0 +1,466 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ room_name }} - Home Automation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #333;
font-size: 28px;
margin-bottom: 8px;
}
.header {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.back-button {
background: none;
border: none;
color: #667eea;
font-size: 16px;
cursor: pointer;
padding: 8px 0;
margin-bottom: 12px;
display: inline-block;
text-decoration: none;
}
.back-button:hover {
text-decoration: underline;
}
.room-info {
color: #666;
font-size: 14px;
}
.devices-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 20px;
}
@media (min-width: 600px) {
.devices-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 900px) {
.devices-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.device-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-height: 110px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-decoration: none;
color: inherit;
width: 100%;
}
.device-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.device-card:active {
transform: translateY(-2px);
}
.device-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.device-icon {
font-size: 24px;
}
.device-title {
font-size: 14px;
font-weight: 600;
color: #333;
flex: 1;
line-height: 1.3;
}
.device-state {
margin-top: auto;
}
.state-primary {
font-size: 20px;
font-weight: 600;
color: #667eea;
margin-bottom: 4px;
}
.state-secondary {
font-size: 14px;
color: #666;
}
.state-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
}
.state-badge.on {
background: #34c759;
color: white;
}
.state-badge.off {
background: #e0e0e0;
color: #666;
}
.state-badge.open {
background: #ff9500;
color: white;
}
.state-badge.closed {
background: #34c759;
color: white;
}
.loading {
text-align: center;
color: white;
font-size: 18px;
padding: 40px;
}
.error {
background: rgba(255, 59, 48, 0.9);
color: white;
padding: 16px;
border-radius: 8px;
text-align: center;
margin-bottom: 20px;
}
.no-devices {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 40px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<a href="/rooms" class="back-button">← Zurück zu Räumen</a>
<h1 id="room-name">Raum wird geladen...</h1>
<div class="room-info" id="room-info"></div>
</div>
<div id="error-container"></div>
<div id="loading" class="loading">Lade Geräte...</div>
<div id="devices-grid" class="devices-grid" style="display: none;"></div>
<div id="no-devices" class="no-devices" style="display: none;">
Keine Geräte in diesem Raum gefunden.
</div>
</div>
<script>
// API configuration from backend
window.API_BASE = '{{ api_base }}';
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script>
// Get room name from URL
const pathParts = window.location.pathname.split('/');
const roomName = decodeURIComponent(pathParts[pathParts.length - 1]);
// Device type to icon mapping
const deviceIcons = {
'light': '💡',
'thermostat': '🌡️',
'contact': '🚪',
'temp_humidity_sensor': '🌡️',
'relay': '💡',
'outlet': '💡',
'cover': '🪟'
};
// Device states
const deviceStates = {};
async function loadRoom() {
const loading = document.getElementById('loading');
const grid = document.getElementById('devices-grid');
const noDevices = document.getElementById('no-devices');
const errorContainer = document.getElementById('error-container');
const roomNameEl = document.getElementById('room-name');
const roomInfoEl = document.getElementById('room-info');
try {
// Load layout and devices using API client
// NEW: Use device layout endpoint for each device in this room
const layoutData = await window.apiClient.getLayout();
const devicesData = await window.apiClient.getDevices();
// Example: For each device in room.devices, you could fetch layout info via
// await window.apiClient.fetch(window.apiClient.api(`/devices/${device_id}/layout`));
console.log('Room name from URL:', roomName);
console.log('Available rooms:', layoutData.rooms.map(r => r.name));
console.log('Total devices:', devicesData.length);
// Find the room using API client helper
const room = window.apiClient.findRoom(layoutData, roomName);
if (!room) {
console.error('Room not found:', roomName);
throw new Error(`Raum "${roomName}" nicht gefunden`);
}
console.log('Found room:', room);
console.log('Room devices:', room.devices);
// Update header
roomNameEl.textContent = room.name;
roomInfoEl.textContent = `${room.devices.length} Gerät${room.devices.length !== 1 ? 'e' : ''}`;
// Create device lookup
const deviceMap = {};
devicesData.forEach(device => {
deviceMap[device.device_id] = device;
});
// Extract device IDs from room devices (they are objects now)
const deviceIds = room.devices.map(d => d.device_id);
console.log('Device IDs from room:', deviceIds);
// Filter devices for this room
const roomDevices = deviceIds
.map(deviceId => deviceMap[deviceId])
.filter(device => device != null);
console.log('Filtered room devices:', roomDevices);
loading.style.display = 'none';
if (roomDevices.length === 0) {
noDevices.style.display = 'block';
return;
}
// Lade nur die States für die Geräte im aktuellen Raum
for (const device of roomDevices) {
try {
deviceStates[device.device_id] = await window.apiClient.getDeviceState(device.device_id);
} catch (err) {
deviceStates[device.device_id] = null;
}
}
console.log('Device states:', deviceStates);
// Render devices
grid.style.display = 'grid';
roomDevices.forEach(device => {
const card = createDeviceCard(device);
grid.appendChild(card);
});
// Start SSE for live updates
connectRealtime();
} catch (error) {
console.error('Error loading room:', error);
loading.style.display = 'none';
errorContainer.innerHTML = `
<div class="error">
⚠️ Fehler beim Laden: ${error.message}
</div>
`;
}
}
function createDeviceCard(device) {
const card = document.createElement('a');
card.className = 'device-card';
card.href = `/device/${device.device_id}`;
card.dataset.deviceId = device.device_id;
const header = document.createElement('div');
header.className = 'device-header';
const icon = document.createElement('div');
icon.className = 'device-icon';
icon.textContent = deviceIcons[device.type] || '📱';
const title = document.createElement('div');
title.className = 'device-title';
title.textContent = device.name;
header.appendChild(icon);
header.appendChild(title);
const stateDiv = document.createElement('div');
stateDiv.className = 'device-state';
updateDeviceCardState(stateDiv, device);
card.appendChild(header);
card.appendChild(stateDiv);
return card;
}
function updateDeviceCardState(stateDiv, device) {
const state = deviceStates[device.device_id];
if (!state) {
stateDiv.innerHTML = '<div class="state-secondary">Status unbekannt</div>';
return;
}
let html = '';
switch (device.type) {
case 'light':
if (state.power) {
const powerState = state.power === 'on';
html = `<span class="state-badge ${powerState ? 'on' : 'off'}">${powerState ? 'An' : 'Aus'}</span>`;
if (powerState && state.brightness != null) {
html += `<div class="state-secondary">${state.brightness}% Helligkeit</div>`;
}
}
break;
case 'thermostat':
if (state.target != null) {
html = `<div class="state-primary">${state.target.toFixed(1)}°C</div>`;
if (state.current != null) {
html += `<div class="state-secondary">Ist: ${state.current}°C</div>`;
}
}
break;
case 'contact':
if (state.contact != null) {
const isOpen = state.contact === 'open';
html = `<span class="state-badge ${isOpen ? 'open' : 'closed'}">${isOpen ? 'Offen' : 'Geschlossen'}</span>`;
}
break;
case 'temp_humidity_sensor':
if (state.temperature != null) {
html = `<div class="state-primary">${state.temperature.toFixed(1)}°C</div>`;
if (state.humidity != null) {
html += `<div class="state-secondary">${state.humidity}% Luftfeuchte</div>`;
}
}
break;
case 'relay':
case 'outlet':
if (state.power) {
const powerState = state.power === 'on';
html = `<span class="state-badge ${powerState ? 'on' : 'off'}">${powerState ? 'An' : 'Aus'}</span>`;
}
break;
case 'cover':
if (state.position != null) {
html = `<div class="state-primary">${state.position}%</div>`;
html += `<div class="state-secondary">Position</div>`;
}
break;
default:
html = '<div class="state-secondary">Kein Status</div>';
}
stateDiv.innerHTML = html;
}
function connectRealtime() {
try {
// Use API client's connectRealtime method
window.apiClient.connectRealtime((event) => {
// Update device state
if (event.device_id && event.state) {
deviceStates[event.device_id] = event.state;
// Update card if visible
const card = document.querySelector(`[data-device-id="${event.device_id}"]`);
if (card) {
const stateDiv = card.querySelector('.device-state');
window.apiClient.getDevices().then(devices => {
const device = window.apiClient.findDevice(devices, event.device_id);
if (device) {
updateDeviceCardState(stateDiv, device);
}
});
}
}
}, (error) => {
console.error('SSE connection error:', error);
});
} catch (error) {
console.error('Failed to connect to realtime events:', error);
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
window.apiClient.disconnectRealtime();
});
// Load room on page load
document.addEventListener('DOMContentLoaded', loadRoom);
</script>
</body>
</html>

View File

@@ -0,0 +1,308 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Räume - Home Automation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
font-size: 28px;
margin-bottom: 24px;
text-align: center;
}
.rooms-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 20px;
}
@media (min-width: 600px) {
.rooms-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 900px) {
.rooms-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.room-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-height: 110px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-decoration: none;
color: inherit;
}
.room-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.room-card:active {
transform: translateY(-2px);
}
.room-icon {
font-size: 32px;
margin-bottom: 8px;
}
.room-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.room-device-count {
font-size: 14px;
color: #666;
}
.room-stats {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.loading {
text-align: center;
color: white;
font-size: 18px;
padding: 40px;
}
.error {
background: rgba(255, 59, 48, 0.9);
color: white;
padding: 16px;
border-radius: 8px;
text-align: center;
margin-bottom: 20px;
}
.back-button {
position: fixed;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
color: #667eea;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
text-decoration: none;
display: inline-block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.back-button:hover {
background: white;
}
</style>
</head>
<body>
<div class="container">
<h1>🏠 Zuhause</h1>
<div id="error-container"></div>
<div id="loading" class="loading">Lade Räume...</div>
<div id="rooms-grid" class="rooms-grid" style="display: none;"></div>
</div>
<script>
// API configuration from backend
window.API_BASE = '{{ api_base }}';
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script>
// Room icon mapping
const roomIcons = {
'wohnzimmer': '🛋️',
'küche': '🍳',
'kueche': '🍳',
'schlafzimmer': '🛏️',
'bad': '🚿',
'badezimmer': '🚿',
'bad oben': '🚿',
'bad unten': '🚿',
'flur': '🚪',
'büro': '💼',
'buero': '💼',
'arbeitszimmer': '💼',
'studierzimmer': '📚',
'esszimmer': '🍽️',
'garten': '🌳',
'terrasse': '🌿',
'garage': '🚗',
'keller': '🔧',
'dachboden': '📦',
'kinderzimmer': '🧸',
'patty': '👤',
'wolfgang': '👤',
'default': '🏡'
};
function getRoomIcon(roomName) {
const normalized = roomName.toLowerCase().trim();
return roomIcons[normalized] || roomIcons['default'];
}
async function loadRooms() {
const loading = document.getElementById('loading');
const grid = document.getElementById('rooms-grid');
const errorContainer = document.getElementById('error-container');
try {
// Load layout and devices using API client
// NEW: Use device layout endpoint for each device
// Fallback: load all rooms as before
const layoutData = await window.apiClient.getLayout();
const devicesData = await window.apiClient.getDevices();
// Example: For each device, you could also fetch layout info via
// await window.apiClient.fetch(window.apiClient.api(`/devices/${device_id}/layout`));
// Create device lookup
const deviceMap = {};
devicesData.forEach(device => {
deviceMap[device.device_id] = device;
});
// Render rooms
loading.style.display = 'none';
grid.style.display = 'grid';
layoutData.rooms.forEach(room => {
const card = createRoomCard(room, deviceMap);
grid.appendChild(card);
});
} catch (error) {
console.error('Error loading rooms:', error);
loading.style.display = 'none';
errorContainer.innerHTML = `
<div class="error">
⚠️ Fehler beim Laden der Räume: ${error.message}
</div>
`;
}
}
function createRoomCard(room, deviceMap) {
const card = document.createElement('a');
card.className = 'room-card';
card.href = `/room/${encodeURIComponent(room.name)}`;
const icon = document.createElement('div');
icon.className = 'room-icon';
icon.textContent = getRoomIcon(room.name);
const name = document.createElement('div');
name.className = 'room-name';
name.textContent = room.name;
const deviceCount = document.createElement('div');
deviceCount.className = 'room-device-count';
deviceCount.textContent = `${room.devices.length} Gerät${room.devices.length !== 1 ? 'e' : ''}`;
// Optional: Calculate stats (lights on, windows open, etc.)
const stats = calculateRoomStats(room.devices, deviceMap);
if (stats) {
const statsDiv = document.createElement('div');
statsDiv.className = 'room-stats';
statsDiv.textContent = stats;
card.appendChild(icon);
card.appendChild(name);
card.appendChild(deviceCount);
card.appendChild(statsDiv);
} else {
card.appendChild(icon);
card.appendChild(name);
card.appendChild(deviceCount);
}
return card;
}
function calculateRoomStats(roomDevices, deviceMap) {
// Extract device IDs from room devices (they are objects now)
const deviceIds = roomDevices.map(d => d.device_id || d);
// Count device types
let lights = 0;
let thermostats = 0;
let contacts = 0;
let sensors = 0;
deviceIds.forEach(deviceId => {
const device = deviceMap[deviceId];
if (!device) return;
switch(device.type) {
case 'light':
lights++;
break;
case 'thermostat':
thermostats++;
break;
case 'contact':
contacts++;
break;
case 'temp_humidity_sensor':
sensors++;
break;
}
});
// Build compact stats string
const parts = [];
if (lights > 0) parts.push(`💡${lights}`);
if (thermostats > 0) parts.push(`🌡️${thermostats}`);
if (contacts > 0) parts.push(`🚪${contacts}`);
return parts.length > 0 ? parts.join(' ') : null;
}
// Load rooms on page load
document.addEventListener('DOMContentLoaded', loadRooms);
</script>
</body>
</html>

View File

@@ -11,12 +11,12 @@ redis:
channel: "ui:updates" channel: "ui:updates"
devices: devices:
- device_id: lampe_semeniere_wohnzimmer - device_id: lampe_semeniere_wohnzimmer
type: light name: Semeniere
cap_version: "light@1.2.0" type: relay
cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b8000015480b" state: "zigbee2mqtt/0xf0d1b8000015480b"
set: "zigbee2mqtt/0xf0d1b8000015480b/set" set: "zigbee2mqtt/0xf0d1b8000015480b/set"
@@ -25,13 +25,35 @@ devices:
ieee_address: "0xf0d1b8000015480b" ieee_address: "0xf0d1b8000015480b"
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: grosse_lampe_wohnzimmer - device_id: stehlampe_esszimmer_spiegel
name: Stehlampe Spiegel
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false brightness: true
topics:
state: "zigbee2mqtt/0x001788010d06ea09"
set: "zigbee2mqtt/0x001788010d06ea09/set"
- device_id: stehlampe_esszimmer_schrank
name: Stehlampe Schrank
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x001788010d09176c"
set: "zigbee2mqtt/0x001788010d09176c/set"
- device_id: grosse_lampe_wohnzimmer
name: grosse Lampe
type: relay
cap_version: "relay@1.0.0"
technology: zigbee2mqtt
features:
power: true
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000151aca" state: "zigbee2mqtt/0xf0d1b80000151aca"
set: "zigbee2mqtt/0xf0d1b80000151aca/set" set: "zigbee2mqtt/0xf0d1b80000151aca/set"
@@ -41,12 +63,12 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: lampe_naehtischchen_wohnzimmer - device_id: lampe_naehtischchen_wohnzimmer
type: light name: Nähtischchen
cap_version: "light@1.2.0" type: relay
cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0x842e14fffee560ee" state: "zigbee2mqtt/0x842e14fffee560ee"
set: "zigbee2mqtt/0x842e14fffee560ee/set" set: "zigbee2mqtt/0x842e14fffee560ee/set"
@@ -55,28 +77,13 @@ devices:
ieee_address: "0x842e14fffee560ee" ieee_address: "0x842e14fffee560ee"
model: "HG06337" model: "HG06337"
vendor: "Lidl" vendor: "Lidl"
- device_id: kleine_lampe_rechts_esszimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: false
topics:
state: "zigbee2mqtt/0xf0d1b80000156645"
set: "zigbee2mqtt/0xf0d1b80000156645/set"
metadata:
friendly_name: "kleine Lampe rechts Esszimmer"
ieee_address: "0xf0d1b80000156645"
model: "AC10691"
vendor: "OSRAM"
- device_id: kleine_lampe_links_esszimmer - device_id: kleine_lampe_links_esszimmer
type: light name: kleine Lampe
cap_version: "light@1.2.0" type: relay
cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000153099" state: "zigbee2mqtt/0xf0d1b80000153099"
set: "zigbee2mqtt/0xf0d1b80000153099/set" set: "zigbee2mqtt/0xf0d1b80000153099/set"
@@ -86,6 +93,7 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: leselampe_esszimmer - device_id: leselampe_esszimmer
name: Leselampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -101,12 +109,12 @@ devices:
model: "LED1842G3" model: "LED1842G3"
vendor: "IKEA" vendor: "IKEA"
- device_id: medusalampe_schlafzimmer - device_id: medusalampe_schlafzimmer
type: light name: Medusa-Lampe
cap_version: "light@1.2.0" type: relay
cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000154c7c" state: "zigbee2mqtt/0xf0d1b80000154c7c"
set: "zigbee2mqtt/0xf0d1b80000154c7c/set" set: "zigbee2mqtt/0xf0d1b80000154c7c/set"
@@ -117,6 +125,7 @@ devices:
vendor: "OSRAM" vendor: "OSRAM"
- device_id: sportlicht_am_fernseher_studierzimmer - device_id: sportlicht_am_fernseher_studierzimmer
type: light type: light
name: am Fernseher
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
@@ -132,6 +141,7 @@ devices:
model: "LED1733G7" model: "LED1733G7"
vendor: "IKEA" vendor: "IKEA"
- device_id: deckenlampe_schlafzimmer - device_id: deckenlampe_schlafzimmer
name: Deckenlampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -147,6 +157,7 @@ devices:
model: "8718699688882" model: "8718699688882"
vendor: "Philips" vendor: "Philips"
- device_id: bettlicht_wolfgang - device_id: bettlicht_wolfgang
name: Bettlicht Wolfgang
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -162,6 +173,7 @@ devices:
model: "9290020399" model: "9290020399"
vendor: "Philips" vendor: "Philips"
- device_id: bettlicht_patty - device_id: bettlicht_patty
name: Bettlicht Patty
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -177,6 +189,7 @@ devices:
model: "9290020399" model: "9290020399"
vendor: "Philips" vendor: "Philips"
- device_id: schranklicht_hinten_patty - device_id: schranklicht_hinten_patty
name: Schranklicht hinten
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -192,12 +205,12 @@ devices:
model: "8718699673147" model: "8718699673147"
vendor: "Philips" vendor: "Philips"
- device_id: schranklicht_vorne_patty - device_id: schranklicht_vorne_patty
type: light name: Schranklicht vorne
cap_version: "light@1.2.0" type: relay
cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000154cf5" state: "zigbee2mqtt/0xf0d1b80000154cf5"
set: "zigbee2mqtt/0xf0d1b80000154cf5/set" set: "zigbee2mqtt/0xf0d1b80000154cf5/set"
@@ -207,6 +220,7 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: leselampe_patty - device_id: leselampe_patty
name: Leselampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -222,6 +236,7 @@ devices:
model: "8718699673147" model: "8718699673147"
vendor: "Philips" vendor: "Philips"
- device_id: deckenlampe_esszimmer - device_id: deckenlampe_esszimmer
name: Deckenlampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -236,23 +251,8 @@ devices:
ieee_address: "0x0017880108a03e45" ieee_address: "0x0017880108a03e45"
model: "929002241201" model: "929002241201"
vendor: "Philips" vendor: "Philips"
- device_id: standlampe_esszimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
color_temperature: true
topics:
state: "zigbee2mqtt/0xbc33acfffe21f547"
set: "zigbee2mqtt/0xbc33acfffe21f547/set"
metadata:
friendly_name: "Standlampe Esszimmer"
ieee_address: "0xbc33acfffe21f547"
model: "LED1732G11"
vendor: "IKEA"
- device_id: haustuer - device_id: haustuer
name: Haustür-Lampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -268,6 +268,7 @@ devices:
model: "LED1842G3" model: "LED1842G3"
vendor: "IKEA" vendor: "IKEA"
- device_id: deckenlampe_flur_oben - device_id: deckenlampe_flur_oben
name: Deckenlampe oben
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -284,6 +285,7 @@ devices:
model: "929003099001" model: "929003099001"
vendor: "Philips" vendor: "Philips"
- device_id: kueche_deckenlampe - device_id: kueche_deckenlampe
name: Deckenlampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -299,6 +301,7 @@ devices:
model: "929002469202" model: "929002469202"
vendor: "Philips" vendor: "Philips"
- device_id: sportlicht_tisch - device_id: sportlicht_tisch
name: am Tisch
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -314,6 +317,7 @@ devices:
model: "4058075729063" model: "4058075729063"
vendor: "LEDVANCE" vendor: "LEDVANCE"
- device_id: sportlicht_regal - device_id: sportlicht_regal
name: am Regal
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -329,6 +333,7 @@ devices:
model: "4058075729063" model: "4058075729063"
vendor: "LEDVANCE" vendor: "LEDVANCE"
- device_id: licht_flur_oben_am_spiegel - device_id: licht_flur_oben_am_spiegel
name: Spiegel
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -345,6 +350,7 @@ devices:
model: "LED1732G11" model: "LED1732G11"
vendor: "IKEA" vendor: "IKEA"
- device_id: experimentlabtest - device_id: experimentlabtest
name: Test Lampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -360,6 +366,7 @@ devices:
model: "4058075208421" model: "4058075208421"
vendor: "LEDVANCE" vendor: "LEDVANCE"
- device_id: thermostat_wolfgang - device_id: thermostat_wolfgang
name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -378,6 +385,7 @@ devices:
model: "GS361A-H04" model: "GS361A-H04"
vendor: "Siterwell" vendor: "Siterwell"
- device_id: thermostat_kueche - device_id: thermostat_kueche
name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -396,6 +404,7 @@ devices:
model: "GS361A-H04" model: "GS361A-H04"
vendor: "Siterwell" vendor: "Siterwell"
- device_id: thermostat_schlafzimmer - device_id: thermostat_schlafzimmer
name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: max technology: max
@@ -414,6 +423,7 @@ devices:
peer_id: "42" peer_id: "42"
channel: "1" channel: "1"
- device_id: thermostat_esszimmer - device_id: thermostat_esszimmer
name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: max technology: max
@@ -432,6 +442,7 @@ devices:
peer_id: "45" peer_id: "45"
channel: "1" channel: "1"
- device_id: thermostat_wohnzimmer - device_id: thermostat_wohnzimmer
name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: max technology: max
@@ -450,6 +461,7 @@ devices:
peer_id: "46" peer_id: "46"
channel: "1" channel: "1"
- device_id: thermostat_patty - device_id: thermostat_patty
name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: max technology: max
@@ -468,6 +480,7 @@ devices:
peer_id: "39" peer_id: "39"
channel: "1" channel: "1"
- device_id: thermostat_bad_oben - device_id: thermostat_bad_oben
name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: max technology: max
@@ -486,6 +499,7 @@ devices:
peer_id: "41" peer_id: "41"
channel: "1" channel: "1"
- device_id: thermostat_bad_unten - device_id: thermostat_bad_unten
name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: max technology: max
@@ -504,12 +518,12 @@ devices:
peer_id: "48" peer_id: "48"
channel: "1" channel: "1"
- device_id: sterne_wohnzimmer - device_id: sterne_wohnzimmer
type: light name: Sterne
cap_version: "light@1.2.0" type: relay
cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000155fc2" state: "zigbee2mqtt/0xf0d1b80000155fc2"
set: "zigbee2mqtt/0xf0d1b80000155fc2/set" set: "zigbee2mqtt/0xf0d1b80000155fc2/set"
@@ -518,10 +532,9 @@ devices:
ieee_address: "0xf0d1b80000155fc2" ieee_address: "0xf0d1b80000155fc2"
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: kontakt_schlafzimmer_strasse - device_id: kontakt_schlafzimmer_strasse
name: Fenster
type: contact type: contact
name: Kontakt Schlafzimmer Straße
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: max technology: max
topics: topics:
@@ -529,39 +542,39 @@ devices:
features: {} features: {}
- device_id: kontakt_esszimmer_strasse_rechts - device_id: kontakt_esszimmer_strasse_rechts
type: contact type: contact
name: Kontakt Esszimmer Straße rechts name: Fenster rechts
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: max technology: max
topics: topics:
state: homegear/instance1/plain/26/1/STATE state: homegear/instance1/plain/26/1/STATE
features: {} features: {}
- device_id: kontakt_esszimmer_strasse_links - device_id: kontakt_esszimmer_strasse_links
name: Fenster links
type: contact type: contact
name: Kontakt Esszimmer Straße links
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: max technology: max
topics: topics:
state: homegear/instance1/plain/27/1/STATE state: homegear/instance1/plain/27/1/STATE
features: {} features: {}
- device_id: kontakt_wohnzimmer_garten_rechts - device_id: kontakt_wohnzimmer_garten_rechts
name: Fenster rechts
type: contact type: contact
name: Kontakt Wohnzimmer Garten rechts
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: max technology: max
topics: topics:
state: homegear/instance1/plain/28/1/STATE state: homegear/instance1/plain/28/1/STATE
features: {} features: {}
- device_id: kontakt_wohnzimmer_garten_links - device_id: kontakt_wohnzimmer_garten_links
name: Fenster links
type: contact type: contact
name: Kontakt Wohnzimmer Garten links
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: max technology: max
topics: topics:
state: homegear/instance1/plain/29/1/STATE state: homegear/instance1/plain/29/1/STATE
features: {} features: {}
- device_id: kontakt_kueche_garten_fenster - device_id: kontakt_kueche_garten_fenster
name: Fenster Garten
type: contact type: contact
name: Kontakt Küche Garten Fenster
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt technology: zigbee2mqtt
topics: topics:
@@ -569,23 +582,23 @@ devices:
features: {} features: {}
- device_id: kontakt_kueche_garten_tuer - device_id: kontakt_kueche_garten_tuer
type: contact type: contact
name: Kontakt Küche Garten Tür name: Terrassentür
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt technology: zigbee2mqtt
topics: topics:
state: zigbee2mqtt/0x00158d008b332788 state: zigbee2mqtt/0x00158d008b332788
features: {} features: {}
- device_id: kontakt_kueche_strasse_rechts - device_id: kontakt_kueche_strasse_rechts
name: Fenster Straße rechts
type: contact type: contact
name: Kontakt Küche Straße rechts
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt technology: zigbee2mqtt
topics: topics:
state: zigbee2mqtt/0x00158d008b151803 state: zigbee2mqtt/0x00158d008b151803
features: {} features: {}
- device_id: kontakt_kueche_strasse_links - device_id: kontakt_kueche_strasse_links
name: Fenster Straße links
type: contact type: contact
name: Kontakt Küche Straße links
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt technology: zigbee2mqtt
topics: topics:
@@ -593,7 +606,7 @@ devices:
features: {} features: {}
- device_id: kontakt_patty_garten_rechts - device_id: kontakt_patty_garten_rechts
type: contact type: contact
name: Kontakt Patty Garten rechts name: Fenster Garten rechts
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: max technology: max
topics: topics:
@@ -601,7 +614,7 @@ devices:
features: {} features: {}
- device_id: kontakt_patty_garten_links - device_id: kontakt_patty_garten_links
type: contact type: contact
name: Kontakt Patty Garten links name: Fenster Garten links
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: max technology: max
topics: topics:
@@ -609,7 +622,7 @@ devices:
features: {} features: {}
- device_id: kontakt_patty_strasse - device_id: kontakt_patty_strasse
type: contact type: contact
name: Kontakt Patty Straße name: Fenster Straße
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt technology: zigbee2mqtt
topics: topics:
@@ -617,7 +630,7 @@ devices:
features: {} features: {}
- device_id: kontakt_wolfgang_garten - device_id: kontakt_wolfgang_garten
type: contact type: contact
name: Kontakt Wolfgang Garten name: Fenster
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt technology: zigbee2mqtt
topics: topics:
@@ -625,7 +638,7 @@ devices:
features: {} features: {}
- device_id: kontakt_bad_oben_strasse - device_id: kontakt_bad_oben_strasse
type: contact type: contact
name: Kontakt Bad Oben Straße name: Fenster
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt technology: zigbee2mqtt
topics: topics:
@@ -633,10 +646,143 @@ devices:
features: {} features: {}
- device_id: kontakt_bad_unten_strasse - device_id: kontakt_bad_unten_strasse
type: contact type: contact
name: Kontakt Bad Unten Straße name: Fenster
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
technology: max technology: max
topics: topics:
state: homegear/instance1/plain/44/1/STATE state: homegear/instance1/plain/44/1/STATE
features: {} features: {}
- device_id: sensor_schlafzimmer
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d00043292dc
features: {}
- device_id: sensor_wohnzimmer
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d0008975707
features: {}
- device_id: sensor_kueche
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d00083299bb
features: {}
- device_id: sensor_arbeitszimmer_patty
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d0003f052b7
features: {}
- device_id: sensor_arbeitszimmer_wolfgang
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d000543fb99
features: {}
- device_id: sensor_bad_oben
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d00093e8987
features: {}
- device_id: sensor_bad_unten
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d00093e662a
features: {}
- device_id: sensor_flur
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d000836ccc6
features: {}
- device_id: sensor_waschkueche
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d000449f3bc
features: {}
- device_id: sensor_sportzimmer
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d0009421422
features: {}
- device_id: licht_spuele_kueche
name: Spüle
type: relay
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/LightKitchenSink/relay/0/command"
state: "shellies/LightKitchenSink/relay/0"
- device_id: licht_schrank_esszimmer
name: Schrank
type: relay
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/schrankesszimmer/relay/0/command"
state: "shellies/schrankesszimmer/relay/0"
- device_id: licht_regal_wohnzimmer
type: relay
name: Regal
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/wohnzimmer-regal/relay/0/command"
state: "shellies/wohnzimmer-regal/relay/0"
- device_id: licht_flur_schrank
type: relay
name: Schrank
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/schrankflur/relay/0/command"
state: "shellies/schrankflur/relay/0"
- device_id: licht_terasse
name: Terrasse
type: relay
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/lichtterasse/relay/0/command"
state: "shellies/lichtterasse/relay/0"

View File

@@ -1,66 +0,0 @@
version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
client_id: "home-automation-abstraction"
username: null
password: null
keepalive: 60
redis:
url: "redis://172.23.1.116:6379/8"
channel: "ui:updates"
devices:
- device_id: test_lampe_1
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
brightness: true
topics:
set: "vendor/test_lampe_1/set"
state: "vendor/test_lampe_1/state"
- device_id: test_lampe_2
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
topics:
set: "vendor/test_lampe_2/set"
state: "vendor/test_lampe_2/state"
- device_id: test_lampe_3
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
brightness: true
topics:
set: "vendor/test_lampe_3/set"
state: "vendor/test_lampe_3/state"
- device_id: test_thermo_1
type: thermostat
cap_version: "thermostat@2.0.0"
technology: simulator
features:
mode: false
target: true
current: true
battery: true
topics:
set: "vendor/test_thermo_1/set"
state: "vendor/test_thermo_1/state"
- device_id: experiment_light_1
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
set: "zigbee2mqtt/0xf0d1b80000195038/set"
state: "zigbee2mqtt/0xf0d1b80000195038"

View File

@@ -1,66 +0,0 @@
version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
client_id: "home-automation-abstraction"
username: null
password: null
keepalive: 60
redis:
url: "redis://172.23.1.116:6379/8"
channel: "ui:updates"
devices:
- device_id: test_lampe_1
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
brightness: true
topics:
set: "vendor/test_lampe_1/set"
state: "vendor/test_lampe_1/state"
- device_id: test_lampe_2
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
topics:
set: "vendor/test_lampe_2/set"
state: "vendor/test_lampe_2/state"
- device_id: test_lampe_3
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
brightness: true
topics:
set: "vendor/test_lampe_3/set"
state: "vendor/test_lampe_3/state"
- device_id: test_thermo_1
type: thermostat
cap_version: "thermostat@2.0.0"
technology: simulator
features:
mode: false
target: true
current: true
battery: true
topics:
set: "vendor/test_thermo_1/set"
state: "vendor/test_thermo_1/state"
- device_id: experiment_light_1
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
set: "zigbee2mqtt/0xf0d1b80000195038/set"
state: "zigbee2mqtt/0xf0d1b80000195038"

36
config/groups.yaml Normal file
View File

@@ -0,0 +1,36 @@
version: 1
groups:
- id: "kueche_lichter"
name: "Küche alle Lampen"
selector:
type: "light"
room: "Küche"
capabilities:
power: true
brightness: true
- id: "alles_lichter"
name: "Alle Lichter"
selector:
type: "light"
capabilities:
power: true
- id: "schlafzimmer_lichter"
name: "Schlafzimmer alle Lampen"
selector:
type: "light"
room: "Schlafzimmer"
capabilities:
power: true
brightness: true
- id: "schlafzimmer_schlummer_licht"
name: "Schlafzimmer Schlummerlicht"
device_ids:
- bettlicht_patty
- bettlicht_wolfgang
- medusalampe_schlafzimmer
capabilities:
power: true
brightness: true

View File

@@ -25,6 +25,10 @@ rooms:
title: Kontakt Straße title: Kontakt Straße
icon: 🪟 icon: 🪟
rank: 46 rank: 46
- device_id: sensor_schlafzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 47
- name: Esszimmer - name: Esszimmer
devices: devices:
- device_id: deckenlampe_esszimmer - device_id: deckenlampe_esszimmer
@@ -35,24 +39,36 @@ rooms:
title: Leselampe Esszimmer title: Leselampe Esszimmer
icon: 💡 icon: 💡
rank: 60 rank: 60
- device_id: standlampe_esszimmer # - device_id: standlampe_esszimmer
title: Standlampe Esszimmer # title: Standlampe Esszimmer
icon: 💡 # icon: 💡
rank: 70 # rank: 70
- device_id: kleine_lampe_links_esszimmer - device_id: kleine_lampe_links_esszimmer
title: kleine Lampe links Esszimmer title: kleine Lampe links Esszimmer
icon: 💡 icon: 💡
rank: 80 rank: 80
- device_id: kleine_lampe_rechts_esszimmer - device_id: stehlampe_esszimmer_spiegel
title: kleine Lampe rechts Esszimmer title: Stehlampe Esszimmer Spiegel
icon: 💡 icon: 💡
rank: 90 rank: 81
- device_id: stehlampe_esszimmer_schrank
title: Stehlampe Esszimmer Schrank
icon: 💡
rank: 82
# - device_id: kleine_lampe_rechts_esszimmer
# title: kleine Lampe rechts Esszimmer
# icon: 💡
# rank: 90
- device_id: licht_schrank_esszimmer
title: Schranklicht Esszimmer
icon: 💡
rank: 92
- device_id: thermostat_esszimmer - device_id: thermostat_esszimmer
title: Thermostat Esszimmer title: Thermostat Esszimmer
icon: 🌡️ icon: 🌡️
rank: 95 rank: 95
- device_id: kontakt_esszimmer_strasse_rechts - device_id: kontakt_esszimmer_strasse_rechts
title: Kontakt Straße rechts title: Kontakt Straße rechtsFtest
icon: 🪟 icon: 🪟
rank: 96 rank: 96
- device_id: kontakt_esszimmer_strasse_links - device_id: kontakt_esszimmer_strasse_links
@@ -77,6 +93,10 @@ rooms:
title: grosse Lampe Wohnzimmer title: grosse Lampe Wohnzimmer
icon: 💡 icon: 💡
rank: 130 rank: 130
- device_id: licht_regal_wohnzimmer
title: Regallicht Wohnzimmer
icon: 💡
rank: 132
- device_id: thermostat_wohnzimmer - device_id: thermostat_wohnzimmer
title: Thermostat Wohnzimmer title: Thermostat Wohnzimmer
icon: 🌡️ icon: 🌡️
@@ -89,12 +109,20 @@ rooms:
title: Kontakt Garten links title: Kontakt Garten links
icon: 🪟 icon: 🪟
rank: 137 rank: 137
- device_id: sensor_wohnzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 138
- name: Küche - name: Küche
devices: devices:
- device_id: kueche_deckenlampe - device_id: kueche_deckenlampe
title: Küche Deckenlampe title: Küche Deckenlampe
icon: 💡 icon: 💡
rank: 140 rank: 140
- device_id: licht_spuele_kueche
title: Küche Spüle
icon: 💡
rank: 142
- device_id: thermostat_kueche - device_id: thermostat_kueche
title: Kueche title: Kueche
icon: 🌡️ icon: 🌡️
@@ -115,6 +143,10 @@ rooms:
title: Kontakt Straße links title: Kontakt Straße links
icon: 🪟 icon: 🪟
rank: 154 rank: 154
- device_id: sensor_kueche
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 155
- name: Arbeitszimmer Patty - name: Arbeitszimmer Patty
devices: devices:
- device_id: leselampe_patty - device_id: leselampe_patty
@@ -145,6 +177,10 @@ rooms:
title: Kontakt Straße title: Kontakt Straße
icon: 🪟 icon: 🪟
rank: 188 rank: 188
- device_id: sensor_arbeitszimmer_patty
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 189
- name: Arbeitszimmer Wolfgang - name: Arbeitszimmer Wolfgang
devices: devices:
- device_id: thermostat_wolfgang - device_id: thermostat_wolfgang
@@ -159,6 +195,10 @@ rooms:
title: Kontakt Garten title: Kontakt Garten
icon: 🪟 icon: 🪟
rank: 201 rank: 201
- device_id: sensor_arbeitszimmer_wolfgang
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 202
- name: Flur - name: Flur
devices: devices:
- device_id: deckenlampe_flur_oben - device_id: deckenlampe_flur_oben
@@ -169,10 +209,18 @@ rooms:
title: Haustür title: Haustür
icon: 💡 icon: 💡
rank: 220 rank: 220
- device_id: licht_flur_schrank
title: Schranklicht Flur
icon: 💡
rank: 222
- device_id: licht_flur_oben_am_spiegel - device_id: licht_flur_oben_am_spiegel
title: Licht Flur oben am Spiegel title: Licht Flur oben am Spiegel
icon: 💡 icon: 💡
rank: 230 rank: 230
- device_id: sensor_flur
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 235
- name: Sportzimmer - name: Sportzimmer
devices: devices:
- device_id: sportlicht_regal - device_id: sportlicht_regal
@@ -187,6 +235,10 @@ rooms:
title: Sportlicht am Fernseher, Studierzimmer title: Sportlicht am Fernseher, Studierzimmer
icon: 🏃 icon: 🏃
rank: 260 rank: 260
- device_id: sensor_sportzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 265
- name: Bad Oben - name: Bad Oben
devices: devices:
- device_id: thermostat_bad_oben - device_id: thermostat_bad_oben
@@ -197,6 +249,10 @@ rooms:
title: Kontakt Straße title: Kontakt Straße
icon: 🪟 icon: 🪟
rank: 271 rank: 271
- device_id: sensor_bad_oben
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 272
- name: Bad Unten - name: Bad Unten
devices: devices:
- device_id: thermostat_bad_unten - device_id: thermostat_bad_unten
@@ -207,4 +263,20 @@ rooms:
title: Kontakt Straße title: Kontakt Straße
icon: 🪟 icon: 🪟
rank: 281 rank: 281
- device_id: sensor_bad_unten
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 282
- name: Waschküche
devices:
- device_id: sensor_waschkueche
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 290
- name: Outdoor
devices:
- device_id: licht_terasse
title: Licht Terasse
icon: 💡
rank: 290

View File

@@ -1,35 +0,0 @@
# UI Layout Configuration
# Defines rooms and device tiles for the home automation UI
rooms:
- name: Wohnzimmer
devices:
- device_id: test_lampe_2
title: Deckenlampe
icon: "💡"
rank: 5
- device_id: test_lampe_1
title: Stehlampe
icon: "🔆"
rank: 10
- device_id: test_thermo_1
title: Thermostat
icon: "🌡️"
rank: 15
- name: Schlafzimmer
devices:
- device_id: test_lampe_3
title: Nachttischlampe
icon: "🛏️"
rank: 10
- name: Lab
devices:
- device_id: experiment_light_1
title: Experimentierlampe
icon: "💡"
rank: 10

View File

@@ -1,23 +0,0 @@
# MAX! Thermostats - Room Assignment
#
# Extracted from layout.yaml
# Format: Room Name | Device ID (if thermostat exists)
#
Schlafzimmer
42
Esszimmer
45
Wohnzimmer
46
Arbeitszimmer Patty
39
Bad Oben
41
Bad Unten
48

View File

@@ -1,33 +0,0 @@
Schlafzimmer
52 Straße
Esszimmer
26 Straße rechts
27 Straße links
Wohnzimmer
28 Garten rechts
29 Garten links
Küche
0x00158d008b332785 Garten Fenster
0x00158d008b332788 Garten Tür
0x00158d008b151803 Straße rechts
0x00158d008b331d0b Straße links
Arbeitszimmer Patty
18 Garten rechts
22 Garten links
0x00158d000af457cf Straße
Arbeitszimmer Wolfgang
0x00158d008b3328da Garten
Bad Oben
0x00158d008b333aec Straße
Bad Unten
44 Straße

94
config/rules.yaml Normal file
View File

@@ -0,0 +1,94 @@
# Rules Configuration
# Auto-generated from devices.yaml
rules:
- id: window_setback_esszimmer
enabled: false
name: Fensterabsenkung Esszimmer
type: window_setback@1.0
objects:
contacts:
- kontakt_esszimmer_strasse_links
- kontakt_esszimmer_strasse_rechts
thermostats:
- thermostat_esszimmer
params:
eco_target: 16.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_kueche
enabled: false
name: Fensterabsenkung Küche
type: window_setback@1.0
objects:
contacts:
- kontakt_kueche_garten_fenster
- kontakt_kueche_garten_tuer
- kontakt_kueche_strasse_links
- kontakt_kueche_strasse_rechts
thermostats:
- thermostat_kueche
params:
eco_target: 16.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_patty
enabled: false
name: Fensterabsenkung Arbeitszimmer Patty
type: window_setback@1.0
objects:
contacts:
- kontakt_patty_garten_links
- kontakt_patty_garten_rechts
- kontakt_patty_strasse
thermostats:
- thermostat_patty
params:
eco_target: 16.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_schlafzimmer
enabled: false
name: Fensterabsenkung Schlafzimmer
type: window_setback@1.0
objects:
contacts:
- kontakt_schlafzimmer_strasse
thermostats:
- thermostat_schlafzimmer
params:
eco_target: 16.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_wohnzimmer
enabled: false
name: Fensterabsenkung Wohnzimmer
type: window_setback@1.0
objects:
contacts:
- kontakt_wohnzimmer_garten_links
- kontakt_wohnzimmer_garten_rechts
thermostats:
- thermostat_wohnzimmer
params:
eco_target: 16.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_wolfgang
enabled: true
name: Fensterabsenkung Arbeitszimmer Wolfgang
type: window_setback@1.0
objects:
contacts:
- kontakt_wolfgang_garten
thermostats:
- thermostat_wolfgang
params:
eco_target: 16.0
open_min_secs: 20
close_min_secs: 20

24
config/scenes.yaml Normal file
View File

@@ -0,0 +1,24 @@
version: 1
scenes:
- id: "alles_aus"
name: "Alles aus"
steps:
- selector: { type: "light" }
action:
type: "light"
payload: { power: "off" }
- selector: { type: "relay" }
action:
type: "relay"
payload: { power: "off" }
- id: "kueche_gemuetlich"
name: "Küche gemütlich"
steps:
- group_id: "kueche_lichter"
action:
type: "light"
payload:
power: "on"
brightness: 35

View File

@@ -0,0 +1,25 @@
abstraction | 2025-11-18 12:04:42,875 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/42/1/SET_TEMPERATURE: 21
abstraction | 2025-11-18 12:04:42,914 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/45/1/SET_TEMPERATURE: 15
abstraction | 2025-11-18 12:04:42,950 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/46/1/SET_TEMPERATURE: 15
abstraction | 2025-11-18 12:04:42,987 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/39/1/SET_TEMPERATURE: 22
abstraction | 2025-11-18 12:04:43,029 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/41/1/SET_TEMPERATURE: 20
abstraction | 2025-11-18 12:04:43,071 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/48/1/SET_TEMPERATURE: 21
abstraction | 2025-11-18 12:04:43,108 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/52/1/STATE: false
abstraction | 2025-11-18 12:04:43,145 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/26/1/STATE: false
abstraction | 2025-11-18 12:04:43,182 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/27/1/STATE: false
abstraction | 2025-11-18 12:04:43,219 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/28/1/STATE: false
abstraction | 2025-11-18 12:04:43,256 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/29/1/STATE: false
abstraction | 2025-11-18 12:04:43,292 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/18/1/STATE: false
abstraction | 2025-11-18 12:04:43,331 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/22/1/STATE: false
abstraction | 2025-11-18 12:04:43,368 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/44/1/STATE: false
abstraction | 2025-11-18 12:04:48,498 - __main__ - DEBUG - MQTT message received on shellies/schrankesszimmer/relay/0: off
abstraction | 2025-11-18 12:04:52,989 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":55.04,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,024 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,061 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.4,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:05:03,058 - __main__ - DEBUG - MQTT message received on shellies/lichtterasse/relay/0: off
abstraction | 2025-11-18 12:05:08,209 - __main__ - DEBUG - MQTT message received on shellies/wohnzimmer-regal/relay/0: off
abstraction | 2025-11-18 12:05:10,881 - __main__ - DEBUG - MQTT message received on shellies/LightKitchenSink/relay/0: on
abstraction | 2025-11-18 12:05:12,622 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,656 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,690 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.7,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:18,507 - __main__ - DEBUG - MQTT message received on shellies/schrankesszimmer/relay/0: off

View File

@@ -0,0 +1,261 @@
abstraction | 2025-11-18 12:04:40,901 - asyncio - DEBUG - Using selector: EpollSelector
abstraction | 2025-11-18 12:04:40,952 - __main__ - INFO - Loaded configuration from /app/config/devices.yaml
abstraction | 2025-11-18 12:04:40,953 - __main__ - INFO - Loaded 64 device(s): lampe_semeniere_wohnzimmer, stehlampe_esszimmer_spiegel, stehlampe_esszimmer_schrank, grosse_lampe_wohnzimmer, lampe_naehtischchen_wohnzimmer, kleine_lampe_rechts_esszimmer, kleine_lampe_links_esszimmer, leselampe_esszimmer, medusalampe_schlafzimmer, sportlicht_am_fernseher_studierzimmer, deckenlampe_schlafzimmer, bettlicht_wolfgang, bettlicht_patty, schranklicht_hinten_patty, schranklicht_vorne_patty, leselampe_patty, deckenlampe_esszimmer, standlampe_esszimmer, haustuer, deckenlampe_flur_oben, kueche_deckenlampe, sportlicht_tisch, sportlicht_regal, licht_flur_oben_am_spiegel, experimentlabtest, thermostat_wolfgang, thermostat_kueche, thermostat_schlafzimmer, thermostat_esszimmer, thermostat_wohnzimmer, thermostat_patty, thermostat_bad_oben, thermostat_bad_unten, sterne_wohnzimmer, kontakt_schlafzimmer_strasse, kontakt_esszimmer_strasse_rechts, kontakt_esszimmer_strasse_links, kontakt_wohnzimmer_garten_rechts, kontakt_wohnzimmer_garten_links, kontakt_kueche_garten_fenster, kontakt_kueche_garten_tuer, kontakt_kueche_strasse_rechts, kontakt_kueche_strasse_links, kontakt_patty_garten_rechts, kontakt_patty_garten_links, kontakt_patty_strasse, kontakt_wolfgang_garten, kontakt_bad_oben_strasse, kontakt_bad_unten_strasse, sensor_schlafzimmer, sensor_wohnzimmer, sensor_kueche, sensor_arbeitszimmer_patty, sensor_arbeitszimmer_wolfgang, sensor_bad_oben, sensor_bad_unten, sensor_flur, sensor_waschkueche, sensor_sportzimmer, licht_spuele_kueche, licht_schrank_esszimmer, licht_regal_wohnzimmer, licht_flur_schrank, licht_terasse
abstraction | 2025-11-18 12:04:40,953 - __main__ - INFO - Loaded 64 device(s) from configuration
abstraction | 2025-11-18 12:04:41,003 - __main__ - INFO - Connected to Redis: redis://172.23.1.116:6379/8
abstraction | 2025-11-18 12:04:41,003 - __main__ - INFO - Abstraction worker started
abstraction | 2025-11-18 12:04:41,003 - __main__ - INFO - Connecting to MQTT broker: 172.23.1.102:1883
abstraction | 2025-11-18 12:04:41,053 - __main__ - INFO - Connected to MQTT broker as home-automation-abstraction-b39304
abstraction | 2025-11-18 12:04:41,072 - __main__ - INFO - Subscribed to abstract SET: home/relay/lampe_semeniere_wohnzimmer/set
abstraction | 2025-11-18 12:04:41,091 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8000015480b
abstraction | 2025-11-18 12:04:41,107 - __main__ - INFO - Subscribed to abstract SET: home/light/stehlampe_esszimmer_spiegel/set
abstraction | 2025-11-18 12:04:41,125 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d06ea09
abstraction | 2025-11-18 12:04:41,141 - __main__ - INFO - Subscribed to abstract SET: home/light/stehlampe_esszimmer_schrank/set
abstraction | 2025-11-18 12:04:41,159 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d09176c
abstraction | 2025-11-18 12:04:41,176 - __main__ - INFO - Subscribed to abstract SET: home/relay/grosse_lampe_wohnzimmer/set
abstraction | 2025-11-18 12:04:41,192 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000151aca
abstraction | 2025-11-18 12:04:41,209 - __main__ - INFO - Subscribed to abstract SET: home/relay/lampe_naehtischchen_wohnzimmer/set
abstraction | 2025-11-18 12:04:41,225 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffee560ee
abstraction | 2025-11-18 12:04:41,242 - __main__ - INFO - Subscribed to abstract SET: home/relay/kleine_lampe_rechts_esszimmer/set
abstraction | 2025-11-18 12:04:41,259 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000156645
abstraction | 2025-11-18 12:04:41,276 - __main__ - INFO - Subscribed to abstract SET: home/relay/kleine_lampe_links_esszimmer/set
abstraction | 2025-11-18 12:04:41,293 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000153099
abstraction | 2025-11-18 12:04:41,310 - __main__ - INFO - Subscribed to abstract SET: home/light/leselampe_esszimmer/set
abstraction | 2025-11-18 12:04:41,327 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xec1bbdfffe7b84f2
abstraction | 2025-11-18 12:04:41,344 - __main__ - INFO - Subscribed to abstract SET: home/relay/medusalampe_schlafzimmer/set
abstraction | 2025-11-18 12:04:41,361 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000154c7c
abstraction | 2025-11-18 12:04:41,378 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_am_fernseher_studierzimmer/set
abstraction | 2025-11-18 12:04:41,395 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffe76a23a
abstraction | 2025-11-18 12:04:41,415 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_schlafzimmer/set
abstraction | 2025-11-18 12:04:41,432 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108a406a7
abstraction | 2025-11-18 12:04:41,449 - __main__ - INFO - Subscribed to abstract SET: home/light/bettlicht_wolfgang/set
abstraction | 2025-11-18 12:04:41,466 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00178801081570bf
abstraction | 2025-11-18 12:04:41,484 - __main__ - INFO - Subscribed to abstract SET: home/light/bettlicht_patty/set
abstraction | 2025-11-18 12:04:41,500 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108158b32
abstraction | 2025-11-18 12:04:41,518 - __main__ - INFO - Subscribed to abstract SET: home/light/schranklicht_hinten_patty/set
abstraction | 2025-11-18 12:04:41,535 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880106e29571
abstraction | 2025-11-18 12:04:41,552 - __main__ - INFO - Subscribed to abstract SET: home/relay/schranklicht_vorne_patty/set
abstraction | 2025-11-18 12:04:41,569 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000154cf5
abstraction | 2025-11-18 12:04:41,586 - __main__ - INFO - Subscribed to abstract SET: home/light/leselampe_patty/set
abstraction | 2025-11-18 12:04:41,603 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010600ec9d
abstraction | 2025-11-18 12:04:41,620 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_esszimmer/set
abstraction | 2025-11-18 12:04:41,637 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108a03e45
abstraction | 2025-11-18 12:04:41,655 - __main__ - INFO - Subscribed to abstract SET: home/light/standlampe_esszimmer/set
abstraction | 2025-11-18 12:04:41,674 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xbc33acfffe21f547
abstraction | 2025-11-18 12:04:41,692 - __main__ - INFO - Subscribed to abstract SET: home/light/haustuer/set
abstraction | 2025-11-18 12:04:41,711 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xec1bbdfffea6a3da
abstraction | 2025-11-18 12:04:41,728 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_flur_oben/set
abstraction | 2025-11-18 12:04:41,746 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d2123a7
abstraction | 2025-11-18 12:04:41,764 - __main__ - INFO - Subscribed to abstract SET: home/light/kueche_deckenlampe/set
abstraction | 2025-11-18 12:04:41,781 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d2c40c4
abstraction | 2025-11-18 12:04:41,798 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_tisch/set
abstraction | 2025-11-18 12:04:41,814 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8be2409f31b
abstraction | 2025-11-18 12:04:41,831 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_regal/set
abstraction | 2025-11-18 12:04:41,848 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8be2409f569
abstraction | 2025-11-18 12:04:41,865 - __main__ - INFO - Subscribed to abstract SET: home/light/licht_flur_oben_am_spiegel/set
abstraction | 2025-11-18 12:04:41,883 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffefe4ba4
abstraction | 2025-11-18 12:04:41,899 - __main__ - INFO - Subscribed to abstract SET: home/light/experimentlabtest/set
abstraction | 2025-11-18 12:04:41,918 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000195038
abstraction | 2025-11-18 12:04:41,936 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_wolfgang/set
abstraction | 2025-11-18 12:04:41,955 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x540f57fffe7e3cfe
abstraction | 2025-11-18 12:04:41,974 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_kueche/set
abstraction | 2025-11-18 12:04:41,991 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x94deb8fffe2e5c06
abstraction | 2025-11-18 12:04:42,008 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_schlafzimmer/set
abstraction | 2025-11-18 12:04:42,025 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/42/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,042 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_esszimmer/set
abstraction | 2025-11-18 12:04:42,059 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/45/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,080 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_wohnzimmer/set
abstraction | 2025-11-18 12:04:42,097 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/46/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,114 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_patty/set
abstraction | 2025-11-18 12:04:42,131 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/39/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,150 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_bad_oben/set
abstraction | 2025-11-18 12:04:42,171 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/41/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,189 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_bad_unten/set
abstraction | 2025-11-18 12:04:42,207 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/48/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,224 - __main__ - INFO - Subscribed to abstract SET: home/relay/sterne_wohnzimmer/set
abstraction | 2025-11-18 12:04:42,240 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000155fc2
abstraction | 2025-11-18 12:04:42,240 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_schlafzimmer_strasse
abstraction | 2025-11-18 12:04:42,258 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/52/1/STATE
abstraction | 2025-11-18 12:04:42,258 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_esszimmer_strasse_rechts
abstraction | 2025-11-18 12:04:42,275 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/26/1/STATE
abstraction | 2025-11-18 12:04:42,275 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_esszimmer_strasse_links
abstraction | 2025-11-18 12:04:42,293 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/27/1/STATE
abstraction | 2025-11-18 12:04:42,293 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wohnzimmer_garten_rechts
abstraction | 2025-11-18 12:04:42,313 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/28/1/STATE
abstraction | 2025-11-18 12:04:42,313 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wohnzimmer_garten_links
abstraction | 2025-11-18 12:04:42,331 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/29/1/STATE
abstraction | 2025-11-18 12:04:42,331 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_garten_fenster
abstraction | 2025-11-18 12:04:42,351 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b332785
abstraction | 2025-11-18 12:04:42,351 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_garten_tuer
abstraction | 2025-11-18 12:04:42,371 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b332788
abstraction | 2025-11-18 12:04:42,371 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_strasse_rechts
abstraction | 2025-11-18 12:04:42,390 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b151803
abstraction | 2025-11-18 12:04:42,390 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_strasse_links
abstraction | 2025-11-18 12:04:42,408 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b331d0b
abstraction | 2025-11-18 12:04:42,408 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_garten_rechts
abstraction | 2025-11-18 12:04:42,424 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/18/1/STATE
abstraction | 2025-11-18 12:04:42,424 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_garten_links
abstraction | 2025-11-18 12:04:42,441 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/22/1/STATE
abstraction | 2025-11-18 12:04:42,441 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_strasse
abstraction | 2025-11-18 12:04:42,462 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000af457cf
abstraction | 2025-11-18 12:04:42,462 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wolfgang_garten
abstraction | 2025-11-18 12:04:42,479 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b3328da
abstraction | 2025-11-18 12:04:42,480 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_bad_oben_strasse
abstraction | 2025-11-18 12:04:42,496 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b333aec
abstraction | 2025-11-18 12:04:42,496 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_bad_unten_strasse
abstraction | 2025-11-18 12:04:42,513 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/44/1/STATE
abstraction | 2025-11-18 12:04:42,513 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_schlafzimmer
abstraction | 2025-11-18 12:04:42,532 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00043292dc
abstraction | 2025-11-18 12:04:42,532 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_wohnzimmer
abstraction | 2025-11-18 12:04:42,552 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0008975707
abstraction | 2025-11-18 12:04:42,552 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_kueche
abstraction | 2025-11-18 12:04:42,571 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00083299bb
abstraction | 2025-11-18 12:04:42,571 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_arbeitszimmer_patty
abstraction | 2025-11-18 12:04:42,589 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0003f052b7
abstraction | 2025-11-18 12:04:42,589 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_arbeitszimmer_wolfgang
abstraction | 2025-11-18 12:04:42,608 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000543fb99
abstraction | 2025-11-18 12:04:42,608 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_bad_oben
abstraction | 2025-11-18 12:04:42,625 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00093e8987
abstraction | 2025-11-18 12:04:42,625 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_bad_unten
abstraction | 2025-11-18 12:04:42,645 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00093e662a
abstraction | 2025-11-18 12:04:42,645 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_flur
abstraction | 2025-11-18 12:04:42,664 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000836ccc6
abstraction | 2025-11-18 12:04:42,664 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_waschkueche
abstraction | 2025-11-18 12:04:42,682 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000449f3bc
abstraction | 2025-11-18 12:04:42,682 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_sportzimmer
abstraction | 2025-11-18 12:04:42,699 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0009421422
abstraction | 2025-11-18 12:04:42,716 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_spuele_kueche/set
abstraction | 2025-11-18 12:04:42,734 - __main__ - INFO - Subscribed to vendor STATE: shellies/LightKitchenSink/relay/0
abstraction | 2025-11-18 12:04:42,751 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_schrank_esszimmer/set
abstraction | 2025-11-18 12:04:42,770 - __main__ - INFO - Subscribed to vendor STATE: shellies/schrankesszimmer/relay/0
abstraction | 2025-11-18 12:04:42,790 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_regal_wohnzimmer/set
abstraction | 2025-11-18 12:04:42,807 - __main__ - INFO - Subscribed to vendor STATE: shellies/wohnzimmer-regal/relay/0
abstraction | 2025-11-18 12:04:42,823 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_flur_schrank/set
abstraction | 2025-11-18 12:04:42,841 - __main__ - INFO - Subscribed to vendor STATE: shellies/schrankflur/relay/0
abstraction | 2025-11-18 12:04:42,858 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_terasse/set
abstraction | 2025-11-18 12:04:42,875 - __main__ - INFO - Subscribed to vendor STATE: shellies/lichtterasse/relay/0
abstraction | 2025-11-18 12:04:42,875 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/42/1/SET_TEMPERATURE: 21
abstraction | 2025-11-18 12:04:42,875 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=21
abstraction | 2025-11-18 12:04:42,876 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 21.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:42,876 - __main__ - INFO - ← abstract STATE thermostat_schlafzimmer: home/thermostat/thermostat_schlafzimmer/state → {"target": 21.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:42,897 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_schlafzimmer", "payload": {"target": 21.0, "mode": "heat"}, "ts": "2025-11-18T12:04:42.897310+00:00"}
abstraction | 2025-11-18 12:04:42,914 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/45/1/SET_TEMPERATURE: 15
abstraction | 2025-11-18 12:04:42,914 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=15
abstraction | 2025-11-18 12:04:42,914 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 15.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:42,914 - __main__ - INFO - ← abstract STATE thermostat_esszimmer: home/thermostat/thermostat_esszimmer/state → {"target": 15.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:42,934 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_esszimmer", "payload": {"target": 15.0, "mode": "heat"}, "ts": "2025-11-18T12:04:42.934255+00:00"}
abstraction | 2025-11-18 12:04:42,950 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/46/1/SET_TEMPERATURE: 15
abstraction | 2025-11-18 12:04:42,950 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=15
abstraction | 2025-11-18 12:04:42,950 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 15.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:42,951 - __main__ - INFO - ← abstract STATE thermostat_wohnzimmer: home/thermostat/thermostat_wohnzimmer/state → {"target": 15.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:42,970 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_wohnzimmer", "payload": {"target": 15.0, "mode": "heat"}, "ts": "2025-11-18T12:04:42.970936+00:00"}
abstraction | 2025-11-18 12:04:42,987 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/39/1/SET_TEMPERATURE: 22
abstraction | 2025-11-18 12:04:42,988 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=22
abstraction | 2025-11-18 12:04:42,988 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 22.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:42,988 - __main__ - INFO - ← abstract STATE thermostat_patty: home/thermostat/thermostat_patty/state → {"target": 22.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:43,009 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_patty", "payload": {"target": 22.0, "mode": "heat"}, "ts": "2025-11-18T12:04:43.009673+00:00"}
abstraction | 2025-11-18 12:04:43,029 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/41/1/SET_TEMPERATURE: 20
abstraction | 2025-11-18 12:04:43,029 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=20
abstraction | 2025-11-18 12:04:43,029 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 20.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:43,029 - __main__ - INFO - ← abstract STATE thermostat_bad_oben: home/thermostat/thermostat_bad_oben/state → {"target": 20.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:43,053 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_bad_oben", "payload": {"target": 20.0, "mode": "heat"}, "ts": "2025-11-18T12:04:43.053895+00:00"}
abstraction | 2025-11-18 12:04:43,071 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/48/1/SET_TEMPERATURE: 21
abstraction | 2025-11-18 12:04:43,071 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=21
abstraction | 2025-11-18 12:04:43,071 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 21.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:43,072 - __main__ - INFO - ← abstract STATE thermostat_bad_unten: home/thermostat/thermostat_bad_unten/state → {"target": 21.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:43,092 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_bad_unten", "payload": {"target": 21.0, "mode": "heat"}, "ts": "2025-11-18T12:04:43.092210+00:00"}
abstraction | 2025-11-18 12:04:43,108 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/52/1/STATE: false
abstraction | 2025-11-18 12:04:43,108 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,108 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,109 - __main__ - INFO - ← abstract STATE kontakt_schlafzimmer_strasse: home/contact/kontakt_schlafzimmer_strasse/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,128 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_schlafzimmer_strasse", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.128506+00:00"}
abstraction | 2025-11-18 12:04:43,145 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/26/1/STATE: false
abstraction | 2025-11-18 12:04:43,145 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,145 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,146 - __main__ - INFO - ← abstract STATE kontakt_esszimmer_strasse_rechts: home/contact/kontakt_esszimmer_strasse_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,165 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_esszimmer_strasse_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.165958+00:00"}
abstraction | 2025-11-18 12:04:43,182 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/27/1/STATE: false
abstraction | 2025-11-18 12:04:43,182 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,183 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,183 - __main__ - INFO - ← abstract STATE kontakt_esszimmer_strasse_links: home/contact/kontakt_esszimmer_strasse_links/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,202 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_esszimmer_strasse_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.202580+00:00"}
abstraction | 2025-11-18 12:04:43,219 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/28/1/STATE: false
abstraction | 2025-11-18 12:04:43,219 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,219 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,220 - __main__ - INFO - ← abstract STATE kontakt_wohnzimmer_garten_rechts: home/contact/kontakt_wohnzimmer_garten_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,239 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_wohnzimmer_garten_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.239653+00:00"}
abstraction | 2025-11-18 12:04:43,256 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/29/1/STATE: false
abstraction | 2025-11-18 12:04:43,256 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,256 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,257 - __main__ - INFO - ← abstract STATE kontakt_wohnzimmer_garten_links: home/contact/kontakt_wohnzimmer_garten_links/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,275 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_wohnzimmer_garten_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.275832+00:00"}
abstraction | 2025-11-18 12:04:43,292 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/18/1/STATE: false
abstraction | 2025-11-18 12:04:43,292 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,292 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,293 - __main__ - INFO - ← abstract STATE kontakt_patty_garten_rechts: home/contact/kontakt_patty_garten_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,314 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_patty_garten_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.314579+00:00"}
abstraction | 2025-11-18 12:04:43,331 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/22/1/STATE: false
abstraction | 2025-11-18 12:04:43,331 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,331 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,332 - __main__ - INFO - ← abstract STATE kontakt_patty_garten_links: home/contact/kontakt_patty_garten_links/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,351 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_patty_garten_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.351704+00:00"}
abstraction | 2025-11-18 12:04:43,368 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/44/1/STATE: false
abstraction | 2025-11-18 12:04:43,368 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,368 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,369 - __main__ - INFO - ← abstract STATE kontakt_bad_unten_strasse: home/contact/kontakt_bad_unten_strasse/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,388 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_bad_unten_strasse", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.388390+00:00"}
abstraction | 2025-11-18 12:04:48,498 - __main__ - DEBUG - MQTT message received on shellies/schrankesszimmer/relay/0: off
abstraction | 2025-11-18 12:04:48,498 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 12:04:48,498 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 12:04:48,498 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 12:04:48,518 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T12:04:48.518525+00:00"}
abstraction | 2025-11-18 12:04:52,989 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":55.04,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:52,989 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":100,"humidity":55.04,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:52,989 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 55.04, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1002.6, 'temperature': 22.13, 'voltage': 3015}
abstraction | 2025-11-18 12:04:52,989 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 55.04, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.6, "temperature": 22.13, "voltage": 3015}
abstraction | 2025-11-18 12:04:53,009 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 55.04, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.6, "temperature": 22.13, "voltage": 3015}, "ts": "2025-11-18T12:04:53.009776+00:00"}
abstraction | 2025-11-18 12:04:53,024 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,025 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,025 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.82, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1002.6, 'temperature': 22.13, 'voltage': 3015}
abstraction | 2025-11-18 12:04:53,025 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.82, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.6, "temperature": 22.13, "voltage": 3015}
abstraction | 2025-11-18 12:04:53,044 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.82, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.6, "temperature": 22.13, "voltage": 3015}, "ts": "2025-11-18T12:04:53.044379+00:00"}
abstraction | 2025-11-18 12:04:53,061 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.4,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,061 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.4,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,061 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.82, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1002.4, 'temperature': 22.13, 'voltage': 3015}
abstraction | 2025-11-18 12:04:53,061 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.82, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.4, "temperature": 22.13, "voltage": 3015}
abstraction | 2025-11-18 12:04:53,084 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.82, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.4, "temperature": 22.13, "voltage": 3015}, "ts": "2025-11-18T12:04:53.083988+00:00"}
abstraction | 2025-11-18 12:05:03,058 - __main__ - DEBUG - MQTT message received on shellies/lichtterasse/relay/0: off
abstraction | 2025-11-18 12:05:03,058 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 12:05:03,058 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 12:05:03,058 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 12:05:03,075 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T12:05:03.075262+00:00"}
abstraction | 2025-11-18 12:05:08,209 - __main__ - DEBUG - MQTT message received on shellies/wohnzimmer-regal/relay/0: off
abstraction | 2025-11-18 12:05:08,210 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 12:05:08,210 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 12:05:08,210 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 12:05:08,228 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T12:05:08.228758+00:00"}
abstraction | 2025-11-18 12:05:10,881 - __main__ - DEBUG - MQTT message received on shellies/LightKitchenSink/relay/0: on
abstraction | 2025-11-18 12:05:10,881 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 12:05:10,881 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 12:05:10,881 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 12:05:10,899 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T12:05:10.899207+00:00"}
abstraction | 2025-11-18 12:05:12,622 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,622 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,622 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 63, 'humidity': 47.69, 'linkquality': 87, 'power_outage_count': 4906, 'pressure': 1009.9, 'temperature': 19.74, 'voltage': 2945}
abstraction | 2025-11-18 12:05:12,622 - __main__ - INFO - ← abstract STATE sensor_kueche: home/temp_humidity/sensor_kueche/state → {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.9, "temperature": 19.74, "voltage": 2945}
abstraction | 2025-11-18 12:05:12,640 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_kueche", "payload": {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.9, "temperature": 19.74, "voltage": 2945}, "ts": "2025-11-18T12:05:12.640129+00:00"}
abstraction | 2025-11-18 12:05:12,656 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,656 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,656 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 63, 'humidity': 47.69, 'linkquality': 87, 'power_outage_count': 4906, 'pressure': 1009.9, 'temperature': 19.74, 'voltage': 2945}
abstraction | 2025-11-18 12:05:12,657 - __main__ - INFO - ← abstract STATE sensor_kueche: home/temp_humidity/sensor_kueche/state → {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.9, "temperature": 19.74, "voltage": 2945}
abstraction | 2025-11-18 12:05:12,674 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_kueche", "payload": {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.9, "temperature": 19.74, "voltage": 2945}, "ts": "2025-11-18T12:05:12.674372+00:00"}
abstraction | 2025-11-18 12:05:12,690 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.7,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,690 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.7,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,690 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 63, 'humidity': 47.69, 'linkquality': 87, 'power_outage_count': 4906, 'pressure': 1009.7, 'temperature': 19.74, 'voltage': 2945}
abstraction | 2025-11-18 12:05:12,690 - __main__ - INFO - ← abstract STATE sensor_kueche: home/temp_humidity/sensor_kueche/state → {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.7, "temperature": 19.74, "voltage": 2945}
abstraction | 2025-11-18 12:05:12,708 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_kueche", "payload": {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.7, "temperature": 19.74, "voltage": 2945}, "ts": "2025-11-18T12:05:12.708715+00:00"}
abstraction | 2025-11-18 12:05:18,507 - __main__ - DEBUG - MQTT message received on shellies/schrankesszimmer/relay/0: off
abstraction | 2025-11-18 12:05:18,508 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 12:05:18,508 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 12:05:18,508 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 12:05:18,526 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T12:05:18.525971+00:00"}

View File

@@ -0,0 +1,311 @@
rules | 2025-11-18 12:04:40,835 - asyncio - DEBUG - Using selector: EpollSelector
rules | 2025-11-18 12:04:40,835 - __main__ - INFO - ============================================================
rules | 2025-11-18 12:04:40,835 - __main__ - INFO - Rules Engine Starting
rules | 2025-11-18 12:04:40,835 - __main__ - INFO - ============================================================
rules | 2025-11-18 12:04:40,835 - __main__ - INFO - Config: /app/config/rules.yaml
rules | 2025-11-18 12:04:40,835 - __main__ - INFO - MQTT: 172.23.1.102:1883
rules | 2025-11-18 12:04:40,835 - __main__ - INFO - Redis: redis://172.23.1.116:6379/8
rules | 2025-11-18 12:04:40,836 - __main__ - INFO - ============================================================
rules | 2025-11-18 12:04:40,836 - __main__ - INFO - Loading rules configuration from /app/config/rules.yaml
rules | 2025-11-18 12:04:40,841 - __main__ - INFO - Loaded 6 rule(s) from configuration
rules | 2025-11-18 12:04:40,841 - __main__ - INFO - - window_setback_esszimmer (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 12:04:40,842 - __main__ - INFO - - window_setback_kueche (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 12:04:40,842 - __main__ - INFO - - window_setback_patty (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 12:04:40,842 - __main__ - INFO - - window_setback_schlafzimmer (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 12:04:40,842 - __main__ - INFO - - window_setback_wohnzimmer (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 12:04:40,846 - __main__ - INFO - - window_setback_wolfgang (type: window_setback@1.0)
rules | 2025-11-18 12:04:40,846 - __main__ - INFO - Successfully loaded 1 rule implementation(s) (5 disabled)
rules | 2025-11-18 12:04:40,846 - __main__ - INFO - Rule window_setback_wolfgang validated: 1 contacts, 1 thermostats
rules | 2025-11-18 12:04:40,846 - __main__ - DEBUG - Rule window_setback_wolfgang subscribes to 2 topic(s)
rules | 2025-11-18 12:04:40,847 - __main__ - INFO - Total MQTT subscriptions needed: 2
rules | 2025-11-18 12:04:40,847 - __main__ - INFO - Starting event processing loop
abstraction | 2025-11-18 12:04:40,901 - asyncio - DEBUG - Using selector: EpollSelector
abstraction | 2025-11-18 12:04:40,952 - __main__ - INFO - Loaded configuration from /app/config/devices.yaml
abstraction | 2025-11-18 12:04:40,953 - __main__ - INFO - Loaded 64 device(s): lampe_semeniere_wohnzimmer, stehlampe_esszimmer_spiegel, stehlampe_esszimmer_schrank, grosse_lampe_wohnzimmer, lampe_naehtischchen_wohnzimmer, kleine_lampe_rechts_esszimmer, kleine_lampe_links_esszimmer, leselampe_esszimmer, medusalampe_schlafzimmer, sportlicht_am_fernseher_studierzimmer, deckenlampe_schlafzimmer, bettlicht_wolfgang, bettlicht_patty, schranklicht_hinten_patty, schranklicht_vorne_patty, leselampe_patty, deckenlampe_esszimmer, standlampe_esszimmer, haustuer, deckenlampe_flur_oben, kueche_deckenlampe, sportlicht_tisch, sportlicht_regal, licht_flur_oben_am_spiegel, experimentlabtest, thermostat_wolfgang, thermostat_kueche, thermostat_schlafzimmer, thermostat_esszimmer, thermostat_wohnzimmer, thermostat_patty, thermostat_bad_oben, thermostat_bad_unten, sterne_wohnzimmer, kontakt_schlafzimmer_strasse, kontakt_esszimmer_strasse_rechts, kontakt_esszimmer_strasse_links, kontakt_wohnzimmer_garten_rechts, kontakt_wohnzimmer_garten_links, kontakt_kueche_garten_fenster, kontakt_kueche_garten_tuer, kontakt_kueche_strasse_rechts, kontakt_kueche_strasse_links, kontakt_patty_garten_rechts, kontakt_patty_garten_links, kontakt_patty_strasse, kontakt_wolfgang_garten, kontakt_bad_oben_strasse, kontakt_bad_unten_strasse, sensor_schlafzimmer, sensor_wohnzimmer, sensor_kueche, sensor_arbeitszimmer_patty, sensor_arbeitszimmer_wolfgang, sensor_bad_oben, sensor_bad_unten, sensor_flur, sensor_waschkueche, sensor_sportzimmer, licht_spuele_kueche, licht_schrank_esszimmer, licht_regal_wohnzimmer, licht_flur_schrank, licht_terasse
abstraction | 2025-11-18 12:04:40,953 - __main__ - INFO - Loaded 64 device(s) from configuration
rules | 2025-11-18 12:04:40,999 - __main__ - INFO - Connecting to MQTT broker 172.23.1.102:1883 (client_id=rule_engine-0d8cce)
abstraction | 2025-11-18 12:04:41,003 - __main__ - INFO - Connected to Redis: redis://172.23.1.116:6379/8
abstraction | 2025-11-18 12:04:41,003 - __main__ - INFO - Abstraction worker started
abstraction | 2025-11-18 12:04:41,003 - __main__ - INFO - Connecting to MQTT broker: 172.23.1.102:1883
rules | 2025-11-18 12:04:41,051 - __main__ - INFO - Connected to MQTT broker 172.23.1.102:1883
abstraction | 2025-11-18 12:04:41,053 - __main__ - INFO - Connected to MQTT broker as home-automation-abstraction-b39304
abstraction | 2025-11-18 12:04:41,072 - __main__ - INFO - Subscribed to abstract SET: home/relay/lampe_semeniere_wohnzimmer/set
rules | 2025-11-18 12:04:41,084 - __main__ - INFO - Subscribed to 2 topic(s): home/thermostat/thermostat_wolfgang/state, home/contact/kontakt_wolfgang_garten/state
rules | 2025-11-18 12:04:41,085 - __main__ - DEBUG - Received event: {'topic': 'home/thermostat/thermostat_wolfgang/state', 'type': 'state', 'cap': 'thermostat', 'device_id': 'thermostat_wolfgang', 'payload': {'target': 23.0, 'current': 23.5, 'mode': 'heat'}, 'ts': '2025-11-18T12:04:41.085220'}
abstraction | 2025-11-18 12:04:41,091 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8000015480b
rules | 2025-11-18 12:04:41,085 - __main__ - DEBUG - Filtering for cap=thermostat, device_id=thermostat_wolfgang
rules | 2025-11-18 12:04:41,085 - __main__ - DEBUG - Rule window_setback_wolfgang: checking thermostats ['thermostat_wolfgang']
rules | 2025-11-18 12:04:41,086 - __main__ - INFO - Event thermostat/thermostat_wolfgang: 1 matching rule(s)
abstraction | 2025-11-18 12:04:41,107 - __main__ - INFO - Subscribed to abstract SET: home/light/stehlampe_esszimmer_spiegel/set
abstraction | 2025-11-18 12:04:41,125 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d06ea09
abstraction | 2025-11-18 12:04:41,141 - __main__ - INFO - Subscribed to abstract SET: home/light/stehlampe_esszimmer_schrank/set
abstraction | 2025-11-18 12:04:41,159 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d09176c
abstraction | 2025-11-18 12:04:41,176 - __main__ - INFO - Subscribed to abstract SET: home/relay/grosse_lampe_wohnzimmer/set
abstraction | 2025-11-18 12:04:41,192 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000151aca
rules | 2025-11-18 12:04:41,197 - __main__ - DEBUG - Rule window_setback_wolfgang: Updated current target for thermostat_wolfgang: 23.0°C
rules | 2025-11-18 12:04:41,197 - __main__ - DEBUG - Received event: {'topic': 'home/contact/kontakt_wolfgang_garten/state', 'type': 'state', 'cap': 'contact', 'device_id': 'kontakt_wolfgang_garten', 'payload': {'contact': 'closed', 'battery': 100, 'linkquality': 32, 'device_temperature': 28, 'voltage': 3025}, 'ts': '2025-11-18T12:04:41.197402'}
rules | 2025-11-18 12:04:41,198 - __main__ - DEBUG - Filtering for cap=contact, device_id=kontakt_wolfgang_garten
rules | 2025-11-18 12:04:41,198 - __main__ - DEBUG - Rule window_setback_wolfgang: checking contacts ['kontakt_wolfgang_garten']
rules | 2025-11-18 12:04:41,199 - __main__ - INFO - Event contact/kontakt_wolfgang_garten: 1 matching rule(s)
abstraction | 2025-11-18 12:04:41,209 - __main__ - INFO - Subscribed to abstract SET: home/relay/lampe_naehtischchen_wohnzimmer/set
abstraction | 2025-11-18 12:04:41,225 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffee560ee
rules | 2025-11-18 12:04:41,233 - __main__ - INFO - Rule window_setback_wolfgang: Window closed, restoring 1 thermostats to previous temperatures
abstraction | 2025-11-18 12:04:41,242 - __main__ - INFO - Subscribed to abstract SET: home/relay/kleine_lampe_rechts_esszimmer/set
rules | 2025-11-18 12:04:41,250 - __main__ - WARNING - No previous target found for thermostat_wolfgang, cannot restore
abstraction | 2025-11-18 12:04:41,259 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000156645
abstraction | 2025-11-18 12:04:41,276 - __main__ - INFO - Subscribed to abstract SET: home/relay/kleine_lampe_links_esszimmer/set
abstraction | 2025-11-18 12:04:41,293 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000153099
abstraction | 2025-11-18 12:04:41,310 - __main__ - INFO - Subscribed to abstract SET: home/light/leselampe_esszimmer/set
abstraction | 2025-11-18 12:04:41,327 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xec1bbdfffe7b84f2
abstraction | 2025-11-18 12:04:41,344 - __main__ - INFO - Subscribed to abstract SET: home/relay/medusalampe_schlafzimmer/set
abstraction | 2025-11-18 12:04:41,361 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000154c7c
abstraction | 2025-11-18 12:04:41,378 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_am_fernseher_studierzimmer/set
abstraction | 2025-11-18 12:04:41,395 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffe76a23a
abstraction | 2025-11-18 12:04:41,415 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_schlafzimmer/set
abstraction | 2025-11-18 12:04:41,432 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108a406a7
abstraction | 2025-11-18 12:04:41,449 - __main__ - INFO - Subscribed to abstract SET: home/light/bettlicht_wolfgang/set
api | INFO: Started server process [1]
api | INFO: Waiting for application startup.
abstraction | 2025-11-18 12:04:41,466 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00178801081570bf
abstraction | 2025-11-18 12:04:41,484 - __main__ - INFO - Subscribed to abstract SET: home/light/bettlicht_patty/set
api | INFO: Application startup complete.
abstraction | 2025-11-18 12:04:41,500 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108158b32
api | INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
abstraction | 2025-11-18 12:04:41,518 - __main__ - INFO - Subscribed to abstract SET: home/light/schranklicht_hinten_patty/set
abstraction | 2025-11-18 12:04:41,535 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880106e29571
abstraction | 2025-11-18 12:04:41,552 - __main__ - INFO - Subscribed to abstract SET: home/relay/schranklicht_vorne_patty/set
abstraction | 2025-11-18 12:04:41,569 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000154cf5
abstraction | 2025-11-18 12:04:41,586 - __main__ - INFO - Subscribed to abstract SET: home/light/leselampe_patty/set
abstraction | 2025-11-18 12:04:41,603 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010600ec9d
abstraction | 2025-11-18 12:04:41,620 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_esszimmer/set
abstraction | 2025-11-18 12:04:41,637 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108a03e45
abstraction | 2025-11-18 12:04:41,655 - __main__ - INFO - Subscribed to abstract SET: home/light/standlampe_esszimmer/set
ui | UI using API_BASE: http://172.19.1.11:8001
ui | UI using BASE_PATH: /
ui | INFO: Started server process [1]
ui | INFO: Waiting for application startup.
ui | INFO: Application startup complete.
abstraction | 2025-11-18 12:04:41,674 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xbc33acfffe21f547
ui | INFO: Uvicorn running on http://0.0.0.0:8002 (Press CTRL+C to quit)
abstraction | 2025-11-18 12:04:41,692 - __main__ - INFO - Subscribed to abstract SET: home/light/haustuer/set
abstraction | 2025-11-18 12:04:41,711 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xec1bbdfffea6a3da
abstraction | 2025-11-18 12:04:41,728 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_flur_oben/set
abstraction | 2025-11-18 12:04:41,746 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d2123a7
abstraction | 2025-11-18 12:04:41,764 - __main__ - INFO - Subscribed to abstract SET: home/light/kueche_deckenlampe/set
abstraction | 2025-11-18 12:04:41,781 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d2c40c4
abstraction | 2025-11-18 12:04:41,798 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_tisch/set
abstraction | 2025-11-18 12:04:41,814 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8be2409f31b
abstraction | 2025-11-18 12:04:41,831 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_regal/set
abstraction | 2025-11-18 12:04:41,848 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8be2409f569
abstraction | 2025-11-18 12:04:41,865 - __main__ - INFO - Subscribed to abstract SET: home/light/licht_flur_oben_am_spiegel/set
abstraction | 2025-11-18 12:04:41,883 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffefe4ba4
abstraction | 2025-11-18 12:04:41,899 - __main__ - INFO - Subscribed to abstract SET: home/light/experimentlabtest/set
abstraction | 2025-11-18 12:04:41,918 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000195038
abstraction | 2025-11-18 12:04:41,936 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_wolfgang/set
abstraction | 2025-11-18 12:04:41,955 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x540f57fffe7e3cfe
abstraction | 2025-11-18 12:04:41,974 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_kueche/set
abstraction | 2025-11-18 12:04:41,991 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x94deb8fffe2e5c06
abstraction | 2025-11-18 12:04:42,008 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_schlafzimmer/set
abstraction | 2025-11-18 12:04:42,025 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/42/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,042 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_esszimmer/set
abstraction | 2025-11-18 12:04:42,059 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/45/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,080 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_wohnzimmer/set
abstraction | 2025-11-18 12:04:42,097 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/46/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,114 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_patty/set
abstraction | 2025-11-18 12:04:42,131 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/39/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,150 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_bad_oben/set
abstraction | 2025-11-18 12:04:42,171 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/41/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,189 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_bad_unten/set
abstraction | 2025-11-18 12:04:42,207 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/48/1/SET_TEMPERATURE
abstraction | 2025-11-18 12:04:42,224 - __main__ - INFO - Subscribed to abstract SET: home/relay/sterne_wohnzimmer/set
abstraction | 2025-11-18 12:04:42,240 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000155fc2
abstraction | 2025-11-18 12:04:42,240 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_schlafzimmer_strasse
abstraction | 2025-11-18 12:04:42,258 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/52/1/STATE
abstraction | 2025-11-18 12:04:42,258 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_esszimmer_strasse_rechts
abstraction | 2025-11-18 12:04:42,275 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/26/1/STATE
abstraction | 2025-11-18 12:04:42,275 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_esszimmer_strasse_links
abstraction | 2025-11-18 12:04:42,293 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/27/1/STATE
abstraction | 2025-11-18 12:04:42,293 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wohnzimmer_garten_rechts
abstraction | 2025-11-18 12:04:42,313 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/28/1/STATE
abstraction | 2025-11-18 12:04:42,313 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wohnzimmer_garten_links
abstraction | 2025-11-18 12:04:42,331 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/29/1/STATE
abstraction | 2025-11-18 12:04:42,331 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_garten_fenster
abstraction | 2025-11-18 12:04:42,351 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b332785
abstraction | 2025-11-18 12:04:42,351 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_garten_tuer
abstraction | 2025-11-18 12:04:42,371 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b332788
abstraction | 2025-11-18 12:04:42,371 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_strasse_rechts
abstraction | 2025-11-18 12:04:42,390 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b151803
abstraction | 2025-11-18 12:04:42,390 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_strasse_links
abstraction | 2025-11-18 12:04:42,408 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b331d0b
abstraction | 2025-11-18 12:04:42,408 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_garten_rechts
abstraction | 2025-11-18 12:04:42,424 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/18/1/STATE
abstraction | 2025-11-18 12:04:42,424 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_garten_links
abstraction | 2025-11-18 12:04:42,441 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/22/1/STATE
abstraction | 2025-11-18 12:04:42,441 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_strasse
abstraction | 2025-11-18 12:04:42,462 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000af457cf
abstraction | 2025-11-18 12:04:42,462 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wolfgang_garten
abstraction | 2025-11-18 12:04:42,479 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b3328da
abstraction | 2025-11-18 12:04:42,480 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_bad_oben_strasse
abstraction | 2025-11-18 12:04:42,496 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b333aec
abstraction | 2025-11-18 12:04:42,496 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_bad_unten_strasse
abstraction | 2025-11-18 12:04:42,513 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/44/1/STATE
abstraction | 2025-11-18 12:04:42,513 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_schlafzimmer
abstraction | 2025-11-18 12:04:42,532 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00043292dc
abstraction | 2025-11-18 12:04:42,532 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_wohnzimmer
abstraction | 2025-11-18 12:04:42,552 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0008975707
abstraction | 2025-11-18 12:04:42,552 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_kueche
abstraction | 2025-11-18 12:04:42,571 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00083299bb
abstraction | 2025-11-18 12:04:42,571 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_arbeitszimmer_patty
abstraction | 2025-11-18 12:04:42,589 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0003f052b7
abstraction | 2025-11-18 12:04:42,589 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_arbeitszimmer_wolfgang
abstraction | 2025-11-18 12:04:42,608 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000543fb99
abstraction | 2025-11-18 12:04:42,608 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_bad_oben
abstraction | 2025-11-18 12:04:42,625 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00093e8987
abstraction | 2025-11-18 12:04:42,625 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_bad_unten
abstraction | 2025-11-18 12:04:42,645 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00093e662a
abstraction | 2025-11-18 12:04:42,645 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_flur
abstraction | 2025-11-18 12:04:42,664 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000836ccc6
abstraction | 2025-11-18 12:04:42,664 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_waschkueche
abstraction | 2025-11-18 12:04:42,682 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000449f3bc
abstraction | 2025-11-18 12:04:42,682 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_sportzimmer
abstraction | 2025-11-18 12:04:42,699 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0009421422
abstraction | 2025-11-18 12:04:42,716 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_spuele_kueche/set
abstraction | 2025-11-18 12:04:42,734 - __main__ - INFO - Subscribed to vendor STATE: shellies/LightKitchenSink/relay/0
abstraction | 2025-11-18 12:04:42,751 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_schrank_esszimmer/set
abstraction | 2025-11-18 12:04:42,770 - __main__ - INFO - Subscribed to vendor STATE: shellies/schrankesszimmer/relay/0
abstraction | 2025-11-18 12:04:42,790 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_regal_wohnzimmer/set
abstraction | 2025-11-18 12:04:42,807 - __main__ - INFO - Subscribed to vendor STATE: shellies/wohnzimmer-regal/relay/0
abstraction | 2025-11-18 12:04:42,823 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_flur_schrank/set
abstraction | 2025-11-18 12:04:42,841 - __main__ - INFO - Subscribed to vendor STATE: shellies/schrankflur/relay/0
abstraction | 2025-11-18 12:04:42,858 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_terasse/set
abstraction | 2025-11-18 12:04:42,875 - __main__ - INFO - Subscribed to vendor STATE: shellies/lichtterasse/relay/0
abstraction | 2025-11-18 12:04:42,875 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/42/1/SET_TEMPERATURE: 21
abstraction | 2025-11-18 12:04:42,875 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=21
abstraction | 2025-11-18 12:04:42,876 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 21.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:42,876 - __main__ - INFO - ← abstract STATE thermostat_schlafzimmer: home/thermostat/thermostat_schlafzimmer/state → {"target": 21.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:42,897 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_schlafzimmer", "payload": {"target": 21.0, "mode": "heat"}, "ts": "2025-11-18T12:04:42.897310+00:00"}
abstraction | 2025-11-18 12:04:42,914 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/45/1/SET_TEMPERATURE: 15
abstraction | 2025-11-18 12:04:42,914 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=15
abstraction | 2025-11-18 12:04:42,914 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 15.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:42,914 - __main__ - INFO - ← abstract STATE thermostat_esszimmer: home/thermostat/thermostat_esszimmer/state → {"target": 15.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:42,934 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_esszimmer", "payload": {"target": 15.0, "mode": "heat"}, "ts": "2025-11-18T12:04:42.934255+00:00"}
abstraction | 2025-11-18 12:04:42,950 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/46/1/SET_TEMPERATURE: 15
abstraction | 2025-11-18 12:04:42,950 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=15
abstraction | 2025-11-18 12:04:42,950 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 15.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:42,951 - __main__ - INFO - ← abstract STATE thermostat_wohnzimmer: home/thermostat/thermostat_wohnzimmer/state → {"target": 15.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:42,970 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_wohnzimmer", "payload": {"target": 15.0, "mode": "heat"}, "ts": "2025-11-18T12:04:42.970936+00:00"}
abstraction | 2025-11-18 12:04:42,987 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/39/1/SET_TEMPERATURE: 22
abstraction | 2025-11-18 12:04:42,988 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=22
abstraction | 2025-11-18 12:04:42,988 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 22.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:42,988 - __main__ - INFO - ← abstract STATE thermostat_patty: home/thermostat/thermostat_patty/state → {"target": 22.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:43,009 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_patty", "payload": {"target": 22.0, "mode": "heat"}, "ts": "2025-11-18T12:04:43.009673+00:00"}
abstraction | 2025-11-18 12:04:43,029 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/41/1/SET_TEMPERATURE: 20
abstraction | 2025-11-18 12:04:43,029 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=20
abstraction | 2025-11-18 12:04:43,029 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 20.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:43,029 - __main__ - INFO - ← abstract STATE thermostat_bad_oben: home/thermostat/thermostat_bad_oben/state → {"target": 20.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:43,053 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_bad_oben", "payload": {"target": 20.0, "mode": "heat"}, "ts": "2025-11-18T12:04:43.053895+00:00"}
abstraction | 2025-11-18 12:04:43,071 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/48/1/SET_TEMPERATURE: 21
abstraction | 2025-11-18 12:04:43,071 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=21
abstraction | 2025-11-18 12:04:43,071 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 21.0, 'mode': 'heat'}
abstraction | 2025-11-18 12:04:43,072 - __main__ - INFO - ← abstract STATE thermostat_bad_unten: home/thermostat/thermostat_bad_unten/state → {"target": 21.0, "mode": "heat"}
abstraction | 2025-11-18 12:04:43,092 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_bad_unten", "payload": {"target": 21.0, "mode": "heat"}, "ts": "2025-11-18T12:04:43.092210+00:00"}
abstraction | 2025-11-18 12:04:43,108 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/52/1/STATE: false
abstraction | 2025-11-18 12:04:43,108 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,108 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,109 - __main__ - INFO - ← abstract STATE kontakt_schlafzimmer_strasse: home/contact/kontakt_schlafzimmer_strasse/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,128 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_schlafzimmer_strasse", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.128506+00:00"}
abstraction | 2025-11-18 12:04:43,145 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/26/1/STATE: false
abstraction | 2025-11-18 12:04:43,145 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,145 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,146 - __main__ - INFO - ← abstract STATE kontakt_esszimmer_strasse_rechts: home/contact/kontakt_esszimmer_strasse_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,165 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_esszimmer_strasse_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.165958+00:00"}
abstraction | 2025-11-18 12:04:43,182 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/27/1/STATE: false
abstraction | 2025-11-18 12:04:43,182 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,183 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,183 - __main__ - INFO - ← abstract STATE kontakt_esszimmer_strasse_links: home/contact/kontakt_esszimmer_strasse_links/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,202 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_esszimmer_strasse_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.202580+00:00"}
abstraction | 2025-11-18 12:04:43,219 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/28/1/STATE: false
abstraction | 2025-11-18 12:04:43,219 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,219 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,220 - __main__ - INFO - ← abstract STATE kontakt_wohnzimmer_garten_rechts: home/contact/kontakt_wohnzimmer_garten_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,239 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_wohnzimmer_garten_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.239653+00:00"}
abstraction | 2025-11-18 12:04:43,256 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/29/1/STATE: false
abstraction | 2025-11-18 12:04:43,256 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,256 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,257 - __main__ - INFO - ← abstract STATE kontakt_wohnzimmer_garten_links: home/contact/kontakt_wohnzimmer_garten_links/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,275 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_wohnzimmer_garten_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.275832+00:00"}
abstraction | 2025-11-18 12:04:43,292 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/18/1/STATE: false
abstraction | 2025-11-18 12:04:43,292 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,292 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,293 - __main__ - INFO - ← abstract STATE kontakt_patty_garten_rechts: home/contact/kontakt_patty_garten_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,314 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_patty_garten_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.314579+00:00"}
abstraction | 2025-11-18 12:04:43,331 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/22/1/STATE: false
abstraction | 2025-11-18 12:04:43,331 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,331 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,332 - __main__ - INFO - ← abstract STATE kontakt_patty_garten_links: home/contact/kontakt_patty_garten_links/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,351 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_patty_garten_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.351704+00:00"}
abstraction | 2025-11-18 12:04:43,368 - __main__ - DEBUG - MQTT message received on homegear/instance1/plain/44/1/STATE: false
abstraction | 2025-11-18 12:04:43,368 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 12:04:43,368 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 12:04:43,369 - __main__ - INFO - ← abstract STATE kontakt_bad_unten_strasse: home/contact/kontakt_bad_unten_strasse/state → {"contact": "closed"}
abstraction | 2025-11-18 12:04:43,388 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_bad_unten_strasse", "payload": {"contact": "closed"}, "ts": "2025-11-18T12:04:43.388390+00:00"}
api | INFO: 172.16.3.98:60163 - "GET /realtime HTTP/1.1" 200 OK
ui | INFO: 127.0.0.1:35036 - "GET /health HTTP/1.1" 200 OK
api | INFO: 172.16.3.98:60172 - "GET /realtime HTTP/1.1" 200 OK
abstraction | 2025-11-18 12:04:48,498 - __main__ - DEBUG - MQTT message received on shellies/schrankesszimmer/relay/0: off
abstraction | 2025-11-18 12:04:48,498 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 12:04:48,498 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 12:04:48,498 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 12:04:48,518 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T12:04:48.518525+00:00"}
api | INFO: 172.16.3.98:60187 - "GET /realtime HTTP/1.1" 200 OK
abstraction | 2025-11-18 12:04:52,989 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":55.04,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:52,989 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":100,"humidity":55.04,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:52,989 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 55.04, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1002.6, 'temperature': 22.13, 'voltage': 3015}
abstraction | 2025-11-18 12:04:52,989 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 55.04, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.6, "temperature": 22.13, "voltage": 3015}
abstraction | 2025-11-18 12:04:53,009 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 55.04, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.6, "temperature": 22.13, "voltage": 3015}, "ts": "2025-11-18T12:04:53.009776+00:00"}
abstraction | 2025-11-18 12:04:53,024 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,025 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.6,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,025 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.82, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1002.6, 'temperature': 22.13, 'voltage': 3015}
abstraction | 2025-11-18 12:04:53,025 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.82, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.6, "temperature": 22.13, "voltage": 3015}
abstraction | 2025-11-18 12:04:53,044 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.82, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.6, "temperature": 22.13, "voltage": 3015}, "ts": "2025-11-18T12:04:53.044379+00:00"}
abstraction | 2025-11-18 12:04:53,061 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d0003f052b7: {"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.4,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,061 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":100,"humidity":54.82,"linkquality":83,"power_outage_count":38416,"pressure":1002.4,"temperature":22.13,"voltage":3015}
abstraction | 2025-11-18 12:04:53,061 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.82, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1002.4, 'temperature': 22.13, 'voltage': 3015}
abstraction | 2025-11-18 12:04:53,061 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.82, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.4, "temperature": 22.13, "voltage": 3015}
abstraction | 2025-11-18 12:04:53,084 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.82, "linkquality": 83, "power_outage_count": 38416, "pressure": 1002.4, "temperature": 22.13, "voltage": 3015}, "ts": "2025-11-18T12:04:53.083988+00:00"}
abstraction | 2025-11-18 12:05:03,058 - __main__ - DEBUG - MQTT message received on shellies/lichtterasse/relay/0: off
abstraction | 2025-11-18 12:05:03,058 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 12:05:03,058 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 12:05:03,058 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 12:05:03,075 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T12:05:03.075262+00:00"}
abstraction | 2025-11-18 12:05:08,209 - __main__ - DEBUG - MQTT message received on shellies/wohnzimmer-regal/relay/0: off
abstraction | 2025-11-18 12:05:08,210 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 12:05:08,210 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 12:05:08,210 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 12:05:08,228 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T12:05:08.228758+00:00"}
abstraction | 2025-11-18 12:05:10,881 - __main__ - DEBUG - MQTT message received on shellies/LightKitchenSink/relay/0: on
abstraction | 2025-11-18 12:05:10,881 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 12:05:10,881 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 12:05:10,881 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 12:05:10,899 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T12:05:10.899207+00:00"}
abstraction | 2025-11-18 12:05:12,622 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,622 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,622 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 63, 'humidity': 47.69, 'linkquality': 87, 'power_outage_count': 4906, 'pressure': 1009.9, 'temperature': 19.74, 'voltage': 2945}
abstraction | 2025-11-18 12:05:12,622 - __main__ - INFO - ← abstract STATE sensor_kueche: home/temp_humidity/sensor_kueche/state → {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.9, "temperature": 19.74, "voltage": 2945}
abstraction | 2025-11-18 12:05:12,640 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_kueche", "payload": {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.9, "temperature": 19.74, "voltage": 2945}, "ts": "2025-11-18T12:05:12.640129+00:00"}
abstraction | 2025-11-18 12:05:12,656 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,656 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.9,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,656 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 63, 'humidity': 47.69, 'linkquality': 87, 'power_outage_count': 4906, 'pressure': 1009.9, 'temperature': 19.74, 'voltage': 2945}
abstraction | 2025-11-18 12:05:12,657 - __main__ - INFO - ← abstract STATE sensor_kueche: home/temp_humidity/sensor_kueche/state → {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.9, "temperature": 19.74, "voltage": 2945}
abstraction | 2025-11-18 12:05:12,674 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_kueche", "payload": {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.9, "temperature": 19.74, "voltage": 2945}, "ts": "2025-11-18T12:05:12.674372+00:00"}
abstraction | 2025-11-18 12:05:12,690 - __main__ - DEBUG - MQTT message received on zigbee2mqtt/0x00158d00083299bb: {"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.7,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,690 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={"battery":63,"humidity":47.69,"linkquality":87,"power_outage_count":4906,"pressure":1009.7,"temperature":19.74,"voltage":2945}
abstraction | 2025-11-18 12:05:12,690 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 63, 'humidity': 47.69, 'linkquality': 87, 'power_outage_count': 4906, 'pressure': 1009.7, 'temperature': 19.74, 'voltage': 2945}
abstraction | 2025-11-18 12:05:12,690 - __main__ - INFO - ← abstract STATE sensor_kueche: home/temp_humidity/sensor_kueche/state → {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.7, "temperature": 19.74, "voltage": 2945}
abstraction | 2025-11-18 12:05:12,708 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_kueche", "payload": {"battery": 63, "humidity": 47.69, "linkquality": 87, "power_outage_count": 4906, "pressure": 1009.7, "temperature": 19.74, "voltage": 2945}, "ts": "2025-11-18T12:05:12.708715+00:00"}
ui | INFO: 127.0.0.1:35638 - "GET /health HTTP/1.1" 200 OK
abstraction | 2025-11-18 12:05:18,507 - __main__ - DEBUG - MQTT message received on shellies/schrankesszimmer/relay/0: off
abstraction | 2025-11-18 12:05:18,508 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 12:05:18,508 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 12:05:18,508 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 12:05:18,526 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T12:05:18.525971+00:00"}

View File

@@ -0,0 +1,268 @@
abstraction | 2025-11-18 10:23:59,179 - asyncio - DEBUG - Using selector: EpollSelector
abstraction | 2025-11-18 10:23:59,240 - __main__ - INFO - Loaded configuration from /app/config/devices.yaml
abstraction | 2025-11-18 10:23:59,240 - __main__ - INFO - Loaded 64 device(s): lampe_semeniere_wohnzimmer, stehlampe_esszimmer_spiegel, stehlampe_esszimmer_schrank, grosse_lampe_wohnzimmer, lampe_naehtischchen_wohnzimmer, kleine_lampe_rechts_esszimmer, kleine_lampe_links_esszimmer, leselampe_esszimmer, medusalampe_schlafzimmer, sportlicht_am_fernseher_studierzimmer, deckenlampe_schlafzimmer, bettlicht_wolfgang, bettlicht_patty, schranklicht_hinten_patty, schranklicht_vorne_patty, leselampe_patty, deckenlampe_esszimmer, standlampe_esszimmer, haustuer, deckenlampe_flur_oben, kueche_deckenlampe, sportlicht_tisch, sportlicht_regal, licht_flur_oben_am_spiegel, experimentlabtest, thermostat_wolfgang, thermostat_kueche, thermostat_schlafzimmer, thermostat_esszimmer, thermostat_wohnzimmer, thermostat_patty, thermostat_bad_oben, thermostat_bad_unten, sterne_wohnzimmer, kontakt_schlafzimmer_strasse, kontakt_esszimmer_strasse_rechts, kontakt_esszimmer_strasse_links, kontakt_wohnzimmer_garten_rechts, kontakt_wohnzimmer_garten_links, kontakt_kueche_garten_fenster, kontakt_kueche_garten_tuer, kontakt_kueche_strasse_rechts, kontakt_kueche_strasse_links, kontakt_patty_garten_rechts, kontakt_patty_garten_links, kontakt_patty_strasse, kontakt_wolfgang_garten, kontakt_bad_oben_strasse, kontakt_bad_unten_strasse, sensor_schlafzimmer, sensor_wohnzimmer, sensor_kueche, sensor_arbeitszimmer_patty, sensor_arbeitszimmer_wolfgang, sensor_bad_oben, sensor_bad_unten, sensor_flur, sensor_waschkueche, sensor_sportzimmer, licht_spuele_kueche, licht_schrank_esszimmer, licht_regal_wohnzimmer, licht_flur_schrank, licht_terasse
abstraction | 2025-11-18 10:23:59,241 - __main__ - INFO - Loaded 64 device(s) from configuration
abstraction | 2025-11-18 10:23:59,292 - __main__ - INFO - Connected to Redis: redis://172.23.1.116:6379/8
abstraction | 2025-11-18 10:23:59,292 - __main__ - INFO - Abstraction worker started
abstraction | 2025-11-18 10:23:59,293 - __main__ - INFO - Connecting to MQTT broker: 172.23.1.102:1883
abstraction | 2025-11-18 10:23:59,341 - __main__ - INFO - Connected to MQTT broker as home-automation-abstraction-2cfdfa
abstraction | 2025-11-18 10:23:59,359 - __main__ - INFO - Subscribed to abstract SET: home/relay/lampe_semeniere_wohnzimmer/set
abstraction | 2025-11-18 10:23:59,377 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8000015480b
abstraction | 2025-11-18 10:23:59,394 - __main__ - INFO - Subscribed to abstract SET: home/light/stehlampe_esszimmer_spiegel/set
abstraction | 2025-11-18 10:23:59,411 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d06ea09
abstraction | 2025-11-18 10:23:59,428 - __main__ - INFO - Subscribed to abstract SET: home/light/stehlampe_esszimmer_schrank/set
abstraction | 2025-11-18 10:23:59,444 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d09176c
abstraction | 2025-11-18 10:23:59,460 - __main__ - INFO - Subscribed to abstract SET: home/relay/grosse_lampe_wohnzimmer/set
abstraction | 2025-11-18 10:23:59,477 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000151aca
abstraction | 2025-11-18 10:23:59,493 - __main__ - INFO - Subscribed to abstract SET: home/relay/lampe_naehtischchen_wohnzimmer/set
abstraction | 2025-11-18 10:23:59,510 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffee560ee
abstraction | 2025-11-18 10:23:59,526 - __main__ - INFO - Subscribed to abstract SET: home/relay/kleine_lampe_rechts_esszimmer/set
abstraction | 2025-11-18 10:23:59,543 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000156645
abstraction | 2025-11-18 10:23:59,560 - __main__ - INFO - Subscribed to abstract SET: home/relay/kleine_lampe_links_esszimmer/set
abstraction | 2025-11-18 10:23:59,578 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000153099
abstraction | 2025-11-18 10:23:59,595 - __main__ - INFO - Subscribed to abstract SET: home/light/leselampe_esszimmer/set
abstraction | 2025-11-18 10:23:59,612 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xec1bbdfffe7b84f2
abstraction | 2025-11-18 10:23:59,630 - __main__ - INFO - Subscribed to abstract SET: home/relay/medusalampe_schlafzimmer/set
abstraction | 2025-11-18 10:23:59,647 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000154c7c
abstraction | 2025-11-18 10:23:59,665 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_am_fernseher_studierzimmer/set
abstraction | 2025-11-18 10:23:59,682 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffe76a23a
abstraction | 2025-11-18 10:23:59,700 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_schlafzimmer/set
abstraction | 2025-11-18 10:23:59,717 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108a406a7
abstraction | 2025-11-18 10:23:59,735 - __main__ - INFO - Subscribed to abstract SET: home/light/bettlicht_wolfgang/set
abstraction | 2025-11-18 10:23:59,753 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00178801081570bf
abstraction | 2025-11-18 10:23:59,770 - __main__ - INFO - Subscribed to abstract SET: home/light/bettlicht_patty/set
abstraction | 2025-11-18 10:23:59,788 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108158b32
abstraction | 2025-11-18 10:23:59,807 - __main__ - INFO - Subscribed to abstract SET: home/light/schranklicht_hinten_patty/set
abstraction | 2025-11-18 10:23:59,825 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880106e29571
abstraction | 2025-11-18 10:23:59,844 - __main__ - INFO - Subscribed to abstract SET: home/relay/schranklicht_vorne_patty/set
abstraction | 2025-11-18 10:23:59,862 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000154cf5
abstraction | 2025-11-18 10:23:59,881 - __main__ - INFO - Subscribed to abstract SET: home/light/leselampe_patty/set
abstraction | 2025-11-18 10:23:59,901 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010600ec9d
abstraction | 2025-11-18 10:23:59,920 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_esszimmer/set
abstraction | 2025-11-18 10:23:59,940 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108a03e45
abstraction | 2025-11-18 10:23:59,959 - __main__ - INFO - Subscribed to abstract SET: home/light/standlampe_esszimmer/set
abstraction | 2025-11-18 10:23:59,979 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xbc33acfffe21f547
abstraction | 2025-11-18 10:23:59,999 - __main__ - INFO - Subscribed to abstract SET: home/light/haustuer/set
abstraction | 2025-11-18 10:24:00,016 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xec1bbdfffea6a3da
abstraction | 2025-11-18 10:24:00,034 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_flur_oben/set
abstraction | 2025-11-18 10:24:00,053 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d2123a7
abstraction | 2025-11-18 10:24:00,072 - __main__ - INFO - Subscribed to abstract SET: home/light/kueche_deckenlampe/set
abstraction | 2025-11-18 10:24:00,090 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d2c40c4
abstraction | 2025-11-18 10:24:00,108 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_tisch/set
abstraction | 2025-11-18 10:24:00,127 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8be2409f31b
abstraction | 2025-11-18 10:24:00,145 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_regal/set
abstraction | 2025-11-18 10:24:00,163 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8be2409f569
abstraction | 2025-11-18 10:24:00,183 - __main__ - INFO - Subscribed to abstract SET: home/light/licht_flur_oben_am_spiegel/set
abstraction | 2025-11-18 10:24:00,201 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffefe4ba4
abstraction | 2025-11-18 10:24:00,218 - __main__ - INFO - Subscribed to abstract SET: home/light/experimentlabtest/set
abstraction | 2025-11-18 10:24:00,237 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000195038
abstraction | 2025-11-18 10:24:00,255 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_wolfgang/set
abstraction | 2025-11-18 10:24:00,271 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x540f57fffe7e3cfe
abstraction | 2025-11-18 10:24:00,292 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_kueche/set
abstraction | 2025-11-18 10:24:00,313 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x94deb8fffe2e5c06
abstraction | 2025-11-18 10:24:00,334 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_schlafzimmer/set
abstraction | 2025-11-18 10:24:00,356 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/42/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,377 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_esszimmer/set
abstraction | 2025-11-18 10:24:00,398 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/45/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,420 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_wohnzimmer/set
abstraction | 2025-11-18 10:24:00,440 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/46/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,457 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_patty/set
abstraction | 2025-11-18 10:24:00,475 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/39/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,493 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_bad_oben/set
abstraction | 2025-11-18 10:24:00,509 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/41/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,530 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_bad_unten/set
abstraction | 2025-11-18 10:24:00,551 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/48/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,572 - __main__ - INFO - Subscribed to abstract SET: home/relay/sterne_wohnzimmer/set
abstraction | 2025-11-18 10:24:00,593 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000155fc2
abstraction | 2025-11-18 10:24:00,593 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_schlafzimmer_strasse
abstraction | 2025-11-18 10:24:00,614 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/52/1/STATE
abstraction | 2025-11-18 10:24:00,614 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_esszimmer_strasse_rechts
abstraction | 2025-11-18 10:24:00,630 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/26/1/STATE
abstraction | 2025-11-18 10:24:00,630 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_esszimmer_strasse_links
abstraction | 2025-11-18 10:24:00,647 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/27/1/STATE
abstraction | 2025-11-18 10:24:00,647 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wohnzimmer_garten_rechts
abstraction | 2025-11-18 10:24:00,668 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/28/1/STATE
abstraction | 2025-11-18 10:24:00,668 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wohnzimmer_garten_links
abstraction | 2025-11-18 10:24:00,691 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/29/1/STATE
abstraction | 2025-11-18 10:24:00,691 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_garten_fenster
abstraction | 2025-11-18 10:24:00,708 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b332785
abstraction | 2025-11-18 10:24:00,708 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_garten_tuer
abstraction | 2025-11-18 10:24:00,728 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b332788
abstraction | 2025-11-18 10:24:00,728 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_strasse_rechts
abstraction | 2025-11-18 10:24:00,747 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b151803
abstraction | 2025-11-18 10:24:00,747 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_strasse_links
abstraction | 2025-11-18 10:24:00,767 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b331d0b
abstraction | 2025-11-18 10:24:00,767 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_garten_rechts
abstraction | 2025-11-18 10:24:00,784 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/18/1/STATE
abstraction | 2025-11-18 10:24:00,784 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_garten_links
abstraction | 2025-11-18 10:24:00,802 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/22/1/STATE
abstraction | 2025-11-18 10:24:00,802 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_strasse
abstraction | 2025-11-18 10:24:00,821 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000af457cf
abstraction | 2025-11-18 10:24:00,821 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wolfgang_garten
abstraction | 2025-11-18 10:24:00,838 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b3328da
abstraction | 2025-11-18 10:24:00,838 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_bad_oben_strasse
abstraction | 2025-11-18 10:24:00,855 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b333aec
abstraction | 2025-11-18 10:24:00,855 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_bad_unten_strasse
abstraction | 2025-11-18 10:24:00,872 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/44/1/STATE
abstraction | 2025-11-18 10:24:00,872 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_schlafzimmer
abstraction | 2025-11-18 10:24:00,891 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00043292dc
abstraction | 2025-11-18 10:24:00,891 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_wohnzimmer
abstraction | 2025-11-18 10:24:00,907 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0008975707
abstraction | 2025-11-18 10:24:00,907 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_kueche
abstraction | 2025-11-18 10:24:00,925 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00083299bb
abstraction | 2025-11-18 10:24:00,925 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_arbeitszimmer_patty
abstraction | 2025-11-18 10:24:00,947 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0003f052b7
abstraction | 2025-11-18 10:24:00,947 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_arbeitszimmer_wolfgang
abstraction | 2025-11-18 10:24:00,969 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000543fb99
abstraction | 2025-11-18 10:24:00,969 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_bad_oben
abstraction | 2025-11-18 10:24:00,986 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00093e8987
abstraction | 2025-11-18 10:24:00,986 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_bad_unten
abstraction | 2025-11-18 10:24:01,004 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00093e662a
abstraction | 2025-11-18 10:24:01,004 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_flur
abstraction | 2025-11-18 10:24:01,022 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000836ccc6
abstraction | 2025-11-18 10:24:01,022 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_waschkueche
abstraction | 2025-11-18 10:24:01,038 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000449f3bc
abstraction | 2025-11-18 10:24:01,038 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_sportzimmer
abstraction | 2025-11-18 10:24:01,058 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0009421422
abstraction | 2025-11-18 10:24:01,074 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_spuele_kueche/set
abstraction | 2025-11-18 10:24:01,090 - __main__ - INFO - Subscribed to vendor STATE: shellies/LightKitchenSink/relay/0
abstraction | 2025-11-18 10:24:01,107 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_schrank_esszimmer/set
abstraction | 2025-11-18 10:24:01,122 - __main__ - INFO - Subscribed to vendor STATE: shellies/schrankesszimmer/relay/0
abstraction | 2025-11-18 10:24:01,139 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_regal_wohnzimmer/set
abstraction | 2025-11-18 10:24:01,155 - __main__ - INFO - Subscribed to vendor STATE: shellies/wohnzimmer-regal/relay/0
abstraction | 2025-11-18 10:24:01,172 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_flur_schrank/set
abstraction | 2025-11-18 10:24:01,189 - __main__ - INFO - Subscribed to vendor STATE: shellies/schrankflur/relay/0
abstraction | 2025-11-18 10:24:01,205 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_terasse/set
abstraction | 2025-11-18 10:24:01,222 - __main__ - INFO - Subscribed to vendor STATE: shellies/lichtterasse/relay/0
abstraction | 2025-11-18 10:24:01,222 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=21
abstraction | 2025-11-18 10:24:01,222 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 21.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,222 - __main__ - INFO - ← abstract STATE thermostat_schlafzimmer: home/thermostat/thermostat_schlafzimmer/state → {"target": 21.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,243 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_schlafzimmer", "payload": {"target": 21.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.243641+00:00"}
abstraction | 2025-11-18 10:24:01,260 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=15
abstraction | 2025-11-18 10:24:01,260 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 15.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,260 - __main__ - INFO - ← abstract STATE thermostat_esszimmer: home/thermostat/thermostat_esszimmer/state → {"target": 15.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,280 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_esszimmer", "payload": {"target": 15.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.280285+00:00"}
abstraction | 2025-11-18 10:24:01,296 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=15
abstraction | 2025-11-18 10:24:01,296 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 15.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,296 - __main__ - INFO - ← abstract STATE thermostat_wohnzimmer: home/thermostat/thermostat_wohnzimmer/state → {"target": 15.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,317 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_wohnzimmer", "payload": {"target": 15.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.317708+00:00"}
abstraction | 2025-11-18 10:24:01,334 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=22
abstraction | 2025-11-18 10:24:01,334 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 22.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,334 - __main__ - INFO - ← abstract STATE thermostat_patty: home/thermostat/thermostat_patty/state → {"target": 22.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,357 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_patty", "payload": {"target": 22.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.357082+00:00"}
abstraction | 2025-11-18 10:24:01,373 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=20
abstraction | 2025-11-18 10:24:01,373 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 20.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,373 - __main__ - INFO - ← abstract STATE thermostat_bad_oben: home/thermostat/thermostat_bad_oben/state → {"target": 20.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,395 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_bad_oben", "payload": {"target": 20.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.395470+00:00"}
abstraction | 2025-11-18 10:24:01,411 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=5
abstraction | 2025-11-18 10:24:01,411 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 5.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,411 - __main__ - INFO - ← abstract STATE thermostat_bad_unten: home/thermostat/thermostat_bad_unten/state → {"target": 5.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,431 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_bad_unten", "payload": {"target": 5.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.431068+00:00"}
abstraction | 2025-11-18 10:24:01,448 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,448 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,449 - __main__ - INFO - ← abstract STATE kontakt_schlafzimmer_strasse: home/contact/kontakt_schlafzimmer_strasse/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,472 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_schlafzimmer_strasse", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.472456+00:00"}
abstraction | 2025-11-18 10:24:01,491 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,491 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,491 - __main__ - INFO - ← abstract STATE kontakt_esszimmer_strasse_rechts: home/contact/kontakt_esszimmer_strasse_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,733 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_esszimmer_strasse_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.733873+00:00"}
abstraction | 2025-11-18 10:24:01,750 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,750 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,750 - __main__ - INFO - ← abstract STATE kontakt_esszimmer_strasse_links: home/contact/kontakt_esszimmer_strasse_links/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,771 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_esszimmer_strasse_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.771380+00:00"}
abstraction | 2025-11-18 10:24:01,788 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,788 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,788 - __main__ - INFO - ← abstract STATE kontakt_wohnzimmer_garten_rechts: home/contact/kontakt_wohnzimmer_garten_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,808 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_wohnzimmer_garten_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.808516+00:00"}
abstraction | 2025-11-18 10:24:01,825 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,825 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,825 - __main__ - INFO - ← abstract STATE kontakt_wohnzimmer_garten_links: home/contact/kontakt_wohnzimmer_garten_links/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,844 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_wohnzimmer_garten_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.844046+00:00"}
abstraction | 2025-11-18 10:24:01,860 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,861 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,861 - __main__ - INFO - ← abstract STATE kontakt_patty_garten_rechts: home/contact/kontakt_patty_garten_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,881 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_patty_garten_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.881922+00:00"}
abstraction | 2025-11-18 10:24:01,898 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,898 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,898 - __main__ - INFO - ← abstract STATE kontakt_patty_garten_links: home/contact/kontakt_patty_garten_links/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,922 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_patty_garten_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.922254+00:00"}
abstraction | 2025-11-18 10:24:01,940 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=true
abstraction | 2025-11-18 10:24:01,940 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'open'}
abstraction | 2025-11-18 10:24:01,940 - __main__ - INFO - ← abstract STATE kontakt_bad_unten_strasse: home/contact/kontakt_bad_unten_strasse/state → {"contact": "open"}
abstraction | 2025-11-18 10:24:01,959 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_bad_unten_strasse", "payload": {"contact": "open"}, "ts": "2025-11-18T10:24:01.959678+00:00"}
abstraction | 2025-11-18 10:24:02,354 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:02,354 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:02,354 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 10:24:02,373 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:02.373461+00:00"}
abstraction | 2025-11-18 10:24:07,440 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:07,440 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:07,441 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:24:07,459 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:07.459082+00:00"}
abstraction | 2025-11-18 10:24:08,817 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.37, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1004.2, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,817 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.37, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1004.2, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,817 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.37, "linkquality": 83, "power_outage_count": 38416, "pressure": 1004.2, "temperature": 22.16, "voltage": 3015}
abstraction | 2025-11-18 10:24:08,835 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.37, "linkquality": 83, "power_outage_count": 38416, "pressure": 1004.2, "temperature": 22.16, "voltage": 3015}, "ts": "2025-11-18T10:24:08.835488+00:00"}
abstraction | 2025-11-18 10:24:08,852 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.22, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1004.2, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,852 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.22, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1004.2, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,852 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.22, "linkquality": 83, "power_outage_count": 38416, "pressure": 1004.2, "temperature": 22.16, "voltage": 3015}
abstraction | 2025-11-18 10:24:08,870 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.22, "linkquality": 83, "power_outage_count": 38416, "pressure": 1004.2, "temperature": 22.16, "voltage": 3015}, "ts": "2025-11-18T10:24:08.870674+00:00"}
abstraction | 2025-11-18 10:24:08,887 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.22, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1003.9, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,887 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.22, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1003.9, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,887 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.22, "linkquality": 83, "power_outage_count": 38416, "pressure": 1003.9, "temperature": 22.16, "voltage": 3015}
abstraction | 2025-11-18 10:24:08,907 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.22, "linkquality": 83, "power_outage_count": 38416, "pressure": 1003.9, "temperature": 22.16, "voltage": 3015}, "ts": "2025-11-18T10:24:08.907729+00:00"}
abstraction | 2025-11-18 10:24:10,178 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 10:24:10,178 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 10:24:10,178 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 10:24:10,196 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T10:24:10.196762+00:00"}
abstraction | 2025-11-18 10:24:17,815 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:17,815 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:17,815 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:24:17,834 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:17.834042+00:00"}
abstraction | 2025-11-18 10:24:32,370 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:32,370 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:32,370 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 10:24:32,405 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:32.405754+00:00"}
abstraction | 2025-11-18 10:24:37,447 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:37,447 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:37,447 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:24:37,465 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:37.465220+00:00"}
abstraction | 2025-11-18 10:24:40,188 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 10:24:40,189 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 10:24:40,189 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 10:24:40,207 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T10:24:40.207222+00:00"}
abstraction | 2025-11-18 10:24:47,833 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:47,833 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:47,833 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:24:47,868 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:47.868787+00:00"}
abstraction | 2025-11-18 10:25:02,363 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:02,363 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:02,363 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 10:25:02,381 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:02.381792+00:00"}
abstraction | 2025-11-18 10:25:07,447 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:07,448 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:07,448 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:25:07,465 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:07.465566+00:00"}
abstraction | 2025-11-18 10:25:10,185 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 10:25:10,185 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 10:25:10,185 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 10:25:10,202 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T10:25:10.202372+00:00"}
abstraction | 2025-11-18 10:25:17,820 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:17,820 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:17,820 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:25:17,838 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:17.838140+00:00"}
abstraction | 2025-11-18 10:25:32,361 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:32,361 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:32,361 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 10:25:32,379 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:32.379286+00:00"}
abstraction | 2025-11-18 10:25:37,455 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:37,455 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:37,455 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:25:37,473 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:37.473171+00:00"}
abstraction | 2025-11-18 10:25:40,193 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 10:25:40,194 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 10:25:40,194 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 10:25:40,211 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T10:25:40.211493+00:00"}
abstraction | 2025-11-18 10:25:47,821 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:47,821 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:47,821 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:25:47,838 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:47.838508+00:00"}

332
debug/2025-11-18-debug.log Normal file
View File

@@ -0,0 +1,332 @@
✔ home-automation-abstraction Built 0.0s
✔ home-automation-api Built 0.0s
✔ home-automation-rules Built 0.0s
✔ home-automation-ui Built 0.0s
Attaching to abstraction, api, rules, ui
abstraction | 2025-11-18 10:23:59,179 - asyncio - DEBUG - Using selector: EpollSelector
rules | 2025-11-18 10:23:59,207 - asyncio - DEBUG - Using selector: EpollSelector
rules | 2025-11-18 10:23:59,208 - __main__ - INFO - ============================================================
rules | 2025-11-18 10:23:59,208 - __main__ - INFO - Rules Engine Starting
rules | 2025-11-18 10:23:59,209 - __main__ - INFO - ============================================================
rules | 2025-11-18 10:23:59,209 - __main__ - INFO - Config: /app/config/rules.yaml
rules | 2025-11-18 10:23:59,210 - __main__ - INFO - MQTT: 172.23.1.102:1883
rules | 2025-11-18 10:23:59,210 - __main__ - INFO - Redis: redis://172.23.1.116:6379/8
rules | 2025-11-18 10:23:59,210 - __main__ - INFO - ============================================================
rules | 2025-11-18 10:23:59,211 - __main__ - INFO - Loading rules configuration from /app/config/rules.yaml
rules | 2025-11-18 10:23:59,217 - __main__ - INFO - Loaded 6 rule(s) from configuration
rules | 2025-11-18 10:23:59,218 - __main__ - INFO - - window_setback_esszimmer (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 10:23:59,218 - __main__ - INFO - - window_setback_kueche (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 10:23:59,219 - __main__ - INFO - - window_setback_patty (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 10:23:59,219 - __main__ - INFO - - window_setback_schlafzimmer (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 10:23:59,220 - __main__ - INFO - - window_setback_wohnzimmer (type: window_setback@1.0) [DISABLED]
rules | 2025-11-18 10:23:59,225 - __main__ - INFO - - window_setback_wolfgang (type: window_setback@1.0)
rules | 2025-11-18 10:23:59,225 - __main__ - INFO - Successfully loaded 1 rule implementation(s) (5 disabled)
rules | 2025-11-18 10:23:59,226 - __main__ - INFO - Rule window_setback_wolfgang validated: 1 contacts, 1 thermostats
rules | 2025-11-18 10:23:59,226 - __main__ - DEBUG - Rule window_setback_wolfgang subscribes to 2 topic(s)
rules | 2025-11-18 10:23:59,227 - __main__ - INFO - Total MQTT subscriptions needed: 2
rules | 2025-11-18 10:23:59,227 - __main__ - INFO - Starting event processing loop
abstraction | 2025-11-18 10:23:59,240 - __main__ - INFO - Loaded configuration from /app/config/devices.yaml
abstraction | 2025-11-18 10:23:59,240 - __main__ - INFO - Loaded 64 device(s): lampe_semeniere_wohnzimmer, stehlampe_esszimmer_spiegel, stehlampe_esszimmer_schrank, grosse_lampe_wohnzimmer, lampe_naehtischchen_wohnzimmer, kleine_lampe_rechts_esszimmer, kleine_lampe_links_esszimmer, leselampe_esszimmer, medusalampe_schlafzimmer, sportlicht_am_fernseher_studierzimmer, deckenlampe_schlafzimmer, bettlicht_wolfgang, bettlicht_patty, schranklicht_hinten_patty, schranklicht_vorne_patty, leselampe_patty, deckenlampe_esszimmer, standlampe_esszimmer, haustuer, deckenlampe_flur_oben, kueche_deckenlampe, sportlicht_tisch, sportlicht_regal, licht_flur_oben_am_spiegel, experimentlabtest, thermostat_wolfgang, thermostat_kueche, thermostat_schlafzimmer, thermostat_esszimmer, thermostat_wohnzimmer, thermostat_patty, thermostat_bad_oben, thermostat_bad_unten, sterne_wohnzimmer, kontakt_schlafzimmer_strasse, kontakt_esszimmer_strasse_rechts, kontakt_esszimmer_strasse_links, kontakt_wohnzimmer_garten_rechts, kontakt_wohnzimmer_garten_links, kontakt_kueche_garten_fenster, kontakt_kueche_garten_tuer, kontakt_kueche_strasse_rechts, kontakt_kueche_strasse_links, kontakt_patty_garten_rechts, kontakt_patty_garten_links, kontakt_patty_strasse, kontakt_wolfgang_garten, kontakt_bad_oben_strasse, kontakt_bad_unten_strasse, sensor_schlafzimmer, sensor_wohnzimmer, sensor_kueche, sensor_arbeitszimmer_patty, sensor_arbeitszimmer_wolfgang, sensor_bad_oben, sensor_bad_unten, sensor_flur, sensor_waschkueche, sensor_sportzimmer, licht_spuele_kueche, licht_schrank_esszimmer, licht_regal_wohnzimmer, licht_flur_schrank, licht_terasse
abstraction | 2025-11-18 10:23:59,241 - __main__ - INFO - Loaded 64 device(s) from configuration
abstraction | 2025-11-18 10:23:59,292 - __main__ - INFO - Connected to Redis: redis://172.23.1.116:6379/8
abstraction | 2025-11-18 10:23:59,292 - __main__ - INFO - Abstraction worker started
abstraction | 2025-11-18 10:23:59,293 - __main__ - INFO - Connecting to MQTT broker: 172.23.1.102:1883
abstraction | 2025-11-18 10:23:59,341 - __main__ - INFO - Connected to MQTT broker as home-automation-abstraction-2cfdfa
abstraction | 2025-11-18 10:23:59,359 - __main__ - INFO - Subscribed to abstract SET: home/relay/lampe_semeniere_wohnzimmer/set
abstraction | 2025-11-18 10:23:59,377 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8000015480b
rules | 2025-11-18 10:23:59,378 - __main__ - INFO - Connecting to MQTT broker 172.23.1.102:1883 (client_id=rule_engine-782522)
abstraction | 2025-11-18 10:23:59,394 - __main__ - INFO - Subscribed to abstract SET: home/light/stehlampe_esszimmer_spiegel/set
abstraction | 2025-11-18 10:23:59,411 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d06ea09
abstraction | 2025-11-18 10:23:59,428 - __main__ - INFO - Subscribed to abstract SET: home/light/stehlampe_esszimmer_schrank/set
rules | 2025-11-18 10:23:59,431 - __main__ - INFO - Connected to MQTT broker 172.23.1.102:1883
abstraction | 2025-11-18 10:23:59,444 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d09176c
abstraction | 2025-11-18 10:23:59,460 - __main__ - INFO - Subscribed to abstract SET: home/relay/grosse_lampe_wohnzimmer/set
rules | 2025-11-18 10:23:59,466 - __main__ - INFO - Subscribed to 2 topic(s): home/thermostat/thermostat_wolfgang/state, home/contact/kontakt_wolfgang_garten/state
rules | 2025-11-18 10:23:59,467 - __main__ - DEBUG - Received event: {'topic': 'home/thermostat/thermostat_wolfgang/state', 'type': 'state', 'cap': 'thermostat', 'device_id': 'thermostat_wolfgang', 'payload': {'target': 23.0, 'current': 22.5, 'mode': 'heat'}, 'ts': '2025-11-18T10:23:59.467177'}
rules | 2025-11-18 10:23:59,467 - __main__ - DEBUG - Filtering for cap=thermostat, device_id=thermostat_wolfgang
rules | 2025-11-18 10:23:59,468 - __main__ - DEBUG - Rule window_setback_wolfgang: checking thermostats ['thermostat_wolfgang']
rules | 2025-11-18 10:23:59,468 - __main__ - INFO - Event thermostat/thermostat_wolfgang: 1 matching rule(s)
abstraction | 2025-11-18 10:23:59,477 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000151aca
abstraction | 2025-11-18 10:23:59,493 - __main__ - INFO - Subscribed to abstract SET: home/relay/lampe_naehtischchen_wohnzimmer/set
abstraction | 2025-11-18 10:23:59,510 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffee560ee
abstraction | 2025-11-18 10:23:59,526 - __main__ - INFO - Subscribed to abstract SET: home/relay/kleine_lampe_rechts_esszimmer/set
abstraction | 2025-11-18 10:23:59,543 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000156645
abstraction | 2025-11-18 10:23:59,560 - __main__ - INFO - Subscribed to abstract SET: home/relay/kleine_lampe_links_esszimmer/set
rules | 2025-11-18 10:23:59,572 - __main__ - DEBUG - Rule window_setback_wolfgang: Updated current target for thermostat_wolfgang: 23.0°C
rules | 2025-11-18 10:23:59,573 - __main__ - DEBUG - Received event: {'topic': 'home/contact/kontakt_wolfgang_garten/state', 'type': 'state', 'cap': 'contact', 'device_id': 'kontakt_wolfgang_garten', 'payload': {'contact': 'closed', 'battery': 100, 'linkquality': 32, 'device_temperature': 26, 'voltage': 3025}, 'ts': '2025-11-18T10:23:59.573073'}
rules | 2025-11-18 10:23:59,573 - __main__ - DEBUG - Filtering for cap=contact, device_id=kontakt_wolfgang_garten
rules | 2025-11-18 10:23:59,573 - __main__ - DEBUG - Rule window_setback_wolfgang: checking contacts ['kontakt_wolfgang_garten']
rules | 2025-11-18 10:23:59,574 - __main__ - INFO - Event contact/kontakt_wolfgang_garten: 1 matching rule(s)
abstraction | 2025-11-18 10:23:59,578 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000153099
abstraction | 2025-11-18 10:23:59,595 - __main__ - INFO - Subscribed to abstract SET: home/light/leselampe_esszimmer/set
rules | 2025-11-18 10:23:59,610 - __main__ - INFO - Rule window_setback_wolfgang: Window closed, restoring 1 thermostats to previous temperatures
abstraction | 2025-11-18 10:23:59,612 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xec1bbdfffe7b84f2
rules | 2025-11-18 10:23:59,627 - __main__ - WARNING - No previous target found for thermostat_wolfgang, cannot restore
abstraction | 2025-11-18 10:23:59,630 - __main__ - INFO - Subscribed to abstract SET: home/relay/medusalampe_schlafzimmer/set
abstraction | 2025-11-18 10:23:59,647 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000154c7c
abstraction | 2025-11-18 10:23:59,665 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_am_fernseher_studierzimmer/set
abstraction | 2025-11-18 10:23:59,682 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffe76a23a
abstraction | 2025-11-18 10:23:59,700 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_schlafzimmer/set
abstraction | 2025-11-18 10:23:59,717 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108a406a7
api | INFO: Started server process [1]
api | INFO: Waiting for application startup.
abstraction | 2025-11-18 10:23:59,735 - __main__ - INFO - Subscribed to abstract SET: home/light/bettlicht_wolfgang/set
api | INFO: Application startup complete.
abstraction | 2025-11-18 10:23:59,753 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00178801081570bf
api | INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
abstraction | 2025-11-18 10:23:59,770 - __main__ - INFO - Subscribed to abstract SET: home/light/bettlicht_patty/set
abstraction | 2025-11-18 10:23:59,788 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108158b32
abstraction | 2025-11-18 10:23:59,807 - __main__ - INFO - Subscribed to abstract SET: home/light/schranklicht_hinten_patty/set
abstraction | 2025-11-18 10:23:59,825 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880106e29571
abstraction | 2025-11-18 10:23:59,844 - __main__ - INFO - Subscribed to abstract SET: home/relay/schranklicht_vorne_patty/set
abstraction | 2025-11-18 10:23:59,862 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000154cf5
abstraction | 2025-11-18 10:23:59,881 - __main__ - INFO - Subscribed to abstract SET: home/light/leselampe_patty/set
abstraction | 2025-11-18 10:23:59,901 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010600ec9d
abstraction | 2025-11-18 10:23:59,920 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_esszimmer/set
abstraction | 2025-11-18 10:23:59,940 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x0017880108a03e45
abstraction | 2025-11-18 10:23:59,959 - __main__ - INFO - Subscribed to abstract SET: home/light/standlampe_esszimmer/set
abstraction | 2025-11-18 10:23:59,979 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xbc33acfffe21f547
ui | UI using API_BASE: http://172.19.1.11:8001
ui | UI using BASE_PATH: /
ui | INFO: Started server process [1]
ui | INFO: Waiting for application startup.
ui | INFO: Application startup complete.
abstraction | 2025-11-18 10:23:59,999 - __main__ - INFO - Subscribed to abstract SET: home/light/haustuer/set
ui | INFO: Uvicorn running on http://0.0.0.0:8002 (Press CTRL+C to quit)
abstraction | 2025-11-18 10:24:00,016 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xec1bbdfffea6a3da
abstraction | 2025-11-18 10:24:00,034 - __main__ - INFO - Subscribed to abstract SET: home/light/deckenlampe_flur_oben/set
abstraction | 2025-11-18 10:24:00,053 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d2123a7
abstraction | 2025-11-18 10:24:00,072 - __main__ - INFO - Subscribed to abstract SET: home/light/kueche_deckenlampe/set
abstraction | 2025-11-18 10:24:00,090 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x001788010d2c40c4
abstraction | 2025-11-18 10:24:00,108 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_tisch/set
abstraction | 2025-11-18 10:24:00,127 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8be2409f31b
abstraction | 2025-11-18 10:24:00,145 - __main__ - INFO - Subscribed to abstract SET: home/light/sportlicht_regal/set
abstraction | 2025-11-18 10:24:00,163 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b8be2409f569
abstraction | 2025-11-18 10:24:00,183 - __main__ - INFO - Subscribed to abstract SET: home/light/licht_flur_oben_am_spiegel/set
abstraction | 2025-11-18 10:24:00,201 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x842e14fffefe4ba4
abstraction | 2025-11-18 10:24:00,218 - __main__ - INFO - Subscribed to abstract SET: home/light/experimentlabtest/set
abstraction | 2025-11-18 10:24:00,237 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000195038
abstraction | 2025-11-18 10:24:00,255 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_wolfgang/set
abstraction | 2025-11-18 10:24:00,271 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x540f57fffe7e3cfe
abstraction | 2025-11-18 10:24:00,292 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_kueche/set
abstraction | 2025-11-18 10:24:00,313 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x94deb8fffe2e5c06
abstraction | 2025-11-18 10:24:00,334 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_schlafzimmer/set
abstraction | 2025-11-18 10:24:00,356 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/42/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,377 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_esszimmer/set
abstraction | 2025-11-18 10:24:00,398 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/45/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,420 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_wohnzimmer/set
abstraction | 2025-11-18 10:24:00,440 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/46/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,457 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_patty/set
abstraction | 2025-11-18 10:24:00,475 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/39/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,493 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_bad_oben/set
abstraction | 2025-11-18 10:24:00,509 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/41/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,530 - __main__ - INFO - Subscribed to abstract SET: home/thermostat/thermostat_bad_unten/set
abstraction | 2025-11-18 10:24:00,551 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/48/1/SET_TEMPERATURE
abstraction | 2025-11-18 10:24:00,572 - __main__ - INFO - Subscribed to abstract SET: home/relay/sterne_wohnzimmer/set
abstraction | 2025-11-18 10:24:00,593 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0xf0d1b80000155fc2
abstraction | 2025-11-18 10:24:00,593 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_schlafzimmer_strasse
abstraction | 2025-11-18 10:24:00,614 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/52/1/STATE
abstraction | 2025-11-18 10:24:00,614 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_esszimmer_strasse_rechts
abstraction | 2025-11-18 10:24:00,630 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/26/1/STATE
abstraction | 2025-11-18 10:24:00,630 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_esszimmer_strasse_links
abstraction | 2025-11-18 10:24:00,647 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/27/1/STATE
abstraction | 2025-11-18 10:24:00,647 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wohnzimmer_garten_rechts
abstraction | 2025-11-18 10:24:00,668 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/28/1/STATE
abstraction | 2025-11-18 10:24:00,668 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wohnzimmer_garten_links
abstraction | 2025-11-18 10:24:00,691 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/29/1/STATE
abstraction | 2025-11-18 10:24:00,691 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_garten_fenster
abstraction | 2025-11-18 10:24:00,708 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b332785
abstraction | 2025-11-18 10:24:00,708 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_garten_tuer
abstraction | 2025-11-18 10:24:00,728 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b332788
abstraction | 2025-11-18 10:24:00,728 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_strasse_rechts
abstraction | 2025-11-18 10:24:00,747 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b151803
abstraction | 2025-11-18 10:24:00,747 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_kueche_strasse_links
abstraction | 2025-11-18 10:24:00,767 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b331d0b
abstraction | 2025-11-18 10:24:00,767 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_garten_rechts
abstraction | 2025-11-18 10:24:00,784 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/18/1/STATE
abstraction | 2025-11-18 10:24:00,784 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_garten_links
abstraction | 2025-11-18 10:24:00,802 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/22/1/STATE
abstraction | 2025-11-18 10:24:00,802 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_patty_strasse
abstraction | 2025-11-18 10:24:00,821 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000af457cf
abstraction | 2025-11-18 10:24:00,821 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_wolfgang_garten
abstraction | 2025-11-18 10:24:00,838 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b3328da
abstraction | 2025-11-18 10:24:00,838 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_bad_oben_strasse
abstraction | 2025-11-18 10:24:00,855 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d008b333aec
abstraction | 2025-11-18 10:24:00,855 - __main__ - INFO - Skipping SET subscription for read-only device: kontakt_bad_unten_strasse
api | INFO: 172.16.3.98:51428 - "GET /realtime HTTP/1.1" 200 OK
abstraction | 2025-11-18 10:24:00,872 - __main__ - INFO - Subscribed to vendor STATE: homegear/instance1/plain/44/1/STATE
abstraction | 2025-11-18 10:24:00,872 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_schlafzimmer
abstraction | 2025-11-18 10:24:00,891 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00043292dc
abstraction | 2025-11-18 10:24:00,891 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_wohnzimmer
api | INFO: 172.16.3.98:51429 - "GET /realtime HTTP/1.1" 200 OK
abstraction | 2025-11-18 10:24:00,907 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0008975707
abstraction | 2025-11-18 10:24:00,907 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_kueche
abstraction | 2025-11-18 10:24:00,925 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00083299bb
abstraction | 2025-11-18 10:24:00,925 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_arbeitszimmer_patty
abstraction | 2025-11-18 10:24:00,947 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0003f052b7
abstraction | 2025-11-18 10:24:00,947 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_arbeitszimmer_wolfgang
abstraction | 2025-11-18 10:24:00,969 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000543fb99
abstraction | 2025-11-18 10:24:00,969 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_bad_oben
abstraction | 2025-11-18 10:24:00,986 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00093e8987
abstraction | 2025-11-18 10:24:00,986 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_bad_unten
abstraction | 2025-11-18 10:24:01,004 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d00093e662a
abstraction | 2025-11-18 10:24:01,004 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_flur
abstraction | 2025-11-18 10:24:01,022 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000836ccc6
abstraction | 2025-11-18 10:24:01,022 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_waschkueche
abstraction | 2025-11-18 10:24:01,038 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d000449f3bc
abstraction | 2025-11-18 10:24:01,038 - __main__ - INFO - Skipping SET subscription for read-only device: sensor_sportzimmer
abstraction | 2025-11-18 10:24:01,058 - __main__ - INFO - Subscribed to vendor STATE: zigbee2mqtt/0x00158d0009421422
abstraction | 2025-11-18 10:24:01,074 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_spuele_kueche/set
abstraction | 2025-11-18 10:24:01,090 - __main__ - INFO - Subscribed to vendor STATE: shellies/LightKitchenSink/relay/0
abstraction | 2025-11-18 10:24:01,107 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_schrank_esszimmer/set
abstraction | 2025-11-18 10:24:01,122 - __main__ - INFO - Subscribed to vendor STATE: shellies/schrankesszimmer/relay/0
abstraction | 2025-11-18 10:24:01,139 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_regal_wohnzimmer/set
abstraction | 2025-11-18 10:24:01,155 - __main__ - INFO - Subscribed to vendor STATE: shellies/wohnzimmer-regal/relay/0
abstraction | 2025-11-18 10:24:01,172 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_flur_schrank/set
abstraction | 2025-11-18 10:24:01,189 - __main__ - INFO - Subscribed to vendor STATE: shellies/schrankflur/relay/0
abstraction | 2025-11-18 10:24:01,205 - __main__ - INFO - Subscribed to abstract SET: home/relay/licht_terasse/set
abstraction | 2025-11-18 10:24:01,222 - __main__ - INFO - Subscribed to vendor STATE: shellies/lichtterasse/relay/0
abstraction | 2025-11-18 10:24:01,222 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=21
abstraction | 2025-11-18 10:24:01,222 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 21.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,222 - __main__ - INFO - ← abstract STATE thermostat_schlafzimmer: home/thermostat/thermostat_schlafzimmer/state → {"target": 21.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,243 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_schlafzimmer", "payload": {"target": 21.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.243641+00:00"}
abstraction | 2025-11-18 10:24:01,260 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=15
abstraction | 2025-11-18 10:24:01,260 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 15.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,260 - __main__ - INFO - ← abstract STATE thermostat_esszimmer: home/thermostat/thermostat_esszimmer/state → {"target": 15.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,280 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_esszimmer", "payload": {"target": 15.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.280285+00:00"}
abstraction | 2025-11-18 10:24:01,296 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=15
abstraction | 2025-11-18 10:24:01,296 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 15.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,296 - __main__ - INFO - ← abstract STATE thermostat_wohnzimmer: home/thermostat/thermostat_wohnzimmer/state → {"target": 15.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,317 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_wohnzimmer", "payload": {"target": 15.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.317708+00:00"}
abstraction | 2025-11-18 10:24:01,334 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=22
abstraction | 2025-11-18 10:24:01,334 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 22.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,334 - __main__ - INFO - ← abstract STATE thermostat_patty: home/thermostat/thermostat_patty/state → {"target": 22.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,357 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_patty", "payload": {"target": 22.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.357082+00:00"}
abstraction | 2025-11-18 10:24:01,373 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=20
abstraction | 2025-11-18 10:24:01,373 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 20.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,373 - __main__ - INFO - ← abstract STATE thermostat_bad_oben: home/thermostat/thermostat_bad_oben/state → {"target": 20.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,395 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_bad_oben", "payload": {"target": 20.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.395470+00:00"}
abstraction | 2025-11-18 10:24:01,411 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=thermostat, tech=max, payload=5
abstraction | 2025-11-18 10:24:01,411 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=thermostat, tech=max, payload={'target': 5.0, 'mode': 'heat'}
abstraction | 2025-11-18 10:24:01,411 - __main__ - INFO - ← abstract STATE thermostat_bad_unten: home/thermostat/thermostat_bad_unten/state → {"target": 5.0, "mode": "heat"}
abstraction | 2025-11-18 10:24:01,431 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "thermostat_bad_unten", "payload": {"target": 5.0, "mode": "heat"}, "ts": "2025-11-18T10:24:01.431068+00:00"}
abstraction | 2025-11-18 10:24:01,448 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,448 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,449 - __main__ - INFO - ← abstract STATE kontakt_schlafzimmer_strasse: home/contact/kontakt_schlafzimmer_strasse/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,472 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_schlafzimmer_strasse", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.472456+00:00"}
abstraction | 2025-11-18 10:24:01,491 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,491 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,491 - __main__ - INFO - ← abstract STATE kontakt_esszimmer_strasse_rechts: home/contact/kontakt_esszimmer_strasse_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,733 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_esszimmer_strasse_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.733873+00:00"}
abstraction | 2025-11-18 10:24:01,750 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,750 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,750 - __main__ - INFO - ← abstract STATE kontakt_esszimmer_strasse_links: home/contact/kontakt_esszimmer_strasse_links/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,771 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_esszimmer_strasse_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.771380+00:00"}
abstraction | 2025-11-18 10:24:01,788 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,788 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,788 - __main__ - INFO - ← abstract STATE kontakt_wohnzimmer_garten_rechts: home/contact/kontakt_wohnzimmer_garten_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,808 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_wohnzimmer_garten_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.808516+00:00"}
abstraction | 2025-11-18 10:24:01,825 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,825 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,825 - __main__ - INFO - ← abstract STATE kontakt_wohnzimmer_garten_links: home/contact/kontakt_wohnzimmer_garten_links/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,844 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_wohnzimmer_garten_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.844046+00:00"}
abstraction | 2025-11-18 10:24:01,860 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,861 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,861 - __main__ - INFO - ← abstract STATE kontakt_patty_garten_rechts: home/contact/kontakt_patty_garten_rechts/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,881 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_patty_garten_rechts", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.881922+00:00"}
abstraction | 2025-11-18 10:24:01,898 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=false
abstraction | 2025-11-18 10:24:01,898 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'closed'}
abstraction | 2025-11-18 10:24:01,898 - __main__ - INFO - ← abstract STATE kontakt_patty_garten_links: home/contact/kontakt_patty_garten_links/state → {"contact": "closed"}
abstraction | 2025-11-18 10:24:01,922 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_patty_garten_links", "payload": {"contact": "closed"}, "ts": "2025-11-18T10:24:01.922254+00:00"}
abstraction | 2025-11-18 10:24:01,940 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=contact, tech=max, payload=true
abstraction | 2025-11-18 10:24:01,940 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=contact, tech=max, payload={'contact': 'open'}
abstraction | 2025-11-18 10:24:01,940 - __main__ - INFO - ← abstract STATE kontakt_bad_unten_strasse: home/contact/kontakt_bad_unten_strasse/state → {"contact": "open"}
abstraction | 2025-11-18 10:24:01,959 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "kontakt_bad_unten_strasse", "payload": {"contact": "open"}, "ts": "2025-11-18T10:24:01.959678+00:00"}
abstraction | 2025-11-18 10:24:02,354 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:02,354 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:02,354 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 10:24:02,373 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:02.373461+00:00"}
ui | INFO: 127.0.0.1:49192 - "GET /health HTTP/1.1" 200 OK
api | INFO: 172.16.3.98:51450 - "GET /realtime HTTP/1.1" 200 OK
abstraction | 2025-11-18 10:24:07,440 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:07,440 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:07,441 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:24:07,459 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:07.459082+00:00"}
abstraction | 2025-11-18 10:24:08,817 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.37, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1004.2, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,817 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.37, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1004.2, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,817 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.37, "linkquality": 83, "power_outage_count": 38416, "pressure": 1004.2, "temperature": 22.16, "voltage": 3015}
abstraction | 2025-11-18 10:24:08,835 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.37, "linkquality": 83, "power_outage_count": 38416, "pressure": 1004.2, "temperature": 22.16, "voltage": 3015}, "ts": "2025-11-18T10:24:08.835488+00:00"}
abstraction | 2025-11-18 10:24:08,852 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.22, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1004.2, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,852 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.22, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1004.2, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,852 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.22, "linkquality": 83, "power_outage_count": 38416, "pressure": 1004.2, "temperature": 22.16, "voltage": 3015}
abstraction | 2025-11-18 10:24:08,870 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.22, "linkquality": 83, "power_outage_count": 38416, "pressure": 1004.2, "temperature": 22.16, "voltage": 3015}, "ts": "2025-11-18T10:24:08.870674+00:00"}
abstraction | 2025-11-18 10:24:08,887 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.22, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1003.9, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,887 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=temp_humidity_sensor, tech=zigbee2mqtt, payload={'battery': 100, 'humidity': 54.22, 'linkquality': 83, 'power_outage_count': 38416, 'pressure': 1003.9, 'temperature': 22.16, 'voltage': 3015}
abstraction | 2025-11-18 10:24:08,887 - __main__ - INFO - ← abstract STATE sensor_arbeitszimmer_patty: home/temp_humidity/sensor_arbeitszimmer_patty/state → {"battery": 100, "humidity": 54.22, "linkquality": 83, "power_outage_count": 38416, "pressure": 1003.9, "temperature": 22.16, "voltage": 3015}
abstraction | 2025-11-18 10:24:08,907 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "sensor_arbeitszimmer_patty", "payload": {"battery": 100, "humidity": 54.22, "linkquality": 83, "power_outage_count": 38416, "pressure": 1003.9, "temperature": 22.16, "voltage": 3015}, "ts": "2025-11-18T10:24:08.907729+00:00"}
abstraction | 2025-11-18 10:24:10,178 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 10:24:10,178 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 10:24:10,178 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 10:24:10,196 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T10:24:10.196762+00:00"}
abstraction | 2025-11-18 10:24:17,815 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:17,815 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:17,815 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:24:17,834 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:17.834042+00:00"}
abstraction | 2025-11-18 10:24:32,370 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:32,370 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:32,370 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 10:24:32,405 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:32.405754+00:00"}
ui | INFO: 127.0.0.1:39276 - "GET /health HTTP/1.1" 200 OK
abstraction | 2025-11-18 10:24:37,447 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:37,447 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:37,447 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:24:37,465 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:37.465220+00:00"}
abstraction | 2025-11-18 10:24:40,188 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 10:24:40,189 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 10:24:40,189 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 10:24:40,207 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T10:24:40.207222+00:00"}
abstraction | 2025-11-18 10:24:47,833 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:24:47,833 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:24:47,833 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:24:47,868 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:24:47.868787+00:00"}
abstraction | 2025-11-18 10:25:02,363 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:02,363 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:02,363 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 10:25:02,381 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:02.381792+00:00"}
ui | INFO: 127.0.0.1:37108 - "GET /health HTTP/1.1" 200 OK
abstraction | 2025-11-18 10:25:07,447 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:07,448 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:07,448 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:25:07,465 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:07.465566+00:00"}
abstraction | 2025-11-18 10:25:10,185 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 10:25:10,185 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 10:25:10,185 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 10:25:10,202 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T10:25:10.202372+00:00"}
abstraction | 2025-11-18 10:25:17,820 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:17,820 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:17,820 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:25:17,838 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:17.838140+00:00"}
api | INFO: 172.16.3.98:51763 - "GET /docs HTTP/1.1" 200 OK
api | /usr/local/lib/python3.14/site-packages/fastapi/openapi/utils.py:207: UserWarning: Duplicate Operation ID get_device_layout_devices__device_id__layout_get for function get_device_layout at /app/apps/api/main.py
api | warnings.warn(message, stacklevel=1)
api | /usr/local/lib/python3.14/site-packages/fastapi/openapi/utils.py:207: UserWarning: Duplicate Operation ID get_device_state_devices__device_id__state_get for function get_device_state at /app/apps/api/main.py
api | warnings.warn(message, stacklevel=1)
api | INFO: 172.16.3.98:51763 - "GET /openapi.json HTTP/1.1" 200 OK
api | INFO: 172.16.3.98:51763 - "GET /devices/states HTTP/1.1" 200 OK
abstraction | 2025-11-18 10:25:32,361 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:32,361 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:32,361 - __main__ - INFO - ← abstract STATE licht_terasse: home/relay/licht_terasse/state → {"power": "off"}
abstraction | 2025-11-18 10:25:32,379 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_terasse", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:32.379286+00:00"}
ui | INFO: 127.0.0.1:41510 - "GET /health HTTP/1.1" 200 OK
abstraction | 2025-11-18 10:25:37,455 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:37,455 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:37,455 - __main__ - INFO - ← abstract STATE licht_regal_wohnzimmer: home/relay/licht_regal_wohnzimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:25:37,473 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_regal_wohnzimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:37.473171+00:00"}
abstraction | 2025-11-18 10:25:40,193 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=on
abstraction | 2025-11-18 10:25:40,194 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'on'}
abstraction | 2025-11-18 10:25:40,194 - __main__ - INFO - ← abstract STATE licht_spuele_kueche: home/relay/licht_spuele_kueche/state → {"power": "on"}
abstraction | 2025-11-18 10:25:40,211 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_spuele_kueche", "payload": {"power": "on"}, "ts": "2025-11-18T10:25:40.211493+00:00"}
abstraction | 2025-11-18 10:25:47,821 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract IN: type=relay, tech=shelly, payload=off
abstraction | 2025-11-18 10:25:47,821 - apps.abstraction.transformation - DEBUG - transform_vendor_to_abstract OUT: type=relay, tech=shelly, payload={'power': 'off'}
abstraction | 2025-11-18 10:25:47,821 - __main__ - INFO - ← abstract STATE licht_schrank_esszimmer: home/relay/licht_schrank_esszimmer/state → {"power": "off"}
abstraction | 2025-11-18 10:25:47,838 - __main__ - INFO - ← Redis PUBLISH ui:updates → {"type": "state", "device_id": "licht_schrank_esszimmer", "payload": {"power": "off"}, "ts": "2025-11-18T10:25:47.838508+00:00"}

60
docker-compose.yaml Normal file
View File

@@ -0,0 +1,60 @@
x-environment: &default-env
MQTT_BROKER: "172.23.1.102"
MQTT_PORT: 1883
REDIS_HOST: "172.23.1.116"
REDIS_PORT: 6379
REDIS_DB: 8
services:
ui:
build:
context: .
dockerfile: apps/ui/Dockerfile
container_name: ui
environment:
UI_PORT: 8002
API_BASE: "http://172.19.1.11:8001"
BASE_PATH: "/"
ports:
- "8002:8002"
depends_on:
- api
api:
build:
context: .
dockerfile: apps/api/Dockerfile
container_name: api
environment:
<<: *default-env
REDIS_CHANNEL: "ui:updates"
volumes:
- ./config:/app/config:ro
ports:
- "8001:8001"
depends_on:
- abstraction
abstraction:
build:
context: .
dockerfile: apps/abstraction/Dockerfile
container_name: abstraction
environment:
<<: *default-env
volumes:
- ./config:/app/config:ro
rules:
build:
context: .
dockerfile: apps/rules/Dockerfile
container_name: rules
environment:
<<: *default-env
RULES_CONFIG: "/app/config/rules.yaml"
volumes:
- ./config:/app/config:ro
depends_on:
- abstraction

View File

@@ -1,23 +0,0 @@
# Infrastructure
This directory contains infrastructure-related files for the home automation project.
## Files
- `docker-compose.yml`: Docker Compose configuration for running services
## Usage
```bash
# Start services
docker-compose up -d
# Stop services
docker-compose down
```
## TODO
- Add service definitions to docker-compose.yml
- Add deployment configurations
- Add monitoring and logging setup

View File

@@ -1,15 +0,0 @@
version: '3.8'
services:
# Placeholder for future services
# Example:
# api:
# build:
# context: ..
# dockerfile: apps/api/Dockerfile
# ports:
# - "8000:8000"
placeholder:
image: alpine:latest
command: echo "Docker Compose placeholder - add your services here"

View File

@@ -6,7 +6,29 @@ from packages.home_capabilities.thermostat import CAP_VERSION as THERMOSTAT_VERS
from packages.home_capabilities.thermostat import ThermostatState from packages.home_capabilities.thermostat import ThermostatState
from packages.home_capabilities.contact_sensor import CAP_VERSION as CONTACT_SENSOR_VERSION from packages.home_capabilities.contact_sensor import CAP_VERSION as CONTACT_SENSOR_VERSION
from packages.home_capabilities.contact_sensor import ContactState from packages.home_capabilities.contact_sensor import ContactState
from packages.home_capabilities.layout import DeviceTile, Room, UiLayout, load_layout from packages.home_capabilities.temp_humidity_sensor import CAP_VERSION as TEMP_HUMIDITY_SENSOR_VERSION
from packages.home_capabilities.temp_humidity_sensor import TempHumidityState
from packages.home_capabilities.relay import CAP_VERSION as RELAY_VERSION
from packages.home_capabilities.relay import RelayState
from packages.home_capabilities.layout import (
DeviceTile,
Room,
UiLayout,
load_layout,
)
from packages.home_capabilities.groups_scenes import (
GroupConfig,
GroupsConfigRoot,
GroupSelector,
SceneConfig,
ScenesConfigRoot,
SceneSelector,
SceneStep,
get_group_by_id,
get_scene_by_id,
load_groups,
load_scenes,
)
__all__ = [ __all__ = [
"LightState", "LightState",
@@ -15,8 +37,23 @@ __all__ = [
"THERMOSTAT_VERSION", "THERMOSTAT_VERSION",
"ContactState", "ContactState",
"CONTACT_SENSOR_VERSION", "CONTACT_SENSOR_VERSION",
"TempHumidityState",
"TEMP_HUMIDITY_SENSOR_VERSION",
"RelayState",
"RELAY_VERSION",
"DeviceTile", "DeviceTile",
"Room", "Room",
"UiLayout", "UiLayout",
"load_layout", "load_layout",
"GroupConfig",
"GroupsConfigRoot",
"GroupSelector",
"SceneConfig",
"ScenesConfigRoot",
"SceneSelector",
"SceneStep",
"get_group_by_id",
"get_scene_by_id",
"load_groups",
"load_scenes",
] ]

View File

@@ -0,0 +1,229 @@
"""
Configuration models and loaders for groups and scenes.
This module provides Pydantic models for validating groups.yaml and scenes.yaml,
along with loader functions that parse YAML files into typed configuration objects.
"""
from pathlib import Path
from typing import Any
import yaml
from pydantic import BaseModel, Field, field_validator, model_validator
# ============================================================================
# GROUP MODELS
# ============================================================================
class GroupSelector(BaseModel):
"""Selector for automatically adding devices to a group."""
type: str = Field(..., description="Device type (e.g., 'light', 'thermostat')")
room: str | None = Field(None, description="Filter by room name")
tags: list[str] | None = Field(None, description="Filter by device tags")
class GroupConfig(BaseModel):
"""Configuration for a device group."""
id: str = Field(..., description="Unique group identifier")
name: str = Field(..., description="Human-readable group name")
selector: GroupSelector | None = Field(None, description="Auto-select devices by criteria")
device_ids: list[str] = Field(default_factory=list, description="Explicit device IDs")
capabilities: dict[str, bool] = Field(
default_factory=dict,
description="Supported capabilities (e.g., {'brightness': True})"
)
class GroupsConfigRoot(BaseModel):
"""Root configuration for groups.yaml."""
version: int = Field(..., description="Configuration schema version")
groups: list[GroupConfig] = Field(default_factory=list, description="List of groups")
@field_validator('groups')
@classmethod
def validate_unique_ids(cls, groups: list[GroupConfig]) -> list[GroupConfig]:
"""Ensure all group IDs are unique."""
ids = [g.id for g in groups]
duplicates = [id for id in ids if ids.count(id) > 1]
if duplicates:
raise ValueError(f"Duplicate group IDs found: {set(duplicates)}")
return groups
# ============================================================================
# SCENE MODELS
# ============================================================================
class SceneSelector(BaseModel):
"""Selector for targeting devices in a scene step."""
type: str | None = Field(None, description="Device type (e.g., 'light', 'outlet')")
room: str | None = Field(None, description="Filter by room name")
tags: list[str] | None = Field(None, description="Filter by device tags")
class SceneStep(BaseModel):
"""A single step in a scene execution."""
selector: SceneSelector | None = Field(None, description="Select devices by criteria")
group_id: str | None = Field(None, description="Target a specific group")
action: dict[str, Any] = Field(..., description="Action to execute (type + payload)")
delay_ms: int | None = Field(None, description="Delay before next step (milliseconds)")
@model_validator(mode='after')
def validate_selector_or_group(self) -> 'SceneStep':
"""Ensure either selector OR group_id is specified, but not both."""
has_selector = self.selector is not None
has_group = self.group_id is not None
if not has_selector and not has_group:
raise ValueError("SceneStep must have either 'selector' or 'group_id'")
if has_selector and has_group:
raise ValueError("SceneStep cannot have both 'selector' and 'group_id'")
return self
class SceneConfig(BaseModel):
"""Configuration for a scene."""
id: str = Field(..., description="Unique scene identifier")
name: str = Field(..., description="Human-readable scene name")
steps: list[SceneStep] = Field(..., description="Ordered list of actions")
class ScenesConfigRoot(BaseModel):
"""Root configuration for scenes.yaml."""
version: int = Field(..., description="Configuration schema version")
scenes: list[SceneConfig] = Field(default_factory=list, description="List of scenes")
@field_validator('scenes')
@classmethod
def validate_unique_ids(cls, scenes: list[SceneConfig]) -> list[SceneConfig]:
"""Ensure all scene IDs are unique."""
ids = [s.id for s in scenes]
duplicates = [id for id in ids if ids.count(id) > 1]
if duplicates:
raise ValueError(f"Duplicate scene IDs found: {set(duplicates)}")
return scenes
# ============================================================================
# LOADER FUNCTIONS
# ============================================================================
def load_groups(path: Path | str) -> GroupsConfigRoot:
"""
Load and validate groups configuration from YAML file.
Args:
path: Path to groups.yaml file
Returns:
Validated GroupsConfigRoot object
Raises:
FileNotFoundError: If config file doesn't exist
ValidationError: If configuration is invalid
ValueError: If duplicate group IDs are found or YAML is empty
"""
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Groups config file not found: {path}")
with open(path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
if data is None:
raise ValueError(f"Groups config file is empty: {path}")
return GroupsConfigRoot.model_validate(data)
def load_scenes(path: Path | str) -> ScenesConfigRoot:
"""
Load and validate scenes configuration from YAML file.
Args:
path: Path to scenes.yaml file
Returns:
Validated ScenesConfigRoot object
Raises:
FileNotFoundError: If config file doesn't exist
ValidationError: If configuration is invalid
ValueError: If duplicate scene IDs, invalid steps, or empty YAML are found
"""
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Scenes config file not found: {path}")
with open(path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
if data is None:
raise ValueError(f"Scenes config file is empty: {path}")
return ScenesConfigRoot.model_validate(data)
# ============================================================================
# CONVENIENCE FUNCTIONS
# ============================================================================
def get_group_by_id(config: GroupsConfigRoot, group_id: str) -> GroupConfig | None:
"""Find a group by its ID."""
for group in config.groups:
if group.id == group_id:
return group
return None
def get_scene_by_id(config: ScenesConfigRoot, scene_id: str) -> SceneConfig | None:
"""Find a scene by its ID."""
for scene in config.scenes:
if scene.id == scene_id:
return scene
return None
# ============================================================================
# EXAMPLE USAGE
# ============================================================================
if __name__ == "__main__":
from pathlib import Path
# Example: Load groups configuration
try:
groups_path = Path(__file__).parent.parent / "config" / "groups.yaml"
groups = load_groups(groups_path)
print(f"✓ Loaded {len(groups.groups)} groups (version {groups.version})")
for group in groups.groups:
print(f" - {group.id}: {group.name}")
if group.selector:
print(f" Selector: type={group.selector.type}, room={group.selector.room}")
if group.device_ids:
print(f" Devices: {', '.join(group.device_ids)}")
except Exception as e:
print(f"✗ Error loading groups: {e}")
print()
# Example: Load scenes configuration
try:
scenes_path = Path(__file__).parent.parent / "config" / "scenes.yaml"
scenes = load_scenes(scenes_path)
print(f"✓ Loaded {len(scenes.scenes)} scenes (version {scenes.version})")
for scene in scenes.scenes:
print(f" - {scene.id}: {scene.name} ({len(scene.steps)} steps)")
for i, step in enumerate(scene.steps, 1):
if step.selector:
print(f" Step {i}: selector type={step.selector.type}")
elif step.group_id:
print(f" Step {i}: group_id={step.group_id}")
print(f" Action: {step.action}")
except Exception as e:
print(f"✗ Error loading scenes: {e}")

View File

@@ -0,0 +1,21 @@
"""
Relay capability model.
A relay is essentially a simple on/off switch, like a light with only power control.
"""
from pydantic import BaseModel, Field
from typing import Literal
# Capability version
CAP_VERSION = "relay@1.0.0"
DISPLAY_NAME = "Relay"
class RelayState(BaseModel):
"""State model for relay devices (on/off only)"""
power: Literal["on", "off"] = Field(..., description="Power state: on or off")
class RelaySetPayload(BaseModel):
"""Payload for setting relay state"""
power: Literal["on", "off"] = Field(..., description="Desired power state: on or off")

View File

@@ -0,0 +1,37 @@
"""
Temperature & Humidity Sensor Capability - temp_humidity_sensor@1.0.0
Read-only sensor for temperature and humidity measurements.
"""
from datetime import datetime
from typing import Annotated
from pydantic import BaseModel, Field
class TempHumidityState(BaseModel):
"""
State model for temperature & humidity sensors.
Required fields:
- temperature: Temperature in degrees Celsius
- humidity: Relative humidity in percent
Optional fields:
- battery: Battery level 0-100%
- linkquality: Signal quality indicator
- voltage: Battery voltage in mV
- ts: Timestamp of measurement
"""
temperature: float = Field(..., description="Temperature in degrees Celsius")
humidity: float = Field(..., description="Relative humidity in percent (0-100)")
battery: Annotated[int, Field(ge=0, le=100)] | None = None
linkquality: int | None = None
voltage: int | None = None
ts: datetime | None = None
# Capability metadata
CAP_VERSION = "temp_humidity_sensor@1.0.0"
DISPLAY_NAME = "Temperature & Humidity Sensor"