Compare commits

...

123 Commits

Author SHA1 Message Date
b99158fd25 regallicht flur 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 22:14:59 +01:00
d86e7eecc9 regallicht flur
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 22:13:48 +01:00
8ab9db796c regallicht kueche 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-12 22:08:06 +01:00
a2ddcf7de2 regallicht kueche
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-12 22:01:29 +01:00
3cc3683e8c group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:53:18 +01:00
e0810c72ea group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:50:11 +01:00
3c1253da08 group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:40:02 +01:00
0efb6fab02 group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:30:50 +01:00
a48d189f85 group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:14:20 +01:00
40c3faa128 loglevel
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 13:53:00 +01:00
5cca44638c aid in homekit 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 12:16:51 +01:00
fb2eef2a42 aid in homekit
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 11:51:54 +01:00
0a2007ee65 config file loading 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 11:40:04 +01:00
bdb25e3550 config file loading
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 11:37:00 +01:00
6c284fa1f6 add homekit_aid and load it
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 10:32:53 +01:00
5346d1b72c licht flur haustuer
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-11 09:29:38 +01:00
d8780b1790 herdlicht 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-10 21:41:35 +01:00
3d5010b4a1 herdlicht
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-10 21:40:13 +01:00
b471ab5edc hottis wifi relay 4
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-10 21:26:19 +01:00
3e0a1b49ab hottis wifi relay 3
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-10 21:21:29 +01:00
befdc8a46c hottis wifi relay 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-10 21:15:49 +01:00
da16c59238 hottis wifi relay
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-10 21:13:00 +01:00
5f3185894d licht keller flur 3
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-10 20:58:45 +01:00
fb828c9a2c licht keller flur 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-10 20:50:34 +01:00
064ee6bbed licht keller flur
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-10 20:47:40 +01:00
d39bcfce26 excluded 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 17:38:46 +01:00
1fd275186a excluded
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 17:28:32 +01:00
da370c9050 room id
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 17:13:51 +01:00
08294ca294 started 4
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 13:34:41 +01:00
e5eb368dca started 3 2025-12-09 13:00:47 +01:00
169d0505cb started 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 12:53:53 +01:00
02a2be92d5 started
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 12:34:05 +01:00
bcfc967460 Hottis PV Modbus sensor 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 12:01:47 +01:00
bd1f3bc8c9 Hottis PV Modbus sensor
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 11:57:49 +01:00
f9df70cf68 Hottis PV Modbus transformation
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 11:17:02 +01:00
5364b855aa add vendor hottis wago modbus 3
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 16:57:57 +01:00
3a1841a8a9 add vendor hottis wago modbus
Some checks failed
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/namespace 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/build/7 Pipeline failed
ci/woodpecker/tag/config unknown status
ci/woodpecker/tag/deploy/5 unknown status
ci/woodpecker/tag/deploy/1 unknown status
ci/woodpecker/tag/deploy/4 unknown status
ci/woodpecker/tag/deploy/3 unknown status
ci/woodpecker/tag/deploy/2 unknown status
ci/woodpecker/tag/deploy/6 unknown status
ci/woodpecker/tag/ingress unknown status
2025-12-08 16:57:18 +01:00
9629850ebb vendor transformations separated 2
All checks were successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 16:48:23 +01:00
000d32b78f vendor transformations separated
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 16:43:17 +01:00
24b2f70caf better stopping
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 16:20:25 +01:00
d3c1ec404a seems to work, client_id with uuid
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 15:42:53 +01:00
9ba478c34d seems to work
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 15:37:03 +01:00
15e132b187 messages fix 3 2025-12-08 14:27:50 +01:00
f40887ec37 messages fix 2 2025-12-08 14:27:25 +01:00
507f6f3854 messages fix 2025-12-08 14:25:31 +01:00
f163bb09bf initial 2025-12-08 13:56:48 +01:00
54fdcc12e1 deckenlampe wohnzimmer
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 13:19:00 +01:00
9f725c4c70 homekit names 3
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 11:47:39 +01:00
f1dbd9344d homekit names 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-08 11:36:17 +01:00
5a67d7b330 homekit names
Some checks failed
ci/woodpecker/tag/config unknown status
ci/woodpecker/tag/namespace Pipeline is pending
ci/woodpecker/tag/build/5 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/build/6 Pipeline failed
ci/woodpecker/tag/deploy/1 unknown status
ci/woodpecker/tag/deploy/2 unknown status
ci/woodpecker/tag/deploy/3 unknown status
ci/woodpecker/tag/deploy/4 unknown status
ci/woodpecker/tag/deploy/5 unknown status
ci/woodpecker/tag/ingress unknown status
2025-12-08 11:20:27 +01:00
cc342245f8 gartenlicht vorne
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 10:48:23 +01:00
50253d536d more lights 6
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 10:28:30 +01:00
e0aa50c9d2 more lights 5
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 09:22:38 +01:00
dc20d9f4b2 more lights 4
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-08 09:15:32 +01:00
ffb35928b4 more lights 3
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-07 22:12:23 +01:00
ac84ff7103 more lights 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-07 21:49:34 +01:00
c185494da3 more lights
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-07 21:44:57 +01:00
ec4a37a268 tasmoto and kommode
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-07 21:32:36 +01:00
d4b1d27b81 accessory name in logging
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-07 21:19:41 +01:00
ad07bc79e2 kugellampe patty
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-07 19:14:32 +01:00
ab41e79cb2 car outlet adjusted 8
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-05 15:53:06 +01:00
fe92d336b1 car outlet adjusted 7
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-05 15:47:54 +01:00
0ca59896ad car outlet adjusted 6
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-05 15:46:25 +01:00
7858996d0f car outlet adjusted 5
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 15:42:41 +01:00
a0f7cc7bd9 car outlet adjusted 4
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 15:25:50 +01:00
a98802437c car outlet adjusted 3
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 15:11:18 +01:00
708e287016 car outlet adjusted 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 13:59:42 +01:00
d11eab8474 car outlet adjusted
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-05 13:49:12 +01:00
eccffbbd55 use image from registry 2025-12-03 22:38:26 +01:00
2b963a33ef build homekit too
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-03 22:32:42 +01:00
1311f7a59b Putzlicht 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 16:53:17 +01:00
a226fa9268 Putzlicht
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 16:26:38 +01:00
3bd8d293a2 fix licht spuele 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 15:46:43 +01:00
be30ad3a3c fix licht spuele 2025-12-01 15:45:12 +01:00
500384b1cd streamline ci 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-01 14:56:56 +01:00
6b4c247413 forgotten files
All checks were successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-01 14:53:50 +01:00
04a1807306 streamline ci
Some checks failed
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/build/1 Pipeline failed
2025-12-01 14:52:50 +01:00
db5e4589d0 fix error for devices with missing state 2025-12-01 14:48:32 +01:00
5399f044a1 separation of ui and static
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
6
2025-12-01 14:24:15 +01:00
16fa5143dd separation of ui and static
All checks were successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
5
2025-12-01 14:15:54 +01:00
cff154c247 separation of ui and static
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
4
2025-12-01 14:11:25 +01:00
038664ec94 separation of ui and static
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
3
2025-12-01 14:06:54 +01:00
2bbf825cf7 separation of ui and static
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2
2025-12-01 14:02:37 +01:00
5e0159047c separation of ui and static
Some checks failed
ci/woodpecker/tag/build/5 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/deploy/2 unknown status
ci/woodpecker/tag/deploy/1 unknown status
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/deploy/4 unknown status
ci/woodpecker/tag/deploy/3 unknown status
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/deploy/5 unknown status
ci/woodpecker/tag/ingress unknown status
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/4 Pipeline failed
2025-12-01 14:00:48 +01:00
b23b624a86 homekit bridge name 2025-12-01 12:45:01 +01:00
9c099e44af drop homekit bridge build script 2025-12-01 11:06:49 +01:00
9c17a73605 build homekit-bridge image 2 2025-12-01 10:54:57 +01:00
a389edcd87 build homekit-bridge image 2025-12-01 10:53:33 +01:00
17c9bca8d1 forgotten file 2
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-01 10:43:33 +01:00
c4fc21d760 forgotten file 2025-12-01 10:42:58 +01:00
e902d221ea fix configMap
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-12-01 10:39:16 +01:00
e19bffc90c ci fix
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-12-01 10:21:52 +01:00
5a13183123 homekit
All checks were successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
2025-11-30 21:56:52 +01:00
deb26c4945 homekit dockerfile
All checks were successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
2025-11-30 20:15:34 +01:00
c0e3ac1fe0 icons 3
All checks were successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
2025-11-30 18:17:47 +01:00
370c16eb42 icons 2
All checks were successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
2025-11-30 18:10:55 +01:00
fd1d5c4f31 icons
All checks were successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
2025-11-30 18:01:12 +01:00
51072424ed Apple Touch Icons added 3
All checks were successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-30 17:03:14 +01:00
722f4f0a8c Apple Touch Icons added 2
All checks were successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
2025-11-30 16:58:08 +01:00
0acabc737e Apple Touch Icons added
All checks were successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
2025-11-30 16:54:47 +01:00
34b0cdef69 encrypted client certificates
All checks were successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
2025-11-30 16:24:21 +01:00
68ca51a242 certs scripts 2 2025-11-30 16:06:01 +01:00
6d0f38965d certs scripts 2025-11-30 16:05:41 +01:00
1078e4cd53 password for client cert 2025-11-30 15:59:57 +01:00
0c2f3f2e83 new mtls approach 4
All checks were successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
2025-11-29 23:09:03 +01:00
418f813e80 new mtls approach 3
All checks were successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
2025-11-29 23:07:32 +01:00
2b2fd92923 new mtls approach 2
Some checks failed
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/predeploy Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/deploy/1 unknown status
ci/woodpecker/push/deploy/2 unknown status
ci/woodpecker/push/deploy/3 unknown status
ci/woodpecker/push/deploy/4 unknown status
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-29 22:58:40 +01:00
8fa81be750 new mtls approach
All checks were successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
2025-11-29 22:55:42 +01:00
205baa7e01 mtls fix 3
Some checks failed
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/deploy/2 unknown status
ci/woodpecker/push/deploy/3 unknown status
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/deploy/1 unknown status
ci/woodpecker/push/deploy/4 unknown status
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
2025-11-29 22:19:12 +01:00
f3f9238d5f mtls fix 2
Some checks failed
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/predeploy Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/deploy/1 unknown status
ci/woodpecker/push/deploy/2 unknown status
ci/woodpecker/push/deploy/3 unknown status
ci/woodpecker/push/deploy/4 unknown status
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-29 22:02:11 +01:00
5decf79bee mTLS 2
Some checks failed
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/deploy/1 unknown status
ci/woodpecker/push/deploy/2 unknown status
ci/woodpecker/push/deploy/3 unknown status
ci/woodpecker/push/deploy/4 unknown status
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-29 21:41:50 +01:00
be2654ac98 ignore ca
Some checks failed
ci/woodpecker/push/predeploy Pipeline is pending
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/deploy/3 unknown status
ci/woodpecker/push/deploy/2 unknown status
ci/woodpecker/push/deploy/1 unknown status
ci/woodpecker/push/deploy/4 unknown status
ci/woodpecker/push/build/2 Pipeline failed
2025-11-29 21:38:20 +01:00
bb27296310 mTLS
All checks were successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
2025-11-29 21:35:35 +01:00
63857671f9 add ingress cors fix
Some checks failed
ci/woodpecker/push/deploy/2 Pipeline is pending
ci/woodpecker/push/deploy/3 Pipeline is pending
ci/woodpecker/push/deploy/4 Pipeline is pending
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-29 21:05:54 +01:00
d008c9fd5a add ingress 2
All checks were successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
2025-11-29 21:02:05 +01:00
1eb0f84659 add ingress
Some checks failed
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline failed
ci/woodpecker/push/deploy/3 Pipeline failed
ci/woodpecker/push/deploy/4 Pipeline failed
ci/woodpecker/push/deploy/2 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
2025-11-29 20:57:49 +01:00
51df63d9f2 config file fix
All checks were successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-29 20:49:48 +01:00
cdaa5deb58 load redis and mqtt only from env
All checks were successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
2025-11-29 20:44:17 +01:00
91ef285a6c fix cluster config
Some checks failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/predeploy Pipeline failed
ci/woodpecker/push/deploy/1 unknown status
ci/woodpecker/push/deploy/2 unknown status
ci/woodpecker/push/deploy/3 unknown status
ci/woodpecker/push/deploy/4 unknown status
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
2025-11-29 20:29:00 +01:00
9afa68a111 deployment 2
All checks were successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-29 20:22:40 +01:00
1119bb529f deployment
Some checks failed
ci/woodpecker/push/deploy/1 unknown status
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/predeploy Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/deploy/4 unknown status
ci/woodpecker/push/deploy/3 unknown status
ci/woodpecker/push/deploy/2 unknown status
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline failed
ci/woodpecker/tag/deploy/1 Pipeline failed
ci/woodpecker/tag/deploy/2 Pipeline failed
ci/woodpecker/tag/deploy/3 Pipeline failed
2025-11-29 20:19:45 +01:00
26286ce194 ci debug 4
All checks were successful
ci/woodpecker/push/predeploy Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/deploy/1 Pipeline was successful
ci/woodpecker/push/deploy/2 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/push/deploy/4 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/push/deploy/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-29 19:56:31 +01:00
7913a0044d ci debug 3
Some checks failed
ci/woodpecker/push/deploy/1 unknown status
ci/woodpecker/push/predeploy Pipeline failed
ci/woodpecker/push/build/2 Pipeline failed
ci/woodpecker/push/build/1 Pipeline failed
ci/woodpecker/push/build/4 Pipeline failed
ci/woodpecker/push/build/3 Pipeline failed
ci/woodpecker/push/deploy/4 unknown status
ci/woodpecker/push/deploy/3 unknown status
ci/woodpecker/push/deploy/2 unknown status
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
2025-11-29 19:39:26 +01:00
112 changed files with 4081 additions and 3088 deletions

4
.gitignore vendored
View File

@@ -64,3 +64,7 @@ poetry.lock
apps/homekit/homekit.state
tools/ca/
tools/clients/
tools/certificates/
tools/certificates.tgz

View File

