From 6540d62ff7359fd1fa5f17ae6de36780d7e76280 Mon Sep 17 00:00:00 2001 From: Pino Date: Wed, 10 Jun 2026 20:46:54 +0200 Subject: [PATCH] ml/rules-recommendations: regelbasierte Heizungsempfehlung und Recommender --- app/rules/__init__.py | 1 + app/rules/heating.py | 15 +++++++++++++++ app/rules/recommender.py | 25 +++++++++++++++++++++++++ pyproject.toml | 2 +- tests/rules/test_heating.py | 26 ++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 app/rules/__init__.py create mode 100644 app/rules/heating.py create mode 100644 app/rules/recommender.py create mode 100644 tests/rules/test_heating.py diff --git a/app/rules/__init__.py b/app/rules/__init__.py new file mode 100644 index 0000000..f390491 --- /dev/null +++ b/app/rules/__init__.py @@ -0,0 +1 @@ +# sillyhome-next.rules \ No newline at end of file diff --git a/app/rules/heating.py b/app/rules/heating.py new file mode 100644 index 0000000..994711a --- /dev/null +++ b/app/rules/heating.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from collections.abc import Sequence + +from app.ha.models import HaEntitySummary +from app.rules.recommender import Rule + + +class HeatingRule(Rule): + def matches(self, entities: Sequence[HaEntitySummary]) -> bool: + domains = {item.domain for item in entities} + return "climate" in domains or "sensor" in domains + + 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/app/rules/recommender.py b/app/rules/recommender.py new file mode 100644 index 0000000..ca44f48 --- /dev/null +++ b/app/rules/recommender.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from collections.abc import Sequence + +from app.ha.models import HaEntitySummary + + +class Rule: + def matches(self, entities: Sequence[HaEntitySummary]) -> bool: + raise NotImplementedError + + def recommendation(self, entities: Sequence[HaEntitySummary]) -> str: + raise NotImplementedError + + +class Recommender: + def __init__(self, rules: Sequence[Rule]) -> None: + self._rules = rules + + def run(self, entities: Sequence[HaEntitySummary]) -> list[str]: + results: list[str] = [] + for rule in self._rules: + if rule.matches(entities): + results.append(rule.recommendation(entities)) + return results \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c28b433..c1d26ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,4 +25,4 @@ strict = true [tool.ruff] line-length = 100 -target-version = "py311" +target-version = "py311" \ No newline at end of file diff --git a/tests/rules/test_heating.py b/tests/rules/test_heating.py new file mode 100644 index 0000000..da4d753 --- /dev/null +++ b/tests/rules/test_heating.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +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 _climate(entity_id: str) -> HaEntitySummary: + return HaEntitySummary(entity_id=entity_id, domain="climate") + + +def test_heating_rule_triggers() -> None: + rule = HeatingRule() + assert rule.matches([_climate("climate.living_room")]) + assert rule.matches([_sensor("sensor.temperature_living")]) + + +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