Compare commits

...

36 Commits

Author SHA1 Message Date
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
25 changed files with 1446 additions and 737 deletions

View File

@@ -4,620 +4,50 @@ This module implements a registry-pattern for vendor-specific transformations:
- Each (device_type, technology, direction) tuple maps to a specific handler function - Each (device_type, technology, direction) tuple maps to a specific handler function
- Handlers transform payloads between abstract and vendor-specific formats - Handlers transform payloads between abstract and vendor-specific formats
- Unknown combinations fall back to pass-through (no transformation) - Unknown combinations fall back to pass-through (no transformation)
Vendor-specific implementations are in the vendors/ subdirectory.
""" """
import logging import logging
import json
from typing import Any, Callable from typing import Any, Callable
from apps.abstraction.vendors import (
simulator,
zigbee2mqtt,
max,
shelly,
tasmota,
hottis_pv_modbus,
hottis_wago_modbus,
hottis_wifi_relay,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ============================================================================
# HANDLER FUNCTIONS: simulator technology
# ============================================================================
def _transform_light_simulator_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_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]) -> 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_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]) -> 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)):
# Convert percentage (0-100) to zigbee2mqtt range (0-254)
vendor_payload["brightness"] = round(abstract_brightness * 254 / 100)
return json.dumps(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]) -> 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:
# zigbee2mqtt expects current_heating_setpoint as string
vendor_payload["current_heating_setpoint"] = str(payload["target"])
return json.dumps(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]) -> str:
"""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 json.dumps(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]) -> str:
"""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 json.dumps(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]) -> str:
"""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 json.dumps(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: relay - zigbee2mqtt technology
# ============================================================================
def _transform_relay_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> str:
"""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 json.dumps(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 - tasmota technology
# ============================================================================
def _transform_relay_tasmota_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Tasmota format.
Tasmota expects plain text 'on' or 'off' (not JSON).
- power: 'on'/'off' -> 'on'/'off' (plain string)
Example:
- Abstract: {'power': 'on'}
- Tasmota: 'on'
"""
power = payload.get("power", "off")
return power
def _transform_relay_tasmota_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Tasmota relay payload to abstract format.
Tasmota sends plain text 'on' or 'off' (not JSON).
- 'on'/'off' -> power: 'on'/'off'
Example:
- Tasmota: 'ON'
- Abstract: {'power': 'on'}
"""
return {"power": payload.strip().lower()}
# ============================================================================
# HANDLER FUNCTIONS: relay - hottis_pv_modbus technology
# ============================================================================
def _transform_relay_hottis_pv_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_pv_modbus_to_abstract(payload: str) -> dict[str, Any]:
def _transform_relay_hottis_pv_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis Modbus relay payload to abstract format.
Hottis Modbus sends JSON like:
{"status": "Ok", "timestamp": "...", "state": false, "cnt": 528}
We only care about the 'state' field:
- state: true -> power: 'on'
- state: false -> power: 'off'
"""
data = json.loads(payload)
state = data.get("state", False)
power = "on" if bool(state) else "off"
return {"power": payload.strip()}
# ============================================================================
# HANDLER FUNCTIONS: three_phase_powermeter - hottis_pv_modbus technology
# ============================================================================
def _transform_three_phase_powermeter_hottis_pv_modbus_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract three_phase_powermeter payload to hottis_pv_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 json.dumps(vendor_payload)
def _transform_three_phase_powermeter_hottis_pv_modbus_to_abstract(payload: str) -> dict[str, Any]:
"""Transform hottis_pv_modbus three_phase_powermeter payload to abstract format.
Transformations:
- Map vendor field names to abstract field names
- 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)
# Helper to read numeric values uniformly as float
def _get_float(key: str, default: float = 0.0) -> float:
return float(data.get(key, default))
# Read all numeric values via helper for consistent error handling
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")
abstract_payload = {
"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,
}
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 # 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] = { # Build registry from vendor modules
# Light transformations TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {}
("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", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_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_pv_modbus", "to_vendor"): _transform_relay_hottis_pv_modbus_to_vendor,
("relay", "hottis_pv_modbus", "to_abstract"): _transform_relay_hottis_pv_modbus_to_abstract,
("relay", "tasmota", "to_vendor"): _transform_relay_tasmota_to_vendor,
("relay", "tasmota", "to_abstract"): _transform_relay_tasmota_to_abstract,
# Three-Phase Powermeter transformations # Register handlers from each vendor module
("three_phase_powermeter", "hottis_pv_modbus", "to_vendor"): _transform_three_phase_powermeter_hottis_pv_modbus_to_vendor, for vendor_name, vendor_module in [
("three_phase_powermeter", "hottis_pv_modbus", "to_abstract"): _transform_three_phase_powermeter_hottis_pv_modbus_to_abstract, ("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),
]:
for (device_type, direction), handler in vendor_module.HANDLERS.items():
key = (device_type, vendor_name, direction)
TRANSFORM_HANDLERS[key] = handler
def _get_transform_handler( def _get_transform_handler(
@@ -656,7 +86,7 @@ def transform_abstract_to_vendor(
device_type: str, device_type: str,
device_technology: str, device_technology: str,
abstract_payload: dict[str, Any] abstract_payload: dict[str, Any]
) -> dict[str, Any]: ) -> str:
"""Transform abstract payload to vendor-specific format. """Transform abstract payload to vendor-specific format.
Args: Args:
@@ -665,7 +95,7 @@ def transform_abstract_to_vendor(
abstract_payload: Payload in abstract home protocol format abstract_payload: Payload in abstract home protocol format
Returns: Returns:
Payload in vendor-specific format Payload in vendor-specific format (as string)
""" """
logger.debug( logger.debug(
f"transform_abstract_to_vendor IN: type={device_type}, tech={device_technology}, " f"transform_abstract_to_vendor IN: type={device_type}, tech={device_technology}, "
@@ -692,7 +122,7 @@ def transform_vendor_to_abstract(
Args: Args:
device_type: Type of device (e.g., "light", "thermostat") device_type: Type of device (e.g., "light", "thermostat")
device_technology: Technology/vendor (e.g., "simulator", "zigbee2mqtt") 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: Returns:
Payload in abstract home protocol format 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,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 \ PYTHONUNBUFFERED=1 \
MQTT_BROKER=172.16.2.16 \ MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883 \ MQTT_PORT=1883 \
REDIS_HOST=localhost \ REDIS_HOST=172.23.1.116 \
REDIS_PORT=6379 \ REDIS_PORT=6379 \
REDIS_DB=0 \ REDIS_DB=8 \
REDIS_CHANNEL=ui:updates REDIS_CHANNEL=ui:updates
# Create non-root user # 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, ContactState,
TempHumidityState, TempHumidityState,
RelayState, 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) # Import resolvers (must be before router imports to avoid circular dependency)
from apps.api.resolvers import ( from apps.api.resolvers import (
DeviceDTO, DeviceDTO,
@@ -99,30 +101,6 @@ async def get_device_state(device_id: str):
except KeyError: except KeyError:
raise HTTPException(status_code=404, detail="Device state not found") raise HTTPException(status_code=404, detail="Device state not found")
# --- Minimal-invasive: Einzelgerät-Layout-Endpunkt ---
@app.get("/devices/{device_id}/layout")
async def get_device_layout(device_id: str):
"""Gibt die layout-spezifischen Informationen für ein einzelnes Gerät zurück."""
layout = load_layout()
for room in layout.get("rooms", []):
for device in room.get("devices", []):
if device.get("device_id") == device_id:
# Rückgabe: Layout-Infos + Raumname
return {
"device_id": device_id,
"room": room.get("name"),
"title": device.get("title"),
"icon": device.get("icon"),
"rank": device.get("rank"),
}
raise HTTPException(status_code=404, detail="Device layout not found")
@app.on_event("startup")
async def startup_event():
"""Include routers after app is initialized to avoid circular imports."""
from apps.api.routes.groups_scenes import router as groups_scenes_router
app.include_router(groups_scenes_router, prefix="")
@app.get("/health") @app.get("/health")
async def health() -> dict[str, str]: async def health() -> dict[str, str]:
@@ -188,6 +166,21 @@ async def redis_state_listener():
async def startup_event(): async def startup_event():
"""Start background tasks on application startup.""" """Start background tasks on application startup."""
global background_task 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()) background_task = asyncio.create_task(redis_state_listener())
logger.info("Started background Redis state listener") logger.info("Started background Redis state listener")
@@ -235,32 +228,11 @@ class DeviceInfo(BaseModel):
device_id: str device_id: str
type: str type: str
name: str name: str
homekit_aid: int
features: dict[str, Any] = {} features: dict[str, Any] = {}
# Configuration helpers # 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]: def get_mqtt_settings() -> tuple[str, int]:
"""Get MQTT broker settings from environment. """Get MQTT broker settings from environment.
@@ -388,6 +360,7 @@ async def get_device(device_id: str) -> DeviceInfo:
device_id=device["device_id"], device_id=device["device_id"],
type=device["type"], type=device["type"],
name=device.get("name", device["device_id"]), name=device.get("name", device["device_id"]),
homekit_aid=device["homekit_aid"],
features=device.get("features", {}) features=device.get("features", {})
) )
@@ -406,6 +379,7 @@ async def get_devices() -> list[DeviceInfo]:
device_id=device["device_id"], device_id=device["device_id"],
type=device["type"], type=device["type"],
name=device.get("name", device["device_id"]), name=device.get("name", device["device_id"]),
homekit_aid=device["homekit_aid"],
features=device.get("features", {}) features=device.get("features", {})
) )
for device in devices for device in devices

View File

@@ -4,12 +4,12 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Any, TypedDict from typing import Any, TypedDict
from apps.api.config import load_layout
from packages.home_capabilities import ( from packages.home_capabilities import (
GroupConfig, GroupConfig,
GroupsConfigRoot, GroupsConfigRoot,
SceneStep, SceneStep,
get_group_by_id, get_group_by_id,
load_layout,
) )
logger = logging.getLogger(__name__) 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
}

View File

@@ -2,6 +2,7 @@ FROM python:3.12-slim
# Environment defaults (can be overridden at runtime) # Environment defaults (can be overridden at runtime)
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
LOG_LEVEL="INFO" \
HOMEKIT_NAME="Home Automation Bridge" \ HOMEKIT_NAME="Home Automation Bridge" \
HOMEKIT_PIN="031-45-154" \ HOMEKIT_PIN="031-45-154" \
HOMEKIT_PORT="51826" \ HOMEKIT_PORT="51826" \

View File

@@ -18,6 +18,7 @@ class Device:
device_id: str device_id: str
type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover" type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover"
name: str # Short name from /devices name: str # Short name from /devices
homekit_aid: int # HomeKit Accessory ID
features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true}) features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true})
read_only: bool # True for sensors that don't accept commands read_only: bool # True for sensors that don't accept commands
@@ -57,6 +58,12 @@ class DeviceRegistry:
logger.warning(f"Device without device_id: {dev_data}") logger.warning(f"Device without device_id: {dev_data}")
continue continue
# 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) # Determine if read-only (sensors don't accept set commands)
device_type = dev_data.get('type', '') device_type = dev_data.get('type', '')
read_only = device_type in ['contact', 'temp_humidity', 'motion', 'smoke'] read_only = device_type in ['contact', 'temp_humidity', 'motion', 'smoke']
@@ -65,6 +72,7 @@ class DeviceRegistry:
device_id=device_id, device_id=device_id,
type=device_type, type=device_type,
name=device_id, name=device_id,
homekit_aid=homekit_aid,
features=dev_data.get('features', {}), features=dev_data.get('features', {}),
read_only=read_only read_only=read_only
) )