@@ -1,9 +1,18 @@
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
matrix:
APP:
- ui
- api
- abstraction
- rules
- static
- pulsegen
- homekit
steps:
build-${APP}:
@@ -18,8 +27,3 @@ steps:
repo: ${FORGE_NAME}/${CI_REPO}/${APP}
auto_tag: true
dockerfile: apps/${APP}/Dockerfile
when:
event: [push, tag]
ref:
exclude:
- refs/tags/*-configchange

26
.woodpecker/config.yml Normal file
View File

@@ -0,0 +1,26 @@
when:
event: [tag]
depends_on:
- namespace
steps:
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.yaml=config/devices.yaml
--from-file=groups.yaml=config/groups.yaml
--from-file=layout.yaml=config/layout.yaml
--from-file=rules.yaml=config/rules.yaml
--from-file=scenes.yaml=config/scenes.yaml
--namespace=$NAMESPACE
--dry-run=client -o yaml | kubectl apply -f -
- kubectl apply -f deployment/configmap.yaml -n $NAMESPACE

View File

@@ -1,12 +1,25 @@
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
depends_on:
- build
- namespace
- config
matrix:
APP:
- ui
- api
- abstraction
- rules
- static
- pulsegen
steps:
deploy-${A}:
deploy-${APP}:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment:
KUBE_CONFIG_CONTENT:
@@ -16,13 +29,7 @@ steps:
commands:
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- echo "Deploying application ${APP} (${IMAGE}) to namespace ${NAMESPACE}"
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
- echo "Deploying application ${APP} ($IMAGE) to namespace $NAMESPACE"
- cat deployment/${APP}-deployment.yaml | sed "s,%IMAGE%,$IMAGE,g" | kubectl apply -n $NAMESPACE -f -
depends_on:
- build
- predeploy

21
.woodpecker/ingress.yml Normal file
View File

@@ -0,0 +1,21 @@
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
depends_on:
- deploy
steps:
apply_ingress:
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 apply -f deployment/ingress.yaml -n $NAMESPACE

15
.woodpecker/namespace.yml Normal file
View File

@@ -0,0 +1,15 @@
when:
event: [tag]
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"

View File

@@ -1,37 +0,0 @@
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

@@ -10,7 +10,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
MQTT_PORT=1883 \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_DB=0
REDIS_DB=0 \
REDIS_CHANNEL=ui:updates
# Create non-root user
RUN addgroup -g 10001 -S app && \

View File

@@ -180,17 +180,10 @@ async def handle_abstract_set(
# Transform abstract payload to vendor-specific format
vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload)
# For MAX! thermostats and Shelly relays, vendor_payload is a plain string
# For other devices, it's a dict that needs JSON encoding
if (device_technology == "max" and device_type == "thermostat") or \
(device_technology == "shelly" and device_type == "relay"):
vendor_message = vendor_payload # Already a string
else:
vendor_message = json.dumps(vendor_payload)
logger.info(f"→ vendor SET {device_id}: {vendor_topic}{vendor_message}")
await mqtt_client.publish(vendor_topic, vendor_message, qos=1)
logger.info(f"→ vendor SET {device_id}: {vendor_topic}{vendor_payload}")
logger.debug(f"MQTT message published on {vendor_topic}: {vendor_payload}")
await mqtt_client.publish(vendor_topic, vendor_payload, qos=1)
async def handle_vendor_state(
@@ -391,9 +384,19 @@ async def async_main() -> None:
validate_devices(devices)
logger.info(f"Loaded {len(devices)} device(s) from configuration")
# Get Redis URL from config or environment variable or use default
redis_config = config.get("redis", {})
redis_url = redis_config.get("url") or os.environ.get("REDIS_URL", "redis://localhost:6379/0")
# Build Redis URL from environment variables or config or use default
redis_host = os.environ.get("REDIS_HOST")
redis_port = os.environ.get("REDIS_PORT")
redis_db = os.environ.get("REDIS_DB")
if redis_host and redis_port and redis_db:
redis_url = f"redis://{redis_host}:{redis_port}/{redis_db}"
logger.info(f"Using Redis from environment variables: {redis_url}")
else:
# Fallback to config file
redis_config = config.get("redis", {})
redis_url = redis_config.get("url") or "redis://localhost:6379/0"
logger.info(f"Using Redis from config file: {redis_url}")
# Connect to Redis with retry
redis_client = await get_redis_client(redis_url)

View File

@@ -4,588 +4,52 @@ This module implements a registry-pattern for vendor-specific transformations:
- Each (device_type, technology, direction) tuple maps to a specific handler function
- Handlers transform payloads between abstract and vendor-specific formats
- Unknown combinations fall back to pass-through (no transformation)
Vendor-specific implementations are in the vendors/ subdirectory.
"""
import logging
import json
from typing import Any, Callable
from apps.abstraction.vendors import (
simulator,
zigbee2mqtt,
max,
shelly,
tasmota,
hottis_pv_modbus,
hottis_wago_modbus,
hottis_wifi_relay,
hottis_led_stripe
)
logger = logging.getLogger(__name__)
# ============================================================================
# HANDLER FUNCTIONS: simulator technology
# ============================================================================
def _transform_light_simulator_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract light payload to simulator format.
Simulator uses same format as abstract protocol (no transformation needed).
"""
return payload
def _transform_light_simulator_to_abstract(payload: str) -> dict[str, Any]:
"""Transform simulator light payload to abstract format.
Simulator uses same format as abstract protocol (no transformation needed).
"""
payload = json.loads(payload)
return payload
def _transform_thermostat_simulator_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract thermostat payload to simulator format.
Simulator uses same format as abstract protocol (no transformation needed).
"""
return payload
def _transform_thermostat_simulator_to_abstract(payload: str) -> dict[str, Any]:
"""Transform simulator thermostat payload to abstract format.
Simulator uses same format as abstract protocol (no transformation needed).
"""
payload = json.loads(payload)
return payload
# ============================================================================
# HANDLER FUNCTIONS: zigbee2mqtt technology
# ============================================================================
def _transform_light_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract light payload to zigbee2mqtt format.
Transformations:
- power: 'on'/'off' -> state: 'ON'/'OFF'
- brightness: 0-100 -> brightness: 0-254
Example:
- Abstract: {'power': 'on', 'brightness': 100}
- zigbee2mqtt: {'state': 'ON', 'brightness': 254}
"""
vendor_payload = payload.copy()
# Transform power -> state with uppercase values
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
# Transform brightness: 0-100 (%) -> 0-254 (zigbee2mqtt range)
if "brightness" in vendor_payload:
abstract_brightness = vendor_payload["brightness"]
if isinstance(abstract_brightness, (int, float)):
# Convert percentage (0-100) to zigbee2mqtt range (0-254)
vendor_payload["brightness"] = round(abstract_brightness * 254 / 100)
return vendor_payload
def _transform_light_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt light payload to abstract format.
Transformations:
- state: 'ON'/'OFF' -> power: 'on'/'off'
- brightness: 0-254 -> brightness: 0-100
Example:
- zigbee2mqtt: {'state': 'ON', 'brightness': 254}
- Abstract: {'power': 'on', 'brightness': 100}
"""
abstract_payload = json.loads(payload)
# Transform state -> power with lowercase values
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
# Transform brightness: 0-254 (zigbee2mqtt range) -> 0-100 (%)
if "brightness" in abstract_payload:
vendor_brightness = abstract_payload["brightness"]
if isinstance(vendor_brightness, (int, float)):
# Convert zigbee2mqtt range (0-254) to percentage (0-100)
abstract_payload["brightness"] = round(vendor_brightness * 100 / 254)
return abstract_payload
def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract thermostat payload to zigbee2mqtt format.
Transformations:
- target -> current_heating_setpoint (as string)
- mode is ignored (zigbee2mqtt thermostats use system_mode in state only)
Example:
- Abstract: {'target': 22.0}
- zigbee2mqtt: {'current_heating_setpoint': '22.0'}
"""
vendor_payload = {}
if "target" in payload:
# zigbee2mqtt expects current_heating_setpoint as string
vendor_payload["current_heating_setpoint"] = str(payload["target"])
return vendor_payload
def _transform_thermostat_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt thermostat payload to abstract format.
Transformations:
- current_heating_setpoint -> target (as float)
- local_temperature -> current (as float)
- system_mode -> mode
Example:
- zigbee2mqtt: {'current_heating_setpoint': 15, 'local_temperature': 23, 'system_mode': 'heat'}
- Abstract: {'target': 15.0, 'current': 23.0, 'mode': 'heat'}
"""
payload = json.loads(payload)
abstract_payload = {}
# Extract target temperature
if "current_heating_setpoint" in payload:
setpoint = payload["current_heating_setpoint"]
abstract_payload["target"] = float(setpoint)
# Extract current temperature
if "local_temperature" in payload:
current = payload["local_temperature"]
abstract_payload["current"] = float(current)
# Extract mode
if "system_mode" in payload:
abstract_payload["mode"] = payload["system_mode"]
return abstract_payload
# ============================================================================
# HANDLER FUNCTIONS: contact_sensor - zigbee2mqtt technology
# ============================================================================
def _transform_contact_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract contact sensor payload to zigbee2mqtt format.
Contact sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
logger.warning("Contact sensors are read-only - SET commands should not be used")
return payload
def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt contact sensor payload to abstract format.
Transformations:
- contact: bool -> "open" | "closed"
- zigbee2mqtt semantics: False = OPEN, True = CLOSED (inverted!)
- battery: pass through (already 0-100)
- linkquality: pass through
- device_temperature: pass through (if present)
- voltage: pass through (if present)
Example:
- zigbee2mqtt: {"contact": false, "battery": 100, "linkquality": 87}
- Abstract: {"contact": "open", "battery": 100, "linkquality": 87}
"""
payload = json.loads(payload)
abstract_payload = {}
# Transform contact state (inverted logic!)
if "contact" in payload:
contact_bool = payload["contact"]
# zigbee2mqtt: False = OPEN, True = CLOSED
abstract_payload["contact"] = "closed" if contact_bool else "open"
# Pass through optional fields
if "battery" in payload:
abstract_payload["battery"] = payload["battery"]
if "linkquality" in payload:
abstract_payload["linkquality"] = payload["linkquality"]
if "device_temperature" in payload:
abstract_payload["device_temperature"] = payload["device_temperature"]
if "voltage" in payload:
abstract_payload["voltage"] = payload["voltage"]
return abstract_payload
# ============================================================================
# HANDLER FUNCTIONS: contact_sensor - max technology (Homegear MAX!)
# ============================================================================
def _transform_contact_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract contact sensor payload to MAX! format.
Contact sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
logger.warning("Contact sensors are read-only - SET commands should not be used")
return payload
def _transform_contact_sensor_max_to_abstract(payload: str) -> dict[str, Any]:
"""Transform MAX! (Homegear) contact sensor payload to abstract format.
MAX! sends "true"/"false" (string or bool) on STATE topic.
Transformations:
- "true" or True -> "open" (window/door open)
- "false" or False -> "closed" (window/door closed)
Example:
- MAX!: "true" or True
- Abstract: {"contact": "open"}
"""
try:
contact_value = payload.strip().lower() == "true"
# MAX! semantics: True = OPEN, False = CLOSED
return {
"contact": "open" if contact_value else "closed"
}
except (ValueError, TypeError) as e:
logger.error(f"MAX! contact sensor failed to parse: {payload}, error: {e}")
return {
"contact": "closed" # Default to closed on error
}
# ============================================================================
# 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: relay - hottis_modbus technology
# ============================================================================
def _transform_relay_hottis_modbus_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis Modbus format.
Hottis Modbus expects plain text 'on' or 'off' (not JSON).
- power: 'on'/'off' -> 'on'/'off' (plain string)
Example:
- Abstract: {'power': 'on'}
- Hottis Modbus: 'on'
"""
power = payload.get("power", "off")
return power
def _transform_relay_hottis_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis Modbus relay payload to abstract format.
Hottis Modbus sends plain text 'on' or 'off' (not JSON).
- 'on'/'off' -> power: 'on'/'off'
Example:
- Hottis Modbus: 'on'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip()}
# ============================================================================
# HANDLER FUNCTIONS: three_phase_powermeter - hottis_modbus technology
# ============================================================================
def _transform_three_phase_powermeter_hottis_modbus_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract three_phase_powermeter payload to hottis_modbus format.
energy: float = Field(..., description="Total energy in kWh")
total_power: float = Field(..., description="Total power in W")
phase1_power: float = Field(..., description="Power for phase 1 in W")
phase2_power: float = Field(..., description="Power for phase 2 in W")
phase3_power: float = Field(..., description="Power for phase 3 in W")
phase1_voltage: float = Field(..., description="Voltage for phase 1 in V")
phase2_voltage: float = Field(..., description="Voltage for phase 2 in V")
phase3_voltage: float = Field(..., description="Voltage for phase 3 in V")
phase1_current: float = Field(..., description="Current for phase 1 in A")
phase2_current: float = Field(..., description="Current for phase 2 in A")
phase3_current: float = Field(..., description="Current for phase 3 in A")
"""
vendor_payload = {
"energy": payload.get("energy", 0.0),
"total_power": payload.get("total_power", 0.0),
"phase1_power": payload.get("phase1_power", 0.0),
"phase2_power": payload.get("phase2_power", 0.0),
"phase3_power": payload.get("phase3_power", 0.0),
"phase1_voltage": payload.get("phase1_voltage", 0.0),
"phase2_voltage": payload.get("phase2_voltage", 0.0),
"phase3_voltage": payload.get("phase3_voltage", 0.0),
"phase1_current": payload.get("phase1_current", 0.0),
"phase2_current": payload.get("phase2_current", 0.0),
"phase3_current": payload.get("phase3_current", 0.0),
}
return vendor_payload
def _transform_three_phase_powermeter_hottis_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform hottis_modbus three_phase_powermeter payload to abstract format.
Transformations:
- Direct mapping of all power meter fields
Example:
- hottis_modbus: {'energy': 123.45, 'total_power': 1500.0, 'phase1_power': 500.0, ...}
- Abstract: {'energy': 123.45, 'total_power': 1500.0, 'phase1_power': 500.0, ...}
"""
payload = json.loads(payload)
abstract_payload = {
"energy": payload.get("energy", 0.0),
"total_power": payload.get("total_power", 0.0),
"phase1_power": payload.get("phase1_power", 0.0),
"phase2_power": payload.get("phase2_power", 0.0),
"phase3_power": payload.get("phase3_power", 0.0),
"phase1_voltage": payload.get("phase1_voltage", 0.0),
"phase2_voltage": payload.get("phase2_voltage", 0.0),
"phase3_voltage": payload.get("phase3_voltage", 0.0),
"phase1_current": payload.get("phase1_current", 0.0),
"phase2_current": payload.get("phase2_current", 0.0),
"phase3_current": payload.get("phase3_current", 0.0),
}
return abstract_payload
# ============================================================================
# HANDLER FUNCTIONS: max technology (Homegear MAX!)
# ============================================================================
def _transform_thermostat_max_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract thermostat payload to MAX! (Homegear) format.
MAX! expects only the integer temperature value (no JSON).
Transformations:
- Extract 'target' temperature from payload
- Convert float to integer (MAX! only accepts integers)
- Return as plain string value
Example:
- Abstract: {'mode': 'heat', 'target': 22.5}
- MAX!: "22"
Note: MAX! ignores mode - it's always in heating mode
"""
if "target" not in payload:
logger.warning(f"MAX! thermostat payload missing 'target': {payload}")
return "21" # Default fallback
target_temp = payload["target"]
# Convert to integer (MAX! protocol requirement)
if isinstance(target_temp, (int, float)):
int_temp = int(round(target_temp))
return str(int_temp)
logger.warning(f"MAX! invalid target temperature type: {type(target_temp)}, value: {target_temp}")
return "21"
def _transform_thermostat_max_to_abstract(payload: str) -> dict[str, Any]:
"""Transform MAX! (Homegear) thermostat payload to abstract format.
MAX! sends only the integer temperature value (no JSON).
Transformations:
- Parse plain string/int value
- Convert to float for abstract protocol
- Wrap in abstract payload structure with mode='heat'
Example:
- MAX!: "22" or 22
- Abstract: {'target': 22.0, 'mode': 'heat'}
Note: MAX! doesn't send current temperature via SET_TEMPERATURE topic
"""
# Handle both string and numeric input
target_temp = float(payload.strip())
return {
"target": target_temp,
"mode": "heat" # MAX! is always in heating mode
}
# ============================================================================
# REGISTRY: Maps (device_type, technology, direction) -> handler function
# ============================================================================
TransformHandler = Callable[[dict[str, Any]], dict[str, Any]]
TransformHandler = Callable[[Any], Any]
TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
# Light transformations
("light", "simulator", "to_vendor"): _transform_light_simulator_to_vendor,
("light", "simulator", "to_abstract"): _transform_light_simulator_to_abstract,
("light", "zigbee2mqtt", "to_vendor"): _transform_light_zigbee2mqtt_to_vendor,
("light", "zigbee2mqtt", "to_abstract"): _transform_light_zigbee2mqtt_to_abstract,
# Thermostat transformations
("thermostat", "simulator", "to_vendor"): _transform_thermostat_simulator_to_vendor,
("thermostat", "simulator", "to_abstract"): _transform_thermostat_simulator_to_abstract,
("thermostat", "zigbee2mqtt", "to_vendor"): _transform_thermostat_zigbee2mqtt_to_vendor,
("thermostat", "zigbee2mqtt", "to_abstract"): _transform_thermostat_zigbee2mqtt_to_abstract,
("thermostat", "max", "to_vendor"): _transform_thermostat_max_to_vendor,
("thermostat", "max", "to_abstract"): _transform_thermostat_max_to_abstract,
# Contact sensor transformations (support both 'contact' and 'contact_sensor' types)
("contact_sensor", "zigbee2mqtt", "to_vendor"): _transform_contact_sensor_zigbee2mqtt_to_vendor,
("contact_sensor", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract,
("contact_sensor", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor,
("contact_sensor", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract,
("contact", "zigbee2mqtt", "to_vendor"): _transform_contact_sensor_zigbee2mqtt_to_vendor,
("contact", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract,
("contact", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor,
("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,
("relay", "hottis_modbus", "to_vendor"): _transform_relay_hottis_modbus_to_vendor,
("relay", "hottis_modbus", "to_abstract"): _transform_relay_hottis_modbus_to_abstract,
# Build registry from vendor modules
TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {}
# Three-Phase Powermeter transformations
("three_phase_powermeter", "hottis_modbus", "to_vendor"): _transform_three_phase_powermeter_hottis_modbus_to_vendor,
("three_phase_powermeter", "hottis_modbus", "to_abstract"): _transform_three_phase_powermeter_hottis_modbus_to_abstract,
}
# Register handlers from each vendor module
for vendor_name, vendor_module in [
("simulator", simulator),
("zigbee2mqtt", zigbee2mqtt),
("max", max),
("shelly", shelly),
("tasmota", tasmota),
("hottis_pv_modbus", hottis_pv_modbus),
("hottis_wago_modbus", hottis_wago_modbus),
("hottis_wifi_relay", hottis_wifi_relay),
("hottis_led_stripe", hottis_led_stripe),
]:
for (device_type, direction), handler in vendor_module.HANDLERS.items():
key = (device_type, vendor_name, direction)
TRANSFORM_HANDLERS[key] = handler
def _get_transform_handler(
@@ -624,7 +88,7 @@ def transform_abstract_to_vendor(
device_type: str,
device_technology: str,
abstract_payload: dict[str, Any]
) -> dict[str, Any]:
) -> str:
"""Transform abstract payload to vendor-specific format.
Args:
@@ -633,7 +97,7 @@ def transform_abstract_to_vendor(
abstract_payload: Payload in abstract home protocol format
Returns:
Payload in vendor-specific format
Payload in vendor-specific format (as string)
"""
logger.debug(
f"transform_abstract_to_vendor IN: type={device_type}, tech={device_technology}, "
@@ -660,7 +124,7 @@ def transform_vendor_to_abstract(
Args:
device_type: Type of device (e.g., "light", "thermostat")
device_technology: Technology/vendor (e.g., "simulator", "zigbee2mqtt")
vendor_payload: Payload in vendor-specific format
vendor_payload: Payload in vendor-specific format (as string)
Returns:
Payload in abstract home protocol format

1
apps/abstraction/vendors/__init__.py vendored Normal file
View File

@@ -0,0 +1 @@
"""Vendor-specific transformation modules."""

View File

@@ -0,0 +1,46 @@
"""Hottis LED Stripe vendor transformations."""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_light_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis LED Stripe format.
Hottis LED Stripe expects plain text 'on' or 'off' (not JSON).
Example:
- Abstract: {'power': 'on'}
- Hottis LED Stripe: 'ON'
"""
bri = 89.0 / 254.0
r = int(255 * bri)
g = int(103 * bri)
b = int(25 * bri)
cmd = f"{r} {g} {b}" if payload.get("power", "off").lower() == "on" else "0 0 0"
return cmd
def transform_light_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis LED Stripe relay payload to abstract format.
Hottis LED Stripe sends plain text 'on' or 'off'.
Example:
- Hottis LED Stripe: 'ON'
- Abstract: {'power': 'on'}
"""
power = "on" if payload.strip() != "0 0 0" else "off"
return {"power": power}
# Registry of handlers for this vendor
HANDLERS = {
("light", "to_vendor"): transform_light_to_vendor,
("light", "to_abstract"): transform_light_to_abstract,
}

View File

@@ -0,0 +1,134 @@
"""Hottis PV Modbus vendor transformations."""
import json
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis Modbus format.
Hottis Modbus expects plain text 'on' or 'off'.
Example:
- Abstract: {'power': 'on'}
- Hottis Modbus: 'on'
"""
power = payload.get("power", "off")
return power
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis Modbus relay payload to abstract format.
Hottis Modbus sends plain text 'on' or 'off'.
Example:
- Hottis PV Modbus: 'on'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip()}
def transform_contact_sensor_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract contact sensor payload to format.
Contact sensors are read-only.
"""
logger.warning("Contact sensors are read-only - SET commands should not be used")
return json.dumps(payload)
def transform_contact_sensor_to_abstract(payload: str) -> dict[str, Any]:
"""Transform contact sensor payload to abstract format.
MAX! sends "true"/"false" (string or bool) on STATE topic.
Transformations:
- "true" or True -> "open" (window/door open)
- "false" or False -> "closed" (window/door closed)
Example:
- contact sensor: "off"
- Abstract: {"contact": "open"}
"""
contact_value = payload.strip().lower() == "off"
return {
"contact": "open" if contact_value else "closed"
}
def transform_three_phase_powermeter_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract three_phase_powermeter payload to hottis_pv_modbus format."""
vendor_payload = {
"energy": payload.get("energy", 0.0),
"total_power": payload.get("total_power", 0.0),
"phase1_power": payload.get("phase1_power", 0.0),
"phase2_power": payload.get("phase2_power", 0.0),
"phase3_power": payload.get("phase3_power", 0.0),
"phase1_voltage": payload.get("phase1_voltage", 0.0),
"phase2_voltage": payload.get("phase2_voltage", 0.0),
"phase3_voltage": payload.get("phase3_voltage", 0.0),
"phase1_current": payload.get("phase1_current", 0.0),
"phase2_current": payload.get("phase2_current", 0.0),
"phase3_current": payload.get("phase3_current", 0.0),
}
return json.dumps(vendor_payload)
def transform_three_phase_powermeter_to_abstract(payload: str) -> dict[str, Any]:
"""Transform hottis_pv_modbus three_phase_powermeter payload to abstract format.
Transformations:
- totalImportEnergy -> energy
- powerL1/powerL2/powerL3 -> phase1_power/phase2_power/phase3_power
- voltageL1/voltageL2/voltageL3 -> phase1_voltage/phase2_voltage/phase3_voltage
- currentL1/currentL2/currentL3 -> phase1_current/phase2_current/phase3_current
- Sum of powerL1..3 -> total_power
"""
data = json.loads(payload)
def _get_float(key: str, default: float = 0.0) -> float:
return float(data.get(key, default))
phase1_power = _get_float("powerL1")
phase2_power = _get_float("powerL2")
phase3_power = _get_float("powerL3")
phase1_voltage = _get_float("voltageL1")
phase2_voltage = _get_float("voltageL2")
phase3_voltage = _get_float("voltageL3")
phase1_current = _get_float("currentL1")
phase2_current = _get_float("currentL2")
phase3_current = _get_float("currentL3")
energy = _get_float("totalImportEnergy")
return {
"energy": energy,
"total_power": phase1_power + phase2_power + phase3_power,
"phase1_power": phase1_power,
"phase2_power": phase2_power,
"phase3_power": phase3_power,
"phase1_voltage": phase1_voltage,
"phase2_voltage": phase2_voltage,
"phase3_voltage": phase3_voltage,
"phase1_current": phase1_current,
"phase2_current": phase2_current,
"phase3_current": phase3_current,
}
# Registry of handlers for this vendor
HANDLERS = {
("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract,
("three_phase_powermeter", "to_vendor"): transform_three_phase_powermeter_to_vendor,
("three_phase_powermeter", "to_abstract"): transform_three_phase_powermeter_to_abstract,
("contact_sensor", "to_vendor"): transform_contact_sensor_to_vendor,
("contact_sensor", "to_abstract"): transform_contact_sensor_to_abstract,
("contact", "to_vendor"): transform_contact_sensor_to_vendor,
("contact", "to_abstract"): transform_contact_sensor_to_abstract,
}

View File

@@ -0,0 +1,58 @@
"""Hottis Wago Modbus vendor transformations."""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis Wago Modbus format.
Hottis Wago Modbus expects plain text 'true' or 'false' (not JSON).
Example:
- Abstract: {'power': 'on'}
- Hottis Wago Modbus: 'true' or 'false'
"""
power = payload.get("power", "off")
# Map abstract "on"/"off" to vendor "true"/"false"
if isinstance(power, str):
power_lower = power.lower()
if power_lower in {"on", "true", "1"}:
return "true"
if power_lower in {"off", "false", "0"}:
return "false"
# Fallback: any truthy value -> "true", else "false"
return "true" if power else "false"
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis Wago Modbus relay payload to abstract format.
Hottis Wago Modbus sends plain text 'true' or 'false'.
Example:
- Hottis Wago Modbus: 'true'
- Abstract: {'power': 'on'}
"""
value = payload.strip().lower()
if value == "true":
power = "on"
elif value == "false":
power = "off"
else:
# Fallback for unexpected values: keep as-is
logger.warning("Unexpected relay payload from Hottis Wago Modbus: %r", payload)
power = value
return {"power": power}
# Registry of handlers for this vendor
HANDLERS = {
("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract,
}

View File

@@ -0,0 +1,38 @@
"""Hottis WiFi Relay vendor transformations."""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis WiFi Relay format.
Hottis WiFi Relay expects plain text 'on' or 'off' (not JSON).
Example:
- Abstract: {'power': 'on'}
- Hottis WiFi Relay: 'ON'
"""
power = payload.get("power", "off").upper()
return power
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis WiFi Relay relay payload to abstract format.
Hottis WiFi Relay sends plain text 'on' or 'off'.
Example:
- Hottis WiFi Relay: 'ON'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip().lower()}
# Registry of handlers for this vendor
HANDLERS = {
("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract,
}

95
apps/abstraction/vendors/max.py vendored Normal file
View File

@@ -0,0 +1,95 @@
"""MAX! (Homegear) vendor transformations."""
import json
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_contact_sensor_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract contact sensor payload to MAX! format.
Contact sensors are read-only.
"""
logger.warning("Contact sensors are read-only - SET commands should not be used")
return json.dumps(payload)
def transform_contact_sensor_to_abstract(payload: str) -> dict[str, Any]:
"""Transform MAX! contact sensor payload to abstract format.
MAX! sends "true"/"false" (string or bool) on STATE topic.
Transformations:
- "true" or True -> "open" (window/door open)
- "false" or False -> "closed" (window/door closed)
Example:
- MAX!: "true"
- Abstract: {"contact": "open"}
"""
try:
contact_value = payload.strip().lower() == "true"
return {
"contact": "open" if contact_value else "closed"
}
except (ValueError, TypeError) as e:
logger.error(f"MAX! contact sensor failed to parse: {payload}, error: {e}")
return {"contact": "closed"}
def transform_thermostat_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract thermostat payload to MAX! format.
MAX! expects only the integer temperature value (no JSON).
Transformations:
- Extract 'target' temperature from payload
- Convert float to integer
- Return as plain string value
Example:
- Abstract: {'target': 22.5}
- MAX!: "22"
"""
if "target" not in payload:
logger.warning(f"MAX! thermostat payload missing 'target': {payload}")
return "21"
target_temp = payload["target"]
if isinstance(target_temp, (int, float)):
int_temp = int(round(target_temp))
return str(int_temp)
logger.warning(f"MAX! invalid target temperature type: {type(target_temp)}")
return "21"
def transform_thermostat_to_abstract(payload: str) -> dict[str, Any]:
"""Transform MAX! thermostat payload to abstract format.
MAX! sends only the integer temperature value (no JSON).
Example:
- MAX!: "22"
- Abstract: {'target': 22.0, 'mode': 'heat'}
"""
target_temp = float(payload.strip())
return {
"target": target_temp,
"mode": "heat"
}
# Registry of handlers for this vendor
HANDLERS = {
("contact_sensor", "to_vendor"): transform_contact_sensor_to_vendor,
("contact_sensor", "to_abstract"): transform_contact_sensor_to_abstract,
("contact", "to_vendor"): transform_contact_sensor_to_vendor,
("contact", "to_abstract"): transform_contact_sensor_to_abstract,
("thermostat", "to_vendor"): transform_thermostat_to_vendor,
("thermostat", "to_abstract"): transform_thermostat_to_abstract,
}

38
apps/abstraction/vendors/shelly.py vendored Normal file
View File

@@ -0,0 +1,38 @@
"""Shelly vendor transformations."""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Shelly format.
Shelly expects plain text 'on' or 'off' (not JSON).
Example:
- Abstract: {'power': 'on'}
- Shelly: 'on'
"""
power = payload.get("power", "off")
return power
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Shelly relay payload to abstract format.
Shelly sends plain text 'on' or 'off'.
Example:
- Shelly: 'on'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip()}
# Registry of handlers for this vendor
HANDLERS = {
("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract,
}

50
apps/abstraction/vendors/simulator.py vendored Normal file
View File

@@ -0,0 +1,50 @@
"""Simulator vendor transformations."""
import json
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_light_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract light payload to simulator format.
Simulator uses same format as abstract protocol (no transformation needed).
"""
return json.dumps(payload)
def transform_light_to_abstract(payload: str) -> dict[str, Any]:
"""Transform simulator light payload to abstract format.
Simulator uses same format as abstract protocol (no transformation needed).
"""
payload = json.loads(payload)
return payload
def transform_thermostat_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract thermostat payload to simulator format.
Simulator uses same format as abstract protocol (no transformation needed).
"""
return json.dumps(payload)
def transform_thermostat_to_abstract(payload: str) -> dict[str, Any]:
"""Transform simulator thermostat payload to abstract format.
Simulator uses same format as abstract protocol (no transformation needed).
"""
payload = json.loads(payload)
return payload
# Registry of handlers for this vendor
HANDLERS = {
("light", "to_vendor"): transform_light_to_vendor,
("light", "to_abstract"): transform_light_to_abstract,
("thermostat", "to_vendor"): transform_thermostat_to_vendor,
("thermostat", "to_abstract"): transform_thermostat_to_abstract,
}

38
apps/abstraction/vendors/tasmota.py vendored Normal file
View File

@@ -0,0 +1,38 @@
"""Tasmota vendor transformations."""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Tasmota format.
Tasmota expects plain text 'on' or 'off' (not JSON).
Example:
- Abstract: {'power': 'on'}
- Tasmota: 'on'
"""
power = payload.get("power", "off")
return power
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Tasmota relay payload to abstract format.
Tasmota sends plain text 'ON' or 'OFF'.
Example:
- Tasmota: 'ON'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip().lower()}
# Registry of handlers for this vendor
HANDLERS = {
("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract,
}

209
apps/abstraction/vendors/zigbee2mqtt.py vendored Normal file
View File

@@ -0,0 +1,209 @@
"""Zigbee2MQTT vendor transformations."""
import json
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_light_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract light payload to zigbee2mqtt format.
Transformations:
- power: 'on'/'off' -> state: 'ON'/'OFF'
- brightness: 0-100 -> brightness: 0-254
Example:
- Abstract: {'power': 'on', 'brightness': 100}
- zigbee2mqtt: {'state': 'ON', 'brightness': 254}
"""
vendor_payload = payload.copy()
# Transform power -> state with uppercase values
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
# Transform brightness: 0-100 (%) -> 0-254 (zigbee2mqtt range)
if "brightness" in vendor_payload:
abstract_brightness = vendor_payload["brightness"]
if isinstance(abstract_brightness, (int, float)):
vendor_payload["brightness"] = round(abstract_brightness * 254 / 100)
return json.dumps(vendor_payload)
def transform_light_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt light payload to abstract format.
Transformations:
- state: 'ON'/'OFF' -> power: 'on'/'off'
- brightness: 0-254 -> brightness: 0-100
Example:
- zigbee2mqtt: {'state': 'ON', 'brightness': 254}
- Abstract: {'power': 'on', 'brightness': 100}
"""
abstract_payload = json.loads(payload)
# Transform state -> power with lowercase values
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
# Transform brightness: 0-254 (zigbee2mqtt range) -> 0-100 (%)
if "brightness" in abstract_payload:
vendor_brightness = abstract_payload["brightness"]
if isinstance(vendor_brightness, (int, float)):
abstract_payload["brightness"] = round(vendor_brightness * 100 / 254)
return abstract_payload
def transform_thermostat_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract thermostat payload to zigbee2mqtt format.
Transformations:
- target -> current_heating_setpoint (as string)
- mode is ignored (zigbee2mqtt thermostats use system_mode in state only)
Example:
- Abstract: {'target': 22.0}
- zigbee2mqtt: {'current_heating_setpoint': '22.0'}
"""
vendor_payload = {}
if "target" in payload:
vendor_payload["current_heating_setpoint"] = str(payload["target"])
return json.dumps(vendor_payload)
def transform_thermostat_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt thermostat payload to abstract format.
Transformations:
- current_heating_setpoint -> target (as float)
- local_temperature -> current (as float)
- system_mode -> mode
Example:
- zigbee2mqtt: {'current_heating_setpoint': 15, 'local_temperature': 23, 'system_mode': 'heat'}
- Abstract: {'target': 15.0, 'current': 23.0, 'mode': 'heat'}
"""
payload = json.loads(payload)
abstract_payload = {}
if "current_heating_setpoint" in payload:
setpoint = payload["current_heating_setpoint"]
abstract_payload["target"] = float(setpoint)
if "local_temperature" in payload:
current = payload["local_temperature"]
abstract_payload["current"] = float(current)
if "system_mode" in payload:
abstract_payload["mode"] = payload["system_mode"]
return abstract_payload
def transform_contact_sensor_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract contact sensor payload to zigbee2mqtt format.
Contact sensors are read-only, so this should not be called for SET commands.
"""
logger.warning("Contact sensors are read-only - SET commands should not be used")
return json.dumps(payload)
def transform_contact_sensor_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt contact sensor payload to abstract format.
Transformations:
- contact: bool -> "open" | "closed"
- zigbee2mqtt semantics: False = OPEN, True = CLOSED (inverted!)
Example:
- zigbee2mqtt: {"contact": false, "battery": 100}
- Abstract: {"contact": "open", "battery": 100}
"""
payload = json.loads(payload)
abstract_payload = {}
if "contact" in payload:
contact_bool = payload["contact"]
abstract_payload["contact"] = "closed" if contact_bool else "open"
# Pass through optional fields
for field in ["battery", "linkquality", "device_temperature", "voltage"]:
if field in payload:
abstract_payload[field] = payload[field]
return abstract_payload
def transform_temp_humidity_sensor_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract temp/humidity sensor payload to zigbee2mqtt format.
Temp/humidity sensors are read-only.
"""
return json.dumps(payload)
def transform_temp_humidity_sensor_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
def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to zigbee2mqtt format.
- 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 json.dumps(vendor_payload)
def transform_relay_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt relay payload to abstract format.
- 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
# Registry of handlers for this vendor
HANDLERS = {
("light", "to_vendor"): transform_light_to_vendor,
("light", "to_abstract"): transform_light_to_abstract,
("thermostat", "to_vendor"): transform_thermostat_to_vendor,
("thermostat", "to_abstract"): transform_thermostat_to_abstract,
("contact_sensor", "to_vendor"): transform_contact_sensor_to_vendor,
("contact_sensor", "to_abstract"): transform_contact_sensor_to_abstract,
("contact", "to_vendor"): transform_contact_sensor_to_vendor,
("contact", "to_abstract"): transform_contact_sensor_to_abstract,
("temp_humidity_sensor", "to_vendor"): transform_temp_humidity_sensor_to_vendor,
("temp_humidity_sensor", "to_abstract"): transform_temp_humidity_sensor_to_abstract,
("temp_humidity", "to_vendor"): transform_temp_humidity_sensor_to_vendor,
("temp_humidity", "to_abstract"): transform_temp_humidity_sensor_to_abstract,
("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract,
}

View File

@@ -8,9 +8,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883 \
REDIS_HOST=localhost \
REDIS_HOST=172.23.1.116 \
REDIS_PORT=6379 \
REDIS_DB=0 \
REDIS_DB=8 \
REDIS_CHANNEL=ui:updates
# Create non-root user

146
apps/api/config.py Normal file
View File

@@ -0,0 +1,146 @@
"""Configuration loading and caching for API application.
This module provides centralized configuration management for devices and layout,
with startup validation and in-memory caching for performance.
"""
import logging
from pathlib import Path
from typing import Any
import yaml
from packages.home_capabilities.layout import UiLayout
logger = logging.getLogger(__name__)
# Global caches (loaded once at startup)
devices_cache: list[dict[str, Any]] = []
layout_cache: UiLayout | None = None
def load_devices_from_file() -> list[dict[str, Any]]:
"""Load devices from configuration file and validate.
Returns:
list: List of device configurations
Raises:
FileNotFoundError: If devices.yaml doesn't exist
KeyError: If any device is missing required homekit_aid field
ValueError: If devices.yaml is invalid or contains duplicate homekit_aid values
"""
config_path = Path(__file__).parent.parent.parent / "config" / "devices.yaml"
if not config_path.exists():
raise FileNotFoundError(f"devices.yaml not found at {config_path}")
with open(config_path, "r") as f:
config = yaml.safe_load(f)
if not config or "devices" not in config:
raise ValueError("devices.yaml must contain 'devices' key")
# Normalize device entries: accept both 'id' and 'device_id', use 'device_id' internally
devices = config.get("devices", [])
for device in devices:
device["device_id"] = device.pop("device_id", device.pop("id", None))
# Validate required homekit_aid field
if "homekit_aid" not in device:
raise KeyError(f"Device {device.get('device_id', 'unknown')} is missing required 'homekit_aid' field")
# Validate unique homekit_aid values
aids = [d["homekit_aid"] for d in devices]
if len(aids) != len(set(aids)):
duplicates = [aid for aid in aids if aids.count(aid) > 1]
raise ValueError(f"Duplicate homekit_aid values found: {set(duplicates)}")
logger.info(f"Loaded {len(devices)} devices with unique homekit_aid values (range: {min(aids)}-{max(aids)})")
return devices
def load_layout_from_file() -> UiLayout:
"""Load UI layout from configuration file and validate.
Returns:
UiLayout: Parsed and validated layout configuration
Raises:
FileNotFoundError: If layout.yaml doesn't exist
ValueError: If layout validation fails
yaml.YAMLError: If YAML parsing fails
"""
config_path = Path(__file__).parent.parent.parent / "config" / "layout.yaml"
if not config_path.exists():
raise FileNotFoundError(
f"Layout configuration not found: {config_path}. "
f"Please create a layout.yaml file with room and device definitions."
)
try:
with open(config_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise yaml.YAMLError(f"Failed to parse YAML in {config_path}: {e}")
if data is None:
raise ValueError(f"Layout file is empty: {config_path}")
try:
layout = UiLayout(**data)
except Exception as e:
raise ValueError(f"Invalid layout configuration in {config_path}: {e}")
total_devices = layout.total_devices()
room_names = [room.name for room in layout.rooms]
logger.info(
f"Loaded layout: {len(layout.rooms)} rooms, "
f"{total_devices} total devices (Rooms: {', '.join(room_names)})"
)
return layout
def load_devices() -> list[dict[str, Any]]:
"""Get devices from in-memory cache.
Returns:
list: List of device configurations (loaded at startup)
"""
return devices_cache
def load_layout() -> UiLayout:
"""Get layout from in-memory cache.
Returns:
UiLayout: Layout configuration (loaded at startup)
Raises:
RuntimeError: If layout cache is not initialized
"""
if layout_cache is None:
raise RuntimeError("Layout cache not initialized. Application startup may have failed.")
return layout_cache
def initialize_config() -> None:
"""Initialize configuration by loading devices and layout.
This function should be called once during application startup.
Raises:
Exception: If configuration loading or validation fails
"""
global devices_cache, layout_cache
# Load devices with validation
devices_cache = load_devices_from_file()
# Load layout with validation
layout_cache = load_layout_from_file()
logger.info("Configuration initialization complete")

View File

@@ -24,9 +24,11 @@ from packages.home_capabilities import (
ContactState,
TempHumidityState,
RelayState,
load_layout,
)
# Import configuration management
from apps.api.config import initialize_config, load_devices, load_layout
# Import resolvers (must be before router imports to avoid circular dependency)
from apps.api.resolvers import (
DeviceDTO,
@@ -67,6 +69,7 @@ app.add_middleware(
"http://localhost:8002",
"http://172.19.1.11:8002",
"http://127.0.0.1:8002",
"https://homea2.hottis.de"
],
allow_credentials=True,
allow_methods=["*"],
@@ -98,30 +101,6 @@ async def get_device_state(device_id: str):
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")
async def health() -> dict[str, str]:
@@ -187,6 +166,21 @@ async def redis_state_listener():
async def startup_event():
"""Start background tasks on application startup."""
global background_task
# Include routers
from apps.api.routes.groups_scenes import router as groups_scenes_router
from apps.api.routes.rooms import router as rooms_router
app.include_router(groups_scenes_router, prefix="")
app.include_router(rooms_router, prefix="")
# Load and validate configuration (devices + layout)
try:
initialize_config()
except Exception as e:
logger.error(f"Failed to initialize configuration: {e}")
raise # Fatal error - application will not start
background_task = asyncio.create_task(redis_state_listener())
logger.info("Started background Redis state listener")
@@ -234,32 +228,11 @@ class DeviceInfo(BaseModel):
device_id: str
type: str
name: str
homekit_aid: int
features: dict[str, Any] = {}
# Configuration helpers
def load_devices() -> list[dict[str, Any]]:
"""Load devices from configuration file.
Returns:
list: List of device configurations
"""
config_path = Path(__file__).parent.parent.parent / "config" / "devices.yaml"
if not config_path.exists():
return []
with open(config_path, "r") as f:
config = yaml.safe_load(f)
# Normalize device entries: accept both 'id' and 'device_id', use 'device_id' internally
devices = config.get("devices", [])
for device in devices:
device["device_id"] = device.pop("device_id", device.pop("id", None))
return devices
def get_mqtt_settings() -> tuple[str, int]:
"""Get MQTT broker settings from environment.
@@ -387,6 +360,7 @@ async def get_device(device_id: str) -> DeviceInfo:
device_id=device["device_id"],
type=device["type"],
name=device.get("name", device["device_id"]),
homekit_aid=device["homekit_aid"],
features=device.get("features", {})
)
@@ -405,6 +379,7 @@ async def get_devices() -> list[DeviceInfo]:
device_id=device["device_id"],
type=device["type"],
name=device.get("name", device["device_id"]),
homekit_aid=device["homekit_aid"],
features=device.get("features", {})
)
for device in devices

View File

@@ -4,12 +4,12 @@ import logging
from pathlib import Path
from typing import Any, TypedDict
from apps.api.config import load_layout
from packages.home_capabilities import (
GroupConfig,
GroupsConfigRoot,
SceneStep,
get_group_by_id,
load_layout,
)
logger = logging.getLogger(__name__)

219
apps/api/routes/rooms.py Normal file
View File

@@ -0,0 +1,219 @@
"""
Room-based device control endpoints.
Provides bulk control operations for devices within rooms:
- /rooms/{room_name}/lights - Control all lights in a room
- /rooms/{room_name}/heating - Control all thermostats in a room
"""
import logging
from typing import Any
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from apps.api.config import load_layout
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Rooms"])
@router.get("/rooms")
async def get_rooms() -> list[dict[str, str]]:
"""Get list of all room IDs and names.
Returns:
List of dicts with room id and name
"""
layout = load_layout()
return [
{
"id": room.id,
"name": room.name
}
for room in layout.rooms
]
class LightsControlRequest(BaseModel):
"""Request model for controlling lights in a room."""
power: str # "on" or "off"
brightness: int | None = None # Optional brightness 0-100
class HeatingControlRequest(BaseModel):
"""Request model for controlling heating in a room."""
target: float # Target temperature
def get_room_devices(room_id: str) -> list[dict[str, Any]]:
"""Get all devices in a specific room from layout.
Args:
room_id: ID of the room
Returns:
List of device dicts with device_id, title, icon, rank, excluded
Raises:
HTTPException: If room not found
"""
layout = load_layout()
for room in layout.rooms:
if room.id == room_id:
return [
{
"device_id": device.device_id,
"title": device.title,
"icon": device.icon,
"rank": device.rank,
"excluded": device.excluded
}
for device in room.devices
]
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Room '{room_id}' not found"
)
@router.post("/rooms/{room_id}/lights", status_code=status.HTTP_202_ACCEPTED)
async def control_room_lights(room_id: str, request: LightsControlRequest) -> dict[str, Any]:
"""Control all lights (light and relay devices) in a room.
Args:
room_id: ID of the room
request: Light control parameters
Returns:
dict with affected device_ids and command summary
"""
from apps.api.main import load_devices, publish_abstract_set
# Get all devices in room
room_devices = get_room_devices(room_id)
# Filter out excluded devices
room_device_ids = {d["device_id"] for d in room_devices if not d.get("excluded", False)}
# Load all devices to filter by type
all_devices = load_devices()
# Filter for light/relay devices in this room
light_devices = [
d for d in all_devices
if d["device_id"] in room_device_ids and d["type"] in ("light", "relay")
]
if not light_devices:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"No light devices found in room '{room_id}'"
)
# Build payload
payload = {"power": request.power}
if request.brightness is not None and request.power == "on":
payload["brightness"] = request.brightness
# Send commands to all light devices
affected_ids = []
errors = []
for device in light_devices:
try:
await publish_abstract_set(
device_type=device["type"],
device_id=device["device_id"],
payload=payload
)
affected_ids.append(device["device_id"])
logger.info(f"Sent command to {device['device_id']}: {payload}")
except Exception as e:
logger.error(f"Failed to control {device['device_id']}: {e}")
errors.append({
"device_id": device["device_id"],
"error": str(e)
})
return {
"room": room_id,
"command": "lights",
"payload": payload,
"affected_devices": affected_ids,
"success_count": len(affected_ids),
"error_count": len(errors),
"errors": errors if errors else None
}
@router.post("/rooms/{room_id}/heating", status_code=status.HTTP_202_ACCEPTED)
async def control_room_heating(room_id: str, request: HeatingControlRequest) -> dict[str, Any]:
"""Control all thermostats in a room.
Args:
room_id: ID of the room
request: Heating control parameters
Returns:
dict with affected device_ids and command summary
"""
from apps.api.main import load_devices, publish_abstract_set
# Get all devices in room
room_devices = get_room_devices(room_id)
# Filter out excluded devices
room_device_ids = {d["device_id"] for d in room_devices if not d.get("excluded", False)}
# Load all devices to filter by type
all_devices = load_devices()
# Filter for thermostat devices in this room
thermostat_devices = [
d for d in all_devices
if d["device_id"] in room_device_ids and d["type"] == "thermostat"
]
if not thermostat_devices:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"No thermostat devices found in room '{room_name}'"
)
# Build payload
payload = {"target": request.target}
# Send commands to all thermostat devices
affected_ids = []
errors = []
for device in thermostat_devices:
try:
await publish_abstract_set(
device_type="thermostat",
device_id=device["device_id"],
payload=payload
)
affected_ids.append(device["device_id"])
logger.info(f"Sent heating command to {device['device_id']}: {payload}")
except Exception as e:
logger.error(f"Failed to control {device['device_id']}: {e}")
errors.append({
"device_id": device["device_id"],
"error": str(e)
})
return {
"room": room_id,
"command": "heating",
"payload": payload,
"affected_devices": affected_ids,
"success_count": len(affected_ids),
"error_count": len(errors),
"errors": errors if errors else None
}

31
apps/homekit/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM python:3.12-slim
# Environment defaults (can be overridden at runtime)
ENV PYTHONUNBUFFERED=1 \
LOG_LEVEL="INFO" \
HOMEKIT_NAME="Home Automation Bridge" \
HOMEKIT_PIN="031-45-154" \
HOMEKIT_PORT="51826" \
API_BASE="http://api:8001" \
HOMEKIT_API_TOKEN="" \
HOMEKIT_PERSIST_FILE="/data/homekit.state"
WORKDIR /app
# Copy only requirements first for better build caching
COPY apps/homekit/requirements.txt ./apps/homekit/requirements.txt
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r apps/homekit/requirements.txt
# Copy full source tree
COPY . /app
# Expose HomeKit TCP port (mDNS uses UDP 5353 via host network)
EXPOSE 51826/tcp
# Volume for persistent HomeKit state (pairings etc.)
VOLUME ["/data"]
# Start the HomeKit bridge
CMD ["python", "-m", "apps.homekit.main"]

View File

@@ -14,7 +14,7 @@ class ContactAccessory(Accessory):
category = CATEGORY_SENSOR
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
def __init__(self, driver, device, api_client, *args, **kwargs):
"""
Initialize the contact sensor accessory.
@@ -22,9 +22,8 @@ class ContactAccessory(Accessory):
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
name = device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client

View File

@@ -16,7 +16,7 @@ class OnOffLightAccessory(Accessory):
category = CATEGORY_LIGHTBULB
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
def __init__(self, driver, device, api_client, *args, **kwargs):
"""
Initialize the light accessory.
@@ -24,9 +24,8 @@ class OnOffLightAccessory(Accessory):
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
name = device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
@@ -57,9 +56,9 @@ class OnOffLightAccessory(Accessory):
class DimmableLightAccessory(OnOffLightAccessory):
"""Dimmable Light with brightness control."""
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
def __init__(self, driver, device, api_client, *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
name = device.name
Accessory.__init__(self, driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client
@@ -106,9 +105,9 @@ class DimmableLightAccessory(OnOffLightAccessory):
class ColorLightAccessory(DimmableLightAccessory):
"""RGB Light with full color control."""
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
def __init__(self, driver, device, api_client, *args, **kwargs):
# Don't call super().__init__() - build everything from scratch
name = display_name or device.friendly_name or device.name
name = device.name
Accessory.__init__(self, driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client

View File

@@ -15,7 +15,7 @@ class OutletAccessory(Accessory):
category = CATEGORY_OUTLET
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
def __init__(self, driver, device, api_client, *args, **kwargs):
"""
Initialize the outlet accessory.
@@ -23,9 +23,8 @@ class OutletAccessory(Accessory):
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
name = device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client

View File

@@ -15,7 +15,7 @@ class TempHumidityAccessory(Accessory):
category = CATEGORY_SENSOR
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
def __init__(self, driver, device, api_client, *args, **kwargs):
"""
Initialize the temp/humidity sensor accessory.
@@ -23,9 +23,8 @@ class TempHumidityAccessory(Accessory):
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
name = device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client

View File

@@ -17,7 +17,7 @@ class ThermostatAccessory(Accessory):
category = CATEGORY_THERMOSTAT
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
def __init__(self, driver, device, api_client, *args, **kwargs):
"""
Initialize the thermostat accessory.
@@ -25,9 +25,8 @@ class ThermostatAccessory(Accessory):
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
name = device.name
super().__init__(driver, name, *args, **kwargs)
self.device = device
self.api_client = api_client

View File

@@ -50,26 +50,7 @@ class ApiClient:
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.

View File

@@ -18,8 +18,7 @@ class Device:
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
homekit_aid: int # HomeKit Accessory ID
features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true})
read_only: bool # True for sensors that don't accept commands
@@ -50,24 +49,7 @@ class DeviceRegistry:
"""
# 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:
@@ -76,8 +58,11 @@ class DeviceRegistry:
logger.warning(f"Device without device_id: {dev_data}")
continue
# Get layout info
room_name, title = layout_map.get(device_id, (None, ''))
# Check for required homekit_aid field
homekit_aid = dev_data.get('homekit_aid')
if homekit_aid is None:
logger.error(f"Device {device_id} is missing required homekit_aid field - skipping")
continue
# Determine if read-only (sensors don't accept set commands)
device_type = dev_data.get('type', '')
@@ -86,9 +71,8 @@ class DeviceRegistry:
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,
name=device_id,
homekit_aid=homekit_aid,
features=dev_data.get('features', {}),
read_only=read_only
)

View File

@@ -0,0 +1,30 @@
services:
homekit-bridge:
image: gitea.hottis.de/wn/home-automation/homekit:0.5.0
build:
context: ../../
dockerfile: apps/homekit/Dockerfile
container_name: homekit-bridge
# Required for mDNS/Bonjour to work properly
network_mode: host
environment:
- LOG_LEVEL=INFO
- HOMEKIT_NAME=Hottis Home Automation Bridge
- HOMEKIT_PIN=031-45-154
- HOMEKIT_PORT=51826
- API_BASE=http://homea2-api-internal.hottis.de
- HOMEKIT_API_TOKEN=
- HOMEKIT_PERSIST_FILE=/data/homekit.state
volumes:
- homekit_data:/data
restart: unless-stopped
volumes:
homekit_data:
driver: local

View File

@@ -31,8 +31,9 @@ from .api_client import ApiClient
from .device_registry import DeviceRegistry
# Configure logging
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=logging.INFO,
level=getattr(logging, LOG_LEVEL, logging.INFO),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@@ -71,14 +72,11 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
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
# Set AID from device configuration
accessory.aid = device.homekit_aid
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'}")
logger.info(f"Added accessory: {device.name} ({device.type}, AID={device.homekit_aid}, {accessory.__class__.__name__})")
else:
logger.warning(f"No accessory mapping for device: {device.name} ({device.type})")
except Exception as e:
@@ -90,23 +88,6 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
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.
@@ -115,32 +96,30 @@ def create_accessory_for_device(device, api_client: ApiClient, driver: Accessory
"""
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)
return ColorLightAccessory(driver, device, api_client)
elif features.get("brightness"):
return DimmableLightAccessory(driver, device, api_client, display_name=display_name)
return DimmableLightAccessory(driver, device, api_client)
else:
return OnOffLightAccessory(driver, device, api_client, display_name=display_name)
return OnOffLightAccessory(driver, device, api_client)
# Thermostat
elif device_type == "thermostat":
return ThermostatAccessory(driver, device, api_client, display_name=display_name)
return ThermostatAccessory(driver, device, api_client)
# Contact sensor
elif device_type == "contact":
return ContactAccessory(driver, device, api_client, display_name=display_name)
return ContactAccessory(driver, device, api_client)
# Temperature/Humidity sensor
elif device_type == "temp_humidity_sensor":
return TempHumidityAccessory(driver, device, api_client, display_name=display_name)
return TempHumidityAccessory(driver, device, api_client)
# Relay/Outlet
elif device_type == "relay":
return OutletAccessory(driver, device, api_client, display_name=display_name)
return OutletAccessory(driver, device, api_client)
# Cover/Blinds (optional)
elif device_type == "cover":

35
apps/pulsegen/Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
# Pulsegen Dockerfile
# MQTT Pulse Generator Worker
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883
# Create non-root user
RUN addgroup -g 10001 -S app && \
adduser -u 10001 -S app -G app
# Set working directory
WORKDIR /app
# Install Python dependencies
COPY apps/pulsegen/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY apps/__init__.py /app/apps/__init__.py
COPY apps/pulsegen/ /app/apps/pulsegen/
# Change ownership to app user
RUN chown -R app:app /app
# Switch to non-root user
USER app
# Run application
CMD ["python", "-m", "apps.pulsegen.main"]

53
apps/pulsegen/README.md Normal file
View File

@@ -0,0 +1,53 @@
# Pulsegen
MQTT-basierte Pulse-Generator Applikation für Home Automation.
## Funktionen
- MQTT-Kommunikation über `aiomqtt`
- Automatische Reconnect-Logik
- Graceful shutdown (SIGTERM/SIGINT)
- JSON message parsing
- Konfigurierbar über Umgebungsvariablen
## Umgebungsvariablen
- `MQTT_BROKER`: MQTT Broker Hostname (default: `localhost`)
- `MQTT_PORT`: MQTT Broker Port (default: `1883`)
## Entwicklung
Lokal starten:
```bash
cd apps/pulsegen
python -m venv venv
source venv/bin/activate # oder venv\Scripts\activate auf Windows
pip install -r requirements.txt
python main.py
```
## Docker
Build:
```bash
docker build -f apps/pulsegen/Dockerfile -t pulsegen .
```
Run:
```bash
docker run -e MQTT_BROKER=172.16.2.16 -e MQTT_PORT=1883 pulsegen
```
## MQTT Topics
### Subscribed
- `pulsegen/command/#` - Kommandos für pulsegen
- `home/+/+/state` - Device state updates
### Published
- `pulsegen/status` - Status-Updates der Applikation

View File

@@ -0,0 +1 @@
"""Pulsegen - MQTT pulse generator application."""

241
apps/pulsegen/main.py Normal file
View File

@@ -0,0 +1,241 @@
"""Pulsegen - MQTT pulse generator application."""
import asyncio
import json
import logging
import os
import signal
import uuid
from typing import Any
from aiomqtt import Client, Message
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
COIL_STATUS_PREFIX = "dt1/di"
COIL_STATUS_TOPIC = f"{COIL_STATUS_PREFIX}/+"
PULSEGEN_COMMAND_PREFIX = "pulsegen/command"
PULSEGEN_COMMAND_TOPIC = f"{PULSEGEN_COMMAND_PREFIX}/+/+"
COIL_COMMAND_PREFIX = "dt1/coil"
PULSEGEN_STATUS_PREFIX = "pulsegen/status"
COIL_STATUS_CACHE: dict[int, bool] = {}
def get_mqtt_settings() -> tuple[str, int]:
"""Get MQTT broker settings from environment variables.
Returns:
tuple: (broker_host, broker_port)
"""
broker = os.getenv("MQTT_BROKER", "localhost")
port = int(os.getenv("MQTT_PORT", "1883"))
logger.info(f"MQTT settings: broker={broker}, port={port}")
return broker, port
async def handle_message(message: Message, client: Client) -> None:
"""Handle incoming MQTT message.
Args:
message: MQTT message object
client: MQTT client instance
"""
try:
payload = message.payload.decode()
logger.info(f"Received message on {message.topic}: {payload}")
try:
topic = str(message.topic)
match topic.split("/"):
case [prefix, di, coil_id] if f"{prefix}/{di}" == COIL_STATUS_PREFIX:
try:
coil_num = int(coil_id)
except ValueError:
logger.debug(f"Invalid coil id in topic: {topic}")
return
state = payload.lower() in ("1", "true", "on")
COIL_STATUS_CACHE[coil_num] = state
logger.info(f"Updated coil {coil_num} status to {state}")
logger.info(f"Publishing pulsegen status for coil {coil_num}: {state}")
await client.publish(
topic=f"{PULSEGEN_STATUS_PREFIX}/{coil_num}",
payload="on" if state else "off",
qos=1,
retain=True,
)
case [prefix, command, coil_in_id, coil_out_id] if f"{prefix}/{command}" == PULSEGEN_COMMAND_PREFIX:
try:
coil_in_id = int(coil_in_id)
coil_out_id = int(coil_out_id)
except ValueError:
logger.debug(f"Invalid coil id in topic: {topic}")
return
try:
coil_state = COIL_STATUS_CACHE[coil_in_id]
except KeyError:
logger.debug(f"Coil {coil_in_id} status unknown, cannot process command")
return
cmd = payload.lower() in ("1", "true", "on")
if cmd == coil_state:
logger.info(f"Coil {coil_in_id} already in desired state {cmd}, ignoring command")
return
logger.info(f"Received pulsegen command on {topic}: {coil_in_id=}, {coil_out_id=}, {cmd=}")
coil_cmd_topic = f"{COIL_COMMAND_PREFIX}/{coil_out_id}"
logger.info(f"Sending raising edge command: topic={coil_cmd_topic}")
await client.publish(
topic=coil_cmd_topic,
payload="1",
qos=1,
retain=False,
)
await asyncio.sleep(0.2)
logger.info(f"Sending falling edge command: topic={coil_cmd_topic}")
await client.publish(
topic=coil_cmd_topic,
payload="0",
qos=1,
retain=False,
)
case _:
logger.debug(f"Ignoring message on unrelated topic: {topic}")
except Exception as e:
logger.debug(f"Exception when handling payload: {e}")
except Exception as e:
logger.error(f"Error handling message: {e}", exc_info=True)
async def publish_example(client: Client) -> None:
"""Example function to publish MQTT messages.
Args:
client: MQTT client instance
"""
topic = "pulsegen/status"
payload = {
"status": "running",
"timestamp": asyncio.get_event_loop().time()
}
await client.publish(
topic=topic,
payload=json.dumps(payload),
qos=1
)
logger.info(f"Published to {topic}: {payload}")
async def mqtt_worker(shutdown_event: asyncio.Event) -> None:
"""Main MQTT worker loop.
Connects to MQTT broker, subscribes to topics, and processes messages.
Args:
shutdown_event: Event to signal shutdown
"""
broker, port = get_mqtt_settings()
reconnect_interval = 5 # seconds
while not shutdown_event.is_set():
try:
logger.info(f"Connecting to MQTT broker {broker}:{port}...")
async with Client(
hostname=broker,
port=port,
identifier=f"pulsegen-{uuid.uuid4()}",
) as client:
logger.info("Connected to MQTT broker")
# Subscribe to topics
for topic in [PULSEGEN_COMMAND_TOPIC, COIL_STATUS_TOPIC]:
await client.subscribe(topic)
logger.info(f"Subscribed to {topic}")
# Publish startup message
await publish_example(client)
# Message loop with timeout to allow shutdown check
async for message in client.messages:
if shutdown_event.is_set():
logger.info("Shutdown event detected, breaking message loop")
break
try:
await handle_message(message, client)
except Exception as e:
logger.error(f"Error in message handler: {e}", exc_info=True)
# If we exit the loop due to shutdown, break the reconnect loop too
if shutdown_event.is_set():
break
except asyncio.CancelledError:
logger.info("MQTT worker cancelled")
break
except Exception as e:
logger.error(f"MQTT error: {e}", exc_info=True)
if not shutdown_event.is_set():
logger.info(f"Reconnecting in {reconnect_interval} seconds...")
await asyncio.sleep(reconnect_interval)
async def main() -> None:
"""Main application entry point."""
logger.info("Starting pulsegen application...")
# Shutdown event for graceful shutdown
shutdown_event = asyncio.Event()
# Setup signal handlers
def signal_handler(sig: int) -> None:
logger.info(f"Received signal {sig}, initiating shutdown...")
shutdown_event.set()
loop = asyncio.get_event_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, lambda s=sig: signal_handler(s))
# Start MQTT worker
worker_task = asyncio.create_task(mqtt_worker(shutdown_event))
# Wait for shutdown signal
await shutdown_event.wait()
# Give worker a moment to finish gracefully
logger.info("Waiting for MQTT worker to finish...")
try:
await asyncio.wait_for(worker_task, timeout=5.0)
except asyncio.TimeoutError:
logger.warning("MQTT worker did not finish in time, cancelling...")
worker_task.cancel()
try:
await worker_task
except asyncio.CancelledError:
pass
logger.info("Pulsegen application stopped")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1 @@
aiomqtt==2.3.0

View File

@@ -6,7 +6,7 @@ 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 \
RULES_CONFIG=/app/config/rules.yaml \
MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883 \
REDIS_HOST=localhost \

15
apps/static/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
# Static assets Dockerfile (minimal webserver for /static only)
FROM nginx:1.27-alpine
WORKDIR /usr/share/nginx/html
# Remove default nginx content
RUN rm -rf ./*
# Copy only static assets from the UI project
COPY apps/static/static/ ./
EXPOSE 80
# Use default nginx config; caller can mount custom config if needed

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

View File

@@ -0,0 +1,4 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="180" height="180" rx="40" fill="#667EEA"/>
<text x="90" y="130" font-size="80" text-anchor="middle" fill="white">🏡</text>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

View File

@@ -0,0 +1,4 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="180" height="180" rx="40" fill="#667EEA"/>
<text x="90" y="130" font-size="80" text-anchor="middle" fill="white">🚗</text>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1 @@
empty

View File

@@ -1,49 +1,41 @@
# UI Service Dockerfile
# FastAPI + Jinja2 + HTMX Dashboard
# UI Service Dockerfile (Application only, without static files)
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UI_PORT=8002 \
API_BASE=http://api:8001 \
BASE_PATH=""
BASE_PATH="" \
STATIC_BASE=http://static:8080
# 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 \
curl \
gcc \
musl-dev \
linux-headers
# Install Python dependencies
COPY apps/ui/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
# Copy only Python code and templates, but exclude static assets
COPY apps/__init__.py /app/apps/__init__.py
COPY apps/ui/ /app/apps/ui/
COPY apps/ui/__init__.py /app/apps/ui/__init__.py
COPY apps/ui/main.py /app/apps/ui/main.py
COPY apps/ui/api_client.py /app/apps/ui/api_client.py
COPY apps/ui/templates/ /app/apps/ui/templates/
# Change ownership to app user
RUN chown -R app:app /app
# Switch to non-root user
USER app
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:${UI_PORT}/health || exit 1
# Expose port
EXPOSE 8002
# Run application
CMD ["python", "-m", "uvicorn", "apps.ui.main:app", "--host", "0.0.0.0", "--port", "8002"]

View File

@@ -5,7 +5,7 @@ import os
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -16,9 +16,11 @@ logger = logging.getLogger(__name__)
# Read configuration from environment variables
API_BASE = os.getenv("API_BASE", "http://localhost:8001")
BASE_PATH = os.getenv("BASE_PATH", "") # e.g., "/ui" for reverse proxy
STATIC_BASE = os.getenv("STATIC_BASE", "/static")
print(f"UI using API_BASE: {API_BASE}")
print(f"UI using BASE_PATH: {BASE_PATH}")
print(f"UI using STATIC_BASE: {STATIC_BASE}")
def api_url(path: str) -> str:
"""Helper function to construct API URLs.
@@ -43,12 +45,53 @@ app = FastAPI(
templates_dir = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(templates_dir))
# Make STATIC_BASE available in all templates
templates.env.globals["STATIC_BASE"] = STATIC_BASE
# Setup static files
static_dir = Path(__file__).parent / "static"
static_dir.mkdir(exist_ok=True)
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
@app.get("/apple-touch-icon.png")
async def apple_touch_icon():
"""Serve Apple Touch Icon with proper headers."""
icon_path = static_dir / "apple-touch-icon.png"
return FileResponse(
path=icon_path,
media_type="image/png",
headers={
"Cache-Control": "public, max-age=31536000",
"Content-Type": "image/png"
}
)
@app.get("/favicon.ico")
async def favicon():
"""Serve favicon."""
icon_path = static_dir / "apple-touch-icon.png"
return FileResponse(
path=icon_path,
media_type="image/png"
)
@app.get("/manifest.json")
async def manifest():
"""Serve Web App Manifest with proper headers."""
manifest_path = static_dir / "manifest.json"
return FileResponse(
path=manifest_path,
media_type="application/manifest+json",
headers={
"Cache-Control": "public, max-age=86400",
"Content-Type": "application/manifest+json"
}
)
@app.get("/health")
async def health() -> JSONResponse:
"""Health check endpoint for Kubernetes/Docker.
@@ -60,7 +103,8 @@ async def health() -> JSONResponse:
"status": "ok",
"service": "ui",
"api_base": API_BASE,
"base_path": BASE_PATH
"base_path": BASE_PATH,
"static_base": STATIC_BASE,
})
@@ -89,7 +133,7 @@ async def rooms(request: Request) -> HTMLResponse:
"""
return templates.TemplateResponse("rooms.html", {
"request": request,
"api_base": API_BASE
"api_base": API_BASE,
})
@@ -107,7 +151,7 @@ async def room_detail(request: Request, room_name: str) -> HTMLResponse:
return templates.TemplateResponse("room.html", {
"request": request,
"api_base": API_BASE,
"room_name": room_name
"room_name": room_name,
})

View File

@@ -1,301 +0,0 @@
# 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

@@ -4,7 +4,18 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Automation</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="{{ STATIC_BASE }}/favicon.svg">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Dashboard">
<meta name="theme-color" content="#667eea">
<style>
* {
margin: 0;

View File

@@ -4,6 +4,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gerät - Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Gerät">
<meta name="theme-color" content="#667eea">
<style>
* {
margin: 0;
@@ -334,8 +345,8 @@
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script src="{{ STATIC_BASE }}/types.js"></script>
<script src="{{ STATIC_BASE }}/api-client.js"></script>
<script>
// Get device ID from URL
@@ -347,6 +358,7 @@
let deviceData = null;
let deviceState = {};
let roomName = '';
let deviceStateUnknown = false;
// Device type icons
const deviceIcons = {
@@ -369,8 +381,19 @@
// 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);
try {
deviceState = await window.apiClient.getDeviceState(deviceId);
console.log("Loaded device state:", deviceState);
if (!deviceState || Object.keys(deviceState).length === 0) {
deviceStateUnknown = true;
deviceState = {};
}
} catch (stateError) {
console.warn('No state for device, using unknown state:', stateError);
deviceStateUnknown = true;
deviceState = {};
}
const layoutInfo = await window.apiClient.getDeviceLayout(deviceId);
console.log("Loaded layout info:", layoutInfo);
roomName = layoutInfo.room;
@@ -506,6 +529,14 @@
}, 0);
}
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card);
}
@@ -541,6 +572,14 @@
`;
card.appendChild(sliderGroup);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card);
setTimeout(() => {
@@ -569,6 +608,14 @@
powerGroup.appendChild(powerButton);
card.appendChild(powerGroup);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card);
}
@@ -587,6 +634,14 @@
`;
card.appendChild(statusDiv);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card);
}

View File

@@ -4,6 +4,18 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Garage - Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/garage-icon-180x180.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/garage-icon-152x152.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/garage-icon-120x120.png">
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_BASE }}/garage-icon-76x76.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/garage-icon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/garage-icon-16x16.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Garage">
<meta name="theme-color" content="#667eea">
<style>
* {
margin: 0;
@@ -293,14 +305,15 @@
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script src="{{ STATIC_BASE }}/types.js"></script>
<script src="{{ STATIC_BASE }}/api-client.js"></script>
<script>
// Device IDs for garage devices
const GARAGE_DEVICES = [
'power_relay_caroutlet',
'powermeter_caroutlet'
'powermeter_caroutlet',
'sensor_caroutlet'
];
// Device states
@@ -398,7 +411,17 @@
renderOutletControls(controlSection, device);
container.appendChild(controlSection);
// 3. Powermeter section
// 3. Feedback section
const feedbackDevice = Object.values(devicesData).find(d => d.device_id === 'sensor_caroutlet');
if (feedbackDevice) {
const feedbackSection = document.createElement('div');
feedbackSection.className = 'device-section';
feedbackSection.dataset.deviceId = feedbackDevice.device_id;
renderFeedbackDisplay(feedbackSection, feedbackDevice);
container.appendChild(feedbackSection);
}
// 4. Powermeter section
const powermeterDevice = Object.values(devicesData).find(d => d.device_id === 'powermeter_caroutlet');
if (powermeterDevice) {
const powermeterSection = document.createElement('div');
@@ -412,7 +435,6 @@
function renderOutletControls(container, device) {
const controlGroup = document.createElement('div');
controlGroup.style.textAlign = 'center';
// controlGroup.style.marginBottom = '8px';
const state = deviceStates[device.device_id];
const currentPower = state?.power === 'on';
@@ -428,36 +450,36 @@
label.className = 'toggle-label';
label.textContent = currentPower ? 'Ein' : 'Aus';
// Status display
// const stateDisplay = document.createElement('div');
// stateDisplay.style.marginTop = '16px';
// stateDisplay.style.fontSize = '18px';
// stateDisplay.style.fontWeight = '600';
// stateDisplay.style.color = currentPower ? '#34c759' : '#666';
// stateDisplay.textContent = `Status: ${currentPower ? 'Eingeschaltet' : 'Ausgeschaltet'}`;
controlGroup.appendChild(toggleSwitch);
controlGroup.appendChild(label);
// controlGroup.appendChild(stateDisplay);
container.appendChild(controlGroup);
}
function renderFeedbackDisplay(container, device) {
const state = deviceStates[device.device_id] || {};
const controlGroup = document.createElement('div');
controlGroup.style.textAlign = 'center';
const label = document.createElement('div');
label.className = 'toggle-label';
console.log(`Rendering feedback for ${device.device_id}:`, state);
if (state.contact === 'closed') {
label.textContent = 'Schütz ✅ eingeschaltet';
} else {
label.textContent = 'Schütz 🅾️ ausgeschaltet';
}
controlGroup.appendChild(label);
container.appendChild(controlGroup);
}
function renderThreePhasePowerDisplay(container, device) {
const state = deviceStates[device.device_id] || {};
// Leistungsmessung Title
// const title = document.createElement('h3');
// title.style.margin = '0 0 20px 0';
// title.style.fontSize = '18px';
// title.style.fontWeight = '600';
// title.style.color = '#333';
// title.textContent = 'Leistungsmessung';
// container.appendChild(title);
// Übersicht
const overviewGrid = document.createElement('div');
overviewGrid.className = 'state-grid';
overviewGrid.innerHTML = `
@@ -472,16 +494,13 @@
`;
container.appendChild(overviewGrid);
// Phasen Title
const phaseTitle = document.createElement('h4');
phaseTitle.style.margin = '20px 0 8px 0';
phaseTitle.style.fontSize = '16px';
phaseTitle.style.fontWeight = '600';
phaseTitle.style.color = '#333';
// phaseTitle.textContent = 'Phasen';
container.appendChild(phaseTitle);
// Phasen Details
const phaseGrid = document.createElement('div');
phaseGrid.className = 'phase-grid';
phaseGrid.innerHTML = `
@@ -589,12 +608,14 @@
const state = deviceStates[deviceId];
console.log(`Updating UI for ${deviceId}:`, state);
switch (device.type) {
case 'relay':
case 'outlet':
switch (deviceId) {
case 'power_relay_caroutlet':
updateOutletUI(deviceId, state);
break;
case 'three_phase_powermeter':
case 'sensor_caroutlet':
updateFeedbackDisplay(deviceId, state);
break;
case 'powermeter_caroutlet':
updateThreePhasePowerUI(deviceId, state);
break;
}
@@ -625,6 +646,29 @@
}
}
function updateFeedbackDisplay(deviceId, state) {
const section = document.querySelector(`[data-device-id="${deviceId}"]`);
if (!section) return;
const label = section.querySelector('.toggle-label');
if (label) {
const isOn = state.contact === 'closed';
label.textContent = isOn ? 'Schütz ✅ eingeschaltet' : 'Schütz 🅾️ ausgeschaltet';
// Update state display in separate card
const cards = section.querySelectorAll('.card');
if (cards.length >= 3) { // Header, Control, State
const stateCard = cards[2];
stateCard.innerHTML = `
<div style="font-size: 18px; font-weight: 600; color: ${isOn ? '#34c759' : '#666'};">
Status: ${isOn ? 'Eingeschaltet' : 'Ausgeschaltet'}
</div>
`;
}
}
}
function updateThreePhasePowerUI(deviceId, state) {
// Update overview
const totalPower = document.getElementById(`total-power-${deviceId}`);

View File

@@ -4,6 +4,18 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_BASE }}/apple-touch-icon-76x76.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon-16x16.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Home Automation">
<meta name="theme-color" content="#667eea">
<style>
* {
margin: 0;

View File

@@ -4,6 +4,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ room_name }} - Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="{{ room_name }}">
<meta name="theme-color" content="#667eea">
<style>
* {
margin: 0;
@@ -217,8 +228,8 @@
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script src="{{ STATIC_BASE }}/types.js"></script>
<script src="{{ STATIC_BASE }}/api-client.js"></script>
<script>
// Get room name from URL

View File

@@ -4,6 +4,18 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Räume - Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_BASE }}/apple-touch-icon-76x76.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon-16x16.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Räume">
<meta name="theme-color" content="#667eea">
<style>
* {
margin: 0;
@@ -150,8 +162,8 @@
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script src="{{ STATIC_BASE }}/types.js"></script>
<script src="{{ STATIC_BASE }}/api-client.js"></script>
<script>
// Room icon mapping

View File

@@ -1,16 +1,7 @@
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: lampe_semeniere_wohnzimmer
homekit_aid: 2
name: Semeniere
type: relay
cap_version: "relay@1.0.0"
@@ -26,6 +17,7 @@ devices:
model: "AC10691"
vendor: "OSRAM"
- device_id: stehlampe_esszimmer_spiegel
homekit_aid: 3
name: Stehlampe Spiegel
type: light
cap_version: "light@1.2.0"
@@ -37,6 +29,7 @@ devices:
state: "zigbee2mqtt/0x001788010d06ea09"
set: "zigbee2mqtt/0x001788010d06ea09/set"
- device_id: stehlampe_esszimmer_schrank
homekit_aid: 4
name: Stehlampe Schrank
type: light
cap_version: "light@1.2.0"
@@ -48,6 +41,7 @@ devices:
state: "zigbee2mqtt/0x001788010d09176c"
set: "zigbee2mqtt/0x001788010d09176c/set"
- device_id: grosse_lampe_wohnzimmer
homekit_aid: 5
name: grosse Lampe
type: relay
cap_version: "relay@1.0.0"
@@ -63,6 +57,7 @@ devices:
model: "AC10691"
vendor: "OSRAM"
- device_id: lampe_naehtischchen_wohnzimmer
homekit_aid: 6
name: Nähtischchen
type: relay
cap_version: "relay@1.0.0"
@@ -78,6 +73,7 @@ devices:
model: "HG06337"
vendor: "Lidl"
- device_id: kleine_lampe_links_esszimmer
homekit_aid: 7
name: kleine Lampe
type: relay
cap_version: "relay@1.0.0"
@@ -93,6 +89,7 @@ devices:
model: "AC10691"
vendor: "OSRAM"
- device_id: leselampe_esszimmer
homekit_aid: 8
name: Leselampe
type: light
cap_version: "light@1.2.0"
@@ -109,6 +106,7 @@ devices:
model: "LED1842G3"
vendor: "IKEA"
- device_id: medusalampe_schlafzimmer
homekit_aid: 9
name: Medusa-Lampe
type: relay
cap_version: "relay@1.0.0"
@@ -124,6 +122,7 @@ devices:
model: "AC10691"
vendor: "OSRAM"
- device_id: sportlicht_am_fernseher_studierzimmer
homekit_aid: 10
type: light
name: am Fernseher
cap_version: "light@1.2.0"
@@ -141,6 +140,7 @@ devices:
model: "LED1733G7"
vendor: "IKEA"
- device_id: deckenlampe_schlafzimmer
homekit_aid: 11
name: Deckenlampe
type: light
cap_version: "light@1.2.0"
@@ -157,6 +157,7 @@ devices:
model: "8718699688882"
vendor: "Philips"
- device_id: bettlicht_wolfgang
homekit_aid: 12
name: Bettlicht Wolfgang
type: light
cap_version: "light@1.2.0"
@@ -173,6 +174,7 @@ devices:
model: "9290020399"
vendor: "Philips"
- device_id: bettlicht_patty
homekit_aid: 13
name: Bettlicht Patty
type: light
cap_version: "light@1.2.0"
@@ -189,6 +191,7 @@ devices:
model: "9290020399"
vendor: "Philips"
- device_id: schranklicht_hinten_patty
homekit_aid: 14
name: Schranklicht hinten
type: light
cap_version: "light@1.2.0"
@@ -205,6 +208,7 @@ devices:
model: "8718699673147"
vendor: "Philips"
- device_id: schranklicht_vorne_patty
homekit_aid: 15
name: Schranklicht vorne
type: relay
cap_version: "relay@1.0.0"
@@ -220,6 +224,7 @@ devices:
model: "AC10691"
vendor: "OSRAM"
- device_id: leselampe_patty
homekit_aid: 16
name: Leselampe
type: light
cap_version: "light@1.2.0"
@@ -236,6 +241,7 @@ devices:
model: "8718699673147"
vendor: "Philips"
- device_id: deckenlampe_esszimmer
homekit_aid: 17
name: Deckenlampe
type: light
cap_version: "light@1.2.0"
@@ -251,23 +257,8 @@ devices:
ieee_address: "0x0017880108a03e45"
model: "929002241201"
vendor: "Philips"
- device_id: haustuer
name: Haustür-Lampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xec1bbdfffea6a3da"
set: "zigbee2mqtt/0xec1bbdfffea6a3da/set"
metadata:
friendly_name: "Haustür"
ieee_address: "0xec1bbdfffea6a3da"
model: "LED1842G3"
vendor: "IKEA"
- device_id: deckenlampe_flur_oben
homekit_aid: 18
name: Deckenlampe oben
type: light
cap_version: "light@1.2.0"
@@ -285,6 +276,7 @@ devices:
model: "929003099001"
vendor: "Philips"
- device_id: kueche_deckenlampe
homekit_aid: 19
name: Deckenlampe
type: light
cap_version: "light@1.2.0"
@@ -301,6 +293,7 @@ devices:
model: "929002469202"
vendor: "Philips"
- device_id: sportlicht_tisch
homekit_aid: 20
name: am Tisch
type: light
cap_version: "light@1.2.0"
@@ -317,6 +310,7 @@ devices:
model: "4058075729063"
vendor: "LEDVANCE"
- device_id: sportlicht_regal
homekit_aid: 21
name: am Regal
type: light
cap_version: "light@1.2.0"
@@ -333,6 +327,7 @@ devices:
model: "4058075729063"
vendor: "LEDVANCE"
- device_id: licht_flur_oben_am_spiegel
homekit_aid: 22
name: Spiegel
type: light
cap_version: "light@1.2.0"
@@ -350,6 +345,7 @@ devices:
model: "LED1732G11"
vendor: "IKEA"
- device_id: experimentlabtest
homekit_aid: 23
name: Test Lampe
type: light
cap_version: "light@1.2.0"
@@ -366,6 +362,7 @@ devices:
model: "4058075208421"
vendor: "LEDVANCE"
- device_id: thermostat_wolfgang
homekit_aid: 24
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
@@ -385,6 +382,7 @@ devices:
model: "GS361A-H04"
vendor: "Siterwell"
- device_id: thermostat_kueche
homekit_aid: 25
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
@@ -404,6 +402,7 @@ devices:
model: "GS361A-H04"
vendor: "Siterwell"
- device_id: thermostat_schlafzimmer
homekit_aid: 26
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
@@ -423,6 +422,7 @@ devices:
peer_id: "42"
channel: "1"
- device_id: thermostat_esszimmer
homekit_aid: 27
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
@@ -442,6 +442,7 @@ devices:
peer_id: "45"
channel: "1"
- device_id: thermostat_wohnzimmer
homekit_aid: 28
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
@@ -461,6 +462,7 @@ devices:
peer_id: "46"
channel: "1"
- device_id: thermostat_patty
homekit_aid: 29
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
@@ -480,6 +482,7 @@ devices:
peer_id: "39"
channel: "1"
- device_id: thermostat_bad_oben
homekit_aid: 30
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
@@ -499,6 +502,7 @@ devices:
peer_id: "41"
channel: "1"
- device_id: thermostat_bad_unten
homekit_aid: 31
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
@@ -518,6 +522,7 @@ devices:
peer_id: "48"
channel: "1"
- device_id: sterne_wohnzimmer
homekit_aid: 32
name: Sterne
type: relay
cap_version: "relay@1.0.0"
@@ -533,6 +538,7 @@ devices:
model: "AC10691"
vendor: "OSRAM"
- device_id: kontakt_schlafzimmer_strasse
homekit_aid: 33
name: Fenster
type: contact
cap_version: contact_sensor@1.0.0
@@ -541,6 +547,7 @@ devices:
state: homegear/instance1/plain/52/1/STATE
features: {}
- device_id: kontakt_esszimmer_strasse_rechts
homekit_aid: 34
type: contact
name: Fenster rechts
cap_version: contact_sensor@1.0.0
@@ -549,6 +556,7 @@ devices:
state: homegear/instance1/plain/26/1/STATE
features: {}
- device_id: kontakt_esszimmer_strasse_links
homekit_aid: 35
name: Fenster links
type: contact
cap_version: contact_sensor@1.0.0
@@ -557,6 +565,7 @@ devices:
state: homegear/instance1/plain/27/1/STATE
features: {}
- device_id: kontakt_wohnzimmer_garten_rechts
homekit_aid: 36
name: Fenster rechts
type: contact
cap_version: contact_sensor@1.0.0
@@ -565,6 +574,7 @@ devices:
state: homegear/instance1/plain/28/1/STATE
features: {}
- device_id: kontakt_wohnzimmer_garten_links
homekit_aid: 37
name: Fenster links
type: contact
cap_version: contact_sensor@1.0.0
@@ -573,6 +583,7 @@ devices:
state: homegear/instance1/plain/29/1/STATE
features: {}
- device_id: kontakt_kueche_garten_fenster
homekit_aid: 38
name: Fenster Garten
type: contact
cap_version: contact_sensor@1.0.0
@@ -581,6 +592,7 @@ devices:
state: zigbee2mqtt/0x00158d008b332785
features: {}
- device_id: kontakt_kueche_garten_tuer
homekit_aid: 39
type: contact
name: Terrassentür
cap_version: contact_sensor@1.0.0
@@ -589,6 +601,7 @@ devices:
state: zigbee2mqtt/0x00158d008b332788
features: {}
- device_id: kontakt_kueche_strasse_rechts
homekit_aid: 40
name: Fenster Straße rechts
type: contact
cap_version: contact_sensor@1.0.0
@@ -597,6 +610,7 @@ devices:
state: zigbee2mqtt/0x00158d008b151803
features: {}
- device_id: kontakt_kueche_strasse_links
homekit_aid: 41
name: Fenster Straße links
type: contact
cap_version: contact_sensor@1.0.0
@@ -605,6 +619,7 @@ devices:
state: zigbee2mqtt/0x00158d008b331d0b
features: {}
- device_id: kontakt_patty_garten_rechts
homekit_aid: 42
type: contact
name: Fenster Garten rechts
cap_version: contact_sensor@1.0.0
@@ -613,6 +628,8 @@ devices:
state: homegear/instance1/plain/18/1/STATE
features: {}
- device_id: kontakt_patty_garten_links
homekit_aid: 43
homekit_aid: 43
type: contact
name: Fenster Garten links
cap_version: contact_sensor@1.0.0
@@ -621,6 +638,7 @@ devices:
state: homegear/instance1/plain/22/1/STATE
features: {}
- device_id: kontakt_patty_strasse
homekit_aid: 44
type: contact
name: Fenster Straße
cap_version: contact_sensor@1.0.0
@@ -629,6 +647,7 @@ devices:
state: zigbee2mqtt/0x00158d000af457cf
features: {}
- device_id: kontakt_wolfgang_garten
homekit_aid: 45
type: contact
name: Fenster
cap_version: contact_sensor@1.0.0
@@ -637,6 +656,7 @@ devices:
state: zigbee2mqtt/0x00158d008b3328da
features: {}
- device_id: kontakt_bad_oben_strasse
homekit_aid: 46
type: contact
name: Fenster
cap_version: contact_sensor@1.0.0
@@ -645,6 +665,7 @@ devices:
state: zigbee2mqtt/0x00158d008b333aec
features: {}
- device_id: kontakt_bad_unten_strasse
homekit_aid: 47
type: contact
name: Fenster
cap_version: contact_sensor@1.0.0
@@ -653,6 +674,7 @@ devices:
state: homegear/instance1/plain/44/1/STATE
features: {}
- device_id: sensor_schlafzimmer
homekit_aid: 48
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -661,6 +683,7 @@ devices:
state: zigbee2mqtt/0x00158d00043292dc
features: {}
- device_id: sensor_wohnzimmer
homekit_aid: 49
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -669,6 +692,7 @@ devices:
state: zigbee2mqtt/0x00158d0008975707
features: {}
- device_id: sensor_kueche
homekit_aid: 50
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -677,6 +701,7 @@ devices:
state: zigbee2mqtt/0x00158d00083299bb
features: {}
- device_id: sensor_arbeitszimmer_patty
homekit_aid: 51
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -685,6 +710,7 @@ devices:
state: zigbee2mqtt/0x00158d0003f052b7
features: {}
- device_id: sensor_arbeitszimmer_wolfgang
homekit_aid: 52
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -693,6 +719,7 @@ devices:
state: zigbee2mqtt/0x00158d000543fb99
features: {}
- device_id: sensor_bad_oben
homekit_aid: 53
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -701,6 +728,7 @@ devices:
state: zigbee2mqtt/0x00158d00093e8987
features: {}
- device_id: sensor_bad_unten
homekit_aid: 54
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -709,6 +737,7 @@ devices:
state: zigbee2mqtt/0x00158d00093e662a
features: {}
- device_id: sensor_flur
homekit_aid: 55
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -717,6 +746,7 @@ devices:
state: zigbee2mqtt/0x00158d000836ccc6
features: {}
- device_id: sensor_waschkueche
homekit_aid: 56
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -725,6 +755,7 @@ devices:
state: zigbee2mqtt/0x00158d000449f3bc
features: {}
- device_id: sensor_sportzimmer
homekit_aid: 57
type: temp_humidity_sensor
name: Thermometer
cap_version: temp_humidity_sensor@1.0.0
@@ -733,6 +764,7 @@ devices:
state: zigbee2mqtt/0x00158d0009421422
features: {}
- device_id: licht_spuele_kueche
homekit_aid: 58
name: Spüle
type: relay
cap_version: "relay@1.0.0"
@@ -740,9 +772,22 @@ devices:
features:
power: true
topics:
set: "shellies/LightKitchenSink/relay/0/command"
state: "shellies/LightKitchenSink/relay/0"
set: "shellies/shellyplug-s-DED4E4/relay/0/command"
state: "shellies/shellyplug-s-DED4E4/relay/0"
- device_id: putzlicht_kueche
homekit_aid: 59
name: Putzlicht
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xa4c138563834406c"
set: "zigbee2mqtt/0xa4c138563834406c/set"
- device_id: licht_schrank_esszimmer
homekit_aid: 60
name: Schrank
type: relay
cap_version: "relay@1.0.0"
@@ -753,6 +798,7 @@ devices:
set: "shellies/schrankesszimmer/relay/0/command"
state: "shellies/schrankesszimmer/relay/0"
- device_id: licht_regal_wohnzimmer
homekit_aid: 61
type: relay
name: Regal
cap_version: "relay@1.0.0"
@@ -762,17 +808,8 @@ devices:
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
homekit_aid: 62
name: Terrasse
type: relay
cap_version: "relay@1.0.0"
@@ -782,24 +819,250 @@ devices:
topics:
set: "shellies/lichtterasse/relay/0/command"
state: "shellies/lichtterasse/relay/0"
- device_id: power_relay_caroutlet
name: Car Outlet
- device_id: kugellampe_patty
homekit_aid: 63
name: Kugellampe Patty
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xbc33acfffe21f547"
set: "zigbee2mqtt/0xbc33acfffe21f547/set"
- device_id: kueche_fensterbank_licht
homekit_aid: 64
name: Fensterbank Küche
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xf0d1b8000017515d"
set: "zigbee2mqtt/0xf0d1b8000017515d/set"
- device_id: licht_kommode_schlafzimmer
homekit_aid: 65
name: Kommode Schlafzimmer
type: relay
cap_version: "relay@1.0.0"
technology: hottis_modbus
technology: tasmota
features:
power: true
topics:
set: "caroutlet/cmd"
state: "caroutlet/state"
set: "cmnd/tasmota/04/POWER"
state: "stat/tasmota/04/POWER"
- device_id: licht_fensterbank_esszimmer
homekit_aid: 66
name: Fensterbank Esszimmer
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/02/POWER"
state: "stat/tasmota/02/POWER"
- device_id: licht_schreibtisch_patty
homekit_aid: 67
name: Schreibtisch Patty
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/03/POWER"
state: "stat/tasmota/03/POWER"
- device_id: kugeln_regal_flur
homekit_aid: 68
name: Kugeln Regal Flur
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/01/POWER"
state: "stat/tasmota/01/POWER"
- device_id: schrank_flur_haustuer
homekit_aid: 69
name: Schrank Flur Haustür
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/05/POWER"
state: "stat/tasmota/05/POWER"
- device_id: gartenlicht_vorne
homekit_aid: 70
name: Gartenlicht vorne
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/06/POWER"
state: "stat/tasmota/06/POWER"
- device_id: power_relay_caroutlet
homekit_aid: 71
name: Car Outlet
type: relay
cap_version: "relay@1.0.0"
technology: hottis_pv_modbus
features:
power: true
topics:
set: "IoT/Car/Control"
state: "IoT/Car/Control/State"
- device_id: powermeter_caroutlet
homekit_aid: 72
name: Car Outlet
type: three_phase_powermeter
cap_version: "three_phase_powermeter@1.0.0"
technology: hottis_modbus
technology: hottis_pv_modbus
topics:
state: "caroutlet/powermeter"
state: "IoT/Car/Values"
- device_id: sensor_caroutlet
homekit_aid: 73
name: Car Outlet
type: contact
cap_version: contact_sensor@1.0.0
technology: hottis_pv_modbus
topics:
state: IoT/Car/Feedback/State
- device_id: schranklicht_flur_vor_kueche
homekit_aid: 74
name: Schranklicht Flur vor Küche
type: light
cap_version: "relay@1.0.0"
technology: zigbee2mqtt
features:
power: true
topics:
state: "zigbee2mqtt/0xf0d1b80000155a1f"
set: "zigbee2mqtt/0xf0d1b80000155a1f/set"
- device_id: deckenlampe_wohnzimmer
homekit_aid: 75
name: Deckenlampe Wohnzimmer
type: light
cap_version: "relay@1.0.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x842e14fffea72027"
set: "zigbee2mqtt/0x842e14fffea72027/set"
- device_id: keller_flur_licht
homekit_aid: 76
name: Keller Flur Licht
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "pulsegen/command/10/21"
state: "pulsegen/status/10"
- device_id: waschkueche_licht
homekit_aid: 77
name: Waschküche Licht
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "pulsegen/command/8/22"
state: "pulsegen/status/8"
- device_id: werkstatt_licht
homekit_aid: 78
name: Werkstatt Licht
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "pulsegen/command/7/19"
state: "pulsegen/status/7"
- device_id: sportzimmer_licht
homekit_aid: 79
name: Sportzimmer Licht
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "pulsegen/command/9/20"
state: "pulsegen/status/9"
- device_id: deckenlampe_patty
homekit_aid: 80
name: Deckenlampe Patty
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "pulsegen/command/4/16"
state: "pulsegen/status/4"
- device_id: regallampe_esszimmer
homekit_aid: 81
name: Regallampe Esszimmer
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wifi_relay
features:
power: true
topics:
set: "IoT/WifiRelay1/State"
state: "IoT/WifiRelay1/State"
- device_id: herdlicht
homekit_aid: 82
name: Herdlicht
type: light
cap_version: "relay@1.0.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/herdlicht"
set: "zigbee2mqtt/herdlicht/set"
- device_id: regallicht_kueche
homekit_aid: 83
name: Regallicht
type: light
cap_version: "relay@1.0.0"
technology: hottis_led_stripe
features:
power: true
topics:
state: "IoT/RgbLedStripeKitchen/ColorCommand"
set: "IoT/RgbLedStripeKitchen/ColorCommand"
- device_id: regallicht_flur
homekit_aid: 84
name: Regallicht Flur
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wifi_relay
features:
power: true
topics:
set: "deconzhelper/flurregallist"
state: "deconzhelper/flurregallist"

View File

@@ -1,10 +1,13 @@
version: 1
groups:
- id: "kueche_lichter"
name: "Küche alle Lampen"
selector:
type: "light"
room: "Küche"
name: "Küche alle Lampen ausser Putzlicht"
device_ids:
- kueche_deckenlampe
- licht_spuele_kueche
- herdlicht
- kueche_fensterbank_licht
- regallicht_kueche
capabilities:
power: true
brightness: true
@@ -16,21 +19,25 @@ groups:
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
- licht_kommode_schlafzimmer
capabilities:
power: true
brightness: true
- id: "arbeitslicht_patty"
name: "Patty Arbeitslicht"
device_ids:
- schranklicht_hinten_patty
- schranklicht_vorne_patty
- leselampe_patty
- kugellampe_patty
- licht_schreibtisch_patty
capabilities:
power: true
brightness: true

View File

@@ -1,5 +1,6 @@
rooms:
- name: Schlafzimmer
- id: schlafzimmer
name: Schlafzimmer
devices:
- device_id: bettlicht_patty
title: Bettlicht Patty
@@ -17,6 +18,10 @@ rooms:
title: Medusa-Lampe Schlafzimmer
icon: 💡
rank: 40
- device_id: licht_kommode_schlafzimmer
title: Kommode Schlafzimmer
icon: 💡
rank: 42
- device_id: thermostat_schlafzimmer
title: Thermostat Schlafzimmer
icon: 🌡️
@@ -29,7 +34,8 @@ rooms:
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 47
- name: Esszimmer
- id: esszimmer
name: Esszimmer
devices:
- device_id: deckenlampe_esszimmer
title: Deckenlampe Esszimmer
@@ -39,10 +45,10 @@ rooms:
title: Leselampe Esszimmer
icon: 💡
rank: 60
# - device_id: standlampe_esszimmer
# title: Standlampe Esszimmer
# icon: 💡
# rank: 70
- device_id: licht_fensterbank_esszimmer
title: Fensterbank Esszimmer
icon: 💡
rank: 70
- device_id: kleine_lampe_links_esszimmer
title: kleine Lampe links Esszimmer
icon: 💡
@@ -55,10 +61,10 @@ rooms:
title: Stehlampe Esszimmer Schrank
icon: 💡
rank: 82
# - device_id: kleine_lampe_rechts_esszimmer
# title: kleine Lampe rechts Esszimmer
# icon: 💡
# rank: 90
- device_id: regallampe_esszimmer
title: Regallampe Esszimmer
icon: 💡
rank: 90
- device_id: licht_schrank_esszimmer
title: Schranklicht Esszimmer
icon: 💡
@@ -75,7 +81,8 @@ rooms:
title: Kontakt Straße links
icon: 🪟
rank: 97
- name: Wohnzimmer
- id: wohnzimmer
name: Wohnzimmer
devices:
- device_id: lampe_naehtischchen_wohnzimmer
title: Lampe Naehtischchen Wohnzimmer
@@ -97,6 +104,10 @@ rooms:
title: Regallicht Wohnzimmer
icon: 💡
rank: 132
- device_id: deckenlampe_wohnzimmer
title: Deckenlampe Wohnzimmer
icon: 💡
rank: 133
- device_id: thermostat_wohnzimmer
title: Thermostat Wohnzimmer
icon: 🌡️
@@ -113,7 +124,8 @@ rooms:
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 138
- name: che
- id: kueche
name: Küche
devices:
- device_id: kueche_deckenlampe
title: Küche Deckenlampe
@@ -123,6 +135,23 @@ rooms:
title: Küche Spüle
icon: 💡
rank: 142
- device_id: putzlicht_kueche
title: Küche Putzlicht
icon: 💡
rank: 143
excluded: true
- device_id: kueche_fensterbank_licht
title: Küche Fensterbank
icon: 💡
rank: 144
- device_id: herdlicht
title: Herdlicht
icon: 💡
rank: 145
- device_id: regallicht_kueche
title: Regallicht Küche
icon: 💡
rank: 146
- device_id: thermostat_kueche
title: Kueche
icon: 🌡️
@@ -147,22 +176,35 @@ rooms:
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 155
- name: Arbeitszimmer Patty
- id: arbeitszimmer_patty
name: Arbeitszimmer Patty
devices:
- device_id: leselampe_patty
title: Leselampe Patty
icon: 💡
rank: 160
- device_id: schranklicht_hinten_patty
title: Schranklicht hinten Patty
title: Schranklicht hinten
icon: 💡
rank: 170
- device_id: schranklicht_vorne_patty
title: Schranklicht vorne Patty
title: Schranklicht vorne
icon: 💡
rank: 180
- device_id: kugellampe_patty
title: Kugellampe
icon: 💡
rank: 181
- device_id: licht_schreibtisch_patty
title: Licht Schreibtisch
icon: 💡
rank: 182
- device_id: deckenlampe_patty
title: Deckenlampe
icon: 💡
rank: 183
- device_id: thermostat_patty
title: Thermostat Patty
title: Thermostat
icon: 🌡️
rank: 185
- device_id: kontakt_patty_garten_rechts
@@ -181,7 +223,8 @@ rooms:
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 189
- name: Arbeitszimmer Wolfgang
- id: arbeitszimmer_wolfgang
name: Arbeitszimmer Wolfgang
devices:
- device_id: thermostat_wolfgang
title: Wolfgang
@@ -199,29 +242,39 @@ rooms:
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 202
- name: Flur
- id: flur
name: Flur
devices:
- device_id: deckenlampe_flur_oben
title: Deckenlampe Flur oben
icon: 💡
rank: 210
- device_id: haustuer
title: Haustür
icon: 💡
rank: 220
- device_id: licht_flur_schrank
title: Schranklicht Flur
- device_id: kugeln_regal_flur
title: Kugeln Regal
icon: 💡
rank: 222
- device_id: licht_flur_oben_am_spiegel
title: Licht Flur oben am Spiegel
title: Licht oben am Spiegel
icon: 💡
rank: 230
- device_id: schrank_flur_haustuer
title: Schranklicht an der Haustür
icon: 💡
rank: 231
- device_id: schranklicht_flur_vor_kueche
title: Schranklicht vor Küche
icon: 💡
rank: 232
- device_id: regallicht_flur_unten
title: Regallicht Flur unten
icon: 💡
rank: 233
- device_id: sensor_flur
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 235
- name: Sportzimmer
- id: sportzimmer
name: Sportzimmer
devices:
- device_id: sportlicht_regal
title: Sportlicht Regal
@@ -235,11 +288,16 @@ rooms:
title: Sportlicht am Fernseher, Studierzimmer
icon: 🏃
rank: 260
- device_id: sportzimmer_licht
title: Deckenlampe
icon: 💡
rank: 262
- device_id: sensor_sportzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 265
- name: Bad Oben
- id: bad_oben
name: Bad Oben
devices:
- device_id: thermostat_bad_oben
title: Thermostat Bad Oben
@@ -253,7 +311,8 @@ rooms:
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 272
- name: Bad Unten
- id: bad_unten
name: Bad Unten
devices:
- device_id: thermostat_bad_unten
title: Thermostat Bad Unten
@@ -267,27 +326,56 @@ rooms:
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 282
- name: Waschküche
- id: waschkueche
name: Waschküche
devices:
- device_id: sensor_waschkueche
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 290
- name: Outdoor
- device_id: waschkueche_licht
title: Waschküche Licht
icon: 💡
rank: 340
- id: outdoor
name: Outdoor
devices:
- device_id: licht_terasse
title: Licht Terasse
icon: 💡
rank: 290
- name: Garage
- device_id: gartenlicht_vorne
title: Gartenlicht vorne
icon: 💡
rank: 291
- id: garage
name: Garage
devices:
- device_id: power_relay_caroutlet
title: Ladestrom
icon:
rank: 310
- device_id: sensor_caroutlet
title: Schützzustand
icon: 🔌
rank: 315
- device_id: powermeter_caroutlet
title: Ladestrom
title: Messwerte
icon: 📊
rank: 320
- id: keller
name: Keller
devices:
- device_id: keller_flur_licht
title: Keller Flur Licht
icon: 💡
rank: 330
- device_id: werkstatt_licht
title: Werkstatt Licht
icon: 💡
rank: 350

30
create_icons.py Normal file
View File

@@ -0,0 +1,30 @@
"""
Script to create additional PNG icon sizes for better iOS compatibility
"""
import os
from pathlib import Path
from PIL import Image
def create_icon_sizes():
static_dir = Path("/Users/wn/Workspace/home-automation/apps/ui/static")
# Sizes that iOS might need
sizes = [16, 32, 57, 60, 72, 76, 114, 120, 144, 152, 180]
# Create home icons
base_icon = Image.open(static_dir / "apple-touch-icon.png")
for size in sizes:
resized = base_icon.resize((size, size), Image.Resampling.LANCZOS)
resized.save(static_dir / f"apple-touch-icon-{size}x{size}.png")
print(f"Created apple-touch-icon-{size}x{size}.png")
# Create garage icons
garage_icon = Image.open(static_dir / "garage-icon.png")
for size in sizes:
resized = garage_icon.resize((size, size), Image.Resampling.LANCZOS)
resized.save(static_dir / f"garage-icon-{size}x{size}.png")
print(f"Created garage-icon-{size}x{size}.png")
if __name__ == "__main__":
create_icon_sizes()

118
create_proper_icons.py Normal file
View File

@@ -0,0 +1,118 @@
"""
Script to create proper PNG icons with house and car symbols
"""
import os
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
def create_proper_icons():
static_dir = Path("/Users/wn/Workspace/home-automation/apps/ui/static")
# Create home icon with house symbol
def create_home_icon(size):
img = Image.new('RGBA', (size, size), color=(102, 126, 234, 255)) # #667EEA
draw = ImageDraw.Draw(img)
# Calculate proportions
margin = size // 10
house_size = size - 2 * margin
# Draw house shape
# Base rectangle
base_height = house_size // 2
base_y = size - margin - base_height
draw.rectangle([margin, base_y, size - margin, size - margin], fill='white')
# Roof triangle
roof_height = house_size // 3
roof_points = [
(size // 2, margin), # top point
(margin, base_y), # bottom left
(size - margin, base_y) # bottom right
]
draw.polygon(roof_points, fill='white')
# Door
door_width = house_size // 6
door_height = base_height // 2
door_x = size // 2 - door_width // 2
door_y = size - margin - door_height
draw.rectangle([door_x, door_y, door_x + door_width, size - margin], fill=(102, 126, 234, 255))
# Window
window_size = house_size // 8
window_x = margin + house_size // 4
window_y = base_y + base_height // 4
draw.rectangle([window_x, window_y, window_x + window_size, window_y + window_size], fill=(102, 126, 234, 255))
return img
# Create car icon with car symbol
def create_car_icon(size):
img = Image.new('RGBA', (size, size), color=(102, 126, 234, 255)) # #667EEA
draw = ImageDraw.Draw(img)
# Calculate proportions
margin = size // 8
car_width = size - 2 * margin
car_height = car_width // 2
car_y = size // 2 - car_height // 2
# Draw car body
draw.rounded_rectangle([margin, car_y, size - margin, car_y + car_height],
radius=size//20, fill='white')
# Draw car roof
roof_margin = car_width // 4
roof_height = car_height // 2
roof_y = car_y - roof_height // 2
draw.rounded_rectangle([margin + roof_margin, roof_y,
size - margin - roof_margin, car_y + roof_height // 2],
radius=size//30, fill='white')
# Draw wheels
wheel_radius = car_height // 4
wheel_y = car_y + car_height - wheel_radius // 2
# Left wheel
left_wheel_x = margin + car_width // 4
draw.ellipse([left_wheel_x - wheel_radius, wheel_y - wheel_radius,
left_wheel_x + wheel_radius, wheel_y + wheel_radius],
fill=(102, 126, 234, 255))
# Right wheel
right_wheel_x = size - margin - car_width // 4
draw.ellipse([right_wheel_x - wheel_radius, wheel_y - wheel_radius,
right_wheel_x + wheel_radius, wheel_y + wheel_radius],
fill=(102, 126, 234, 255))
return img
# Sizes to create
sizes = [16, 32, 57, 60, 72, 76, 114, 120, 144, 152, 180]
# Create home icons
for size in sizes:
home_icon = create_home_icon(size)
home_icon.save(static_dir / f"apple-touch-icon-{size}x{size}.png")
print(f"Created apple-touch-icon-{size}x{size}.png")
# Also create the main apple-touch-icon.png
main_icon = create_home_icon(180)
main_icon.save(static_dir / "apple-touch-icon.png")
print("Created apple-touch-icon.png")
# Create garage icons
for size in sizes:
car_icon = create_car_icon(size)
car_icon.save(static_dir / f"garage-icon-{size}x{size}.png")
print(f"Created garage-icon-{size}x{size}.png")
# Also create the main garage-icon.png
main_garage = create_car_icon(180)
main_garage.save(static_dir / "garage-icon.png")
print("Created garage-icon.png")
if __name__ == "__main__":
create_proper_icons()

View File

@@ -0,0 +1,73 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: abstraction
labels:
app: abstraction
component: home-automation
spec:
replicas: 1
selector:
matchLabels:
app: abstraction
template:
metadata:
annotations:
reloader.stakater.com/auto: "true"
configmap.reloader.stakater.com/reload: "home-automation-environment,home-automation-config"
labels:
app: abstraction
component: home-automation
spec:
containers:
- name: abstraction
image: %IMAGE%
env:
- name: MQTT_BROKER
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_BROKER
- name: MQTT_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_PORT
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_HOST
- name: REDIS_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_PORT
- name: REDIS_DB
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_DB
volumeMounts:
- name: config-volume
mountPath: /app/config
readOnly: true
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "ps aux | grep -v grep | grep python"
initialDelaySeconds: 30
periodSeconds: 10
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
volumes:
- name: config-volume
configMap:
name: home-automation-config

View File

@@ -0,0 +1,97 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: homea2
labels:
app: api
component: home-automation
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
annotations:
reloader.stakater.com/auto: "true"
configmap.reloader.stakater.com/reload: "home-automation-environment,home-automation-config"
labels:
app: api
component: home-automation
spec:
containers:
- name: api
image: %IMAGE%
ports:
- containerPort: 8001
name: http
env:
- name: MQTT_BROKER
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_BROKER
- name: MQTT_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_PORT
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_HOST
- name: REDIS_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_PORT
- name: REDIS_DB
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_DB
volumeMounts:
- name: config-volume
mountPath: /app/config
readOnly: true
livenessProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 5
periodSeconds: 5
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 200m
memory: 256Mi
volumes:
- name: config-volume
configMap:
name: home-automation-config
---
apiVersion: v1
kind: Service
metadata:
name: api
labels:
app: api
component: home-automation
spec:
selector:
app: api
ports:
- port: 80
targetPort: 8001
name: http
type: ClusterIP

22
deployment/configmap.yaml Normal file
View File

@@ -0,0 +1,22 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: home-automation-environment
namespace: homea2
data:
# Default environment variables
SHARED_MQTT_BROKER: "emqx01-anonymous-cluster-internal.broker.svc.cluster.local"
SHARED_MQTT_PORT: "1883"
SHARED_REDIS_HOST: "redis-master.redis.svc.cluster.local"
SHARED_REDIS_PORT: "6379"
SHARED_REDIS_DB: "8"
# UI specific environment variables
UI_UI_PORT: "8002"
UI_API_BASE: "https://homea2-api.hottis.de"
UI_STATIC_BASE: "https://homea2-static.hottis.de"
UI_BASE_PATH: "/"

96
deployment/ingress.yaml Normal file
View File

@@ -0,0 +1,96 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: homea2-cert
spec:
secretName: homea2-cert
issuerRef:
name: letsencrypt-production-http
kind: ClusterIssuer
commonName: homea2.hottis.de
dnsNames:
- homea2.hottis.de
- homea2-api.hottis.de
- homea2-static.hottis.de
---
apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
metadata:
name: mtls-required
spec:
clientAuth:
clientAuthType: RequireAndVerifyClientCert
secretNames:
- mtls-ca-cert
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ui
spec:
entryPoints:
- websecure
tls:
secretName: homea2-cert
options:
name: mtls-required
namespace: homea2
routes:
- match: Host(`homea2.hottis.de`)
kind: Rule
services:
- name: ui
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: api
spec:
entryPoints:
- websecure
tls:
secretName: homea2-cert
options:
name: mtls-required
namespace: homea2
routes:
- match: Host(`homea2-api.hottis.de`)
kind: Rule
services:
- name: api
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: static
spec:
entryPoints:
- websecure
tls:
secretName: homea2-cert
routes:
- match: Host(`homea2-static.hottis.de`)
kind: Rule
services:
- name: static
port: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-internal
spec:
ingressClassName: traefik-internal
rules:
- host: homea2-api-internal.hottis.de
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80

View File

@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: pulsegen
namespace: homea2
labels:
app: pulsegen
component: home-automation
spec:
replicas: 1
selector:
matchLabels:
app: pulsegen
template:
metadata:
annotations:
reloader.stakater.com/auto: "true"
configmap.reloader.stakater.com/reload: "home-automation-environment"
labels:
app: pulsegen
component: home-automation
spec:
containers:
- name: pulsegen
image: %IMAGE%
env:
- name: MQTT_BROKER
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_BROKER
- name: MQTT_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_PORT
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 200m
memory: 256Mi
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "ps aux | grep -v grep | grep python"
initialDelaySeconds: 30
periodSeconds: 10

View File

@@ -0,0 +1,73 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: rules
labels:
app: rules
component: home-automation
spec:
replicas: 1
selector:
matchLabels:
app: rules
template:
metadata:
annotations:
reloader.stakater.com/auto: "true"
configmap.reloader.stakater.com/reload: "home-automation-environment,home-automation-config"
labels:
app: rules
component: home-automation
spec:
containers:
- name: rules
image: %IMAGE%
env:
- name: MQTT_BROKER
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_BROKER
- name: MQTT_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_MQTT_PORT
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_HOST
- name: REDIS_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_PORT
- name: REDIS_DB
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: SHARED_REDIS_DB
volumeMounts:
- name: config-volume
mountPath: /app/config
readOnly: true
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "ps aux | grep -v grep | grep python"
initialDelaySeconds: 30
periodSeconds: 10
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
volumes:
- name: config-volume
configMap:
name: home-automation-config

View File

@@ -0,0 +1,61 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: static
namespace: homea2
labels:
app: static
component: home-automation
spec:
replicas: 1
selector:
matchLabels:
app: static
template:
metadata:
labels:
app: static
component: home-automation
spec:
containers:
- name: static
image: %IMAGE%
ports:
- containerPort: 80
name: http
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
name: static
namespace: homea2
labels:
app: static
component: home-automation
spec:
selector:
app: static
ports:
- port: 80
targetPort: 80
name: http
type: ClusterIP

View File

@@ -0,0 +1,84 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ui
namespace: homea2
labels:
app: ui
component: home-automation
spec:
replicas: 1
selector:
matchLabels:
app: ui
template:
metadata:
annotations:
reloader.stakater.com/auto: "true"
configmap.reloader.stakater.com/reload: "home-automation-environment"
labels:
app: ui
component: home-automation
spec:
containers:
- name: ui
image: %IMAGE%
ports:
- containerPort: 8002
name: http
env:
- name: UI_PORT
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: UI_UI_PORT
- name: API_BASE
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: UI_API_BASE
- name: STATIC_BASE
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: UI_STATIC_BASE
- name: BASE_PATH
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: UI_BASE_PATH
livenessProbe:
httpGet:
path: /
port: 8002
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8002
initialDelaySeconds: 5
periodSeconds: 5
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: ui
labels:
app: ui
component: home-automation
spec:
selector:
app: ui
ports:
- port: 80
targetPort: 8002
name: http
type: ClusterIP

59
icon-generator.html Normal file
View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<title>Icon Generator</title>
</head>
<body>
<canvas id="homeCanvas" width="180" height="180" style="border: 1px solid #ccc;"></canvas>
<canvas id="garageCanvas" width="180" height="180" style="border: 1px solid #ccc;"></canvas>
<br><br>
<button onclick="downloadHome()">Download Home Icon</button>
<button onclick="downloadGarage()">Download Garage Icon</button>
<script>
// Home Icon
const homeCanvas = document.getElementById('homeCanvas');
const homeCtx = homeCanvas.getContext('2d');
// Fill background
homeCtx.fillStyle = '#667EEA';
homeCtx.fillRect(0, 0, 180, 180);
// Add house emoji
homeCtx.font = '80px Arial';
homeCtx.fillStyle = 'white';
homeCtx.textAlign = 'center';
homeCtx.textBaseline = 'middle';
homeCtx.fillText('🏡', 90, 90);
// Garage Icon
const garageCanvas = document.getElementById('garageCanvas');
const garageCtx = garageCanvas.getContext('2d');
// Fill background
garageCtx.fillStyle = '#667EEA';
garageCtx.fillRect(0, 0, 180, 180);
// Add car emoji
garageCtx.font = '80px Arial';
garageCtx.fillStyle = 'white';
garageCtx.textAlign = 'center';
garageCtx.textBaseline = 'middle';
garageCtx.fillText('🚗', 90, 90);
function downloadHome() {
const link = document.createElement('a');
link.download = 'apple-touch-icon.png';
link.href = homeCanvas.toDataURL();
link.click();
}
function downloadGarage() {
const link = document.createElement('a');
link.download = 'garage-icon.png';
link.href = garageCanvas.toDataURL();
link.click();
}
</script>
</body>
</html>

77
icon-test.html Normal file
View File

@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Icon Test</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.icon-container {
display: flex;
gap: 20px;
margin: 20px 0;
}
.icon-test {
background: white;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.icon-test img {
display: block;
margin: 10px auto;
border-radius: 8px;
}
.icon-sizes {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
margin-top: 10px;
}
</style>
</head>
<body>
<h1>Apple Touch Icon Test</h1>
<div class="icon-container">
<div class="icon-test">
<h3>Home Icon</h3>
<img src="apps/ui/static/apple-touch-icon.png" alt="Home Icon" width="120" height="120">
<p>Haupticon für die Home Automation App</p>
<div class="icon-sizes">
<img src="apps/ui/static/apple-touch-icon-76x76.png" alt="Home 76px" width="76" height="76">
<img src="apps/ui/static/apple-touch-icon-60x60.png" alt="Home 60px" width="60" height="60">
<img src="apps/ui/static/apple-touch-icon-32x32.png" alt="Home 32px" width="32" height="32">
</div>
</div>
<div class="icon-test">
<h3>Garage Icon</h3>
<img src="apps/ui/static/garage-icon.png" alt="Garage Icon" width="120" height="120">
<p>Icon für die Garage-Seite</p>
<div class="icon-sizes">
<img src="apps/ui/static/garage-icon-76x76.png" alt="Garage 76px" width="76" height="76">
<img src="apps/ui/static/garage-icon-60x60.png" alt="Garage 60px" width="60" height="60">
<img src="apps/ui/static/garage-icon-32x32.png" alt="Garage 32px" width="32" height="32">
</div>
</div>
</div>
<h2>iPhone Homescreen Test</h2>
<p>Um die Icons auf dem iPhone zu testen:</p>
<ol>
<li>Öffnen Sie Ihre Home Automation App im Safari</li>
<li>Tippen Sie auf das Teilen-Symbol</li>
<li>Wählen Sie "Zum Home-Bildschirm hinzufügen"</li>
<li>Das Icon sollte jetzt als Haus-Symbol erscheinen</li>
</ol>
<p><strong>Hinweis:</strong> Falls das alte Icon noch angezeigt wird, löschen Sie die bestehende App vom Homescreen und fügen Sie sie neu hinzu.</p>
</body>
</html>

View File

@@ -18,6 +18,7 @@ class DeviceTile(BaseModel):
title: Display title for the device
icon: Icon name or emoji for the device
rank: Sort order within the room (lower = first)
excluded: Optional flag to exclude device from certain operations
"""
device_id: str = Field(
@@ -40,16 +41,27 @@ class DeviceTile(BaseModel):
ge=0,
description="Sort order (lower values appear first)"
)
excluded: bool = Field(
default=False,
description="Exclude device from bulk operations"
)
class Room(BaseModel):
"""Represents a room containing devices.
Attributes:
id: Unique room identifier (used for API endpoints)
name: Room name (e.g., "Wohnzimmer", "Küche")
devices: List of device tiles in this room
"""
id: str = Field(
...,
description="Unique room identifier"
)
name: str = Field(
...,
description="Room name"

29
test-icons.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Script to test icon accessibility over HTTPS/mTLS
echo "Testing Apple Touch Icon accessibility..."
# Test base icons
echo "1. Testing main apple-touch-icon.png:"
curl -I "https://your-domain.com/static/apple-touch-icon-180x180.png" || echo "FAILED"
echo "2. Testing garage icon:"
curl -I "https://your-domain.com/static/garage-icon-180x180.png" || echo "FAILED"
echo "3. Testing manifest:"
curl -I "https://your-domain.com/manifest.json" || echo "FAILED"
echo "4. Testing favicon route:"
curl -I "https://your-domain.com/favicon.ico" || echo "FAILED"
echo "5. Testing apple-touch-icon route:"
curl -I "https://your-domain.com/apple-touch-icon.png" || echo "FAILED"
echo ""
echo "Testing mTLS with client certificate:"
echo "6. Testing with client cert:"
curl -I --cert client.crt --key client.key "https://your-domain.com/static/apple-touch-icon-180x180.png" || echo "FAILED"
echo ""
echo "Note: Replace 'your-domain.com' with your actual domain"
echo "Note: Use actual client certificate files if testing mTLS"

View File

@@ -1,190 +0,0 @@
# Device Simulator
Unified MQTT device simulator für das Home Automation System.
## Übersicht
Dieser Simulator ersetzt die einzelnen Simulatoren (`sim_test_lampe.py`, `sim_thermo.py`) und vereint alle Device-Typen in einer einzigen Anwendung.
## Unterstützte Geräte
### Lampen (3 Geräte)
- `test_lampe_1` - Mit Power und Brightness
- `test_lampe_2` - Mit Power und Brightness
- `test_lampe_3` - Mit Power und Brightness
**Features:**
- `power`: "on" oder "off"
- `brightness`: 0-100
### Thermostaten (1 Gerät)
- `test_thermo_1` - Vollständiger Thermostat mit Temperatur-Simulation
**Features:**
- `mode`: "off", "heat", oder "auto"
- `target`: Soll-Temperatur (5.0-30.0°C)
- `current`: Ist-Temperatur (wird simuliert)
- `battery`: Batteriestand (90%)
- `window_open`: Fensterstatus (false)
**Temperatur-Simulation:**
- Alle 5 Sekunden wird die Ist-Temperatur angepasst
- **HEAT/AUTO Mode**: Drift zu `target` (+0.2°C pro Intervall)
- **OFF Mode**: Drift zu Ambient-Temperatur 18°C (-0.2°C pro Intervall)
## MQTT-Konfiguration
- **Broker**: 172.16.2.16:1883 (konfigurierbar via ENV)
- **QoS**: 1 für alle Publishes
- **Retained**: Ja für alle State-Messages
- **Client ID**: device_simulator
### Topics
Für jedes Gerät:
- Subscribe: `vendor/{device_id}/set` (QoS 1)
- Publish: `vendor/{device_id}/state` (QoS 1, retained)
## Verwendung
### Starten
```bash
poetry run python tools/device_simulator.py
```
Oder im Hintergrund:
```bash
poetry run python tools/device_simulator.py > /tmp/simulator.log 2>&1 &
```
### Umgebungsvariablen
```bash
export MQTT_BROKER="172.16.2.16" # MQTT Broker Host
export MQTT_PORT="1883" # MQTT Broker Port
```
## Testen
Ein umfassendes Test-Skript ist verfügbar:
```bash
./tools/test_device_simulator.sh
```
Das Test-Skript:
1. Stoppt alle laufenden Services
2. Startet Abstraction Layer, API und Simulator
3. Testet alle Lampen-Operationen
4. Testet alle Thermostat-Operationen
5. Verifiziert MQTT State Messages
6. Zeigt Simulator-Logs
## Beispiele
### Lampe einschalten
```bash
curl -X POST http://localhost:8001/devices/test_lampe_1/set \
-H "Content-Type: application/json" \
-d '{"type":"light","payload":{"power":"on"}}'
```
### Helligkeit setzen
```bash
curl -X POST http://localhost:8001/devices/test_lampe_1/set \
-H "Content-Type: application/json" \
-d '{"type":"light","payload":{"brightness":75}}'
```
### Thermostat Mode setzen
```bash
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}}'
```
### State abfragen via MQTT
```bash
# Lampe
mosquitto_sub -h 172.16.2.16 -t 'vendor/test_lampe_1/state' -C 1
# Thermostat
mosquitto_sub -h 172.16.2.16 -t 'vendor/test_thermo_1/state' -C 1
```
## Architektur
```
Browser/API
↓ POST /devices/{id}/set
API Server (Port 8001)
↓ MQTT: home/{type}/{id}/set
Abstraction Layer
↓ MQTT: vendor/{id}/set
Device Simulator
↓ MQTT: vendor/{id}/state (retained)
Abstraction Layer
↓ MQTT: home/{type}/{id}/state (retained)
↓ Redis Pub/Sub: ui:updates
UI / Dashboard
```
## Logs
Der Simulator loggt alle Aktivitäten:
- Startup und MQTT-Verbindung
- Empfangene SET-Commands
- State-Änderungen
- Temperature-Drift (Thermostaten)
- Publizierte State-Messages
Log-Level: INFO
## Troubleshooting
### Simulator startet nicht
```bash
# Prüfe ob Port bereits belegt
lsof -ti:1883
# Prüfe MQTT Broker
mosquitto_sub -h 172.16.2.16 -t '#' -C 1
```
### Keine State-Updates
```bash
# Prüfe Simulator-Log
tail -f /tmp/simulator.log
# Prüfe MQTT Topics
mosquitto_sub -h 172.16.2.16 -t 'vendor/#' -v
```
### API antwortet nicht
```bash
# Prüfe ob API läuft
curl http://localhost:8001/devices
# Prüfe API-Log
tail -f /tmp/api.log
```
## Integration
Der Simulator integriert sich nahtlos in das Home Automation System:
1. **Abstraction Layer** empfängt Commands und sendet sie an Simulator
2. **Simulator** reagiert und publiziert neuen State
3. **Abstraction Layer** empfängt State und publiziert zu Redis
4. **UI** empfängt Updates via SSE und aktualisiert Dashboard
Alle Komponenten arbeiten vollständig asynchron über MQTT.

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