Compare commits
2 Commits
otto/integ
...
ml/rules-r
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d7636448d | |||
| dfc97c24ee |
@@ -1,3 +0,0 @@
|
|||||||
# Copy to .env for local development. Do not commit real tokens.
|
|
||||||
SILLYHOME_HA_URL=http://homeassistant.local:8123
|
|
||||||
SILLYHOME_HA_TOKEN=replace-with-a-long-lived-access-token
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
name: Quality
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.11"
|
|
||||||
|
|
||||||
- name: Install project
|
|
||||||
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]"
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: pytest -q
|
|
||||||
|
|
||||||
- name: Run Ruff
|
|
||||||
run: ruff check .
|
|
||||||
|
|
||||||
- name: Run Mypy
|
|
||||||
run: mypy app tests
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,4 +10,3 @@ __pycache__/
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -12,53 +12,3 @@ TheSillyHome zeigte die Idee: statt statischer Regeln das Zuhause aus Verhaltens
|
|||||||
- Automationen vorschlagen und direkt generieren
|
- Automationen vorschlagen und direkt generieren
|
||||||
- Lokal-first ohne Cloudpflicht
|
- Lokal-first ohne Cloudpflicht
|
||||||
- Erweiterbar, testbar, dokumentiert
|
- Erweiterbar, testbar, dokumentiert
|
||||||
|
|
||||||
## Lokaler Quickstart
|
|
||||||
|
|
||||||
Voraussetzung ist Python 3.11 oder neuer.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv .venv
|
|
||||||
. .venv/bin/activate
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
python -m pip install -e ".[dev]"
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
In `.env` müssen für echte Home-Assistant-Daten diese Werte gesetzt werden:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SILLYHOME_HA_URL=http://homeassistant.local:8123
|
|
||||||
SILLYHOME_HA_TOKEN=<long-lived-access-token>
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternativ werden aus Kompatibilitätsgründen auch `HA_URL` und `HA_TOKEN` gelesen.
|
|
||||||
Tokens bleiben lokal und dürfen nicht committed, geloggt oder in Issues kopiert werden.
|
|
||||||
|
|
||||||
API starten:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uvicorn app.main:app --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Nützliche Checks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8000/health
|
|
||||||
curl http://127.0.0.1:8000/v1/entities
|
|
||||||
```
|
|
||||||
|
|
||||||
Die interaktive API-Dokumentation liegt unter `http://127.0.0.1:8000/docs`.
|
|
||||||
|
|
||||||
## Qualität
|
|
||||||
|
|
||||||
Vor jedem Pull Request lokal laufen lassen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pytest -q
|
|
||||||
ruff check .
|
|
||||||
mypy app tests
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Gitea-Actions-Workflow in `.gitea/workflows/quality.yml` führt dieselben Checks für
|
|
||||||
Pushes und Pull Requests aus.
|
|
||||||
|
|||||||
@@ -6,26 +6,10 @@ from app.ha.models import HaEntitySummary
|
|||||||
from app.rules.recommender import Rule
|
from app.rules.recommender import Rule
|
||||||
|
|
||||||
|
|
||||||
HEATING_SENSOR_DEVICE_CLASSES = frozenset({"temperature", "humidity"})
|
|
||||||
HEATING_BINARY_SENSOR_DEVICE_CLASSES = frozenset({"occupancy", "presence"})
|
|
||||||
|
|
||||||
|
|
||||||
class HeatingRule(Rule):
|
class HeatingRule(Rule):
|
||||||
def matches(self, entities: Sequence[HaEntitySummary]) -> bool:
|
def matches(self, entities: Sequence[HaEntitySummary]) -> bool:
|
||||||
for entity in entities:
|
domains = {item.domain for item in entities}
|
||||||
if entity.domain == "climate":
|
return "climate" in domains or "sensor" in domains
|
||||||
return True
|
|
||||||
if (
|
|
||||||
entity.domain == "sensor"
|
|
||||||
and entity.device_class in HEATING_SENSOR_DEVICE_CLASSES
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
if (
|
|
||||||
entity.domain == "binary_sensor"
|
|
||||||
and entity.device_class in HEATING_BINARY_SENSOR_DEVICE_CLASSES
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def recommendation(self, entities: Sequence[HaEntitySummary]) -> str:
|
def recommendation(self, entities: Sequence[HaEntitySummary]) -> str:
|
||||||
return "Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit."
|
return "Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit."
|
||||||
@@ -5,16 +5,8 @@ from app.rules.heating import HeatingRule
|
|||||||
from app.rules.recommender import Recommender
|
from app.rules.recommender import Recommender
|
||||||
|
|
||||||
|
|
||||||
def _sensor(entity_id: str, device_class: str | None = None) -> HaEntitySummary:
|
def _sensor(entity_id: str) -> HaEntitySummary:
|
||||||
return HaEntitySummary(entity_id=entity_id, domain="sensor", device_class=device_class)
|
return HaEntitySummary(entity_id=entity_id, domain="sensor")
|
||||||
|
|
||||||
|
|
||||||
def _binary_sensor(entity_id: str, device_class: str | None = None) -> HaEntitySummary:
|
|
||||||
return HaEntitySummary(
|
|
||||||
entity_id=entity_id,
|
|
||||||
domain="binary_sensor",
|
|
||||||
device_class=device_class,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _climate(entity_id: str) -> HaEntitySummary:
|
def _climate(entity_id: str) -> HaEntitySummary:
|
||||||
@@ -24,25 +16,11 @@ def _climate(entity_id: str) -> HaEntitySummary:
|
|||||||
def test_heating_rule_triggers() -> None:
|
def test_heating_rule_triggers() -> None:
|
||||||
rule = HeatingRule()
|
rule = HeatingRule()
|
||||||
assert rule.matches([_climate("climate.living_room")])
|
assert rule.matches([_climate("climate.living_room")])
|
||||||
assert rule.matches([_sensor("sensor.temperature_living", device_class="temperature")])
|
assert rule.matches([_sensor("sensor.temperature_living")])
|
||||||
assert rule.matches([_sensor("sensor.humidity_bath", device_class="humidity")])
|
|
||||||
assert rule.matches([_binary_sensor("binary_sensor.occupancy_living", "occupancy")])
|
|
||||||
assert rule.matches([_binary_sensor("binary_sensor.presence_entry", "presence")])
|
|
||||||
|
|
||||||
|
|
||||||
def test_heating_rule_ignores_non_relevant_sensors() -> None:
|
|
||||||
rule = HeatingRule()
|
|
||||||
assert not rule.matches([_sensor("sensor.temperature_living")])
|
|
||||||
assert not rule.matches([_sensor("sensor.power", device_class="power")])
|
|
||||||
assert not rule.matches([_sensor("sensor.voltage", device_class="voltage")])
|
|
||||||
assert not rule.matches([_sensor("sensor.door", device_class="door")])
|
|
||||||
assert not rule.matches([_sensor("sensor.window", device_class="window")])
|
|
||||||
assert not rule.matches([_sensor("sensor.light", device_class="illuminance")])
|
|
||||||
assert not rule.matches([_binary_sensor("binary_sensor.window", device_class="window")])
|
|
||||||
|
|
||||||
|
|
||||||
def test_recommender_uses_rule() -> None:
|
def test_recommender_uses_rule() -> None:
|
||||||
recommender = Recommender(rules=[HeatingRule()])
|
recommender = Recommender(rules=[HeatingRule()])
|
||||||
assert recommender.run([_climate("climate.living_room")]) == [
|
assert recommender.run([_climate("climate.living_room")]) == [
|
||||||
"Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit."
|
"Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit."
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user