11 Commits

Author SHA1 Message Date
0f43f37823 shellies 2025-11-11 11:39:10 +01:00
93e70da97d add spuele 3 2025-11-11 11:11:14 +01:00
62d302bf41 add spuele 2 2025-11-11 11:10:31 +01:00
3d6130f2c2 add spuele 2025-11-11 11:09:08 +01:00
2a8d569bb5 shelly 2025-11-11 11:01:52 +01:00
6a5f814cb4 fix in layout, drop test entry 2025-11-11 10:28:27 +01:00
cc3c15078c change relays to type relay 2025-11-11 10:24:09 +01:00
7772dac000 medusa lampe to relay 2025-11-11 10:12:25 +01:00
97ea853483 add type relay 2025-11-11 10:10:22 +01:00
86d1933c1f sensoren 2 2025-11-11 09:13:46 +01:00
9458381593 sensoren 2025-11-11 09:12:35 +01:00
10 changed files with 613 additions and 53 deletions

View File

@@ -15,7 +15,7 @@ import uuid
from aiomqtt import Client from aiomqtt import Client
from pydantic import ValidationError from pydantic import ValidationError
from packages.home_capabilities import LightState, ThermostatState, ContactState from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState, RelayState
from apps.abstraction.transformation import ( from apps.abstraction.transformation import (
transform_abstract_to_vendor, transform_abstract_to_vendor,
transform_vendor_to_abstract transform_vendor_to_abstract
@@ -154,6 +154,9 @@ async def handle_abstract_set(
if device_type == "light": if device_type == "light":
# Validate light SET payload (power and/or brightness) # Validate light SET payload (power and/or brightness)
LightState.model_validate(abstract_payload) LightState.model_validate(abstract_payload)
elif device_type == "relay":
# Validate relay SET payload (power only)
RelayState.model_validate(abstract_payload)
elif device_type == "thermostat": elif device_type == "thermostat":
# For thermostat SET: only allow mode and target fields # For thermostat SET: only allow mode and target fields
allowed_set_fields = {"mode", "target"} allowed_set_fields = {"mode", "target"}
@@ -178,9 +181,10 @@ async def handle_abstract_set(
# Transform abstract payload to vendor-specific format # Transform abstract payload to vendor-specific format
vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload) vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload)
# For MAX! thermostats, vendor_payload is a plain string (integer temperature) # For MAX! thermostats and Shelly relays, vendor_payload is a plain string
# For other devices, it's a dict that needs JSON encoding # For other devices, it's a dict that needs JSON encoding
if device_technology == "max" and device_type == "thermostat": if (device_technology == "max" and device_type == "thermostat") or \
(device_technology == "shelly" and device_type == "relay"):
vendor_message = vendor_payload # Already a string vendor_message = vendor_payload # Already a string
else: else:
vendor_message = json.dumps(vendor_payload) vendor_message = json.dumps(vendor_payload)
@@ -216,18 +220,24 @@ async def handle_vendor_state(
try: try:
if device_type == "light": if device_type == "light":
LightState.model_validate(abstract_payload) LightState.model_validate(abstract_payload)
elif device_type == "relay":
RelayState.model_validate(abstract_payload)
elif device_type == "thermostat": elif device_type == "thermostat":
# Validate thermostat state: mode, target, current (required), battery, window_open # Validate thermostat state: mode, target, current (required), battery, window_open
ThermostatState.model_validate(abstract_payload) ThermostatState.model_validate(abstract_payload)
elif device_type in {"contact", "contact_sensor"}: elif device_type in {"contact", "contact_sensor"}:
# Validate contact sensor state # Validate contact sensor state
ContactState.model_validate(abstract_payload) 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: except ValidationError as e:
logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}") logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}")
return return
# Normalize device type for topic (use 'contact' for both 'contact' and 'contact_sensor') # 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 = "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) # Publish to abstract state topic (retained)
abstract_topic = f"home/{topic_type}/{device_id}/state" abstract_topic = f"home/{topic_type}/{device_id}/state"
@@ -330,9 +340,21 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
max_device_type = device["type"] max_device_type = device["type"]
break break
# Check for Shelly relay (also sends plain text)
is_shelly_relay = False
shelly_device_id = None
shelly_device_type = None
for device_id, device in devices.items():
if device.get("technology") == "shelly" and device.get("type") == "relay":
if topic == device["topics"]["state"]:
is_shelly_relay = True
shelly_device_id = device_id
shelly_device_type = device["type"]
break
# Parse payload based on device technology # Parse payload based on device technology
if is_max_device: if is_max_device or is_shelly_relay:
# MAX! sends plain integer/string, not JSON # MAX! and Shelly send plain text, not JSON
payload = payload_str.strip() payload = payload_str.strip()
else: else:
# All other technologies use JSON # All other technologies use JSON
@@ -368,6 +390,14 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
client, redis_client, max_device_id, max_device_type, client, redis_client, max_device_id, max_device_type,
device_technology, payload, redis_channel device_technology, payload, redis_channel
) )
# For Shelly relay devices, we already identified them above
elif is_shelly_relay:
device = devices[shelly_device_id]
device_technology = device.get("technology", "unknown")
await handle_vendor_state(
client, redis_client, shelly_device_id, shelly_device_type,
device_technology, payload, redis_channel
)
else: else:
# Find device by vendor state topic for other technologies # Find device by vendor state topic for other technologies
for device_id, device in devices.items(): for device_id, device in devices.items():

