From e2bc0644aef2bc83052684803946c57aebca9646 Mon Sep 17 00:00:00 2001 From: Pino Date: Thu, 11 Jun 2026 00:12:19 +0200 Subject: [PATCH] main: HeatingRule auf heizungsrelevante Sensoren begrenzen --- app/rules/heating.py | 23 +++++++++++-- tests/rules/test_heating.py | 68 +++++++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/app/rules/heating.py b/app/rules/heating.py index 994711a..10a2181 100644 --- a/app/rules/heating.py +++ b/app/rules/heating.py @@ -7,9 +7,28 @@ from app.rules.recommender import Rule class HeatingRule(Rule): + """Heizungsregel: Nur auf heizungsrelevante Entitäten reagieren. + + Triggert bei: + - `climate`-Entitäten direkt + - `sensor` mit `device_class` in {temperature, humidity} + - `binary_sensor` mit `device_class` in {occupancy, presence} + + Alle anderen Domains/Device-Klassen bleiben ohne Effekt. + """ + + HEATING_SENSOR_CLASSES: frozenset[str] = frozenset({"temperature", "humidity"}) + HEATING_PRESENCE_CLASSES: frozenset[str] = frozenset({"occupancy", "presence"}) + def matches(self, entities: Sequence[HaEntitySummary]) -> bool: - domains = {item.domain for item in entities} - return "climate" in domains or "sensor" in domains + for item in entities: + if item.domain == "climate": + return True + if item.domain == "sensor" and item.device_class in self.HEATING_SENSOR_CLASSES: + return True + if item.domain == "binary_sensor" and item.device_class in self.HEATING_PRESENCE_CLASSES: + return True + return False def recommendation(self, entities: Sequence[HaEntitySummary]) -> str: return "Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit." \ No newline at end of file diff --git a/tests/rules/test_heating.py b/tests/rules/test_heating.py index da4d753..02c96a9 100644 --- a/tests/rules/test_heating.py +++ b/tests/rules/test_heating.py @@ -1,26 +1,64 @@ from __future__ import annotations +import pytest + from app.ha.models import HaEntitySummary from app.rules.heating import HeatingRule -from app.rules.recommender import Recommender -def _sensor(entity_id: str) -> HaEntitySummary: - return HaEntitySummary(entity_id=entity_id, domain="sensor") +def _entity(entity_id: str, domain: str, device_class: str | None = None) -> HaEntitySummary: + return HaEntitySummary(entity_id=entity_id, domain=domain, device_class=device_class) -def _climate(entity_id: str) -> HaEntitySummary: - return HaEntitySummary(entity_id=entity_id, domain="climate") - - -def test_heating_rule_triggers() -> None: +# --- positive cases -------------------------------------------------------- +@pytest.mark.parametrize( + "entity", + [ + _entity("climate.living_room", "climate"), + _entity("sensor.temperature_living", "sensor", "temperature"), + _entity("sensor.humidity_bathroom", "sensor", "humidity"), + _entity("binary_sensor.living_room_occupancy", "binary_sensor", "occupancy"), + _entity("binary_sensor.entrance_presence", "binary_sensor", "presence"), + ], + ids=lambda e: e.entity_id, +) +def test_heating_rule_triggers_for_relevant_entities(entity: HaEntitySummary) -> None: rule = HeatingRule() - assert rule.matches([_climate("climate.living_room")]) - assert rule.matches([_sensor("sensor.temperature_living")]) + assert rule.matches([entity]) is True -def test_recommender_uses_rule() -> None: - recommender = Recommender(rules=[HeatingRule()]) - assert recommender.run([_climate("climate.living_room")]) == [ - "Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit." - ] \ No newline at end of file +# --- negative cases ------------------------------------------------------- +@pytest.mark.parametrize( + "entity", + [ + _entity("sensor.power_consumption", "sensor", "power"), + _entity("sensor.door", "sensor", "door"), + _entity("sensor.energy", "sensor", "energy"), + _entity("binary_sensor.door_window", "binary_sensor", "door"), + _entity("binary_sensor.motion", "binary_sensor", "motion"), + _entity("light.living_room", "light"), + _entity("switch.plug", "switch"), + _entity("sensor.some_random", "sensor"), + _entity("binary_sensor.some_binary", "binary_sensor"), + ], + ids=lambda e: e.entity_id, +) +def test_heating_rule_ignores_non_heating_entities(entity: HaEntitySummary) -> None: + rule = HeatingRule() + assert rule.matches([entity]) is False + + +def test_heating_rule_mixed_list_returns_true() -> None: + rule = HeatingRule() + entities = [ + _entity("sensor.power", "sensor", "power"), + _entity("climate.living_room", "climate"), + _entity("light.ceiling", "light"), + ] + assert rule.matches(entities) is True + + +def test_heating_rule_recommendation_is_stable() -> None: + rule = HeatingRule() + expected = "Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit." + assert rule.recommendation([_entity("climate.living_room", "climate")]) == expected \ No newline at end of file