This commit is contained in:
2025-11-11 19:58:06 +01:00
parent d3d96ed3e9
commit b6b441c0ca
5 changed files with 245 additions and 89 deletions

View File

@@ -7,9 +7,17 @@ This rule lowers thermostat temperature when a window is opened.
from typing import Any from typing import Any
from pydantic import BaseModel, Field, ValidationError
from apps.rules.rule_interface import Rule, RuleDescriptor, RuleContext from apps.rules.rule_interface import Rule, RuleDescriptor, RuleContext
class WindowSetbackObjects(BaseModel):
"""Object structure for window setback rule"""
contacts: list[str] = Field(..., min_length=1, description="Contact sensors to monitor")
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.
@@ -18,22 +26,67 @@ class WindowSetbackRule(Rule):
When closed for a minimum duration, restore previous target temperature. When closed for a minimum duration, restore previous target temperature.
Configuration: Configuration:
targets: objects:
contacts: List of contact sensor device IDs contacts: List of contact sensor device IDs to monitor (required, min 1)
thermostats: List of thermostat device IDs 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) 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) 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: State storage (Redis keys):
Redis keys:
rule:{rule_id}:contact:{device_id}:state -> "open" | "closed" rule:{rule_id}:contact:{device_id}:state -> "open" | "closed"
rule:{rule_id}:contact:{device_id}:ts -> ISO timestamp of last change rule:{rule_id}:contact:{device_id}:ts -> ISO timestamp of last change
rule:{rule_id}:thermo:{device_id}:previous -> Previous target temperature 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)
Logic:
1. Thermostat STATE events → update current_target in Redis
2. Window opens → copy current_target to previous, then set to eco_target
3. Window closes → restore from previous, then delete previous key
""" """
def __init__(self):
super().__init__()
self._validated_objects: dict[str, WindowSetbackObjects] = {}
async def setup(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
"""Validate objects structure during setup"""
try:
validated = WindowSetbackObjects(**desc.objects)
self._validated_objects[desc.id] = validated
ctx.logger.info(
f"Rule {desc.id} validated: {len(validated.contacts)} contacts, "
f"{len(validated.thermostats)} thermostats"
)
except ValidationError as e:
raise ValueError(
f"Invalid objects configuration for rule {desc.id}: {e}"
) from e
def get_subscriptions(self, desc: RuleDescriptor) -> list[str]:
"""
Return MQTT topics to subscribe to.
Subscribe to:
- Contact sensor state changes (to detect window open/close)
- Thermostat state changes (to track current target temperature)
"""
topics = []
# Subscribe to contact sensors
contacts = desc.objects.get('contacts', [])
for contact_id in contacts:
topics.append(f"home/contact/{contact_id}/state")
# Subscribe to thermostats to track their current target temperature
thermostats = desc.objects.get('thermostats', [])
for thermo_id in thermostats:
topics.append(f"home/thermostat/{thermo_id}/state")
return topics
async def on_event( async def on_event(
self, self,
evt: dict[str, Any], evt: dict[str, Any],
@@ -52,9 +105,9 @@ class WindowSetbackRule(Rule):
cap = evt['cap'] cap = evt['cap']
payload = evt['payload'] payload = evt['payload']
# Only process events for devices in our targets # Only process events for devices in our objects
target_contacts = desc.targets.contacts or [] target_contacts = desc.objects.get('contacts', [])
target_thermostats = desc.targets.thermostats or [] target_thermostats = desc.objects.get('thermostats', [])
if cap == 'contact' and device_id in target_contacts: if cap == 'contact' and device_id in target_contacts:
await self._handle_contact_event(evt, desc, ctx) await self._handle_contact_event(evt, desc, ctx)
@@ -90,16 +143,39 @@ class WindowSetbackRule(Rule):
await self._on_window_closed(desc, ctx) await self._on_window_closed(desc, ctx)
async def _on_window_opened(self, desc: RuleDescriptor, ctx: RuleContext) -> None: async def _on_window_opened(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
"""Window opened - set thermostats to eco temperature.""" """
Window opened - save current temperatures, then set thermostats to eco.
Important: We must save the current target BEFORE setting to eco,
otherwise we'll save the eco temperature instead of the original.
"""
eco_target = desc.params.get('eco_target', 16.0) eco_target = desc.params.get('eco_target', 16.0)
target_thermostats = desc.targets.thermostats or [] target_thermostats = desc.objects.get('thermostats', [])
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}: Window opened, setting {len(target_thermostats)} "
f"thermostats to eco temperature {eco_target}°C" f"thermostats to eco temperature {eco_target}°C"
) )
# Set all thermostats to eco temperature # FIRST: Save current target temperatures as "previous" (before we change them!)
for thermo_id in target_thermostats:
current_key = f"rule:{desc.id}:thermo:{thermo_id}:current_target"
current_temp_str = await ctx.redis.get(current_key)
if current_temp_str:
# Save current as previous (with TTL)
prev_key = f"rule:{desc.id}:thermo:{thermo_id}:previous"
await ctx.redis.set(prev_key, current_temp_str, ttl_secs=ttl_secs)
ctx.logger.debug(
f"Saved previous target for {thermo_id}: {current_temp_str}°C"
)
else:
ctx.logger.warning(
f"No current target found for {thermo_id}, cannot save previous"
)
# THEN: Set all thermostats to eco temperature
for thermo_id in target_thermostats: for thermo_id in target_thermostats:
try: try:
await ctx.mqtt.publish_set_thermostat(thermo_id, eco_target) await ctx.mqtt.publish_set_thermostat(thermo_id, eco_target)
@@ -109,13 +185,12 @@ class WindowSetbackRule(Rule):
async def _on_window_closed(self, desc: RuleDescriptor, ctx: RuleContext) -> None: async def _on_window_closed(self, desc: RuleDescriptor, ctx: RuleContext) -> None:
""" """
Window closed - restore previous temperatures if closed long enough. Window closed - restore previous temperatures.
Note: This is simplified. A production implementation would check Note: This is simplified. A production implementation would check
close_min_secs and use a timer/scheduler. close_min_secs and use a timer/scheduler.
""" """
target_thermostats = desc.targets.thermostats or [] target_thermostats = desc.objects.get('thermostats', [])
ttl_secs = desc.params.get('previous_target_ttl_secs', 86400)
ctx.logger.info( ctx.logger.info(
f"Rule {desc.id}: Window closed, restoring {len(target_thermostats)} " f"Rule {desc.id}: Window closed, restoring {len(target_thermostats)} "
@@ -132,8 +207,15 @@ class WindowSetbackRule(Rule):
prev_temp = float(prev_temp_str) prev_temp = float(prev_temp_str)
await ctx.mqtt.publish_set_thermostat(thermo_id, prev_temp) await ctx.mqtt.publish_set_thermostat(thermo_id, prev_temp)
ctx.logger.debug(f"Restored {thermo_id} to {prev_temp}°C") ctx.logger.debug(f"Restored {thermo_id} to {prev_temp}°C")
# Delete the previous key after restoring
await ctx.redis.delete(prev_key)
except Exception as e: except Exception as e:
ctx.logger.error(f"Failed to restore {thermo_id}: {e}") ctx.logger.error(f"Failed to restore {thermo_id}: {e}")
else:
ctx.logger.warning(
f"No previous target found for {thermo_id}, cannot restore"
)
async def _handle_thermostat_event( async def _handle_thermostat_event(
self, self,
@@ -142,9 +224,13 @@ class WindowSetbackRule(Rule):
ctx: RuleContext ctx: RuleContext
) -> None: ) -> None:
""" """
Handle thermostat state change - remember current target. Handle thermostat state change - track current target temperature.
This allows us to restore the temperature when window closes. This keeps a record of the thermostat's current target, so we can
save it as "previous" when a window opens.
Important: We store in "current_target", NOT "previous". The "previous"
key is only written when a window opens, to avoid race conditions.
""" """
device_id = evt['device_id'] device_id = evt['device_id']
payload = evt['payload'] payload = evt['payload']
@@ -153,14 +239,14 @@ class WindowSetbackRule(Rule):
if current_target is None: if current_target is None:
return # No target in this state update return # No target in this state update
# Store as previous target with TTL # Store current target (always update, even if it's the eco temperature)
prev_key = f"rule:{desc.id}:thermo:{device_id}:previous" current_key = f"rule:{desc.id}:thermo:{device_id}:current_target"
ttl_secs = desc.params.get('previous_target_ttl_secs', 86400) ttl_secs = desc.params.get('previous_target_ttl_secs', 86400)
await ctx.redis.set(prev_key, str(current_target), ttl_secs=ttl_secs) await ctx.redis.set(current_key, str(current_target), ttl_secs=ttl_secs)
ctx.logger.debug( ctx.logger.debug(
f"Rule {desc.id}: Stored previous target for {device_id}: {current_target}°C" f"Rule {desc.id}: Updated current target for {device_id}: {current_target}°C"
) )

View File

@@ -24,7 +24,7 @@ from apps.rules.rule_interface import (
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -63,11 +63,12 @@ class RuleEngine:
self.mqtt_client: MQTTClient | None = None self.mqtt_client: MQTTClient | None = None
self.redis_state: RedisState | None = None self.redis_state: RedisState | None = None
self.context: RuleContext | None = None self.context: RuleContext | None = None
self._mqtt_topics: list[str] = [] # Topics to subscribe to
# For graceful shutdown # For graceful shutdown
self._shutdown_event = asyncio.Event() self._shutdown_event = asyncio.Event()
def setup(self) -> None: async def setup(self) -> None:
""" """
Load configuration and instantiate rules. Load configuration and instantiate rules.
@@ -102,14 +103,55 @@ class RuleEngine:
disabled_count = total_count - enabled_count disabled_count = total_count - enabled_count
logger.info(f"Successfully loaded {enabled_count} rule implementation(s) ({disabled_count} disabled)") logger.info(f"Successfully loaded {enabled_count} rule implementation(s) ({disabled_count} disabled)")
# Call setup on each rule for validation
for rule_id, rule_instance in self.rules.items():
desc = next((d for d in self.rule_descriptors if d.id == rule_id), None)
if desc:
try:
ctx = RuleContext(
logger=logger,
mqtt_publisher=self.mqtt_client,
redis_state=self.redis_state
)
await rule_instance.setup(desc, ctx)
except Exception as e:
logger.error(f"Failed to setup rule {rule_id}: {e}")
raise
# Collect MQTT subscriptions from all enabled rules
all_topics = set()
for rule_id, rule_instance in self.rules.items():
desc = next((d for d in self.rule_descriptors if d.id == rule_id), None)
if desc:
try:
topics = rule_instance.get_subscriptions(desc)
all_topics.update(topics)
logger.debug(f"Rule {rule_id} subscribes to {len(topics)} topic(s)")
except Exception as e:
logger.error(f"Failed to get subscriptions for rule {rule_id}: {e}")
raise
logger.info(f"Total MQTT subscriptions needed: {len(all_topics)}")
# Create unique client ID to avoid conflicts
import uuid
import os
client_id_base = "rule_engine"
client_suffix = os.environ.get("MQTT_CLIENT_ID_SUFFIX") or uuid.uuid4().hex[:6]
unique_client_id = f"{client_id_base}-{client_suffix}"
# Initialize MQTT client # Initialize MQTT client
self.mqtt_client = MQTTClient( self.mqtt_client = MQTTClient(
broker=self.mqtt_broker, broker=self.mqtt_broker,
port=self.mqtt_port, port=self.mqtt_port,
client_id="rule_engine" client_id=unique_client_id
) )
self.mqtt_client.set_logger(logger) self.mqtt_client.set_logger(logger)
# Store topics for connection
self._mqtt_topics = list(all_topics)
# Initialize Redis state # Initialize Redis state
self.redis_state = RedisState(self.redis_url) self.redis_state = RedisState(self.redis_url)
@@ -129,10 +171,7 @@ class RuleEngine:
""" """
Filter rules that should receive this event. Filter rules that should receive this event.
Rules match if: Rules match if the event's device_id is in the rule's objects.
- For contact events: device_id in targets.contacts
- For thermostat events: device_id in targets.thermostats
- (Room-based filtering could be added here)
Args: Args:
event: Normalized MQTT event event: Normalized MQTT event
@@ -149,27 +188,36 @@ class RuleEngine:
logger.debug(f"Filtering for cap={cap}, device_id={device_id}") logger.debug(f"Filtering for cap={cap}, device_id={device_id}")
for rule_id, desc in [(r.id, r) for r in self.rule_descriptors]: # Only check enabled rules (rules in self.rules dict)
targets = desc.targets for rule_id, rule_instance in self.rules.items():
desc = next((d for d in self.rule_descriptors if d.id == rule_id), None)
if not desc:
continue
# Check if this device is in the rule's targets objects = desc.objects
# Check if this device is in the rule's objects
matched = False matched = False
if cap == 'contact' and targets.contacts: if cap == 'contact' and objects.get('contacts'):
logger.debug(f"Rule {rule_id}: checking contacts {targets.contacts}") logger.debug(f"Rule {rule_id}: checking contacts {objects.get('contacts')}")
if device_id in targets.contacts: if device_id in objects.get('contacts', []):
matched = True matched = True
elif cap == 'thermostat' and targets.thermostats: elif cap == 'thermostat' and objects.get('thermostats'):
logger.debug(f"Rule {rule_id}: checking thermostats {targets.thermostats}") logger.debug(f"Rule {rule_id}: checking thermostats {objects.get('thermostats')}")
if device_id in targets.thermostats: if device_id in objects.get('thermostats', []):
matched = True matched = True
# Could add room-based filtering here: elif cap == 'light' and objects.get('lights'):
# elif 'rooms' in targets: logger.debug(f"Rule {rule_id}: checking lights {objects.get('lights')}")
# device_room = get_device_room(device_id) if device_id in objects.get('lights', []):
# if device_room in targets['rooms']: matched = True
# matched = True
elif cap == 'relay' and objects.get('relays'):
logger.debug(f"Rule {rule_id}: checking relays {objects.get('relays')}")
if device_id in objects.get('relays', []):
matched = True
if matched: if matched:
matching_rules.append((rule_id, desc)) matching_rules.append((rule_id, desc))
@@ -227,7 +275,7 @@ class RuleEngine:
logger.info("Starting event processing loop") logger.info("Starting event processing loop")
try: try:
async for event in self.mqtt_client.connect(): async for event in self.mqtt_client.connect(topics=self._mqtt_topics):
# Check for shutdown # Check for shutdown
if self._shutdown_event.is_set(): if self._shutdown_event.is_set():
logger.info("Shutdown signal received, stopping event loop") logger.info("Shutdown signal received, stopping event loop")
@@ -284,24 +332,28 @@ async def main_async() -> None:
# Load rules # Load rules
try: try:
engine.setup() await engine.setup()
except Exception as e: except Exception as e:
logger.error(f"Failed to setup engine: {e}", exc_info=True) logger.error(f"Failed to setup engine: {e}", exc_info=True)
sys.exit(1) sys.exit(1)
# Setup signal handlers for graceful shutdown # Setup signal handlers for graceful shutdown
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
main_task = None
def signal_handler(): def signal_handler():
logger.info("Received shutdown signal") logger.info("Received shutdown signal")
asyncio.create_task(engine.shutdown()) engine._shutdown_event.set()
if main_task and not main_task.done():
main_task.cancel()
for sig in (signal.SIGTERM, signal.SIGINT): for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, signal_handler) loop.add_signal_handler(sig, signal_handler)
# Run engine # Run engine
try: try:
await engine.run() main_task = asyncio.create_task(engine.run())
await main_task
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("Main task cancelled") logger.info("Main task cancelled")
finally: finally:

View File

@@ -21,14 +21,18 @@ class RuleDescriptor(BaseModel):
This is the validated representation of a rule from rules.yaml. This is the validated representation of a rule from rules.yaml.
The engine loads these and passes them to rule implementations. The engine loads these and passes them to rule implementations.
The 'objects' field is intentionally flexible (dict) to allow different
rule types to define their own object structures.
""" """
id: str = Field(..., description="Unique identifier for this rule instance") id: str = Field(..., description="Unique identifier for this rule instance")
name: Optional[str] = Field(None, description="Optional human-readable name") name: Optional[str] = Field(None, description="Optional human-readable name")
type: str = Field(..., description="Rule type with version (e.g., 'window_setback@1.0')") type: str = Field(..., description="Rule type with version (e.g., 'window_setback@1.0')")
targets: dict[str, Any] = Field( enabled: bool = Field(default=True, description="Whether this rule is enabled")
objects: dict[str, Any] = Field(
default_factory=dict, default_factory=dict,
description="Rule-specific target specification (rooms, devices, etc.)" description="Objects this rule monitors or controls (structure varies by rule type)"
) )
params: dict[str, Any] = Field( params: dict[str, Any] = Field(
default_factory=dict, default_factory=dict,
@@ -309,16 +313,22 @@ class MQTTClient:
else: else:
print(f"[{level.upper()}] {msg}") print(f"[{level.upper()}] {msg}")
async def connect(self): async def connect(self, topics: list[str] = None):
""" """
Connect to MQTT broker with automatic reconnection. Connect to MQTT broker with automatic reconnection.
This method manages the connection and automatically reconnects This method manages the connection and automatically reconnects
with exponential backoff if the connection is lost. with exponential backoff if the connection is lost.
Args:
topics: List of MQTT topics to subscribe to. If None, subscribes to nothing.
""" """
import aiomqtt import aiomqtt
from aiomqtt import Client from aiomqtt import Client
if topics is None:
topics = []
reconnect_delay = self._reconnect_interval reconnect_delay = self._reconnect_interval
while True: while True:
@@ -333,10 +343,11 @@ class MQTTClient:
self._client = client self._client = client
self._log("info", f"Connected to MQTT broker {self._broker}:{self._port}") self._log("info", f"Connected to MQTT broker {self._broker}:{self._port}")
# Subscribe to device state topics # Subscribe to provided topics
await client.subscribe("home/contact/+/state") if topics:
await client.subscribe("home/thermostat/+/state") for topic in topics:
self._log("info", "Subscribed to home/contact/+/state, home/thermostat/+/state") await client.subscribe(topic)
self._log("info", f"Subscribed to {len(topics)} topic(s): {', '.join(topics[:5])}{'...' if len(topics) > 5 else ''}")
# Reset reconnect delay on successful connection # Reset reconnect delay on successful connection
reconnect_delay = self._reconnect_interval reconnect_delay = self._reconnect_interval
@@ -542,6 +553,13 @@ class Rule(ABC):
Example implementation: Example implementation:
class WindowSetbackRule(Rule): class WindowSetbackRule(Rule):
def get_subscriptions(self, desc: RuleDescriptor) -> list[str]:
# Subscribe to contact sensor state topics
topics = []
for contact_id in desc.objects.contacts or []:
topics.append(f"home/contact/{contact_id}/state")
return topics
async def on_event(self, evt: dict, desc: RuleDescriptor, ctx: RuleContext) -> None: async def on_event(self, evt: dict, desc: RuleDescriptor, ctx: RuleContext) -> None:
device_id = evt['device_id'] device_id = evt['device_id']
cap = evt['cap'] cap = evt['cap']
@@ -550,11 +568,31 @@ class Rule(ABC):
contact_state = evt['payload'].get('contact') contact_state = evt['payload'].get('contact')
if contact_state == 'open': if contact_state == 'open':
# Window opened - set thermostats to eco # Window opened - set thermostats to eco
for thermo_id in desc.targets.get('thermostats', []): for thermo_id in desc.objects.thermostats or []:
eco_temp = desc.params.get('eco_target', 16.0) eco_temp = desc.params.get('eco_target', 16.0)
await ctx.mqtt.publish_set_thermostat(thermo_id, eco_temp) await ctx.mqtt.publish_set_thermostat(thermo_id, eco_temp)
""" """
@abstractmethod
def get_subscriptions(self, desc: RuleDescriptor) -> list[str]:
"""
Return list of MQTT topics this rule needs to subscribe to.
Called once during rule engine setup. The rule examines its configuration
(desc.objects) and returns the specific state topics it needs to monitor.
Args:
desc: Rule configuration from rules.yaml
Returns:
List of MQTT topic patterns/strings to subscribe to
Example:
For a window setback rule monitoring 2 contacts:
['home/contact/sensor_bedroom/state', 'home/contact/sensor_kitchen/state']
"""
pass
@abstractmethod @abstractmethod
async def on_event( async def on_event(
self, self,

View File

@@ -11,22 +11,13 @@ import yaml
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
class RuleTargets(BaseModel):
"""Targets for a rule (rooms, devices, etc.)"""
rooms: list[str] = Field(default_factory=list)
contacts: list[str] = Field(default_factory=list)
thermostats: list[str] = Field(default_factory=list)
lights: list[str] = Field(default_factory=list)
relays: list[str] = Field(default_factory=list)
class Rule(BaseModel): class Rule(BaseModel):
"""Single rule configuration""" """Single rule configuration"""
id: str = Field(..., description="Unique rule identifier") id: str = Field(..., description="Unique rule identifier")
name: Optional[str] = Field(None, description="Optional human-readable name") name: Optional[str] = Field(None, description="Optional human-readable name")
type: str = Field(..., description="Rule type (e.g., 'window_setback@1.0')") type: str = Field(..., description="Rule type (e.g., 'window_setback@1.0')")
enabled: bool = Field(default=True, description="Whether this rule is enabled") enabled: bool = Field(default=True, description="Whether this rule is enabled")
targets: RuleTargets = Field(..., description="Target rooms and devices") objects: dict[str, Any] = Field(default_factory=dict, description="Objects this rule monitors or controls")
params: dict[str, Any] = Field(default_factory=dict, description="Rule-specific parameters") params: dict[str, Any] = Field(default_factory=dict, description="Rule-specific parameters")
@field_validator('id') @field_validator('id')
@@ -121,9 +112,11 @@ if __name__ == "__main__":
print(f"✅ Loaded {len(config.rules)} rules:") print(f"✅ Loaded {len(config.rules)} rules:")
for rule in config.rules: for rule in config.rules:
name = f" ({rule.name})" if rule.name else "" name = f" ({rule.name})" if rule.name else ""
print(f" - {rule.id}{name}: {rule.type}") enabled = "" if rule.enabled else ""
print(f" Targets: {len(rule.targets.rooms)} rooms, " print(f" [{enabled}] {rule.id}{name}: {rule.type}")
f"{len(rule.targets.contacts)} contacts, " if rule.objects:
f"{len(rule.targets.thermostats)} thermostats") obj_summary = ", ".join(f"{k}: {len(v) if isinstance(v, list) else v}"
for k, v in rule.objects.items())
print(f" Objects: {obj_summary}")
except Exception as e: except Exception as e:
print(f"❌ Configuration error: {e}") print(f"❌ Configuration error: {e}")

View File

@@ -6,9 +6,7 @@ rules:
enabled: false enabled: false
name: Fensterabsenkung Esszimmer name: Fensterabsenkung Esszimmer
type: window_setback@1.0 type: window_setback@1.0
targets: objects:
rooms:
- Esszimmer
contacts: contacts:
- kontakt_esszimmer_strasse_links - kontakt_esszimmer_strasse_links
- kontakt_esszimmer_strasse_rechts - kontakt_esszimmer_strasse_rechts
@@ -23,9 +21,7 @@ rules:
enabled: false enabled: false
name: Fensterabsenkung Küche name: Fensterabsenkung Küche
type: window_setback@1.0 type: window_setback@1.0
targets: objects:
rooms:
- Küche
contacts: contacts:
- kontakt_kueche_garten_fenster - kontakt_kueche_garten_fenster
- kontakt_kueche_garten_tuer - kontakt_kueche_garten_tuer
@@ -42,9 +38,7 @@ rules:
enabled: false enabled: false
name: Fensterabsenkung Arbeitszimmer Patty name: Fensterabsenkung Arbeitszimmer Patty
type: window_setback@1.0 type: window_setback@1.0
targets: objects:
rooms:
- Arbeitszimmer Patty
contacts: contacts:
- kontakt_patty_garten_links - kontakt_patty_garten_links
- kontakt_patty_garten_rechts - kontakt_patty_garten_rechts
@@ -60,9 +54,7 @@ rules:
enabled: false enabled: false
name: Fensterabsenkung Schlafzimmer name: Fensterabsenkung Schlafzimmer
type: window_setback@1.0 type: window_setback@1.0
targets: objects:
rooms:
- Schlafzimmer
contacts: contacts:
- kontakt_schlafzimmer_strasse - kontakt_schlafzimmer_strasse
thermostats: thermostats:
@@ -76,9 +68,7 @@ rules:
enabled: false enabled: false
name: Fensterabsenkung Wohnzimmer name: Fensterabsenkung Wohnzimmer
type: window_setback@1.0 type: window_setback@1.0
targets: objects:
rooms:
- Wohnzimmer
contacts: contacts:
- kontakt_wohnzimmer_garten_links - kontakt_wohnzimmer_garten_links
- kontakt_wohnzimmer_garten_rechts - kontakt_wohnzimmer_garten_rechts
@@ -93,9 +83,7 @@ rules:
enabled: true enabled: true
name: Fensterabsenkung Arbeitszimmer Wolfgang name: Fensterabsenkung Arbeitszimmer Wolfgang
type: window_setback@1.0 type: window_setback@1.0
targets: objects:
rooms:
- Arbeitszimmer Wolfgang
contacts: contacts:
- kontakt_wolfgang_garten - kontakt_wolfgang_garten
thermostats: thermostats:
@@ -104,4 +92,3 @@ rules:
eco_target: 16.0 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