View File

@@ -1,12 +1,16 @@
services: services:
homekit-bridge: homekit-bridge:
image: gitea.hottis.de/wn/home-automation/homekit:0.5.0 image: gitea.hottis.de/wn/home-automation/homekit:0.5.0
build:
context: ../../
dockerfile: apps/homekit/Dockerfile
container_name: homekit-bridge container_name: homekit-bridge
# Required for mDNS/Bonjour to work properly # Required for mDNS/Bonjour to work properly
network_mode: host network_mode: host
environment: environment:
- LOG_LEVEL=INFO
- HOMEKIT_NAME=Hottis Home Automation Bridge - HOMEKIT_NAME=Hottis Home Automation Bridge
- HOMEKIT_PIN=031-45-154 - HOMEKIT_PIN=031-45-154
- HOMEKIT_PORT=51826 - HOMEKIT_PORT=51826

View File

@@ -31,8 +31,9 @@ from .api_client import ApiClient
from .device_registry import DeviceRegistry from .device_registry import DeviceRegistry
# Configure logging # Configure logging
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=getattr(logging, LOG_LEVEL, logging.INFO),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -71,9 +72,11 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
try: try:
accessory = create_accessory_for_device(device, api_client, driver) accessory = create_accessory_for_device(device, api_client, driver)
if accessory: if accessory:
# Set AID from device configuration
accessory.aid = device.homekit_aid
bridge.add_accessory(accessory) bridge.add_accessory(accessory)
accessory_map[device.device_id] = accessory accessory_map[device.device_id] = accessory
logger.info(f"Added accessory: {device.name} ({device.type}, {accessory.__class__.__name__})") logger.info(f"Added accessory: {device.name} ({device.type}, AID={device.homekit_aid}, {accessory.__class__.__name__})")
else: else:
logger.warning(f"No accessory mapping for device: {device.name} ({device.type})") logger.warning(f"No accessory mapping for device: {device.name} ({device.type})")
except Exception as e: except Exception as e:

