From 9458381593439c06ad385826ac23e26eb6b25a20 Mon Sep 17 00:00:00 2001 From: Wolfgang Hottgenroth Date: Tue, 11 Nov 2025 09:12:35 +0100 Subject: [PATCH] sensoren --- apps/abstraction/main.py | 6 +- apps/abstraction/transformation.py | 52 +++++++++++ apps/api/main.py | 13 ++- apps/ui/templates/dashboard.html | 115 +++++++++++++++++++++++++ config/devices.yaml | 81 +++++++++++++++++ config/layout.yaml | 42 +++++++++ config/raeume.txt | 34 ++++---- packages/home_capabilities/__init__.py | 4 + 8 files changed, 326 insertions(+), 21 deletions(-) diff --git a/apps/abstraction/main.py b/apps/abstraction/main.py index 3da42f4..54616df 100644 --- a/apps/abstraction/main.py +++ b/apps/abstraction/main.py @@ -15,7 +15,7 @@ import uuid from aiomqtt import Client from pydantic import ValidationError -from packages.home_capabilities import LightState, ThermostatState, ContactState +from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState from apps.abstraction.transformation import ( transform_abstract_to_vendor, transform_vendor_to_abstract @@ -222,12 +222,16 @@ async def handle_vendor_state( elif device_type in {"contact", "contact_sensor"}: # Validate contact sensor state ContactState.model_validate(abstract_payload) + elif device_type in {"temp_humidity", "temp_humidity_sensor"}: + # Validate temperature & humidity sensor state + TempHumidityState.model_validate(abstract_payload) except ValidationError as e: logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}") return # Normalize device type for topic (use 'contact' for both 'contact' and 'contact_sensor') topic_type = "contact" if device_type in {"contact", "contact_sensor"} else device_type + topic_type = "temp_humidity" if device_type in {"temp_humidity", "temp_humidity_sensor"} else topic_type # Publish to abstract state topic (retained) abstract_topic = f"home/{topic_type}/{device_id}/state" diff --git a/apps/abstraction/transformation.py b/apps/abstraction/transformation.py index f84f1af..3c56f73 100644 --- a/apps/abstraction/transformation.py +++ b/apps/abstraction/transformation.py @@ -265,6 +265,48 @@ def _transform_contact_sensor_max_to_abstract(payload: str | bool | dict[str, An } +# ============================================================================ +# HANDLER FUNCTIONS: temp_humidity_sensor - zigbee2mqtt technology +# ============================================================================ + +def _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: + """Transform abstract temp/humidity sensor payload to zigbee2mqtt format. + + Temp/humidity sensors are read-only, so this should not be called for SET commands. + Returns payload as-is for compatibility. + """ + return payload + + +def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]: + """Transform zigbee2mqtt temp/humidity sensor payload to abstract format. + + Passthrough - zigbee2mqtt provides temperature, humidity, battery, linkquality directly. + """ + return payload + + +# ============================================================================ +# HANDLER FUNCTIONS: temp_humidity_sensor - MAX! technology +# ============================================================================ + +def _transform_temp_humidity_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: + """Transform abstract temp/humidity sensor payload to MAX! format. + + Temp/humidity sensors are read-only, so this should not be called for SET commands. + Returns payload as-is for compatibility. + """ + return payload + + +def _transform_temp_humidity_sensor_max_to_abstract(payload: dict[str, Any]) -> dict[str, Any]: + """Transform MAX! temp/humidity sensor payload to abstract format. + + Passthrough - MAX! provides temperature, humidity, battery directly. + """ + return payload + + # ============================================================================ # HANDLER FUNCTIONS: max technology (Homegear MAX!) # ============================================================================ @@ -368,6 +410,16 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = { ("contact", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract, ("contact", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor, ("contact", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract, + + # Temperature & humidity sensor transformations (support both type aliases) + ("temp_humidity_sensor", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor, + ("temp_humidity_sensor", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract, + ("temp_humidity_sensor", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor, + ("temp_humidity_sensor", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract, + ("temp_humidity", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor, + ("temp_humidity", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract, + ("temp_humidity", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor, + ("temp_humidity", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract, } diff --git a/apps/api/main.py b/apps/api/main.py index c083099..461c8a7 100644 --- a/apps/api/main.py +++ b/apps/api/main.py @@ -19,9 +19,11 @@ from packages.home_capabilities import ( LIGHT_VERSION, THERMOSTAT_VERSION, CONTACT_SENSOR_VERSION, + TEMP_HUMIDITY_SENSOR_VERSION, LightState, ThermostatState, - ContactState + ContactState, + TempHumidityState ) logger = logging.getLogger(__name__) @@ -145,7 +147,8 @@ async def spec() -> dict[str, dict[str, str]]: "capabilities": { "light": LIGHT_VERSION, "thermostat": THERMOSTAT_VERSION, - "contact": CONTACT_SENSOR_VERSION + "contact": CONTACT_SENSOR_VERSION, + "temp_humidity": TEMP_HUMIDITY_SENSOR_VERSION } } @@ -377,6 +380,12 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Contact sensors are read-only devices" ) + elif request.type in {"temp_humidity", "temp_humidity_sensor"}: + # Temperature & humidity sensors are read-only + raise HTTPException( + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, + detail="Temperature & humidity sensors are read-only devices" + ) else: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, diff --git a/apps/ui/templates/dashboard.html b/apps/ui/templates/dashboard.html index 2be5834..a9f4c1c 100644 --- a/apps/ui/templates/dashboard.html +++ b/apps/ui/templates/dashboard.html @@ -426,6 +426,58 @@ text-align: center; } + /* Temperature & Humidity Sensor Styles */ + .temp-humidity-display { + padding: 1rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; + margin: 1rem 0; + } + + .temp-humidity-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + } + + .temp-humidity-row:not(:last-child) { + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + } + + .temp-humidity-label { + font-size: 0.875rem; + font-weight: 500; + opacity: 0.9; + } + + .temp-humidity-value { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.5px; + } + + .temp-humidity-battery { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.8); + text-align: center; + padding: 0.5rem; + margin-top: 0.5rem; + background: rgba(0, 0, 0, 0.1); + border-radius: 6px; + } + + .temp-humidity-info { + font-size: 0.75rem; + color: #999; + margin-top: 1rem; + padding: 0.5rem; + background: #f8f9fa; + border-radius: 4px; + text-align: center; + } + .events { margin-top: 2rem; background: white; @@ -607,6 +659,28 @@ 🔒 Nur-Lesen Gerät • Keine Steuerung möglich + {% elif device.type == "temp_humidity" or device.type == "temp_humidity_sensor" %} +
+
+ 🌡️ Temperatur: + -- + °C +
+
+ 💧 Luftfeuchte: + -- + % +
+
+ + + +
+ 🔒 Nur-Lesen Gerät • Keine Steuerung möglich +
+ {% endif %} {% endfor %} @@ -922,6 +996,39 @@ } } + // Update temperature & humidity sensor UI + function updateTempHumidityUI(deviceId, payload) { + const tempSpan = document.getElementById(`th-${deviceId}-t`); + const humiditySpan = document.getElementById(`th-${deviceId}-h`); + const batteryDiv = document.getElementById(`th-${deviceId}-battery`); + const batteryValueSpan = document.getElementById(`th-${deviceId}-battery-value`); + + if (!tempSpan || !humiditySpan) { + console.warn(`No temp/humidity elements found for device ${deviceId}`); + return; + } + + // Update temperature (rounded to 1 decimal) + if (payload.temperature !== undefined && payload.temperature !== null) { + tempSpan.textContent = payload.temperature.toFixed(1); + } + + // Update humidity (rounded to 0-1 decimals) + if (payload.humidity !== undefined && payload.humidity !== null) { + // Round to 1 decimal if has decimals, otherwise integer + const humidity = payload.humidity; + humiditySpan.textContent = (humidity % 1 === 0) ? humidity.toFixed(0) : humidity.toFixed(1); + } + + // Update battery if present + if (payload.battery !== undefined && payload.battery !== null && batteryDiv && batteryValueSpan) { + batteryValueSpan.textContent = payload.battery; + batteryDiv.style.display = 'block'; + } else if (batteryDiv) { + batteryDiv.style.display = 'none'; + } + } + // Add event to list function addEvent(event) { const eventList = document.getElementById('event-list'); @@ -992,6 +1099,11 @@ if (data.payload.contact !== undefined) { updateContactUI(data.device_id, data.payload.contact); } + + // Check if it's a temp/humidity sensor + if (data.payload.temperature !== undefined || data.payload.humidity !== undefined) { + updateTempHumidityUI(data.device_id, data.payload); + } } }; @@ -1113,6 +1225,9 @@ } else if (state.contact !== undefined) { // It's a contact sensor updateContactUI(deviceId, state.contact); + } else if (state.temperature !== undefined || state.humidity !== undefined) { + // It's a temp/humidity sensor + updateTempHumidityUI(deviceId, state); } } } catch (error) { diff --git a/config/devices.yaml b/config/devices.yaml index 91ef9cf..f0476d5 100644 --- a/config/devices.yaml +++ b/config/devices.yaml @@ -639,4 +639,85 @@ devices: topics: state: homegear/instance1/plain/44/1/STATE features: {} +- device_id: sensor_schlafzimmer + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d00043292dc + features: {} +- device_id: sensor_wohnzimmer + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d0008975707 + features: {} +- device_id: sensor_kueche + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d00083299bb + features: {} +- device_id: sensor_arbeitszimmer_patty + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d0003f052b7 + features: {} +- device_id: sensor_arbeitszimmer_wolfgang + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d000543fb99 + features: {} +- device_id: sensor_bad_oben + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d00093e8987 + features: {} +- device_id: sensor_bad_unten + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d00093e662a + features: {} +- device_id: sensor_flur + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d000836ccc6 + features: {} +- device_id: sensor_waschkueche + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d000449f3bc + features: {} +- device_id: sensor_sportzimmer + type: temp_humidity_sensor + name: Temperatur & Luftfeuchte + cap_version: temp_humidity_sensor@1.0.0 + technology: zigbee2mqtt + topics: + state: zigbee2mqtt/0x00158d0009421422 + features: {} + diff --git a/config/layout.yaml b/config/layout.yaml index 77f78bc..1599af1 100644 --- a/config/layout.yaml +++ b/config/layout.yaml @@ -25,6 +25,10 @@ rooms: title: Kontakt Straße icon: 🪟 rank: 46 + - device_id: sensor_schlafzimmer + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 47 - name: Esszimmer devices: - device_id: deckenlampe_esszimmer @@ -89,6 +93,10 @@ rooms: title: Kontakt Garten links icon: 🪟 rank: 137 + - device_id: sensor_wohnzimmer + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 138 - name: Küche devices: - device_id: kueche_deckenlampe @@ -115,6 +123,10 @@ rooms: title: Kontakt Straße links icon: 🪟 rank: 154 + - device_id: sensor_kueche + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 155 - name: Arbeitszimmer Patty devices: - device_id: leselampe_patty @@ -145,6 +157,10 @@ rooms: title: Kontakt Straße icon: 🪟 rank: 188 + - device_id: sensor_arbeitszimmer_patty + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 189 - name: Arbeitszimmer Wolfgang devices: - device_id: thermostat_wolfgang @@ -159,6 +175,10 @@ rooms: title: Kontakt Garten icon: 🪟 rank: 201 + - device_id: sensor_arbeitszimmer_wolfgang + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 202 - name: Flur devices: - device_id: deckenlampe_flur_oben @@ -173,6 +193,10 @@ rooms: title: Licht Flur oben am Spiegel icon: 💡 rank: 230 + - device_id: sensor_flur + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 235 - name: Sportzimmer devices: - device_id: sportlicht_regal @@ -187,6 +211,10 @@ rooms: title: Sportlicht am Fernseher, Studierzimmer icon: 🏃 rank: 260 + - device_id: sensor_sportzimmer + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 265 - name: Bad Oben devices: - device_id: thermostat_bad_oben @@ -197,6 +225,10 @@ rooms: title: Kontakt Straße icon: 🪟 rank: 271 + - device_id: sensor_bad_oben + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 272 - name: Bad Unten devices: - device_id: thermostat_bad_unten @@ -207,4 +239,14 @@ rooms: title: Kontakt Straße icon: 🪟 rank: 281 + - device_id: sensor_bad_unten + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 282 +- name: Waschküche + devices: + - device_id: sensor_waschkueche + title: Temperatur & Luftfeuchte + icon: 🌡️ + rank: 290 diff --git a/config/raeume.txt b/config/raeume.txt index b845d74..3a6f88e 100644 --- a/config/raeume.txt +++ b/config/raeume.txt @@ -1,33 +1,31 @@ Schlafzimmer - 52 Straße - + 0x00158d00043292dc Esszimmer - 26 Straße rechts - 27 Straße links Wohnzimmer - 28 Garten rechts - 29 Garten links + 0x00158d0008975707 Küche - 0x00158d008b332785 Garten Fenster - 0x00158d008b332788 Garten Tür - 0x00158d008b151803 Straße rechts - 0x00158d008b331d0b Straße links - + 0x00158d00083299bb Arbeitszimmer Patty - 18 Garten rechts - 22 Garten links - 0x00158d000af457cf Straße + 0x00158d0003f052b7 Arbeitszimmer Wolfgang - 0x00158d008b3328da Garten - + 0x00158d000543fb99 Bad Oben - 0x00158d008b333aec Straße + 0x00158d00093e8987 Bad Unten - 44 Straße \ No newline at end of file + 0x00158d00093e662a + +Flur + 0x00158d000836ccc6 + +Waschküche + 0x00158d000449f3bc + +Sportzimmer + 0x00158d0009421422 \ No newline at end of file diff --git a/packages/home_capabilities/__init__.py b/packages/home_capabilities/__init__.py index 0b164e2..069d54f 100644 --- a/packages/home_capabilities/__init__.py +++ b/packages/home_capabilities/__init__.py @@ -6,6 +6,8 @@ from packages.home_capabilities.thermostat import CAP_VERSION as THERMOSTAT_VERS from packages.home_capabilities.thermostat import ThermostatState from packages.home_capabilities.contact_sensor import CAP_VERSION as CONTACT_SENSOR_VERSION from packages.home_capabilities.contact_sensor import ContactState +from packages.home_capabilities.temp_humidity_sensor import CAP_VERSION as TEMP_HUMIDITY_SENSOR_VERSION +from packages.home_capabilities.temp_humidity_sensor import TempHumidityState from packages.home_capabilities.layout import DeviceTile, Room, UiLayout, load_layout __all__ = [ @@ -15,6 +17,8 @@ __all__ = [ "THERMOSTAT_VERSION", "ContactState", "CONTACT_SENSOR_VERSION", + "TempHumidityState", + "TEMP_HUMIDITY_SENSOR_VERSION", "DeviceTile", "Room", "UiLayout",