Compare commits

...

27 Commits

Author SHA1 Message Date
55937d5900 fix window setback logic for multiple windows, fix 1
All checks were successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 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/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 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
2026-01-13 15:33:57 +01:00
38762d60f2 fix window setback logic for multiple windows
All checks were 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/3 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2026-01-13 15:25:18 +01:00
4f5bcd7dbf test devices
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/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2026-01-13 13:04:04 +01:00
3bcaa93570 merged 2026-01-13 12:59:25 +01:00
331945f789 changes in devices.yaml 2026-01-13 12:56:15 +01:00
52235be637 changes in rules.yaml 2026-01-13 12:55:43 +01:00
94589f52d7 initial 2026-01-13 12:52:49 +01:00
474b41ffce deploy confguuration script 2026-01-06 14:02:11 +01:00
79081e7480 thermostat bad unten replaced 2 2026-01-06 13:48:29 +01:00
424f1d6743 thermostat bad unten replaced 2026-01-06 13:47:56 +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
10 changed files with 507 additions and 186 deletions

View File

@@ -1,5 +1,8 @@
when: when:
event: [tag] event: [tag]
ref:
exclude:
- refs/tags/*-configchange
steps: steps:
create_namespace: create_namespace:

View File

@@ -15,6 +15,7 @@ from apps.abstraction.vendors import (
simulator, simulator,
zigbee2mqtt, zigbee2mqtt,
max, max,
test,
shelly, shelly,
tasmota, tasmota,
hottis_pv_modbus, hottis_pv_modbus,
@@ -40,6 +41,7 @@ for vendor_name, vendor_module in [
("simulator", simulator), ("simulator", simulator),
("zigbee2mqtt", zigbee2mqtt), ("zigbee2mqtt", zigbee2mqtt),
("max", max), ("max", max),
("test", test),
("shelly", shelly), ("shelly", shelly),
("tasmota", tasmota), ("tasmota", tasmota),
("hottis_pv_modbus", hottis_pv_modbus), ("hottis_pv_modbus", hottis_pv_modbus),

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

62
apps/abstraction/vendors/test.py vendored Normal file
View File

@@ -0,0 +1,62 @@
"""test vendor transformations."""
import json
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_contact_sensor_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract contact sensor payload to MAX! format.
Contact sensors are read-only.
"""
logger.warning("Contact sensors are read-only - SET commands should not be used")
return json.dumps(payload)
def transform_contact_sensor_to_abstract(payload: str) -> dict[str, Any]:
try:
contact_value = payload.strip().lower()
return {
"contact": "open" if (contact_value == "open") else "closed"
}
except (ValueError, TypeError) as e:
logger.error(f"contact sensor failed to parse: {payload}, error: {e}")
return {"contact": "closed"}
def transform_thermostat_to_vendor(payload: dict[str, Any]) -> str:
if "target" not in payload:
logger.warning(f"thermostat payload missing 'target': {payload}")
return "21"
target_temp = payload["target"]
if isinstance(target_temp, (int, float)):
int_temp = int(round(target_temp))
return str(int_temp)
logger.warning(f"invalid target temperature type: {type(target_temp)}")
return "21"
def transform_thermostat_to_abstract(payload: str) -> dict[str, Any]:
target_temp = float(payload.strip())
return {
"target": target_temp,
"mode": "heat"
}
# Registry of handlers for this vendor
HANDLERS = {
("contact_sensor", "to_vendor"): transform_contact_sensor_to_vendor,
("contact_sensor", "to_abstract"): transform_contact_sensor_to_abstract,
("contact", "to_vendor"): transform_contact_sensor_to_vendor,
("contact", "to_abstract"): transform_contact_sensor_to_abstract,
("thermostat", "to_vendor"): transform_thermostat_to_vendor,
("thermostat", "to_abstract"): transform_thermostat_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

@@ -18,6 +18,7 @@ class WindowSetbackObjects(BaseModel):
thermostats: list[str] = Field(..., min_length=1, description="Thermostats to control") thermostats: list[str] = Field(..., min_length=1, description="Thermostats to control")
class WindowSetbackRule(Rule): class WindowSetbackRule(Rule):
""" """
Window setback automation rule. Window setback automation rule.
@@ -31,15 +32,13 @@ class WindowSetbackRule(Rule):
thermostats: List of thermostat device IDs to control (required, min 1) thermostats: List of thermostat device IDs to control (required, min 1)
params: params:
eco_target: Temperature to set when window opens (default: 16.0) eco_target: Temperature to set when window opens (default: 16.0)
open_min_secs: Minimum seconds window must be open before triggering (default: 20)
close_min_secs: Minimum seconds window must be closed before restoring (default: 20)
previous_target_ttl_secs: How long to remember previous temperature (default: 86400) previous_target_ttl_secs: How long to remember previous temperature (default: 86400)
State storage (Redis keys): State storage (Redis keys):
rule:{rule_id}:contact:{device_id}:state -> "open" | "closed"
rule:{rule_id}:contact:{device_id}:ts -> ISO timestamp of last change
rule:{rule_id}:thermo:{device_id}:current_target -> Current target temp (updated on every STATE) rule:{rule_id}:thermo:{device_id}:current_target -> Current target temp (updated on every STATE)
rule:{rule_id}:thermo:{device_id}:previous -> Previous target temp (saved on window open, deleted on restore) rule:{rule_id}:thermo:{device_id}:previous -> Previous target temp (saved on window open, deleted on restore)
rule:{rule_id}:contact:{device_id}:is_open -> "1" if open, "0" if closed
rule:{rule_id}:state -> Overall rule state -> "1" if thermostats set to eco, "0" otherwise
Logic: Logic:
1. Thermostat STATE events → update current_target in Redis 1. Thermostat STATE events → update current_target in Redis
@@ -47,6 +46,23 @@ class WindowSetbackRule(Rule):
3. Window closes → restore from previous, then delete previous key 3. Window closes → restore from previous, then delete previous key
""" """
@staticmethod
def __get_redis_key_current_target(rule_id: str, thermo_id: str) -> str:
"""Get Redis key for current target temperature of a thermostat"""
return f"rule:{rule_id}:thermo:{thermo_id}:current_target"
@staticmethod
def __get_redis_key_previous_target(rule_id: str, thermo_id: str) -> str:
"""Get Redis key for previous target temperature of a thermostat"""
return f"rule:{rule_id}:thermo:{thermo_id}:previous"
@staticmethod
def __get_redis_key_contact_state(rule_id: str, contact_id: str) -> str:
"""Get Redis key for contact sensor state"""
return f"rule:{rule_id}:contact:{contact_id}:is_open"
@staticmethod
def __get_redis_key_rule_state(rule_id: str) -> str:
"""Get Redis key for overall rule state"""
return f"rule:{rule_id}:state"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._validated_objects: dict[str, WindowSetbackObjects] = {} self._validated_objects: dict[str, WindowSetbackObjects] = {}
@@ -130,42 +146,49 @@ class WindowSetbackRule(Rule):
ctx.logger.warning(f"Contact event missing 'contact' field: {evt}") ctx.logger.warning(f"Contact event missing 'contact' field: {evt}")
return return
# Store current state and timestamp contact_state_key = WindowSetbackRule.__get_redis_key_contact_state(desc.id, device_id)
state_key = f"rule:{desc.id}:contact:{device_id}:state" await ctx.redis.set(contact_state_key, '1' if contact_state == 'open' else '0')
ts_key = f"rule:{desc.id}:contact:{device_id}:ts"
await ctx.redis.set(state_key, contact_state) # Check if any contact is open
await ctx.redis.set(ts_key, event_ts) is_open = False
for contact_id in desc.objects.get('contacts', []):
state_key = WindowSetbackRule.__get_redis_key_contact_state(desc.id, contact_id)
state_val = await ctx.redis.get(state_key)
if state_val == '1':
is_open = True
break
if contact_state == 'open': rule_state_key = WindowSetbackRule.__get_redis_key_rule_state(desc.id)
await self._on_window_opened(desc, ctx) current_rule_state = await ctx.redis.get(rule_state_key)
elif contact_state == 'closed': if is_open and current_rule_state != '1':
await self._on_window_closed(desc, ctx) # At least one contact is open, and we are not already in eco mode
await self._set_eco_mode(desc, ctx)
await ctx.redis.set(rule_state_key, '1')
elif not is_open and current_rule_state != '0':
# All contacts are closed, and we are currently in eco mode
await self._unset_eco_mode(desc, ctx)
await ctx.redis.set(rule_state_key, '0')
async def _on_window_opened(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
"""
Window opened - save current temperatures, then set thermostats to eco.
Important: We must save the current target BEFORE setting to eco, async def _set_eco_mode(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
otherwise we'll save the eco temperature instead of the original. """Set thermostats to eco temperature when window opens."""
""" eco_target = desc.params.get('eco_target', 7.0)
eco_target = desc.params.get('eco_target', 16.0)
target_thermostats = desc.objects.get('thermostats', []) target_thermostats = desc.objects.get('thermostats', [])
ttl_secs = desc.params.get('previous_target_ttl_secs', 86400) ttl_secs = desc.params.get('previous_target_ttl_secs', 86400)
ctx.logger.info( ctx.logger.info(
f"Rule {desc.id}: Window opened, setting {len(target_thermostats)} " f"Rule {desc.id}: At least one window is opened, setting {len(target_thermostats)} "
f"thermostats to eco temperature {eco_target}°C" f"thermostats to eco temperature {eco_target}°C"
) )
# FIRST: Save current target temperatures as "previous" (before we change them!) # FIRST: Save current target temperatures as "previous" (before we change them!)
for thermo_id in target_thermostats: for thermo_id in target_thermostats:
current_key = f"rule:{desc.id}:thermo:{thermo_id}:current_target" current_key = WindowSetbackRule.__get_redis_key_current_target(desc.id, thermo_id)
current_temp_str = await ctx.redis.get(current_key) current_temp_str = await ctx.redis.get(current_key)
if current_temp_str: if current_temp_str:
# Save current as previous (with TTL) # Save current as previous (with TTL)
prev_key = f"rule:{desc.id}:thermo:{thermo_id}:previous" prev_key = WindowSetbackRule.__get_redis_key_previous_target(desc.id, thermo_id)
await ctx.redis.set(prev_key, current_temp_str, ttl_secs=ttl_secs) await ctx.redis.set(prev_key, current_temp_str, ttl_secs=ttl_secs)
ctx.logger.debug( ctx.logger.debug(
f"Saved previous target for {thermo_id}: {current_temp_str}°C" f"Saved previous target for {thermo_id}: {current_temp_str}°C"
@@ -183,23 +206,19 @@ class WindowSetbackRule(Rule):
except Exception as e: except Exception as e:
ctx.logger.error(f"Failed to set {thermo_id}: {e}") ctx.logger.error(f"Failed to set {thermo_id}: {e}")
async def _on_window_closed(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
"""
Window closed - restore previous temperatures.
Note: This is simplified. A production implementation would check async def _unset_eco_mode(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
close_min_secs and use a timer/scheduler. """Restore thermostats to previous temperature when window closes."""
"""
target_thermostats = desc.objects.get('thermostats', []) target_thermostats = desc.objects.get('thermostats', [])
ctx.logger.info( ctx.logger.info(
f"Rule {desc.id}: Window closed, restoring {len(target_thermostats)} " f"Rule {desc.id}: All windows closed, restoring {len(target_thermostats)} "
f"thermostats to previous temperatures" f"thermostats to previous temperatures"
) )
# Restore previous temperatures # Restore previous temperatures
for thermo_id in target_thermostats: for thermo_id in target_thermostats:
prev_key = f"rule:{desc.id}:thermo:{thermo_id}:previous" prev_key = WindowSetbackRule.__get_redis_key_previous_target(desc.id, thermo_id)
prev_temp_str = await ctx.redis.get(prev_key) prev_temp_str = await ctx.redis.get(prev_key)
if prev_temp_str: if prev_temp_str:
@@ -240,7 +259,7 @@ class WindowSetbackRule(Rule):
return # No target in this state update return # No target in this state update
# Store current target (always update, even if it's the eco temperature) # Store current target (always update, even if it's the eco temperature)
current_key = f"rule:{desc.id}:thermo:{device_id}:current_target" current_key = WindowSetbackRule.__get_redis_key_current_target(desc.id, device_id)
ttl_secs = desc.params.get('previous_target_ttl_secs', 86400) ttl_secs = desc.params.get('previous_target_ttl_secs', 86400)
await ctx.redis.set(current_key, str(current_target), ttl_secs=ttl_secs) await ctx.redis.set(current_key, str(current_target), ttl_secs=ttl_secs)

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
@@ -506,21 +471,16 @@ devices:
name: Heizung name: Heizung
type: thermostat type: thermostat
cap_version: "thermostat@1.0.0" cap_version: "thermostat@1.0.0"
technology: max technology: zigbee2mqtt
features: features:
mode: true heating: true
target: true temperature_range:
current: false - 5
- 30
temperature_step: 0.5
topics: topics:
set: "homegear/instance1/set/48/1/SET_TEMPERATURE" state: "zigbee2mqtt/0x003c84fffebdcc28"
state: "homegear/instance1/plain/48/1/SET_TEMPERATURE" set: "zigbee2mqtt/0x003c84fffebdcc28/set"
metadata:
friendly_name: "Thermostat Bad Unten"
location: "Bad Unten"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "48"
channel: "1"
- device_id: sterne_wohnzimmer - device_id: sterne_wohnzimmer
homekit_aid: 32 homekit_aid: 32
name: Sterne name: Sterne
@@ -843,17 +803,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 +983,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 +1003,187 @@ 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"
- device_id: kontakt_test_1
homekit_aid: 97
name: Kontakt Test 1
type: contact
cap_version: contact_sensor@1.0.0
technology: test
topics:
state: test/kontakt1/state
features: {}
- device_id: kontakt_test_2
homekit_aid: 98
name: Kontakt Test 2
type: contact
cap_version: contact_sensor@1.0.0
technology: test
topics:
state: test/kontakt2/state
features: {}
- device_id: thermostat_test
homekit_aid: 99
name: Thermostat Test
type: thermostat
cap_version: "thermostat@1.0.0"
technology: test
features:
heating: true
temperature_range:
- 5
- 30
temperature_step: 0.5
topics:
state: "test/thermostat1/state"
set: "test/thermostat1/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,33 @@ 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
close_min_secs: 20
- id: window_setback_test
enabled: true
name: Fensterabsenkung Test
type: window_setback@1.0
objects:
contacts:
- kontakt_test_1
- kontakt_test_2
thermostats:
- thermostat_test
params:
eco_target: 7.0
open_min_secs: 20 open_min_secs: 20
close_min_secs: 20 close_min_secs: 20

15
tools/deploy-configuration.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
NAMESPACE=homea2
kubectl create configmap home-automation-config \
--from-file=devices.yaml=config/devices.yaml \
--from-file=groups.yaml=config/groups.yaml \
--from-file=layout.yaml=config/layout.yaml \
--from-file=rules.yaml=config/rules.yaml \
--from-file=scenes.yaml=config/scenes.yaml \
--namespace=$NAMESPACE \
--dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f deployment/configmap.yaml -n $NAMESPACE