View File

@@ -5,6 +5,7 @@ import json
import logging import logging
import os import os
import signal import signal
import uuid
from typing import Any from typing import Any
from aiomqtt import Client, Message from aiomqtt import Client, Message
@@ -163,7 +164,7 @@ async def mqtt_worker(shutdown_event: asyncio.Event) -> None:
async with Client( async with Client(
hostname=broker, hostname=broker,
port=port, port=port,
identifier="pulsegen" identifier=f"pulsegen-{uuid.uuid4()}",
) as client: ) as client:
logger.info("Connected to MQTT broker") logger.info("Connected to MQTT broker")
@@ -175,14 +176,19 @@ async def mqtt_worker(shutdown_event: asyncio.Event) -> None:
# Publish startup message # Publish startup message
await publish_example(client) await publish_example(client)
# Message loop # Message loop with timeout to allow shutdown check
async for message in client.messages: async for message in client.messages:
if shutdown_event.is_set(): if shutdown_event.is_set():
logger.info("Shutdown event detected, breaking message loop")
break break
try: try:
await handle_message(message, client) await handle_message(message, client)
except Exception as e: except Exception as e:
logger.error(f"Error in message handler: {e}", exc_info=True) 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: except asyncio.CancelledError:
logger.info("MQTT worker cancelled") logger.info("MQTT worker cancelled")
@@ -216,8 +222,17 @@ async def main() -> None:
# Wait for shutdown signal # Wait for shutdown signal
await shutdown_event.wait() await shutdown_event.wait()
# Wait for worker to finish # Give worker a moment to finish gracefully
await worker_task 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") logger.info("Pulsegen application stopped")

View File

