Compare commits
11 Commits
window_con
...
shellies_r
| Author | SHA1 | Date | |
|---|---|---|---|
|
0f43f37823
|
|||
|
93e70da97d
|
|||
|
62d302bf41
|
|||
|
3d6130f2c2
|
|||
|
2a8d569bb5
|
|||
|
6a5f814cb4
|
|||
|
cc3c15078c
|
|||
|
7772dac000
|
|||
|
97ea853483
|
|||
|
86d1933c1f
|
|||
|
9458381593
|
@@ -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, RelayState
|
||||
from apps.abstraction.transformation import (
|
||||
transform_abstract_to_vendor,
|
||||
transform_vendor_to_abstract
|
||||
@@ -154,6 +154,9 @@ async def handle_abstract_set(
|
||||
if device_type == "light":
|
||||
# Validate light SET payload (power and/or brightness)
|
||||
LightState.model_validate(abstract_payload)
|
||||
elif device_type == "relay":
|
||||
# Validate relay SET payload (power only)
|
||||
RelayState.model_validate(abstract_payload)
|
||||
elif device_type == "thermostat":
|
||||
# For thermostat SET: only allow mode and target fields
|
||||
allowed_set_fields = {"mode", "target"}
|
||||
@@ -178,9 +181,10 @@ async def handle_abstract_set(
|
||||
# Transform abstract payload to vendor-specific format
|
||||
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
|
||||
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
|
||||
else:
|
||||
vendor_message = json.dumps(vendor_payload)
|
||||
@@ -216,18 +220,24 @@ async def handle_vendor_state(
|
||||
try:
|
||||
if device_type == "light":
|
||||
LightState.model_validate(abstract_payload)
|
||||
elif device_type == "relay":
|
||||
RelayState.model_validate(abstract_payload)
|
||||
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/{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"]
|
||||
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
|
||||
if is_max_device:
|
||||
# MAX! sends plain integer/string, not JSON
|
||||
if is_max_device or is_shelly_relay:
|
||||
# MAX! and Shelly send plain text, not JSON
|
||||
payload = payload_str.strip()
|
||||
else:
|
||||
# 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,
|
||||
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:
|
||||
# Find device by vendor state topic for other technologies
|
||||
for device_id, device in devices.items():
|
||||
|
||||
@@ -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!)
|
||||
# ============================================================================
|
||||
@@ -368,6 +480,22 @@ 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,
|
||||
|
||||
# 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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,9 +19,13 @@ from packages.home_capabilities import (
|
||||
LIGHT_VERSION,
|
||||
THERMOSTAT_VERSION,
|
||||
CONTACT_SENSOR_VERSION,
|
||||
TEMP_HUMIDITY_SENSOR_VERSION,
|
||||
RELAY_VERSION,
|
||||
LightState,
|
||||
ThermostatState,
|
||||
ContactState
|
||||
ContactState,
|
||||
TempHumidityState,
|
||||
RelayState
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -145,7 +149,9 @@ 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,
|
||||
"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,
|
||||
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":
|
||||
try:
|
||||
# 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,
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
@@ -519,6 +571,8 @@
|
||||
{% if device.type == "light" %}
|
||||
Light
|
||||
{% if device.features.brightness %}• Dimmbar{% endif %}
|
||||
{% elif device.type == "relay" %}
|
||||
Relay
|
||||
{% elif device.type == "thermostat" %}
|
||||
Thermostat
|
||||
{% elif device.type == "contact" or device.type == "contact_sensor" %}
|
||||
@@ -569,6 +623,21 @@
|
||||
</div>
|
||||
{% 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" %}
|
||||
<div class="thermostat-display">
|
||||
<div class="temp-reading">
|
||||
@@ -607,6 +676,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 %}
|
||||
@@ -733,11 +824,13 @@
|
||||
let eventSource = null;
|
||||
let currentState = {};
|
||||
let thermostatTargets = {};
|
||||
let deviceTypes = {};
|
||||
|
||||
// Initialize device states
|
||||
{% for room in rooms %}
|
||||
{% 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';
|
||||
{% elif device.type == "thermostat" %}
|
||||
thermostatTargets['{{ device.device_id }}'] = 21.0;
|
||||
@@ -748,6 +841,7 @@
|
||||
// Toggle device state
|
||||
async function toggleDevice(deviceId) {
|
||||
const newState = currentState[deviceId] === 'on' ? 'off' : 'on';
|
||||
const deviceType = deviceTypes[deviceId] || 'light';
|
||||
|
||||
try {
|
||||
const response = await fetch(api(`/devices/${deviceId}/set`), {
|
||||
@@ -756,7 +850,7 @@
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'light',
|
||||
type: deviceType,
|
||||
payload: {
|
||||
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
|
||||
function addEvent(event) {
|
||||
const eventList = document.getElementById('event-list');
|
||||
@@ -992,6 +1119,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 +1245,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) {
|
||||
|
||||
@@ -11,12 +11,11 @@ redis:
|
||||
channel: "ui:updates"
|
||||
devices:
|
||||
- device_id: lampe_semeniere_wohnzimmer
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: false
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b8000015480b"
|
||||
set: "zigbee2mqtt/0xf0d1b8000015480b/set"
|
||||
@@ -26,12 +25,11 @@ devices:
|
||||
model: "AC10691"
|
||||
vendor: "OSRAM"
|
||||
- device_id: grosse_lampe_wohnzimmer
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: false
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b80000151aca"
|
||||
set: "zigbee2mqtt/0xf0d1b80000151aca/set"
|
||||
@@ -41,12 +39,11 @@ devices:
|
||||
model: "AC10691"
|
||||
vendor: "OSRAM"
|
||||
- device_id: lampe_naehtischchen_wohnzimmer
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: false
|
||||
topics:
|
||||
state: "zigbee2mqtt/0x842e14fffee560ee"
|
||||
set: "zigbee2mqtt/0x842e14fffee560ee/set"
|
||||
@@ -56,12 +53,11 @@ devices:
|
||||
model: "HG06337"
|
||||
vendor: "Lidl"
|
||||
- device_id: kleine_lampe_rechts_esszimmer
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: false
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b80000156645"
|
||||
set: "zigbee2mqtt/0xf0d1b80000156645/set"
|
||||
@@ -71,12 +67,11 @@ devices:
|
||||
model: "AC10691"
|
||||
vendor: "OSRAM"
|
||||
- device_id: kleine_lampe_links_esszimmer
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: false
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b80000153099"
|
||||
set: "zigbee2mqtt/0xf0d1b80000153099/set"
|
||||
@@ -101,12 +96,11 @@ devices:
|
||||
model: "LED1842G3"
|
||||
vendor: "IKEA"
|
||||
- device_id: medusalampe_schlafzimmer
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: false
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b80000154c7c"
|
||||
set: "zigbee2mqtt/0xf0d1b80000154c7c/set"
|
||||
@@ -192,12 +186,11 @@ devices:
|
||||
model: "8718699673147"
|
||||
vendor: "Philips"
|
||||
- device_id: schranklicht_vorne_patty
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: false
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b80000154cf5"
|
||||
set: "zigbee2mqtt/0xf0d1b80000154cf5/set"
|
||||
@@ -504,12 +497,11 @@ devices:
|
||||
peer_id: "48"
|
||||
channel: "1"
|
||||
- device_id: sterne_wohnzimmer
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: false
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b80000155fc2"
|
||||
set: "zigbee2mqtt/0xf0d1b80000155fc2/set"
|
||||
@@ -518,7 +510,6 @@ devices:
|
||||
ieee_address: "0xf0d1b80000155fc2"
|
||||
model: "AC10691"
|
||||
vendor: "OSRAM"
|
||||
|
||||
- device_id: kontakt_schlafzimmer_strasse
|
||||
type: contact
|
||||
name: Kontakt Schlafzimmer Straße
|
||||
@@ -639,4 +630,132 @@ 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: {}
|
||||
- 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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -47,12 +51,16 @@ rooms:
|
||||
title: kleine Lampe rechts Esszimmer
|
||||
icon: 💡
|
||||
rank: 90
|
||||
- device_id: licht_schrank_esszimmer
|
||||
title: Schranklicht Esszimmer
|
||||
icon: 💡
|
||||
rank: 92
|
||||
- device_id: thermostat_esszimmer
|
||||
title: Thermostat Esszimmer
|
||||
icon: 🌡️
|
||||
rank: 95
|
||||
- device_id: kontakt_esszimmer_strasse_rechts
|
||||
title: Kontakt Straße rechts
|
||||
title: Kontakt Straße rechtsFtest
|
||||
icon: 🪟
|
||||
rank: 96
|
||||
- device_id: kontakt_esszimmer_strasse_links
|
||||
@@ -77,6 +85,10 @@ rooms:
|
||||
title: grosse Lampe Wohnzimmer
|
||||
icon: 💡
|
||||
rank: 130
|
||||
- device_id: licht_regal_wohnzimmer
|
||||
title: Regallicht Wohnzimmer
|
||||
icon: 💡
|
||||
rank: 132
|
||||
- device_id: thermostat_wohnzimmer
|
||||
title: Thermostat Wohnzimmer
|
||||
icon: 🌡️
|
||||
@@ -89,12 +101,20 @@ 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
|
||||
title: Küche Deckenlampe
|
||||
icon: 💡
|
||||
rank: 140
|
||||
- device_id: licht_spuele_kueche
|
||||
title: Küche Spüle
|
||||
icon: 💡
|
||||
rank: 142
|
||||
- device_id: thermostat_kueche
|
||||
title: Kueche
|
||||
icon: 🌡️
|
||||
@@ -115,6 +135,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 +169,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 +187,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
|
||||
@@ -169,10 +201,18 @@ rooms:
|
||||
title: Haustür
|
||||
icon: 💡
|
||||
rank: 220
|
||||
- device_id: licht_flur_schrank
|
||||
title: Schranklicht Flur
|
||||
icon: 💡
|
||||
rank: 222
|
||||
- device_id: licht_flur_oben_am_spiegel
|
||||
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 +227,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 +241,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 +255,20 @@ 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
|
||||
- name: Outdoor
|
||||
devices:
|
||||
- device_id: licht_terasse
|
||||
title: Licht Terasse
|
||||
icon: 💡
|
||||
rank: 290
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.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.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
|
||||
|
||||
__all__ = [
|
||||
@@ -15,6 +19,10 @@ __all__ = [
|
||||
"THERMOSTAT_VERSION",
|
||||
"ContactState",
|
||||
"CONTACT_SENSOR_VERSION",
|
||||
"TempHumidityState",
|
||||
"TEMP_HUMIDITY_SENSOR_VERSION",
|
||||
"RelayState",
|
||||
"RELAY_VERSION",
|
||||
"DeviceTile",
|
||||
"Room",
|
||||
"UiLayout",
|
||||
|
||||
21
packages/home_capabilities/relay.py
Normal file
21
packages/home_capabilities/relay.py
Normal 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")
|
||||
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