Compare commits
4 Commits
e728dd58e4
...
temperatur
| Author | SHA1 | Date | |
|---|---|---|---|
|
86d1933c1f
|
|||
|
9458381593
|
|||
|
f389115841
|
|||
|
19a6a603d5
|
@@ -15,7 +15,7 @@ import uuid
|
||||
from aiomqtt import Client
|
||||
from pydantic import ValidationError
|
||||
|
||||
from packages.home_capabilities import LightState, ThermostatState
|
||||
from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState
|
||||
from apps.abstraction.transformation import (
|
||||
transform_abstract_to_vendor,
|
||||
transform_vendor_to_abstract
|
||||
@@ -89,11 +89,12 @@ def validate_devices(devices: list[dict[str, Any]]) -> None:
|
||||
if "topics" not in device:
|
||||
raise ValueError(f"Device {device_id} missing 'topics'")
|
||||
|
||||
if "set" not in device["topics"]:
|
||||
raise ValueError(f"Device {device_id} missing 'topics.set'")
|
||||
|
||||
# 'state' topic is required for all devices
|
||||
if "state" not in device["topics"]:
|
||||
raise ValueError(f"Device {device_id} missing 'topics.state'")
|
||||
|
||||
# 'set' topic is optional (read-only devices like contact sensors don't have it)
|
||||
# No validation needed for topics.set
|
||||
|
||||
# Log loaded devices
|
||||
device_ids = [d["device_id"] for d in devices]
|
||||
@@ -166,6 +167,10 @@ async def handle_abstract_set(
|
||||
|
||||
# Validate against ThermostatState (current/battery/window_open are optional)
|
||||
ThermostatState.model_validate(abstract_payload)
|
||||
elif device_type in {"contact", "contact_sensor"}:
|
||||
# Contact sensors are read-only - SET commands should not occur
|
||||
logger.warning(f"Contact sensor {device_id} received SET command - ignoring (read-only device)")
|
||||
return
|
||||
except ValidationError as e:
|
||||
logger.error(f"Validation failed for {device_type} SET {device_id}: {e}")
|
||||
return
|
||||
@@ -214,12 +219,22 @@ async def handle_vendor_state(
|
||||
elif device_type == "thermostat":
|
||||
# Validate thermostat state: mode, target, current (required), battery, window_open
|
||||
ThermostatState.model_validate(abstract_payload)
|
||||
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/{device_type}/{device_id}/state"
|
||||
abstract_topic = f"home/{topic_type}/{device_id}/state"
|
||||
abstract_message = json.dumps(abstract_payload)
|
||||
|
||||
logger.info(f"← abstract STATE {device_id}: {abstract_topic} → {abstract_message}")
|
||||
@@ -273,15 +288,22 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
|
||||
keepalive=keepalive,
|
||||
timeout=10.0 # Add explicit timeout for operations
|
||||
) as client:
|
||||
logger.info(f"Connected to MQTT broker as {client_id}")
|
||||
logger.info(f"Connected to MQTT broker as {unique_client_id}")
|
||||
|
||||
# Subscribe to abstract SET topics for all devices
|
||||
# Subscribe to topics for all devices
|
||||
for device in devices.values():
|
||||
abstract_set_topic = f"home/{device['type']}/{device['device_id']}/set"
|
||||
await client.subscribe(abstract_set_topic)
|
||||
logger.info(f"Subscribed to abstract SET: {abstract_set_topic}")
|
||||
device_id = device['device_id']
|
||||
device_type = device['type']
|
||||
|
||||
# Subscribe to vendor STATE topics
|
||||
# Subscribe to abstract SET topic only if device has a SET topic (not read-only)
|
||||
if "set" in device["topics"]:
|
||||
abstract_set_topic = f"home/{device_type}/{device_id}/set"
|
||||
await client.subscribe(abstract_set_topic)
|
||||
logger.info(f"Subscribed to abstract SET: {abstract_set_topic}")
|
||||
else:
|
||||
logger.info(f"Skipping SET subscription for read-only device: {device_id}")
|
||||
|
||||
# Subscribe to vendor STATE topics (all devices have state)
|
||||
vendor_state_topic = device["topics"]["state"]
|
||||
await client.subscribe(vendor_state_topic)
|
||||
logger.info(f"Subscribed to vendor STATE: {vendor_state_topic}")
|
||||
|
||||
@@ -159,6 +159,154 @@ def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> di
|
||||
return abstract_payload
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HANDLER FUNCTIONS: contact_sensor - zigbee2mqtt technology
|
||||
# ============================================================================
|
||||
|
||||
def _transform_contact_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Transform abstract contact sensor payload to zigbee2mqtt format.
|
||||
|
||||
Contact sensors are read-only, so this should not be called for SET commands.
|
||||
Returns payload as-is for compatibility.
|
||||
"""
|
||||
logger.warning("Contact sensors are read-only - SET commands should not be used")
|
||||
return payload
|
||||
|
||||
|
||||
def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> 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}
|
||||
"""
|
||||
abstract_payload = {}
|
||||
|
||||
# Transform contact state (inverted logic!)
|
||||
if "contact" in payload:
|
||||
contact_bool = payload["contact"]
|
||||
# zigbee2mqtt: False = OPEN, True = CLOSED
|
||||
abstract_payload["contact"] = "closed" if contact_bool else "open"
|
||||
|
||||
# Pass through optional fields
|
||||
if "battery" in payload:
|
||||
abstract_payload["battery"] = payload["battery"]
|
||||
|
||||
if "linkquality" in payload:
|
||||
abstract_payload["linkquality"] = payload["linkquality"]
|
||||
|
||||
if "device_temperature" in payload:
|
||||
abstract_payload["device_temperature"] = payload["device_temperature"]
|
||||
|
||||
if "voltage" in payload:
|
||||
abstract_payload["voltage"] = payload["voltage"]
|
||||
|
||||
return abstract_payload
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HANDLER FUNCTIONS: contact_sensor - max technology (Homegear MAX!)
|
||||
# ============================================================================
|
||||
|
||||
def _transform_contact_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Transform abstract contact sensor payload to MAX! format.
|
||||
|
||||
Contact sensors are read-only, so this should not be called for SET commands.
|
||||
Returns payload as-is for compatibility.
|
||||
"""
|
||||
logger.warning("Contact sensors are read-only - SET commands should not be used")
|
||||
return payload
|
||||
|
||||
|
||||
def _transform_contact_sensor_max_to_abstract(payload: str | bool | dict[str, Any]) -> 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:
|
||||
# Handle string, bool, or dict input
|
||||
if isinstance(payload, dict):
|
||||
# If already a dict, extract contact field
|
||||
contact_value = payload.get("contact", False)
|
||||
elif isinstance(payload, str):
|
||||
# Parse string to bool
|
||||
contact_value = payload.strip().lower() == "true"
|
||||
elif isinstance(payload, bool):
|
||||
# Use bool directly
|
||||
contact_value = payload
|
||||
else:
|
||||
logger.warning(f"MAX! contact sensor unexpected payload type: {type(payload)}, value: {payload}")
|
||||
contact_value = False
|
||||
|
||||
# MAX! semantics: True = OPEN, False = CLOSED
|
||||
return {
|
||||
"contact": "open" if contact_value else "closed"
|
||||
}
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"MAX! contact sensor failed to parse: {payload}, error: {e}")
|
||||
return {
|
||||
"contact": "closed" # Default to closed on error
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HANDLER FUNCTIONS: temp_humidity_sensor - zigbee2mqtt technology
|
||||
# ============================================================================
|
||||
|
||||
def _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Transform abstract temp/humidity sensor payload to zigbee2mqtt format.
|
||||
|
||||
Temp/humidity sensors are read-only, so this should not be called for SET commands.
|
||||
Returns payload as-is for compatibility.
|
||||
"""
|
||||
return payload
|
||||
|
||||
|
||||
def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: 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!)
|
||||
# ============================================================================
|
||||
@@ -252,6 +400,26 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
|
||||
("thermostat", "zigbee2mqtt", "to_abstract"): _transform_thermostat_zigbee2mqtt_to_abstract,
|
||||
("thermostat", "max", "to_vendor"): _transform_thermostat_max_to_vendor,
|
||||
("thermostat", "max", "to_abstract"): _transform_thermostat_max_to_abstract,
|
||||
|
||||
# Contact sensor transformations (support both 'contact' and 'contact_sensor' types)
|
||||
("contact_sensor", "zigbee2mqtt", "to_vendor"): _transform_contact_sensor_zigbee2mqtt_to_vendor,
|
||||
("contact_sensor", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract,
|
||||
("contact_sensor", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor,
|
||||
("contact_sensor", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract,
|
||||
("contact", "zigbee2mqtt", "to_vendor"): _transform_contact_sensor_zigbee2mqtt_to_vendor,
|
||||
("contact", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract,
|
||||
("contact", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor,
|
||||
("contact", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract,
|
||||
|
||||
# Temperature & humidity sensor transformations (support both type aliases)
|
||||
("temp_humidity_sensor", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
|
||||
("temp_humidity_sensor", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
|
||||
("temp_humidity_sensor", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
|
||||
("temp_humidity_sensor", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
|
||||
("temp_humidity", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
|
||||
("temp_humidity", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
|
||||
("temp_humidity", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
|
||||
("temp_humidity", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,16 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from packages.home_capabilities import LIGHT_VERSION, THERMOSTAT_VERSION, LightState, ThermostatState
|
||||
from packages.home_capabilities import (
|
||||
LIGHT_VERSION,
|
||||
THERMOSTAT_VERSION,
|
||||
CONTACT_SENSOR_VERSION,
|
||||
TEMP_HUMIDITY_SENSOR_VERSION,
|
||||
LightState,
|
||||
ThermostatState,
|
||||
ContactState,
|
||||
TempHumidityState
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -137,7 +146,9 @@ async def spec() -> dict[str, dict[str, str]]:
|
||||
return {
|
||||
"capabilities": {
|
||||
"light": LIGHT_VERSION,
|
||||
"thermostat": THERMOSTAT_VERSION
|
||||
"thermostat": THERMOSTAT_VERSION,
|
||||
"contact": CONTACT_SENSOR_VERSION,
|
||||
"temp_humidity": TEMP_HUMIDITY_SENSOR_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +342,13 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str
|
||||
detail=f"Device {device_id} not found"
|
||||
)
|
||||
|
||||
# Check if device is read-only (contact sensors, etc.)
|
||||
if "topics" in device and "set" not in device["topics"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
detail="Device is read-only"
|
||||
)
|
||||
|
||||
# Validate payload based on device type
|
||||
if request.type == "light":
|
||||
try:
|
||||
@@ -356,6 +374,18 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Invalid payload for thermostat: {e}"
|
||||
)
|
||||
elif request.type in {"contact", "contact_sensor"}:
|
||||
# Contact sensors are read-only
|
||||
raise HTTPException(
|
||||
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,
|
||||
|
||||
@@ -386,7 +386,97 @@
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
|
||||
/* Contact Sensor Styles */
|
||||
.contact-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.contact-badge {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.contact-badge.open {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.contact-badge.closed {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
font-size: 0.75rem;
|
||||
color: #999;
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
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;
|
||||
@@ -483,6 +573,8 @@
|
||||
{% if device.features.brightness %}• Dimmbar{% endif %}
|
||||
{% elif device.type == "thermostat" %}
|
||||
Thermostat
|
||||
{% elif device.type == "contact" or device.type == "contact_sensor" %}
|
||||
Contact Sensor • Read-Only
|
||||
{% else %}
|
||||
{{ device.type or "Unknown" }}
|
||||
{% endif %}
|
||||
@@ -555,6 +647,40 @@
|
||||
+1.0
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% elif device.type == "contact" or device.type == "contact_sensor" %}
|
||||
<div class="contact-status">
|
||||
<span class="contact-badge closed" id="state-{{ device.device_id }}">
|
||||
Geschlossen
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="contact-info">
|
||||
🔒 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 %}
|
||||
@@ -852,6 +978,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Update contact sensor UI
|
||||
function updateContactUI(deviceId, contactState) {
|
||||
const badge = document.getElementById(`state-${deviceId}`);
|
||||
if (!badge) {
|
||||
console.warn(`No contact badge found for device ${deviceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// contactState is either "open" or "closed"
|
||||
if (contactState === "open") {
|
||||
badge.textContent = "Geöffnet";
|
||||
badge.className = "contact-badge open";
|
||||
} else if (contactState === "closed") {
|
||||
badge.textContent = "Geschlossen";
|
||||
badge.className = "contact-badge closed";
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
@@ -917,6 +1094,16 @@
|
||||
data.payload.target
|
||||
);
|
||||
}
|
||||
|
||||
// Check if it's a contact sensor
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1035,6 +1222,12 @@
|
||||
// It's a thermostat
|
||||
if (state.target) thermostatTargets[deviceId] = state.target;
|
||||
updateThermostatUI(deviceId, state.current, state.target);
|
||||
} 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) {
|
||||
|
||||
@@ -518,3 +518,206 @@ devices:
|
||||
ieee_address: "0xf0d1b80000155fc2"
|
||||
model: "AC10691"
|
||||
vendor: "OSRAM"
|
||||
|
||||
- device_id: kontakt_schlafzimmer_strasse
|
||||
type: contact
|
||||
name: Kontakt Schlafzimmer Straße
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: max
|
||||
topics:
|
||||
state: homegear/instance1/plain/52/1/STATE
|
||||
features: {}
|
||||
- device_id: kontakt_esszimmer_strasse_rechts
|
||||
type: contact
|
||||
name: Kontakt Esszimmer Straße rechts
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: max
|
||||
topics:
|
||||
state: homegear/instance1/plain/26/1/STATE
|
||||
features: {}
|
||||
- device_id: kontakt_esszimmer_strasse_links
|
||||
type: contact
|
||||
name: Kontakt Esszimmer Straße links
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: max
|
||||
topics:
|
||||
state: homegear/instance1/plain/27/1/STATE
|
||||
features: {}
|
||||
- device_id: kontakt_wohnzimmer_garten_rechts
|
||||
type: contact
|
||||
name: Kontakt Wohnzimmer Garten rechts
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: max
|
||||
topics:
|
||||
state: homegear/instance1/plain/28/1/STATE
|
||||
features: {}
|
||||
- device_id: kontakt_wohnzimmer_garten_links
|
||||
type: contact
|
||||
name: Kontakt Wohnzimmer Garten links
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: max
|
||||
topics:
|
||||
state: homegear/instance1/plain/29/1/STATE
|
||||
features: {}
|
||||
- device_id: kontakt_kueche_garten_fenster
|
||||
type: contact
|
||||
name: Kontakt Küche Garten Fenster
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: zigbee2mqtt
|
||||
topics:
|
||||
state: zigbee2mqtt/0x00158d008b332785
|
||||
features: {}
|
||||
- device_id: kontakt_kueche_garten_tuer
|
||||
type: contact
|
||||
name: Kontakt Küche Garten Tür
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: zigbee2mqtt
|
||||
topics:
|
||||
state: zigbee2mqtt/0x00158d008b332788
|
||||
features: {}
|
||||
- device_id: kontakt_kueche_strasse_rechts
|
||||
type: contact
|
||||
name: Kontakt Küche Straße rechts
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: zigbee2mqtt
|
||||
topics:
|
||||
state: zigbee2mqtt/0x00158d008b151803
|
||||
features: {}
|
||||
- device_id: kontakt_kueche_strasse_links
|
||||
type: contact
|
||||
name: Kontakt Küche Straße links
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: zigbee2mqtt
|
||||
topics:
|
||||
state: zigbee2mqtt/0x00158d008b331d0b
|
||||
features: {}
|
||||
- device_id: kontakt_patty_garten_rechts
|
||||
type: contact
|
||||
name: Kontakt Patty Garten rechts
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: max
|
||||
topics:
|
||||
state: homegear/instance1/plain/18/1/STATE
|
||||
features: {}
|
||||
- device_id: kontakt_patty_garten_links
|
||||
type: contact
|
||||
name: Kontakt Patty Garten links
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: max
|
||||
topics:
|
||||
state: homegear/instance1/plain/22/1/STATE
|
||||
features: {}
|
||||
- device_id: kontakt_patty_strasse
|
||||
type: contact
|
||||
name: Kontakt Patty Straße
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: zigbee2mqtt
|
||||
topics:
|
||||
state: zigbee2mqtt/0x00158d000af457cf
|
||||
features: {}
|
||||
- device_id: kontakt_wolfgang_garten
|
||||
type: contact
|
||||
name: Kontakt Wolfgang Garten
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: zigbee2mqtt
|
||||
topics:
|
||||
state: zigbee2mqtt/0x00158d008b3328da
|
||||
features: {}
|
||||
- device_id: kontakt_bad_oben_strasse
|
||||
type: contact
|
||||
name: Kontakt Bad Oben Straße
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: zigbee2mqtt
|
||||
topics:
|
||||
state: zigbee2mqtt/0x00158d008b333aec
|
||||
features: {}
|
||||
- device_id: kontakt_bad_unten_strasse
|
||||
type: contact
|
||||
name: Kontakt Bad Unten Straße
|
||||
cap_version: contact_sensor@1.0.0
|
||||
technology: max
|
||||
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: {}
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,14 @@ rooms:
|
||||
title: Thermostat Schlafzimmer
|
||||
icon: 🌡️
|
||||
rank: 45
|
||||
- device_id: kontakt_schlafzimmer_strasse
|
||||
title: Kontakt Straße
|
||||
icon: 🪟
|
||||
rank: 46
|
||||
- device_id: sensor_schlafzimmer
|
||||
title: Temperatur & Luftfeuchte
|
||||
icon: 🌡️
|
||||
rank: 47
|
||||
- name: Esszimmer
|
||||
devices:
|
||||
- device_id: deckenlampe_esszimmer
|
||||
@@ -47,6 +55,14 @@ rooms:
|
||||
title: Thermostat Esszimmer
|
||||
icon: 🌡️
|
||||
rank: 95
|
||||
- device_id: kontakt_esszimmer_strasse_rechts
|
||||
title: Kontakt Straße rechts
|
||||
icon: 🪟
|
||||
rank: 96
|
||||
- device_id: kontakt_esszimmer_strasse_links
|
||||
title: Kontakt Straße links
|
||||
icon: 🪟
|
||||
rank: 97
|
||||
- name: Wohnzimmer
|
||||
devices:
|
||||
- device_id: lampe_naehtischchen_wohnzimmer
|
||||
@@ -69,6 +85,18 @@ rooms:
|
||||
title: Thermostat Wohnzimmer
|
||||
icon: 🌡️
|
||||
rank: 135
|
||||
- device_id: kontakt_wohnzimmer_garten_rechts
|
||||
title: Kontakt Garten rechts
|
||||
icon: 🪟
|
||||
rank: 136
|
||||
- device_id: kontakt_wohnzimmer_garten_links
|
||||
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
|
||||
@@ -79,6 +107,26 @@ rooms:
|
||||
title: Kueche
|
||||
icon: 🌡️
|
||||
rank: 150
|
||||
- device_id: kontakt_kueche_garten_fenster
|
||||
title: Kontakt Garten Fenster
|
||||
icon: 🪟
|
||||
rank: 151
|
||||
- device_id: kontakt_kueche_garten_tuer
|
||||
title: Kontakt Garten Tür
|
||||
icon: 🪟
|
||||
rank: 152
|
||||
- device_id: kontakt_kueche_strasse_rechts
|
||||
title: Kontakt Straße rechts
|
||||
icon: 🪟
|
||||
rank: 153
|
||||
- device_id: kontakt_kueche_strasse_links
|
||||
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
|
||||
@@ -97,6 +145,22 @@ rooms:
|
||||
title: Thermostat Patty
|
||||
icon: 🌡️
|
||||
rank: 185
|
||||
- device_id: kontakt_patty_garten_rechts
|
||||
title: Kontakt Garten rechts
|
||||
icon: 🪟
|
||||
rank: 186
|
||||
- device_id: kontakt_patty_garten_links
|
||||
title: Kontakt Garten links
|
||||
icon: 🪟
|
||||
rank: 187
|
||||
- device_id: kontakt_patty_strasse
|
||||
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
|
||||
@@ -107,6 +171,14 @@ rooms:
|
||||
title: ExperimentLabTest
|
||||
icon: 💡
|
||||
rank: 200
|
||||
- device_id: kontakt_wolfgang_garten
|
||||
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
|
||||
@@ -121,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
|
||||
@@ -135,15 +211,42 @@ 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
|
||||
title: Thermostat Bad Oben
|
||||
icon: 🌡️
|
||||
rank: 270
|
||||
- device_id: kontakt_bad_oben_strasse
|
||||
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
|
||||
title: Thermostat Bad Unten
|
||||
icon: 🌡️
|
||||
rank: 280
|
||||
- device_id: kontakt_bad_unten_strasse
|
||||
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
|
||||
|
||||
|
||||
31
config/raeume.txt
Normal file
31
config/raeume.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
Schlafzimmer
|
||||
0x00158d00043292dc
|
||||
|
||||
Esszimmer
|
||||
|
||||
Wohnzimmer
|
||||
0x00158d0008975707
|
||||
|
||||
Küche
|
||||
0x00158d00083299bb
|
||||
|
||||
Arbeitszimmer Patty
|
||||
0x00158d0003f052b7
|
||||
|
||||
Arbeitszimmer Wolfgang
|
||||
0x00158d000543fb99
|
||||
|
||||
Bad Oben
|
||||
0x00158d00093e8987
|
||||
|
||||
Bad Unten
|
||||
0x00158d00093e662a
|
||||
|
||||
Flur
|
||||
0x00158d000836ccc6
|
||||
|
||||
Waschküche
|
||||
0x00158d000449f3bc
|
||||
|
||||
Sportzimmer
|
||||
0x00158d0009421422
|
||||
@@ -4,6 +4,10 @@ from packages.home_capabilities.light import CAP_VERSION as LIGHT_VERSION
|
||||
from packages.home_capabilities.light import LightState
|
||||
from packages.home_capabilities.thermostat import CAP_VERSION as THERMOSTAT_VERSION
|
||||
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__ = [
|
||||
@@ -11,6 +15,10 @@ __all__ = [
|
||||
"LIGHT_VERSION",
|
||||
"ThermostatState",
|
||||
"THERMOSTAT_VERSION",
|
||||
"ContactState",
|
||||
"CONTACT_SENSOR_VERSION",
|
||||
"TempHumidityState",
|
||||
"TEMP_HUMIDITY_SENSOR_VERSION",
|
||||
"DeviceTile",
|
||||
"Room",
|
||||
"UiLayout",
|
||||
|
||||
96
packages/home_capabilities/contact_sensor.py
Normal file
96
packages/home_capabilities/contact_sensor.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Contact Sensor Capability - Fensterkontakt (read-only).
|
||||
|
||||
This module defines the ContactState model for door/window contact sensors.
|
||||
These sensors report their open/closed state and are read-only devices.
|
||||
|
||||
Capability Version: contact_sensor@1.0.0
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
# Capability metadata
|
||||
CAP_VERSION = "contact_sensor@1.0.0"
|
||||
DISPLAY_NAME = "Contact Sensor"
|
||||
|
||||
|
||||
class ContactState(BaseModel):
|
||||
"""State model for contact sensors (door/window sensors).
|
||||
|
||||
Contact sensors are read-only devices that report whether a door or window
|
||||
is open or closed. They typically also report battery level and signal quality.
|
||||
|
||||
Attributes:
|
||||
contact: Current state of the contact ("open" or "closed")
|
||||
battery: Battery level percentage (0-100), optional
|
||||
linkquality: MQTT link quality indicator, optional
|
||||
device_temperature: Internal device temperature in °C, optional
|
||||
voltage: Battery voltage in mV, optional
|
||||
ts: Timestamp of the state reading, optional
|
||||
|
||||
Examples:
|
||||
>>> ContactState(contact="open")
|
||||
ContactState(contact='open', battery=None, ...)
|
||||
|
||||
>>> ContactState(contact="closed", battery=95, linkquality=87)
|
||||
ContactState(contact='closed', battery=95, linkquality=87, ...)
|
||||
"""
|
||||
|
||||
contact: Literal["open", "closed"] = Field(
|
||||
...,
|
||||
description="Contact state: 'open' for open door/window, 'closed' for closed"
|
||||
)
|
||||
|
||||
battery: Annotated[int, Field(ge=0, le=100)] | None = Field(
|
||||
None,
|
||||
description="Battery level in percent (0-100)"
|
||||
)
|
||||
|
||||
linkquality: int | None = Field(
|
||||
None,
|
||||
description="Link quality indicator (typically 0-255)"
|
||||
)
|
||||
|
||||
device_temperature: float | None = Field(
|
||||
None,
|
||||
description="Internal device temperature in degrees Celsius"
|
||||
)
|
||||
|
||||
voltage: int | None = Field(
|
||||
None,
|
||||
description="Battery voltage in millivolts"
|
||||
)
|
||||
|
||||
ts: datetime | None = Field(
|
||||
None,
|
||||
description="Timestamp of the state reading"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def normalize_bool(is_open: bool) -> "ContactState":
|
||||
"""Convert boolean to ContactState.
|
||||
|
||||
Helper method to convert a boolean value to a ContactState instance.
|
||||
Useful when integrating with systems that use True/False for contact state.
|
||||
|
||||
Args:
|
||||
is_open: True if contact is open, False if closed
|
||||
|
||||
Returns:
|
||||
ContactState instance with appropriate contact value
|
||||
|
||||
Examples:
|
||||
>>> ContactState.normalize_bool(True)
|
||||
ContactState(contact='open', ...)
|
||||
|
||||
>>> ContactState.normalize_bool(False)
|
||||
ContactState(contact='closed', ...)
|
||||
"""
|
||||
return ContactState(contact="open" if is_open else "closed")
|
||||
|
||||
|
||||
# Public API
|
||||
__all__ = ["ContactState", "CAP_VERSION", "DISPLAY_NAME"]
|
||||
37
packages/home_capabilities/temp_humidity_sensor.py
Normal file
37
packages/home_capabilities/temp_humidity_sensor.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Temperature & Humidity Sensor Capability - temp_humidity_sensor@1.0.0
|
||||
|
||||
Read-only sensor for temperature and humidity measurements.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TempHumidityState(BaseModel):
|
||||
"""
|
||||
State model for temperature & humidity sensors.
|
||||
|
||||
Required fields:
|
||||
- temperature: Temperature in degrees Celsius
|
||||
- humidity: Relative humidity in percent
|
||||
|
||||
Optional fields:
|
||||
- battery: Battery level 0-100%
|
||||
- linkquality: Signal quality indicator
|
||||
- voltage: Battery voltage in mV
|
||||
- ts: Timestamp of measurement
|
||||
"""
|
||||
temperature: float = Field(..., description="Temperature in degrees Celsius")
|
||||
humidity: float = Field(..., description="Relative humidity in percent (0-100)")
|
||||
battery: Annotated[int, Field(ge=0, le=100)] | None = None
|
||||
linkquality: int | None = None
|
||||
voltage: int | None = None
|
||||
ts: datetime | None = None
|
||||
|
||||
|
||||
# Capability metadata
|
||||
CAP_VERSION = "temp_humidity_sensor@1.0.0"
|
||||
DISPLAY_NAME = "Temperature & Humidity Sensor"
|
||||
Reference in New Issue
Block a user