@@ -312,7 +312,8 @@
// Device IDs for garage devices // Device IDs for garage devices
const GARAGE_DEVICES = [ const GARAGE_DEVICES = [
'power_relay_caroutlet', 'power_relay_caroutlet',
'powermeter_caroutlet' 'powermeter_caroutlet',
'sensor_caroutlet'
]; ];
// Device states // Device states
@@ -410,7 +411,17 @@
renderOutletControls(controlSection, device); renderOutletControls(controlSection, device);
container.appendChild(controlSection); 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'); const powermeterDevice = Object.values(devicesData).find(d => d.device_id === 'powermeter_caroutlet');
if (powermeterDevice) { if (powermeterDevice) {
const powermeterSection = document.createElement('div'); const powermeterSection = document.createElement('div');
@@ -424,7 +435,6 @@
function renderOutletControls(container, device) { function renderOutletControls(container, device) {
const controlGroup = document.createElement('div'); const controlGroup = document.createElement('div');
controlGroup.style.textAlign = 'center'; controlGroup.style.textAlign = 'center';
// controlGroup.style.marginBottom = '8px';
const state = deviceStates[device.device_id]; const state = deviceStates[device.device_id];
const currentPower = state?.power === 'on'; const currentPower = state?.power === 'on';
@@ -440,36 +450,36 @@
label.className = 'toggle-label'; label.className = 'toggle-label';
label.textContent = currentPower ? 'Ein' : 'Aus'; 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(toggleSwitch);
controlGroup.appendChild(label); controlGroup.appendChild(label);
// controlGroup.appendChild(stateDisplay);
container.appendChild(controlGroup); 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) { function renderThreePhasePowerDisplay(container, device) {
const state = deviceStates[device.device_id] || {}; 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'); const overviewGrid = document.createElement('div');
overviewGrid.className = 'state-grid'; overviewGrid.className = 'state-grid';
overviewGrid.innerHTML = ` overviewGrid.innerHTML = `
@@ -484,16 +494,13 @@
`; `;
container.appendChild(overviewGrid); container.appendChild(overviewGrid);
// Phasen Title
const phaseTitle = document.createElement('h4'); const phaseTitle = document.createElement('h4');
phaseTitle.style.margin = '20px 0 8px 0'; phaseTitle.style.margin = '20px 0 8px 0';
phaseTitle.style.fontSize = '16px'; phaseTitle.style.fontSize = '16px';
phaseTitle.style.fontWeight = '600'; phaseTitle.style.fontWeight = '600';
phaseTitle.style.color = '#333'; phaseTitle.style.color = '#333';
// phaseTitle.textContent = 'Phasen';
container.appendChild(phaseTitle); container.appendChild(phaseTitle);
// Phasen Details
const phaseGrid = document.createElement('div'); const phaseGrid = document.createElement('div');
phaseGrid.className = 'phase-grid'; phaseGrid.className = 'phase-grid';
phaseGrid.innerHTML = ` phaseGrid.innerHTML = `
@@ -601,12 +608,14 @@
const state = deviceStates[deviceId]; const state = deviceStates[deviceId];
console.log(`Updating UI for ${deviceId}:`, state); console.log(`Updating UI for ${deviceId}:`, state);
switch (device.type) { switch (deviceId) {
case 'relay': case 'power_relay_caroutlet':
case 'outlet':
updateOutletUI(deviceId, state); updateOutletUI(deviceId, state);
break; break;
case 'three_phase_powermeter': case 'sensor_caroutlet':
updateFeedbackDisplay(deviceId, state);
break;
case 'powermeter_caroutlet':
updateThreePhasePowerUI(deviceId, state); updateThreePhasePowerUI(deviceId, state);
break; break;
} }
@@ -637,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) { function updateThreePhasePowerUI(deviceId, state) {
// Update overview // Update overview
const totalPower = document.getElementById(`total-power-${deviceId}`); const totalPower = document.getElementById(`total-power-${deviceId}`);

View File

@@ -1,6 +1,7 @@
version: 1 version: 1
devices: devices:
- device_id: lampe_semeniere_wohnzimmer - device_id: lampe_semeniere_wohnzimmer
homekit_aid: 2
name: Semeniere name: Semeniere
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -16,6 +17,7 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: stehlampe_esszimmer_spiegel - device_id: stehlampe_esszimmer_spiegel
homekit_aid: 3
name: Stehlampe Spiegel name: Stehlampe Spiegel
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -27,6 +29,7 @@ devices:
state: "zigbee2mqtt/0x001788010d06ea09" state: "zigbee2mqtt/0x001788010d06ea09"
set: "zigbee2mqtt/0x001788010d06ea09/set" set: "zigbee2mqtt/0x001788010d06ea09/set"
- device_id: stehlampe_esszimmer_schrank - device_id: stehlampe_esszimmer_schrank
homekit_aid: 4
name: Stehlampe Schrank name: Stehlampe Schrank
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -38,6 +41,7 @@ devices:
state: "zigbee2mqtt/0x001788010d09176c" state: "zigbee2mqtt/0x001788010d09176c"
set: "zigbee2mqtt/0x001788010d09176c/set" set: "zigbee2mqtt/0x001788010d09176c/set"
- device_id: grosse_lampe_wohnzimmer - device_id: grosse_lampe_wohnzimmer
homekit_aid: 5
name: grosse Lampe name: grosse Lampe
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -53,6 +57,7 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: lampe_naehtischchen_wohnzimmer - device_id: lampe_naehtischchen_wohnzimmer
homekit_aid: 6
name: Nähtischchen name: Nähtischchen
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -68,6 +73,7 @@ devices:
model: "HG06337" model: "HG06337"
vendor: "Lidl" vendor: "Lidl"
- device_id: kleine_lampe_links_esszimmer - device_id: kleine_lampe_links_esszimmer
homekit_aid: 7
name: kleine Lampe name: kleine Lampe
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -83,6 +89,7 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: leselampe_esszimmer - device_id: leselampe_esszimmer
homekit_aid: 8
name: Leselampe name: Leselampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -99,6 +106,7 @@ devices:
model: "LED1842G3" model: "LED1842G3"
vendor: "IKEA" vendor: "IKEA"
- device_id: medusalampe_schlafzimmer - device_id: medusalampe_schlafzimmer
homekit_aid: 9
name: Medusa-Lampe name: Medusa-Lampe
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -114,6 +122,7 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: sportlicht_am_fernseher_studierzimmer - device_id: sportlicht_am_fernseher_studierzimmer
homekit_aid: 10
type: light type: light
name: am Fernseher name: am Fernseher
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -131,6 +140,7 @@ devices:
model: "LED1733G7" model: "LED1733G7"
vendor: "IKEA" vendor: "IKEA"
- device_id: deckenlampe_schlafzimmer - device_id: deckenlampe_schlafzimmer
homekit_aid: 11
name: Deckenlampe name: Deckenlampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -147,6 +157,7 @@ devices:
model: "8718699688882" model: "8718699688882"
vendor: "Philips" vendor: "Philips"
- device_id: bettlicht_wolfgang - device_id: bettlicht_wolfgang
homekit_aid: 12
name: Bettlicht Wolfgang name: Bettlicht Wolfgang
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -163,6 +174,7 @@ devices:
model: "9290020399" model: "9290020399"
vendor: "Philips" vendor: "Philips"
- device_id: bettlicht_patty - device_id: bettlicht_patty
homekit_aid: 13
name: Bettlicht Patty name: Bettlicht Patty
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -179,6 +191,7 @@ devices:
model: "9290020399" model: "9290020399"
vendor: "Philips" vendor: "Philips"
- device_id: schranklicht_hinten_patty - device_id: schranklicht_hinten_patty
homekit_aid: 14
name: Schranklicht hinten name: Schranklicht hinten
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -195,6 +208,7 @@ devices:
model: "8718699673147" model: "8718699673147"
vendor: "Philips" vendor: "Philips"
- device_id: schranklicht_vorne_patty - device_id: schranklicht_vorne_patty
homekit_aid: 15
name: Schranklicht vorne name: Schranklicht vorne
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -210,6 +224,7 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: leselampe_patty - device_id: leselampe_patty
homekit_aid: 16
name: Leselampe name: Leselampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -226,6 +241,7 @@ devices:
model: "8718699673147" model: "8718699673147"
vendor: "Philips" vendor: "Philips"
- device_id: deckenlampe_esszimmer - device_id: deckenlampe_esszimmer
homekit_aid: 17
name: Deckenlampe name: Deckenlampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -242,6 +258,7 @@ devices:
model: "929002241201" model: "929002241201"
vendor: "Philips" vendor: "Philips"
- device_id: deckenlampe_flur_oben - device_id: deckenlampe_flur_oben
homekit_aid: 18
name: Deckenlampe oben name: Deckenlampe oben
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -259,6 +276,7 @@ devices:
model: "929003099001" model: "929003099001"
vendor: "Philips" vendor: "Philips"
- device_id: kueche_deckenlampe - device_id: kueche_deckenlampe
homekit_aid: 19
name: Deckenlampe name: Deckenlampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -275,6 +293,7 @@ devices:
model: "929002469202" model: "929002469202"
vendor: "Philips" vendor: "Philips"
- device_id: sportlicht_tisch - device_id: sportlicht_tisch
homekit_aid: 20
name: am Tisch name: am Tisch
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -291,6 +310,7 @@ devices:
model: "4058075729063" model: "4058075729063"
vendor: "LEDVANCE" vendor: "LEDVANCE"
- device_id: sportlicht_regal - device_id: sportlicht_regal
homekit_aid: 21
name: am Regal name: am Regal
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -307,6 +327,7 @@ devices:
model: "4058075729063" model: "4058075729063"
vendor: "LEDVANCE" vendor: "LEDVANCE"
- device_id: licht_flur_oben_am_spiegel - device_id: licht_flur_oben_am_spiegel
homekit_aid: 22
name: Spiegel name: Spiegel
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -324,6 +345,7 @@ devices:
model: "LED1732G11" model: "LED1732G11"
vendor: "IKEA" vendor: "IKEA"
- device_id: experimentlabtest - device_id: experimentlabtest
homekit_aid: 23
name: Test Lampe name: Test Lampe
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -340,6 +362,7 @@ devices:
model: "4058075208421" model: "4058075208421"
vendor: "LEDVANCE" vendor: "LEDVANCE"
- device_id: thermostat_wolfgang - device_id: thermostat_wolfgang
homekit_aid: 24
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
@@ -359,6 +382,7 @@ devices:
model: "GS361A-H04" model: "GS361A-H04"
vendor: "Siterwell" vendor: "Siterwell"
- device_id: thermostat_kueche - device_id: thermostat_kueche
homekit_aid: 25
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
@@ -378,6 +402,7 @@ devices:
model: "GS361A-H04" model: "GS361A-H04"
vendor: "Siterwell" vendor: "Siterwell"
- device_id: thermostat_schlafzimmer - device_id: thermostat_schlafzimmer
homekit_aid: 26
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
@@ -397,6 +422,7 @@ devices:
peer_id: "42" peer_id: "42"
channel: "1" channel: "1"
- device_id: thermostat_esszimmer - device_id: thermostat_esszimmer
homekit_aid: 27
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
@@ -416,6 +442,7 @@ devices:
peer_id: "45" peer_id: "45"
channel: "1" channel: "1"
- device_id: thermostat_wohnzimmer - device_id: thermostat_wohnzimmer
homekit_aid: 28
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
@@ -435,6 +462,7 @@ devices:
peer_id: "46" peer_id: "46"
channel: "1" channel: "1"
- device_id: thermostat_patty - device_id: thermostat_patty
homekit_aid: 29
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
@@ -454,6 +482,7 @@ devices:
peer_id: "39" peer_id: "39"
channel: "1" channel: "1"
- device_id: thermostat_bad_oben - device_id: thermostat_bad_oben
homekit_aid: 30
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
@@ -473,6 +502,7 @@ devices:
peer_id: "41" peer_id: "41"
channel: "1" channel: "1"
- device_id: thermostat_bad_unten - device_id: thermostat_bad_unten
homekit_aid: 31
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
@@ -492,6 +522,7 @@ devices:
peer_id: "48" peer_id: "48"
channel: "1" channel: "1"
- device_id: sterne_wohnzimmer - device_id: sterne_wohnzimmer
homekit_aid: 32
name: Sterne name: Sterne
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -507,6 +538,7 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: kontakt_schlafzimmer_strasse - device_id: kontakt_schlafzimmer_strasse
homekit_aid: 33
name: Fenster name: Fenster
type: contact type: contact
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -515,6 +547,7 @@ devices:
state: homegear/instance1/plain/52/1/STATE state: homegear/instance1/plain/52/1/STATE
features: {} features: {}
- device_id: kontakt_esszimmer_strasse_rechts - device_id: kontakt_esszimmer_strasse_rechts
homekit_aid: 34
type: contact type: contact
name: Fenster rechts name: Fenster rechts
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -523,6 +556,7 @@ devices:
state: homegear/instance1/plain/26/1/STATE state: homegear/instance1/plain/26/1/STATE
features: {} features: {}
- device_id: kontakt_esszimmer_strasse_links - device_id: kontakt_esszimmer_strasse_links
homekit_aid: 35
name: Fenster links name: Fenster links
type: contact type: contact
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -531,6 +565,7 @@ devices:
state: homegear/instance1/plain/27/1/STATE state: homegear/instance1/plain/27/1/STATE
features: {} features: {}
- device_id: kontakt_wohnzimmer_garten_rechts - device_id: kontakt_wohnzimmer_garten_rechts
homekit_aid: 36
name: Fenster rechts name: Fenster rechts
type: contact type: contact
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -539,6 +574,7 @@ devices:
state: homegear/instance1/plain/28/1/STATE state: homegear/instance1/plain/28/1/STATE
features: {} features: {}
- device_id: kontakt_wohnzimmer_garten_links - device_id: kontakt_wohnzimmer_garten_links
homekit_aid: 37
name: Fenster links name: Fenster links
type: contact type: contact
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -547,6 +583,7 @@ devices:
state: homegear/instance1/plain/29/1/STATE state: homegear/instance1/plain/29/1/STATE
features: {} features: {}
- device_id: kontakt_kueche_garten_fenster - device_id: kontakt_kueche_garten_fenster
homekit_aid: 38
name: Fenster Garten name: Fenster Garten
type: contact type: contact
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -555,6 +592,7 @@ devices:
state: zigbee2mqtt/0x00158d008b332785 state: zigbee2mqtt/0x00158d008b332785
features: {} features: {}
- device_id: kontakt_kueche_garten_tuer - device_id: kontakt_kueche_garten_tuer
homekit_aid: 39
type: contact type: contact
name: Terrassentür name: Terrassentür
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -563,6 +601,7 @@ devices:
state: zigbee2mqtt/0x00158d008b332788 state: zigbee2mqtt/0x00158d008b332788
features: {} features: {}
- device_id: kontakt_kueche_strasse_rechts - device_id: kontakt_kueche_strasse_rechts
homekit_aid: 40
name: Fenster Straße rechts name: Fenster Straße rechts
type: contact type: contact
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -571,6 +610,7 @@ devices:
state: zigbee2mqtt/0x00158d008b151803 state: zigbee2mqtt/0x00158d008b151803
features: {} features: {}
- device_id: kontakt_kueche_strasse_links - device_id: kontakt_kueche_strasse_links
homekit_aid: 41
name: Fenster Straße links name: Fenster Straße links
type: contact type: contact
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -579,6 +619,7 @@ devices:
state: zigbee2mqtt/0x00158d008b331d0b state: zigbee2mqtt/0x00158d008b331d0b
features: {} features: {}
- device_id: kontakt_patty_garten_rechts - device_id: kontakt_patty_garten_rechts
homekit_aid: 42
type: contact type: contact
name: Fenster Garten rechts name: Fenster Garten rechts
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -587,6 +628,8 @@ devices:
state: homegear/instance1/plain/18/1/STATE state: homegear/instance1/plain/18/1/STATE
features: {} features: {}
- device_id: kontakt_patty_garten_links - device_id: kontakt_patty_garten_links
homekit_aid: 43
homekit_aid: 43
type: contact type: contact
name: Fenster Garten links name: Fenster Garten links
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -595,6 +638,7 @@ devices:
state: homegear/instance1/plain/22/1/STATE state: homegear/instance1/plain/22/1/STATE
features: {} features: {}
- device_id: kontakt_patty_strasse - device_id: kontakt_patty_strasse
homekit_aid: 44
type: contact type: contact
name: Fenster Straße name: Fenster Straße
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -603,6 +647,7 @@ devices:
state: zigbee2mqtt/0x00158d000af457cf state: zigbee2mqtt/0x00158d000af457cf
features: {} features: {}
- device_id: kontakt_wolfgang_garten - device_id: kontakt_wolfgang_garten
homekit_aid: 45
type: contact type: contact
name: Fenster name: Fenster
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -611,6 +656,7 @@ devices:
state: zigbee2mqtt/0x00158d008b3328da state: zigbee2mqtt/0x00158d008b3328da
features: {} features: {}
- device_id: kontakt_bad_oben_strasse - device_id: kontakt_bad_oben_strasse
homekit_aid: 46
type: contact type: contact
name: Fenster name: Fenster
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -619,6 +665,7 @@ devices:
state: zigbee2mqtt/0x00158d008b333aec state: zigbee2mqtt/0x00158d008b333aec
features: {} features: {}
- device_id: kontakt_bad_unten_strasse - device_id: kontakt_bad_unten_strasse
homekit_aid: 47
type: contact type: contact
name: Fenster name: Fenster
cap_version: contact_sensor@1.0.0 cap_version: contact_sensor@1.0.0
@@ -627,6 +674,7 @@ devices:
state: homegear/instance1/plain/44/1/STATE state: homegear/instance1/plain/44/1/STATE
features: {} features: {}
- device_id: sensor_schlafzimmer - device_id: sensor_schlafzimmer
homekit_aid: 48
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -635,6 +683,7 @@ devices:
state: zigbee2mqtt/0x00158d00043292dc state: zigbee2mqtt/0x00158d00043292dc
features: {} features: {}
- device_id: sensor_wohnzimmer - device_id: sensor_wohnzimmer
homekit_aid: 49
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -643,6 +692,7 @@ devices:
state: zigbee2mqtt/0x00158d0008975707 state: zigbee2mqtt/0x00158d0008975707
features: {} features: {}
- device_id: sensor_kueche - device_id: sensor_kueche
homekit_aid: 50
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -651,6 +701,7 @@ devices:
state: zigbee2mqtt/0x00158d00083299bb state: zigbee2mqtt/0x00158d00083299bb
features: {} features: {}
- device_id: sensor_arbeitszimmer_patty - device_id: sensor_arbeitszimmer_patty
homekit_aid: 51
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -659,6 +710,7 @@ devices:
state: zigbee2mqtt/0x00158d0003f052b7 state: zigbee2mqtt/0x00158d0003f052b7
features: {} features: {}
- device_id: sensor_arbeitszimmer_wolfgang - device_id: sensor_arbeitszimmer_wolfgang
homekit_aid: 52
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -667,6 +719,7 @@ devices:
state: zigbee2mqtt/0x00158d000543fb99 state: zigbee2mqtt/0x00158d000543fb99
features: {} features: {}
- device_id: sensor_bad_oben - device_id: sensor_bad_oben
homekit_aid: 53
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -675,6 +728,7 @@ devices:
state: zigbee2mqtt/0x00158d00093e8987 state: zigbee2mqtt/0x00158d00093e8987
features: {} features: {}
- device_id: sensor_bad_unten - device_id: sensor_bad_unten
homekit_aid: 54
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -683,6 +737,7 @@ devices:
state: zigbee2mqtt/0x00158d00093e662a state: zigbee2mqtt/0x00158d00093e662a
features: {} features: {}
- device_id: sensor_flur - device_id: sensor_flur
homekit_aid: 55
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -691,6 +746,7 @@ devices:
state: zigbee2mqtt/0x00158d000836ccc6 state: zigbee2mqtt/0x00158d000836ccc6
features: {} features: {}
- device_id: sensor_waschkueche - device_id: sensor_waschkueche
homekit_aid: 56
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -699,6 +755,7 @@ devices:
state: zigbee2mqtt/0x00158d000449f3bc state: zigbee2mqtt/0x00158d000449f3bc
features: {} features: {}
- device_id: sensor_sportzimmer - device_id: sensor_sportzimmer
homekit_aid: 57
type: temp_humidity_sensor type: temp_humidity_sensor
name: Thermometer name: Thermometer
cap_version: temp_humidity_sensor@1.0.0 cap_version: temp_humidity_sensor@1.0.0
@@ -707,6 +764,7 @@ devices:
state: zigbee2mqtt/0x00158d0009421422 state: zigbee2mqtt/0x00158d0009421422
features: {} features: {}
- device_id: licht_spuele_kueche - device_id: licht_spuele_kueche
homekit_aid: 58
name: Spüle name: Spüle
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -717,6 +775,7 @@ devices:
set: "shellies/shellyplug-s-DED4E4/relay/0/command" set: "shellies/shellyplug-s-DED4E4/relay/0/command"
state: "shellies/shellyplug-s-DED4E4/relay/0" state: "shellies/shellyplug-s-DED4E4/relay/0"
- device_id: putzlicht_kueche - device_id: putzlicht_kueche
homekit_aid: 59
name: Putzlicht name: Putzlicht
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -728,6 +787,7 @@ devices:
state: "zigbee2mqtt/0xa4c138563834406c" state: "zigbee2mqtt/0xa4c138563834406c"
set: "zigbee2mqtt/0xa4c138563834406c/set" set: "zigbee2mqtt/0xa4c138563834406c/set"
- device_id: licht_schrank_esszimmer - device_id: licht_schrank_esszimmer
homekit_aid: 60
name: Schrank name: Schrank
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -738,6 +798,7 @@ devices:
set: "shellies/schrankesszimmer/relay/0/command" set: "shellies/schrankesszimmer/relay/0/command"
state: "shellies/schrankesszimmer/relay/0" state: "shellies/schrankesszimmer/relay/0"
- device_id: licht_regal_wohnzimmer - device_id: licht_regal_wohnzimmer
homekit_aid: 61
type: relay type: relay
name: Regal name: Regal
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -747,17 +808,8 @@ devices:
topics: topics:
set: "shellies/wohnzimmer-regal/relay/0/command" set: "shellies/wohnzimmer-regal/relay/0/command"
state: "shellies/wohnzimmer-regal/relay/0" 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 - device_id: licht_terasse
homekit_aid: 62
name: Terrasse name: Terrasse
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -768,6 +820,7 @@ devices:
set: "shellies/lichtterasse/relay/0/command" set: "shellies/lichtterasse/relay/0/command"
state: "shellies/lichtterasse/relay/0" state: "shellies/lichtterasse/relay/0"
- device_id: kugellampe_patty - device_id: kugellampe_patty
homekit_aid: 63
name: Kugellampe Patty name: Kugellampe Patty
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -779,6 +832,7 @@ devices:
state: "zigbee2mqtt/0xbc33acfffe21f547" state: "zigbee2mqtt/0xbc33acfffe21f547"
set: "zigbee2mqtt/0xbc33acfffe21f547/set" set: "zigbee2mqtt/0xbc33acfffe21f547/set"
- device_id: kueche_fensterbank_licht - device_id: kueche_fensterbank_licht
homekit_aid: 64
name: Fensterbank Küche name: Fensterbank Küche
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
@@ -790,6 +844,7 @@ devices:
state: "zigbee2mqtt/0xf0d1b8000017515d" state: "zigbee2mqtt/0xf0d1b8000017515d"
set: "zigbee2mqtt/0xf0d1b8000017515d/set" set: "zigbee2mqtt/0xf0d1b8000017515d/set"
- device_id: licht_kommode_schlafzimmer - device_id: licht_kommode_schlafzimmer
homekit_aid: 65
name: Kommode Schlafzimmer name: Kommode Schlafzimmer
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -800,6 +855,7 @@ devices:
set: "cmnd/tasmota/04/POWER" set: "cmnd/tasmota/04/POWER"
state: "stat/tasmota/04/POWER" state: "stat/tasmota/04/POWER"
- device_id: licht_fensterbank_esszimmer - device_id: licht_fensterbank_esszimmer
homekit_aid: 66
name: Fensterbank Esszimmer name: Fensterbank Esszimmer
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -810,6 +866,7 @@ devices:
set: "cmnd/tasmota/02/POWER" set: "cmnd/tasmota/02/POWER"
state: "stat/tasmota/02/POWER" state: "stat/tasmota/02/POWER"
- device_id: licht_schreibtisch_patty - device_id: licht_schreibtisch_patty
homekit_aid: 67
name: Schreibtisch Patty name: Schreibtisch Patty
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -820,6 +877,7 @@ devices:
set: "cmnd/tasmota/03/POWER" set: "cmnd/tasmota/03/POWER"
state: "stat/tasmota/03/POWER" state: "stat/tasmota/03/POWER"
- device_id: kugeln_regal_flur - device_id: kugeln_regal_flur
homekit_aid: 68
name: Kugeln Regal Flur name: Kugeln Regal Flur
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -829,7 +887,8 @@ devices:
topics: topics:
set: "cmnd/tasmota/01/POWER" set: "cmnd/tasmota/01/POWER"
state: "stat/tasmota/01/POWER" state: "stat/tasmota/01/POWER"
- device_id: schrank_flur_haustür - device_id: schrank_flur_haustuer
homekit_aid: 69
name: Schrank Flur Haustür name: Schrank Flur Haustür
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -840,6 +899,7 @@ devices:
set: "cmnd/tasmota/05/POWER" set: "cmnd/tasmota/05/POWER"
state: "stat/tasmota/05/POWER" state: "stat/tasmota/05/POWER"
- device_id: gartenlicht_vorne - device_id: gartenlicht_vorne
homekit_aid: 70
name: Gartenlicht vorne name: Gartenlicht vorne
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -851,6 +911,7 @@ devices:
state: "stat/tasmota/06/POWER" state: "stat/tasmota/06/POWER"
- device_id: power_relay_caroutlet - device_id: power_relay_caroutlet
homekit_aid: 71
name: Car Outlet name: Car Outlet
type: relay type: relay
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -860,16 +921,25 @@ devices:
topics: topics:
set: "IoT/Car/Control" set: "IoT/Car/Control"
state: "IoT/Car/Control/State" state: "IoT/Car/Control/State"
- device_id: powermeter_caroutlet - device_id: powermeter_caroutlet
homekit_aid: 72
name: Car Outlet name: Car Outlet
type: three_phase_powermeter type: three_phase_powermeter
cap_version: "three_phase_powermeter@1.0.0" cap_version: "three_phase_powermeter@1.0.0"
technology: hottis_pv_modbus technology: hottis_pv_modbus
topics: topics:
state: "IoT/Car/Values" 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 - device_id: schranklicht_flur_vor_kueche
homekit_aid: 74
name: Schranklicht Flur vor Küche name: Schranklicht Flur vor Küche
type: light type: light
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -880,6 +950,7 @@ devices:
state: "zigbee2mqtt/0xf0d1b80000155a1f" state: "zigbee2mqtt/0xf0d1b80000155a1f"
set: "zigbee2mqtt/0xf0d1b80000155a1f/set" set: "zigbee2mqtt/0xf0d1b80000155a1f/set"
- device_id: deckenlampe_wohnzimmer - device_id: deckenlampe_wohnzimmer
homekit_aid: 75
name: Deckenlampe Wohnzimmer name: Deckenlampe Wohnzimmer
type: light type: light
cap_version: "relay@1.0.0" cap_version: "relay@1.0.0"
@@ -892,3 +963,82 @@ devices:
set: "zigbee2mqtt/0x842e14fffea72027/set" 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"

View File

@@ -1,10 +1,12 @@
version: 1 version: 1
groups: groups:
- id: "kueche_lichter" - id: "kueche_lichter"
name: "Küche alle Lampen" name: "Küche alle Lampen ausser Putzlicht"
selector: device_ids:
type: "light" - deckenlampe_kueche
room: "Küche" - licht_spuele_kueche
- herdlicht
- kueche_fensterbank_licht
capabilities: capabilities:
power: true power: true
brightness: true brightness: true
@@ -16,21 +18,25 @@ groups:
capabilities: capabilities:
power: true power: true
- id: "schlafzimmer_lichter"
name: "Schlafzimmer alle Lampen"
selector:
type: "light"
room: "Schlafzimmer"
capabilities:
power: true
brightness: true
- id: "schlafzimmer_schlummer_licht" - id: "schlafzimmer_schlummer_licht"
name: "Schlafzimmer Schlummerlicht" name: "Schlafzimmer Schlummerlicht"
device_ids: device_ids:
- bettlicht_patty - bettlicht_patty
- bettlicht_wolfgang - bettlicht_wolfgang
- medusalampe_schlafzimmer - 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: capabilities:
power: true power: true
brightness: true brightness: true

View File

@@ -1,5 +1,6 @@
rooms: rooms:
- name: Schlafzimmer - id: schlafzimmer
name: Schlafzimmer
devices: devices:
- device_id: bettlicht_patty - device_id: bettlicht_patty
title: Bettlicht Patty title: Bettlicht Patty
@@ -33,7 +34,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 47 rank: 47
- name: Esszimmer - id: esszimmer
name: Esszimmer
devices: devices:
- device_id: deckenlampe_esszimmer - device_id: deckenlampe_esszimmer
title: Deckenlampe Esszimmer title: Deckenlampe Esszimmer
@@ -59,10 +61,10 @@ rooms:
title: Stehlampe Esszimmer Schrank title: Stehlampe Esszimmer Schrank
icon: 💡 icon: 💡
rank: 82 rank: 82
# - device_id: kleine_lampe_rechts_esszimmer - device_id: regallampe_esszimmer
# title: kleine Lampe rechts Esszimmer title: Regallampe Esszimmer
# icon: 💡 icon: 💡
# rank: 90 rank: 90
- device_id: licht_schrank_esszimmer - device_id: licht_schrank_esszimmer
title: Schranklicht Esszimmer title: Schranklicht Esszimmer
icon: 💡 icon: 💡
@@ -79,7 +81,8 @@ rooms:
title: Kontakt Straße links title: Kontakt Straße links
icon: 🪟 icon: 🪟
rank: 97 rank: 97
- name: Wohnzimmer - id: wohnzimmer
name: Wohnzimmer
devices: devices:
- device_id: lampe_naehtischchen_wohnzimmer - device_id: lampe_naehtischchen_wohnzimmer
title: Lampe Naehtischchen Wohnzimmer title: Lampe Naehtischchen Wohnzimmer
@@ -121,7 +124,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 138 rank: 138
- name: che - id: kueche
name: Küche
devices: devices:
- device_id: kueche_deckenlampe - device_id: kueche_deckenlampe
title: Küche Deckenlampe title: Küche Deckenlampe
@@ -135,10 +139,15 @@ rooms:
title: Küche Putzlicht title: Küche Putzlicht
icon: 💡 icon: 💡
rank: 143 rank: 143
excluded: true
- device_id: kueche_fensterbank_licht - device_id: kueche_fensterbank_licht
title: Küche Fensterbank title: Küche Fensterbank
icon: 💡 icon: 💡
rank: 144 rank: 144
- device_id: herdlicht
title: Herdlicht
icon: 💡
rank: 145
- device_id: thermostat_kueche - device_id: thermostat_kueche
title: Kueche title: Kueche
icon: 🌡️ icon: 🌡️
@@ -163,30 +172,35 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 155 rank: 155
- name: Arbeitszimmer Patty - id: arbeitszimmer_patty
name: Arbeitszimmer Patty
devices: devices:
- device_id: leselampe_patty - device_id: leselampe_patty
title: Leselampe Patty title: Leselampe Patty
icon: 💡 icon: 💡
rank: 160 rank: 160
- device_id: schranklicht_hinten_patty - device_id: schranklicht_hinten_patty
title: Schranklicht hinten Patty title: Schranklicht hinten
icon: 💡 icon: 💡
rank: 170 rank: 170
- device_id: schranklicht_vorne_patty - device_id: schranklicht_vorne_patty
title: Schranklicht vorne Patty title: Schranklicht vorne
icon: 💡 icon: 💡
rank: 180 rank: 180
- device_id: kugellampe_patty - device_id: kugellampe_patty
title: Kugellampe Patty title: Kugellampe
icon: 💡 icon: 💡
rank: 181 rank: 181
- device_id: licht_schreibtisch_patty - device_id: licht_schreibtisch_patty
title: Licht Schreibtisch Patty title: Licht Schreibtisch
icon: 💡 icon: 💡
rank: 182 rank: 182
- device_id: deckenlampe_patty
title: Deckenlampe
icon: 💡
rank: 183
- device_id: thermostat_patty - device_id: thermostat_patty
title: Thermostat Patty title: Thermostat
icon: 🌡️ icon: 🌡️
rank: 185 rank: 185
- device_id: kontakt_patty_garten_rechts - device_id: kontakt_patty_garten_rechts
@@ -205,7 +219,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 189 rank: 189
- name: Arbeitszimmer Wolfgang - id: arbeitszimmer_wolfgang
name: Arbeitszimmer Wolfgang
devices: devices:
- device_id: thermostat_wolfgang - device_id: thermostat_wolfgang
title: Wolfgang title: Wolfgang
@@ -223,7 +238,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 202 rank: 202
- name: Flur - id: flur
name: Flur
devices: devices:
- device_id: deckenlampe_flur_oben - device_id: deckenlampe_flur_oben
title: Deckenlampe Flur oben title: Deckenlampe Flur oben
@@ -237,7 +253,7 @@ rooms:
title: Licht oben am Spiegel title: Licht oben am Spiegel
icon: 💡 icon: 💡
rank: 230 rank: 230
- device_id: schrank_flur_haustür - device_id: schrank_flur_haustuer
title: Schranklicht an der Haustür title: Schranklicht an der Haustür
icon: 💡 icon: 💡
rank: 231 rank: 231
@@ -249,7 +265,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 235 rank: 235
- name: Sportzimmer - id: sportzimmer
name: Sportzimmer
devices: devices:
- device_id: sportlicht_regal - device_id: sportlicht_regal
title: Sportlicht Regal title: Sportlicht Regal
@@ -263,11 +280,16 @@ rooms:
title: Sportlicht am Fernseher, Studierzimmer title: Sportlicht am Fernseher, Studierzimmer
icon: 🏃 icon: 🏃
rank: 260 rank: 260
- device_id: sportzimmer_licht
title: Deckenlampe
icon: 💡
rank: 262
- device_id: sensor_sportzimmer - device_id: sensor_sportzimmer
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 265 rank: 265
- name: Bad Oben - id: bad_oben
name: Bad Oben
devices: devices:
- device_id: thermostat_bad_oben - device_id: thermostat_bad_oben
title: Thermostat Bad Oben title: Thermostat Bad Oben
@@ -281,7 +303,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 272 rank: 272
- name: Bad Unten - id: bad_unten
name: Bad Unten
devices: devices:
- device_id: thermostat_bad_unten - device_id: thermostat_bad_unten
title: Thermostat Bad Unten title: Thermostat Bad Unten
@@ -295,13 +318,20 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 282 rank: 282
- name: Waschküche - id: waschkueche
name: Waschküche
devices: devices:
- device_id: sensor_waschkueche - device_id: sensor_waschkueche
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 290 rank: 290
- name: Outdoor - device_id: waschkueche_licht
title: Waschküche Licht
icon: 💡
rank: 340
- id: outdoor
name: Outdoor
devices: devices:
- device_id: licht_terasse - device_id: licht_terasse
title: Licht Terasse title: Licht Terasse
@@ -311,15 +341,33 @@ rooms:
title: Gartenlicht vorne title: Gartenlicht vorne
icon: 💡 icon: 💡
rank: 291 rank: 291
- name: Garage - id: garage
name: Garage
devices: devices:
- device_id: power_relay_caroutlet - device_id: power_relay_caroutlet
title: Ladestrom title: Ladestrom
icon: icon:
rank: 310 rank: 310
- device_id: sensor_caroutlet
title: Schützzustand
icon: 🔌
rank: 315
- device_id: powermeter_caroutlet - device_id: powermeter_caroutlet
title: Ladestrom title: Messwerte
icon: 📊 icon: 📊
rank: 320 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

View File

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