Compare commits

..

19 Commits

Author SHA1 Message Date
d162664ac7 5.0 when window open
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2026-01-10 16:05:48 +01:00
a190ba208b switch added 2026-01-03 22:21:33 +01:00
7212a3bd5a Lampentausch
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2026-01-03 20:52:10 +01:00
7e0801d21a event_generator fix
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-25 19:48:07 +01:00
49e555ce51 redis_state_listener fix 2025-12-25 19:36:19 +01:00
62f68fb513 Merge branch 'main' of gitea.hottis.de:wn/home-automation
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-22 19:18:32 +01:00
66f180755b heating rules 2025-12-22 19:18:23 +01:00
b9ba9cbd16 herdlicht again 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-18 19:04:26 +01:00
14c4c7c850 herdlicht again 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-18 19:01:33 +01:00
edb8b3313b herdlicht again
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-18 18:57:42 +01:00
68015905b0 herdlicht 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-17 16:22:11 +01:00
223c6e58b9 herdlicht
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-17 16:18:42 +01:00
0548996110 steckdose strandkorb 3
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-15 12:15:38 +01:00
35141f71a4 steckdose strandkorb 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-15 12:11:48 +01:00
eb5532739c steckdose strandkorb
All checks were successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
2025-12-15 11:52:57 +01:00
42411b1377 regallicht flur 3
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 22:17:44 +01:00
b99158fd25 regallicht flur 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 22:14:59 +01:00
d86e7eecc9 regallicht flur
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 22:13:48 +01:00
8ab9db796c regallicht kueche 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-12 22:08:06 +01:00
9 changed files with 409 additions and 133 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, TempHumidityState, RelayState, ThreePhasePowerState from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState, RelayState, ThreePhasePowerState, SwitchState
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
@@ -174,6 +174,10 @@ async def handle_abstract_set(
# Contact sensors are read-only - SET commands should not occur # Contact sensors are read-only - SET commands should not occur
logger.warning(f"Contact sensor {device_id} received SET command - ignoring (read-only device)") logger.warning(f"Contact sensor {device_id} received SET command - ignoring (read-only device)")
return return
elif device_type == "switch":
# Switches are read-only - SET commands should not occur
logger.warning(f"Switch {device_id} received SET command - ignoring (read-only device)")
return
except ValidationError as e: except ValidationError as e:
logger.error(f"Validation failed for {device_type} SET {device_id}: {e}") logger.error(f"Validation failed for {device_type} SET {device_id}: {e}")
return return
@@ -227,6 +231,9 @@ async def handle_vendor_state(
elif device_type == "three_phase_powermeter": elif device_type == "three_phase_powermeter":
# Validate three-phase powermeter state # Validate three-phase powermeter state
ThreePhasePowerState.model_validate(abstract_payload) ThreePhasePowerState.model_validate(abstract_payload)
elif device_type == "switch":
# Validate switch state
SwitchState.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

View File

@@ -1,4 +1,4 @@
"""Hottis WiFi Relay vendor transformations.""" """Hottis LED Stripe vendor transformations."""
import logging import logging
from typing import Any from typing import Any
@@ -6,33 +6,41 @@ from typing import Any
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def transform_relay_to_vendor(payload: dict[str, Any]) -> str: def transform_light_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis WiFi Relay format. """Transform abstract relay payload to Hottis LED Stripe format.
Hottis WiFi Relay expects plain text 'on' or 'off' (not JSON). Hottis LED Stripe expects plain text 'on' or 'off' (not JSON).
Example: Example:
- Abstract: {'power': 'on'} - Abstract: {'power': 'on'}
- Hottis WiFi Relay: 'ON' - Hottis LED Stripe: 'ON'
""" """
power = payload.get("power", "off").upper()
return power bri = 89.0 / 254.0
r = int(255 * bri)
g = int(103 * bri)
b = int(25 * bri)
cmd = f"{r} {g} {b}" if payload.get("power", "off").lower() == "on" else "0 0 0"
return cmd
def transform_relay_to_abstract(payload: str) -> dict[str, Any]: def transform_light_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis WiFi Relay relay payload to abstract format. """Transform Hottis LED Stripe relay payload to abstract format.
Hottis WiFi Relay sends plain text 'on' or 'off'. Hottis LED Stripe sends plain text 'on' or 'off'.
Example: Example:
- Hottis WiFi Relay: 'ON' - Hottis LED Stripe: 'ON'
- Abstract: {'power': 'on'} - Abstract: {'power': 'on'}
""" """
return {"power": payload.strip().lower()}
power = "on" if payload.strip() != "0 0 0" else "off"
return {"power": power}
# Registry of handlers for this vendor # Registry of handlers for this vendor
HANDLERS = { HANDLERS = {
("relay", "to_vendor"): transform_relay_to_vendor, ("light", "to_vendor"): transform_light_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract, ("light", "to_abstract"): transform_light_to_abstract,
} }

View File

@@ -161,6 +161,24 @@ def transform_temp_humidity_sensor_to_abstract(payload: str) -> dict[str, Any]:
return payload return payload
def transform_switch_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract switch payload to zigbee2mqtt format.
Switches are read-only, so this should not be called for SET commands.
"""
logger.warning("Switches are read-only - SET commands should not be used")
return json.dumps(payload)
def transform_switch_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt switch payload to abstract format.
Passthrough - zigbee2mqtt provides action, battery, linkquality directly.
"""
payload = json.loads(payload)
return payload
def transform_relay_to_vendor(payload: dict[str, Any]) -> str: def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to zigbee2mqtt format. """Transform abstract relay payload to zigbee2mqtt format.
@@ -204,6 +222,8 @@ HANDLERS = {
("temp_humidity_sensor", "to_abstract"): transform_temp_humidity_sensor_to_abstract, ("temp_humidity_sensor", "to_abstract"): transform_temp_humidity_sensor_to_abstract,
("temp_humidity", "to_vendor"): transform_temp_humidity_sensor_to_vendor, ("temp_humidity", "to_vendor"): transform_temp_humidity_sensor_to_vendor,
("temp_humidity", "to_abstract"): transform_temp_humidity_sensor_to_abstract, ("temp_humidity", "to_abstract"): transform_temp_humidity_sensor_to_abstract,
("switch", "to_vendor"): transform_switch_to_vendor,
("switch", "to_abstract"): transform_switch_to_abstract,
("relay", "to_vendor"): transform_relay_to_vendor, ("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_to_abstract, ("relay", "to_abstract"): transform_relay_to_abstract,
} }

View File

@@ -127,27 +127,19 @@ async def redis_state_listener():
logger.info("Redis state listener connected") logger.info("Redis state listener connected")
while True: # listen() blocks async and waits for messages - prevents busy loop
try: async for message in pubsub.listen():
message = await asyncio.wait_for( if message["type"] == "message":
pubsub.get_message(ignore_subscribe_messages=True), data = message["data"]
timeout=1.0 try:
) state_data = json.loads(data)
if state_data.get("type") == "state" and state_data.get("device_id"):
if message and message["type"] == "message": device_id = state_data["device_id"]
data = message["data"] payload = state_data.get("payload", {})
try: device_states[device_id] = payload
state_data = json.loads(data) logger.debug(f"Updated state cache for {device_id}: {payload}")
if state_data.get("type") == "state" and state_data.get("device_id"): except Exception as e:
device_id = state_data["device_id"] logger.warning(f"Failed to parse state data: {e}")
payload = state_data.get("payload", {})
device_states[device_id] = payload
logger.debug(f"Updated state cache for {device_id}: {payload}")
except Exception as e:
logger.warning(f"Failed to parse state data: {e}")
except asyncio.TimeoutError:
pass # No message, continue
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("Redis state listener cancelled") logger.info("Redis state listener cancelled")
@@ -567,25 +559,31 @@ async def event_generator(request: Request) -> AsyncGenerator[str, None]:
redis_client = None redis_client = None
pubsub = None pubsub = None
# Heartbeat tracking
last_heartbeat = asyncio.get_event_loop().time()
heartbeat_interval = 15 # Safari-friendly: shorter interval heartbeat_interval = 15 # Safari-friendly: shorter interval
# Use listen() iterator for blocking reads with heartbeat timeout
if pubsub:
listener = pubsub.listen()
else:
listener = None
while True: while True:
# Check if client disconnected # Check if client disconnected
if await request.is_disconnected(): if await request.is_disconnected():
logger.info("SSE client disconnected") logger.info("SSE client disconnected")
break break
# Try to get message from Redis (if available) # Try to get message from Redis with timeout for heartbeat
if pubsub: if listener:
try: try:
# Wait for message with heartbeat timeout
# If no message arrives within timeout, send heartbeat
message = await asyncio.wait_for( message = await asyncio.wait_for(
pubsub.get_message(ignore_subscribe_messages=True), anext(listener),
timeout=0.1 timeout=heartbeat_interval
) )
if message and message["type"] == "message": if message["type"] == "message":
data = message["data"] data = message["data"]
logger.debug(f"Sending SSE message: {data[:100]}...") logger.debug(f"Sending SSE message: {data[:100]}...")
@@ -598,24 +596,21 @@ async def event_generator(request: Request) -> AsyncGenerator[str, None]:
logger.warning(f"Failed to parse state data for cache: {e}") logger.warning(f"Failed to parse state data for cache: {e}")
yield f"event: message\ndata: {data}\n\n" yield f"event: message\ndata: {data}\n\n"
last_heartbeat = asyncio.get_event_loop().time()
continue # Skip sleep, check for more messages immediately
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass # No message, continue to heartbeat check # No message within heartbeat interval - send heartbeat
yield ": ping\n\n"
except StopAsyncIteration:
logger.warning("Redis listener stopped")
break
except Exception as e: except Exception as e:
logger.error(f"Redis error: {e}") logger.error(f"Redis error: {e}")
# Continue with heartbeats even if Redis fails # Continue with heartbeat-only mode
listener = None
# Sleep briefly to avoid busy loop else:
await asyncio.sleep(0.1) # Heartbeat-only mode (no Redis)
await asyncio.sleep(heartbeat_interval)
# Send heartbeat if interval elapsed
current_time = asyncio.get_event_loop().time()
if current_time - last_heartbeat >= heartbeat_interval:
# Comment-style ping (Safari-compatible, no event type)
yield ": ping\n\n" yield ": ping\n\n"
last_heartbeat = current_time
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("SSE connection cancelled by client") logger.info("SSE connection cancelled by client")

View File

@@ -326,41 +326,6 @@ devices:
ieee_address: "0xf0d1b8be2409f569" ieee_address: "0xf0d1b8be2409f569"
model: "4058075729063" model: "4058075729063"
vendor: "LEDVANCE" vendor: "LEDVANCE"
- device_id: licht_flur_oben_am_spiegel
homekit_aid: 22
name: Spiegel
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
color_temperature: true
topics:
state: "zigbee2mqtt/0x842e14fffefe4ba4"
set: "zigbee2mqtt/0x842e14fffefe4ba4/set"
metadata:
friendly_name: "Licht Flur oben am Spiegel"
ieee_address: "0x842e14fffefe4ba4"
model: "LED1732G11"
vendor: "IKEA"
- device_id: experimentlabtest
homekit_aid: 23
name: Test Lampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xf0d1b80000195038"
set: "zigbee2mqtt/0xf0d1b80000195038/set"
metadata:
friendly_name: "ExperimentLabTest"
ieee_address: "0xf0d1b80000195038"
model: "4058075208421"
vendor: "LEDVANCE"
- device_id: thermostat_wolfgang - device_id: thermostat_wolfgang
homekit_aid: 24 homekit_aid: 24
name: Heizung name: Heizung
@@ -843,17 +808,6 @@ devices:
topics: topics:
state: "zigbee2mqtt/0xf0d1b8000017515d" state: "zigbee2mqtt/0xf0d1b8000017515d"
set: "zigbee2mqtt/0xf0d1b8000017515d/set" set: "zigbee2mqtt/0xf0d1b8000017515d/set"
- device_id: licht_kommode_schlafzimmer
homekit_aid: 65
name: Kommode Schlafzimmer
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/04/POWER"
state: "stat/tasmota/04/POWER"
- device_id: licht_fensterbank_esszimmer - device_id: licht_fensterbank_esszimmer
homekit_aid: 66 homekit_aid: 66
name: Fensterbank Esszimmer name: Fensterbank Esszimmer
@@ -1034,14 +988,14 @@ devices:
homekit_aid: 82 homekit_aid: 82
name: Herdlicht name: Herdlicht
type: light type: light
cap_version: "relay@1.0.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
features: features:
power: true power: true
brightness: true brightness: true
topics: topics:
state: "zigbee2mqtt/herdlicht" state: "zigbee2mqtt/0x64028ffffe50e79e"
set: "zigbee2mqtt/herdlicht/set" set: "zigbee2mqtt/0x64028ffffe50e79e/set"
- device_id: regallicht_kueche - device_id: regallicht_kueche
homekit_aid: 83 homekit_aid: 83
@@ -1054,3 +1008,152 @@ devices:
topics: topics:
state: "IoT/RgbLedStripeKitchen/ColorCommand" state: "IoT/RgbLedStripeKitchen/ColorCommand"
set: "IoT/RgbLedStripeKitchen/ColorCommand" set: "IoT/RgbLedStripeKitchen/ColorCommand"
- device_id: regallicht_flur
homekit_aid: 84
name: Regallicht Flur
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wifi_relay
features:
power: true
topics:
set: "deconzhelper/flurregallist"
state: "deconzhelper/flurregallist"
- device_id: steckdose_strandkorb
homekit_aid: 85
name: Steckdose Strandkorb
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/8"
state: "dt1/ci/8"
- device_id: steckdose_vor_waschkueche
homekit_aid: 86
name: Steckdose vor Waschküche
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/9"
state: "dt1/ci/9"
- device_id: wasser_vorne
homekit_aid: 87
name: Wasser Vorgarten
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/13"
state: "dt1/ci/13"
- device_id: wasser_hinten
homekit_aid: 88
name: Wasser Garten
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/12"
state: "dt1/ci/12"
- device_id: lampe_haustuer
homekit_aid: 89
name: Lampe Haustür
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/3"
state: "dt1/ci/3"
- device_id: power_relay_oven
homekit_aid: 90
name: Schütz Herd
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/1"
state: "dt1/di/1"
- device_id: power_relay_kitchen
homekit_aid: 91
name: Schütz Küche
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/0"
state: "dt1/di/0"
- device_id: power_relay_laundry
homekit_aid: 92
name: Schütz Waschküche
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/2"
state: "dt1/di/2"
- device_id: spot_garden
homekit_aid: 93
name: Spot Garten
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/6"
state: "dt1/ci/6"
- device_id: licht_schuppen
homekit_aid: 94
name: Licht Schuppen
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "pulsegen/command/5/18"
state: "pulsegen/status/5"
- device_id: licht_flur_oben_am_spiegel
homekit_aid: 95
name: Spiegel
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xf0d1b80000195038"
set: "zigbee2mqtt/0xf0d1b80000195038/set"
- device_id: licht_kommode_schlafzimmer
homekit_aid: 96
name: Kommode Schlafzimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
color_temperature: true
topics:
state: "zigbee2mqtt/0x842e14fffefe4ba4"
set: "zigbee2mqtt/0x842e14fffefe4ba4/set"

View File

@@ -265,6 +265,14 @@ rooms:
title: Schranklicht vor Küche title: Schranklicht vor Küche
icon: 💡 icon: 💡
rank: 232 rank: 232
- device_id: regallicht_flur
title: Regallicht Flur
icon: 💡
rank: 233
- device_id: lampe_haustuer
title: Lampe Haustür
icon: 💡
rank: 234
- device_id: sensor_flur - device_id: sensor_flur
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
@@ -345,6 +353,30 @@ rooms:
title: Gartenlicht vorne title: Gartenlicht vorne
icon: 💡 icon: 💡
rank: 291 rank: 291
- device_id: spot_garden
title: Spot Garten
icon: 💡
rank: 292
- device_id: licht_schuppen
title: Licht Schuppen
icon: 💡
rank: 293
- device_id: steckdose_strandkorb
title: Steckdose Strandkorb
icon: 🔌
rank: 294
- device_id: steckdose_vor_waschkueche
title: Steckdose vor Waschküche
icon: 🔌
rank: 295
- device_id: wasser_vorne
title: Wasser Vorgarten
icon: 💧
rank: 296
- device_id: wasser_hinten
title: Wasser Garten
icon: 💧
rank: 297
- id: garage - id: garage
name: Garage name: Garage
devices: devices:
@@ -371,6 +403,21 @@ rooms:
title: Werkstatt Licht title: Werkstatt Licht
icon: 💡 icon: 💡
rank: 350 rank: 350
- id: devices
name: Devices
devices:
- device_id: power_relay_oven
title: Schütz Herd
icon:
rank: 400
- device_id: power_relay_kitchen
title: Schütz Küche
icon:
rank: 405
- device_id: power_relay_laundry
title: Schütz Waschküche
icon:
rank: 410

View File

@@ -1,9 +1,19 @@
# Rules Configuration
# Auto-generated from devices.yaml
rules: rules:
- id: window_setback_bad_unten
enabled: true
name: Fensterabsenkung Bad Unten
type: window_setback@1.0
objects:
contacts:
- kontakt_bad_unten_strasse
thermostats:
- thermostat_bad_unten
params:
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
- id: window_setback_esszimmer - id: window_setback_esszimmer
enabled: false enabled: true
name: Fensterabsenkung Esszimmer name: Fensterabsenkung Esszimmer
type: window_setback@1.0 type: window_setback@1.0
objects: objects:
@@ -13,12 +23,27 @@ rules:
thermostats: thermostats:
- thermostat_esszimmer - thermostat_esszimmer
params: params:
eco_target: 16.0 eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_wohnzimmer
enabled: true
name: Fensterabsenkung Wohnzimmer
type: window_setback@1.0
objects:
contacts:
- kontakt_wohnzimmer_garten_links
- kontakt_wohnzimmer_garten_rechts
thermostats:
- thermostat_wohnzimmer
params:
eco_target: 5.0
open_min_secs: 20 open_min_secs: 20
close_min_secs: 20 close_min_secs: 20
previous_target_ttl_secs: 86400 previous_target_ttl_secs: 86400
- id: window_setback_kueche - id: window_setback_kueche
enabled: false enabled: true
name: Fensterabsenkung Küche name: Fensterabsenkung Küche
type: window_setback@1.0 type: window_setback@1.0
objects: objects:
@@ -30,12 +55,12 @@ rules:
thermostats: thermostats:
- thermostat_kueche - thermostat_kueche
params: params:
eco_target: 16.0 eco_target: 5.0
open_min_secs: 20 open_min_secs: 20
close_min_secs: 20 close_min_secs: 20
previous_target_ttl_secs: 86400 previous_target_ttl_secs: 86400
- id: window_setback_patty - id: window_setback_patty
enabled: false enabled: true
name: Fensterabsenkung Arbeitszimmer Patty name: Fensterabsenkung Arbeitszimmer Patty
type: window_setback@1.0 type: window_setback@1.0
objects: objects:
@@ -46,12 +71,12 @@ rules:
thermostats: thermostats:
- thermostat_patty - thermostat_patty
params: params:
eco_target: 16.0 eco_target: 5.0
open_min_secs: 20 open_min_secs: 20
close_min_secs: 20 close_min_secs: 20
previous_target_ttl_secs: 86400 previous_target_ttl_secs: 86400
- id: window_setback_schlafzimmer - id: window_setback_schlafzimmer
enabled: false enabled: true
name: Fensterabsenkung Schlafzimmer name: Fensterabsenkung Schlafzimmer
type: window_setback@1.0 type: window_setback@1.0
objects: objects:
@@ -60,22 +85,7 @@ rules:
thermostats: thermostats:
- thermostat_schlafzimmer - thermostat_schlafzimmer
params: params:
eco_target: 16.0 eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_wohnzimmer
enabled: false
name: Fensterabsenkung Wohnzimmer
type: window_setback@1.0
objects:
contacts:
- kontakt_wohnzimmer_garten_links
- kontakt_wohnzimmer_garten_rechts
thermostats:
- thermostat_wohnzimmer
params:
eco_target: 16.0
open_min_secs: 20 open_min_secs: 20
close_min_secs: 20 close_min_secs: 20
previous_target_ttl_secs: 86400 previous_target_ttl_secs: 86400
@@ -89,6 +99,19 @@ rules:
thermostats: thermostats:
- thermostat_wolfgang - thermostat_wolfgang
params: params:
eco_target: 16.0 eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
- id: window_setback_bad_oben
enabled: true
name: Fensterabsenkung Bad Oben
type: window_setback@1.0
objects:
contacts:
- kontakt_bad_oben_strasse
thermostats:
- thermostat_bad_oben
params:
eco_target: 5.0
open_min_secs: 20 open_min_secs: 20
close_min_secs: 20 close_min_secs: 20

View File

@@ -8,6 +8,8 @@ from packages.home_capabilities.contact_sensor import CAP_VERSION as CONTACT_SEN
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 CAP_VERSION as TEMP_HUMIDITY_SENSOR_VERSION
from packages.home_capabilities.temp_humidity_sensor import TempHumidityState from packages.home_capabilities.temp_humidity_sensor import TempHumidityState
from packages.home_capabilities.switch import CAP_VERSION as SWITCH_VERSION
from packages.home_capabilities.switch import SwitchState
from packages.home_capabilities.relay import CAP_VERSION as RELAY_VERSION from packages.home_capabilities.relay import CAP_VERSION as RELAY_VERSION
from packages.home_capabilities.relay import RelayState from packages.home_capabilities.relay import RelayState
from packages.home_capabilities.three_phase_powermeter import CAP_VERSION as THREE_PHASE_POWERMETER_VERSION from packages.home_capabilities.three_phase_powermeter import CAP_VERSION as THREE_PHASE_POWERMETER_VERSION
@@ -42,6 +44,8 @@ __all__ = [
"CONTACT_SENSOR_VERSION", "CONTACT_SENSOR_VERSION",
"TempHumidityState", "TempHumidityState",
"TEMP_HUMIDITY_SENSOR_VERSION", "TEMP_HUMIDITY_SENSOR_VERSION",
"SwitchState",
"SWITCH_VERSION",
"RelayState", "RelayState",
"RELAY_VERSION", "RELAY_VERSION",
"DeviceTile", "DeviceTile",

View File

@@ -0,0 +1,69 @@
"""Switch Capability - Wireless Button/Switch (read-only).
This module defines the SwitchState model for wireless switches/buttons.
These devices report action events (e.g., button presses) and are read-only devices.
Capability Version: switch@1.0.0
"""
from datetime import datetime
from typing import Annotated
from pydantic import BaseModel, Field
# Capability metadata
CAP_VERSION = "switch@1.0.0"
DISPLAY_NAME = "Switch"
class SwitchState(BaseModel):
"""State model for wireless switches/buttons.
Wireless switches are read-only devices that report button actions such as
single press, double press, long press, etc. They typically also report
battery level and signal quality.
Attributes:
action: Action type (e.g., "single", "double", "long", "hold", etc.)
battery: Battery level percentage (0-100), optional
linkquality: MQTT link quality indicator, optional
voltage: Battery voltage in mV, optional
ts: Timestamp of the action event, optional
Examples:
>>> SwitchState(action="single")
SwitchState(action='single', battery=None, ...)
>>> SwitchState(action="double", battery=95, linkquality=87)
SwitchState(action='double', battery=95, linkquality=87, ...)
"""
action: str = Field(
...,
description="Action type: 'single', 'double', 'long', 'hold', etc."
)
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)"
)
voltage: int | None = Field(
None,
description="Battery voltage in millivolts"
)
ts: datetime | None = Field(
None,
description="Timestamp of the action event"
)
# Public API
__all__ = ["SwitchState", "CAP_VERSION", "DISPLAY_NAME"]