This commit is contained in:
2025-11-11 09:12:35 +01:00
parent f389115841
commit 9458381593
8 changed files with 326 additions and 21 deletions

View File

@@ -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"

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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
</div>
{% elif device.type == "temp_humidity" or device.type == "temp_humidity_sensor" %}
<div class="temp-humidity-display">
<div class="temp-humidity-row">
<span class="temp-humidity-label">🌡️ Temperatur:</span>
<span class="temp-humidity-value" id="th-{{ device.device_id }}-t">--</span>
<span style="font-size: 1.25rem; font-weight: 600;">°C</span>
</div>
<div class="temp-humidity-row">
<span class="temp-humidity-label">💧 Luftfeuchte:</span>
<span class="temp-humidity-value" id="th-{{ device.device_id }}-h">--</span>
<span style="font-size: 1.25rem; font-weight: 600;">%</span>
</div>
</div>
<div class="temp-humidity-battery" id="th-{{ device.device_id }}-battery" style="display: none;">
🔋 <span id="th-{{ device.device_id }}-battery-value">--</span>%
</div>
<div class="temp-humidity-info">
🔒 Nur-Lesen Gerät • Keine Steuerung möglich
</div>
{% endif %}
</div>
{% 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) {

View File

@@ -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: {}

View File

@@ -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

View File

@@ -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
0x00158d00093e662a
Flur
0x00158d000836ccc6
Waschküche
0x00158d000449f3bc
Sportzimmer
0x00158d0009421422

View File

@@ -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",