View File

@@ -265,6 +265,118 @@ 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: relay - zigbee2mqtt technology
# ============================================================================
def _transform_relay_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract relay payload to zigbee2mqtt format.
Relay only has power on/off, same transformation as light.
- power: 'on'/'off' -> state: 'ON'/'OFF'
"""
vendor_payload = payload.copy()
if "power" in vendor_payload:
power_value = vendor_payload.pop("power")
vendor_payload["state"] = power_value.upper() if isinstance(power_value, str) else power_value
return vendor_payload
def _transform_relay_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> 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'
"""
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'}
"""
# Shelly payload is a plain string, not a dict
if isinstance(payload, str):
return {"power": payload.strip()}
# Fallback if it's already a dict (shouldn't happen)
return payload
# ============================================================================ # ============================================================================
# HANDLER FUNCTIONS: max technology (Homegear MAX!) # HANDLER FUNCTIONS: max technology (Homegear MAX!)
# ============================================================================ # ============================================================================
@@ -368,6 +480,22 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
("contact", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract, ("contact", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract,
("contact", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor, ("contact", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor,
("contact", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract, ("contact", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract,
# Temperature & humidity sensor transformations (support both type aliases)
("temp_humidity_sensor", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity_sensor", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
("temp_humidity_sensor", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
("temp_humidity_sensor", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
("temp_humidity", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
("temp_humidity", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
("temp_humidity", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
# Relay transformations
("relay", "zigbee2mqtt", "to_vendor"): _transform_relay_zigbee2mqtt_to_vendor,
("relay", "zigbee2mqtt", "to_abstract"): _transform_relay_zigbee2mqtt_to_abstract,
("relay", "shelly", "to_vendor"): _transform_relay_shelly_to_vendor,
("relay", "shelly", "to_abstract"): _transform_relay_shelly_to_abstract,
} }

View File

@@ -19,9 +19,13 @@ from packages.home_capabilities import (
LIGHT_VERSION, LIGHT_VERSION,
THERMOSTAT_VERSION, THERMOSTAT_VERSION,
CONTACT_SENSOR_VERSION, CONTACT_SENSOR_VERSION,
TEMP_HUMIDITY_SENSOR_VERSION,
RELAY_VERSION,
LightState, LightState,
ThermostatState, ThermostatState,
ContactState ContactState,
TempHumidityState,
RelayState
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -145,7 +149,9 @@ async def spec() -> dict[str, dict[str, str]]:
"capabilities": { "capabilities": {
"light": LIGHT_VERSION, "light": LIGHT_VERSION,
"thermostat": THERMOSTAT_VERSION, "thermostat": THERMOSTAT_VERSION,
"contact": CONTACT_SENSOR_VERSION "contact": CONTACT_SENSOR_VERSION,
"temp_humidity": TEMP_HUMIDITY_SENSOR_VERSION,
"relay": RELAY_VERSION
} }
} }
@@ -355,6 +361,14 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid payload for light: {e}" detail=f"Invalid payload for light: {e}"
) )
elif request.type == "relay":
try:
RelayState(**request.payload)
except ValidationError as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid payload for relay: {e}"
)
elif request.type == "thermostat": elif request.type == "thermostat":
try: try:
# For thermostat SET: only allow mode and target # For thermostat SET: only allow mode and target
@@ -377,6 +391,12 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str
status_code=status.HTTP_405_METHOD_NOT_ALLOWED, status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Contact sensors are read-only devices" 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: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,

View File

@@ -426,6 +426,58 @@
text-align: center; 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 { .events {
margin-top: 2rem; margin-top: 2rem;
background: white; background: white;
@@ -519,6 +571,8 @@
{% if device.type == "light" %} {% if device.type == "light" %}
Light Light
{% if device.features.brightness %}• Dimmbar{% endif %} {% if device.features.brightness %}• Dimmbar{% endif %}
{% elif device.type == "relay" %}
Relay
{% elif device.type == "thermostat" %} {% elif device.type == "thermostat" %}
Thermostat Thermostat
{% elif device.type == "contact" or device.type == "contact_sensor" %} {% elif device.type == "contact" or device.type == "contact_sensor" %}
@@ -569,6 +623,21 @@
</div> </div>
{% endif %} {% endif %}
{% elif device.type == "relay" %}
<div class="device-state">
<span class="state-label">Status:</span>
<span class="state-value off" id="state-{{ device.device_id }}">off</span>
</div>
<div class="controls">
<button
class="toggle-button off"
id="toggle-{{ device.device_id }}"
onclick="toggleDevice('{{ device.device_id }}')">
Einschalten
</button>
</div>
{% elif device.type == "thermostat" %} {% elif device.type == "thermostat" %}
<div class="thermostat-display"> <div class="thermostat-display">
<div class="temp-reading"> <div class="temp-reading">
@@ -607,6 +676,28 @@
🔒 Nur-Lesen Gerät • Keine Steuerung möglich 🔒 Nur-Lesen Gerät • Keine Steuerung möglich
</div> </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 %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
@@ -733,11 +824,13 @@
let eventSource = null; let eventSource = null;
let currentState = {}; let currentState = {};
let thermostatTargets = {}; let thermostatTargets = {};
let deviceTypes = {};
// Initialize device states // Initialize device states
{% for room in rooms %} {% for room in rooms %}
{% for device in room.devices %} {% for device in room.devices %}
{% if device.type == "light" %} deviceTypes['{{ device.device_id }}'] = '{{ device.type }}';
{% if device.type == "light" or device.type == "relay" %}
currentState['{{ device.device_id }}'] = 'off'; currentState['{{ device.device_id }}'] = 'off';
{% elif device.type == "thermostat" %} {% elif device.type == "thermostat" %}
thermostatTargets['{{ device.device_id }}'] = 21.0; thermostatTargets['{{ device.device_id }}'] = 21.0;
@@ -748,6 +841,7 @@
// Toggle device state // Toggle device state
async function toggleDevice(deviceId) { async function toggleDevice(deviceId) {
const newState = currentState[deviceId] === 'on' ? 'off' : 'on'; const newState = currentState[deviceId] === 'on' ? 'off' : 'on';
const deviceType = deviceTypes[deviceId] || 'light';
try { try {
const response = await fetch(api(`/devices/${deviceId}/set`), { const response = await fetch(api(`/devices/${deviceId}/set`), {
@@ -756,7 +850,7 @@
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
type: 'light', type: deviceType,
payload: { payload: {
power: newState power: newState
} }
@@ -922,6 +1016,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 // Add event to list
function addEvent(event) { function addEvent(event) {
const eventList = document.getElementById('event-list'); const eventList = document.getElementById('event-list');
@@ -992,6 +1119,11 @@
if (data.payload.contact !== undefined) { if (data.payload.contact !== undefined) {
updateContactUI(data.device_id, data.payload.contact); 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 +1245,9 @@
} else if (state.contact !== undefined) { } else if (state.contact !== undefined) {
// It's a contact sensor // It's a contact sensor
updateContactUI(deviceId, state.contact); updateContactUI(deviceId, state.contact);
} else if (state.temperature !== undefined || state.humidity !== undefined) {
// It's a temp/humidity sensor
updateTempHumidityUI(deviceId, state);
} }
} }
} catch (error) { } catch (error) {

View File

@@ -11,12 +11,11 @@ redis:
channel: "ui:updates" channel: "ui:updates"
devices: devices:
- device_id: lampe_semeniere_wohnzimmer - device_id: lampe_semeniere_wohnzimmer
type: light type: relay
cap_version: "light@1.2.0" cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b8000015480b" state: "zigbee2mqtt/0xf0d1b8000015480b"
set: "zigbee2mqtt/0xf0d1b8000015480b/set" set: "zigbee2mqtt/0xf0d1b8000015480b/set"
@@ -26,12 +25,11 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: grosse_lampe_wohnzimmer - device_id: grosse_lampe_wohnzimmer
type: light type: relay
cap_version: "light@1.2.0" cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000151aca" state: "zigbee2mqtt/0xf0d1b80000151aca"
set: "zigbee2mqtt/0xf0d1b80000151aca/set" set: "zigbee2mqtt/0xf0d1b80000151aca/set"
@@ -41,12 +39,11 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: lampe_naehtischchen_wohnzimmer - device_id: lampe_naehtischchen_wohnzimmer
type: light type: relay
cap_version: "light@1.2.0" cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0x842e14fffee560ee" state: "zigbee2mqtt/0x842e14fffee560ee"
set: "zigbee2mqtt/0x842e14fffee560ee/set" set: "zigbee2mqtt/0x842e14fffee560ee/set"
@@ -56,12 +53,11 @@ devices:
model: "HG06337" model: "HG06337"
vendor: "Lidl" vendor: "Lidl"
- device_id: kleine_lampe_rechts_esszimmer - device_id: kleine_lampe_rechts_esszimmer
type: light type: relay
cap_version: "light@1.2.0" cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000156645" state: "zigbee2mqtt/0xf0d1b80000156645"
set: "zigbee2mqtt/0xf0d1b80000156645/set" set: "zigbee2mqtt/0xf0d1b80000156645/set"
@@ -71,12 +67,11 @@ devices:
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: kleine_lampe_links_esszimmer - device_id: kleine_lampe_links_esszimmer
type: light type: relay
cap_version: "light@1.2.0" cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000153099" state: "zigbee2mqtt/0xf0d1b80000153099"
set: "zigbee2mqtt/0xf0d1b80000153099/set" set: "zigbee2mqtt/0xf0d1b80000153099/set"
@@ -101,12 +96,11 @@ devices:
model: "LED1842G3" model: "LED1842G3"
vendor: "IKEA" vendor: "IKEA"
- device_id: medusalampe_schlafzimmer - device_id: medusalampe_schlafzimmer
type: light type: relay
cap_version: "light@1.2.0" cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000154c7c" state: "zigbee2mqtt/0xf0d1b80000154c7c"
set: "zigbee2mqtt/0xf0d1b80000154c7c/set" set: "zigbee2mqtt/0xf0d1b80000154c7c/set"
@@ -192,12 +186,11 @@ devices:
model: "8718699673147" model: "8718699673147"
vendor: "Philips" vendor: "Philips"
- device_id: schranklicht_vorne_patty - device_id: schranklicht_vorne_patty
type: light type: relay
cap_version: "light@1.2.0" cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000154cf5" state: "zigbee2mqtt/0xf0d1b80000154cf5"
set: "zigbee2mqtt/0xf0d1b80000154cf5/set" set: "zigbee2mqtt/0xf0d1b80000154cf5/set"
@@ -504,12 +497,11 @@ devices:
peer_id: "48" peer_id: "48"
channel: "1" channel: "1"
- device_id: sterne_wohnzimmer - device_id: sterne_wohnzimmer
type: light type: relay
cap_version: "light@1.2.0" cap_version: "relay@1.0.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
state: "zigbee2mqtt/0xf0d1b80000155fc2" state: "zigbee2mqtt/0xf0d1b80000155fc2"
set: "zigbee2mqtt/0xf0d1b80000155fc2/set" set: "zigbee2mqtt/0xf0d1b80000155fc2/set"
@@ -518,7 +510,6 @@ devices:
ieee_address: "0xf0d1b80000155fc2" ieee_address: "0xf0d1b80000155fc2"
model: "AC10691" model: "AC10691"
vendor: "OSRAM" vendor: "OSRAM"
- device_id: kontakt_schlafzimmer_strasse - device_id: kontakt_schlafzimmer_strasse
type: contact type: contact
name: Kontakt Schlafzimmer Straße name: Kontakt Schlafzimmer Straße
@@ -639,4 +630,132 @@ devices:
topics: topics:
state: homegear/instance1/plain/44/1/STATE state: homegear/instance1/plain/44/1/STATE
features: {} 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: {}
- device_id: licht_spuele_kueche
type: relay
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/LightKitchenSink/relay/0/command"
state: "shellies/LightKitchenSink/relay/0"
- device_id: licht_schrank_esszimmer
type: relay
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/schrankesszimmer/relay/0/command"
state: "shellies/schrankesszimmer/relay/0"
- device_id: licht_regal_wohnzimmer
type: relay
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/wohnzimmer-regal/relay/0/command"
state: "shellies/wohnzimmer-regal/relay/0"
- device_id: licht_flur_schrank
type: relay
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
type: relay
cap_version: "relay@1.0.0"
technology: shelly
features:
power: true
topics:
set: "shellies/lichtterasse/relay/0/command"
state: "shellies/lichtterasse/relay/0"

View File

@@ -25,6 +25,10 @@ rooms:
title: Kontakt Straße title: Kontakt Straße
icon: 🪟 icon: 🪟
rank: 46 rank: 46
- device_id: sensor_schlafzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 47
- name: Esszimmer - name: Esszimmer
devices: devices:
- device_id: deckenlampe_esszimmer - device_id: deckenlampe_esszimmer
@@ -47,12 +51,16 @@ rooms:
title: kleine Lampe rechts Esszimmer title: kleine Lampe rechts Esszimmer
icon: 💡 icon: 💡
rank: 90 rank: 90
- device_id: licht_schrank_esszimmer
title: Schranklicht Esszimmer
icon: 💡
rank: 92
- device_id: thermostat_esszimmer - device_id: thermostat_esszimmer
title: Thermostat Esszimmer title: Thermostat Esszimmer
icon: 🌡️ icon: 🌡️
rank: 95 rank: 95
- device_id: kontakt_esszimmer_strasse_rechts - device_id: kontakt_esszimmer_strasse_rechts
title: Kontakt Straße rechts title: Kontakt Straße rechtsFtest
icon: 🪟 icon: 🪟
rank: 96 rank: 96
- device_id: kontakt_esszimmer_strasse_links - device_id: kontakt_esszimmer_strasse_links
@@ -77,6 +85,10 @@ rooms:
title: grosse Lampe Wohnzimmer title: grosse Lampe Wohnzimmer
icon: 💡 icon: 💡
rank: 130 rank: 130
- device_id: licht_regal_wohnzimmer
title: Regallicht Wohnzimmer
icon: 💡
rank: 132
- device_id: thermostat_wohnzimmer - device_id: thermostat_wohnzimmer
title: Thermostat Wohnzimmer title: Thermostat Wohnzimmer
icon: 🌡️ icon: 🌡️
@@ -89,12 +101,20 @@ rooms:
title: Kontakt Garten links title: Kontakt Garten links
icon: 🪟 icon: 🪟
rank: 137 rank: 137
- device_id: sensor_wohnzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 138
- name: Küche - name: Küche
devices: devices:
- device_id: kueche_deckenlampe - device_id: kueche_deckenlampe
title: Küche Deckenlampe title: Küche Deckenlampe
icon: 💡 icon: 💡
rank: 140 rank: 140
- device_id: licht_spuele_kueche
title: Küche Spüle
icon: 💡
rank: 142
- device_id: thermostat_kueche - device_id: thermostat_kueche
title: Kueche title: Kueche
icon: 🌡️ icon: 🌡️
@@ -115,6 +135,10 @@ rooms:
title: Kontakt Straße links title: Kontakt Straße links
icon: 🪟 icon: 🪟
rank: 154 rank: 154
- device_id: sensor_kueche
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 155
- name: Arbeitszimmer Patty - name: Arbeitszimmer Patty
devices: devices:
- device_id: leselampe_patty - device_id: leselampe_patty
@@ -145,6 +169,10 @@ rooms:
title: Kontakt Straße title: Kontakt Straße
icon: 🪟 icon: 🪟
rank: 188 rank: 188
- device_id: sensor_arbeitszimmer_patty
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 189
- name: Arbeitszimmer Wolfgang - name: Arbeitszimmer Wolfgang
devices: devices:
- device_id: thermostat_wolfgang - device_id: thermostat_wolfgang
@@ -159,6 +187,10 @@ rooms:
title: Kontakt Garten title: Kontakt Garten
icon: 🪟 icon: 🪟
rank: 201 rank: 201
- device_id: sensor_arbeitszimmer_wolfgang
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 202
- name: Flur - name: Flur
devices: devices:
- device_id: deckenlampe_flur_oben - device_id: deckenlampe_flur_oben
@@ -169,10 +201,18 @@ rooms:
title: Haustür title: Haustür
icon: 💡 icon: 💡
rank: 220 rank: 220
- device_id: licht_flur_schrank
title: Schranklicht Flur
icon: 💡
rank: 222
- device_id: licht_flur_oben_am_spiegel - device_id: licht_flur_oben_am_spiegel
title: Licht Flur oben am Spiegel title: Licht Flur oben am Spiegel
icon: 💡 icon: 💡
rank: 230 rank: 230
- device_id: sensor_flur
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 235
- name: Sportzimmer - name: Sportzimmer
devices: devices:
- device_id: sportlicht_regal - device_id: sportlicht_regal
@@ -187,6 +227,10 @@ rooms:
title: Sportlicht am Fernseher, Studierzimmer title: Sportlicht am Fernseher, Studierzimmer
icon: 🏃 icon: 🏃
rank: 260 rank: 260
- device_id: sensor_sportzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 265
- name: Bad Oben - name: Bad Oben
devices: devices:
- device_id: thermostat_bad_oben - device_id: thermostat_bad_oben
@@ -197,6 +241,10 @@ rooms:
title: Kontakt Straße title: Kontakt Straße
icon: 🪟 icon: 🪟
rank: 271 rank: 271
- device_id: sensor_bad_oben
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 272
- name: Bad Unten - name: Bad Unten
devices: devices:
- device_id: thermostat_bad_unten - device_id: thermostat_bad_unten
@@ -207,4 +255,20 @@ rooms:
title: Kontakt Straße title: Kontakt Straße
icon: 🪟 icon: 🪟
rank: 281 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
- name: Outdoor
devices:
- device_id: licht_terasse
title: Licht Terasse
icon: 💡
rank: 290

View File

@@ -1,33 +1,31 @@
Schlafzimmer Schlafzimmer
52 Straße 0x00158d00043292dc
Esszimmer Esszimmer
26 Straße rechts
27 Straße links
Wohnzimmer Wohnzimmer
28 Garten rechts 0x00158d0008975707
29 Garten links
Küche Küche
0x00158d008b332785 Garten Fenster 0x00158d00083299bb
0x00158d008b332788 Garten Tür
0x00158d008b151803 Straße rechts
0x00158d008b331d0b Straße links
Arbeitszimmer Patty Arbeitszimmer Patty
18 Garten rechts 0x00158d0003f052b7
22 Garten links
0x00158d000af457cf Straße
Arbeitszimmer Wolfgang Arbeitszimmer Wolfgang
0x00158d008b3328da Garten 0x00158d000543fb99
Bad Oben Bad Oben
0x00158d008b333aec Straße 0x00158d00093e8987
Bad Unten Bad Unten
44 Straße 0x00158d00093e662a
Flur
0x00158d000836ccc6
Waschküche
0x00158d000449f3bc
Sportzimmer
0x00158d0009421422

View File

@@ -6,6 +6,10 @@ from packages.home_capabilities.thermostat import CAP_VERSION as THERMOSTAT_VERS
from packages.home_capabilities.thermostat import ThermostatState 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 CAP_VERSION as CONTACT_SENSOR_VERSION
from packages.home_capabilities.contact_sensor import ContactState 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.relay import CAP_VERSION as RELAY_VERSION
from packages.home_capabilities.relay import RelayState
from packages.home_capabilities.layout import DeviceTile, Room, UiLayout, load_layout from packages.home_capabilities.layout import DeviceTile, Room, UiLayout, load_layout
__all__ = [ __all__ = [
@@ -15,6 +19,10 @@ __all__ = [
"THERMOSTAT_VERSION", "THERMOSTAT_VERSION",
"ContactState", "ContactState",
"CONTACT_SENSOR_VERSION", "CONTACT_SENSOR_VERSION",
"TempHumidityState",
"TEMP_HUMIDITY_SENSOR_VERSION",
"RelayState",
"RELAY_VERSION",
"DeviceTile", "DeviceTile",
"Room", "Room",
"UiLayout", "UiLayout",

View File

@@ -0,0 +1,21 @@
"""
Relay capability model.
A relay is essentially a simple on/off switch, like a light with only power control.
"""
from pydantic import BaseModel, Field
from typing import Literal
# Capability version
CAP_VERSION = "relay@1.0.0"
DISPLAY_NAME = "Relay"
class RelayState(BaseModel):
"""State model for relay devices (on/off only)"""
power: Literal["on", "off"] = Field(..., description="Power state: on or off")
class RelaySetPayload(BaseModel):
"""Payload for setting relay state"""
power: Literal["on", "off"] = Field(..., description="Desired power state: on or off")

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