Integration: Pino-Heizungsregel, CI und Quickstart #7

Closed
Otto wants to merge 1 commits from otto/integration-quality-docs into otto/ha-client-errors
6 changed files with 130 additions and 7 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
# 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

View File

@@ -0,0 +1,31 @@
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
View File

@@ -10,3 +10,4 @@ __pycache__/
.env
.env.local
.env.*
!.env.example

View File

@@ -12,3 +12,53 @@ TheSillyHome zeigte die Idee: statt statischer Regeln das Zuhause aus Verhaltens
- Automationen vorschlagen und direkt generieren
- Lokal-first ohne Cloudpflicht
- 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.

View File

@@ -6,10 +6,26 @@ from app.ha.models import HaEntitySummary
from app.rules.recommender import Rule
HEATING_SENSOR_DEVICE_CLASSES = frozenset({"temperature", "humidity"})
HEATING_BINARY_SENSOR_DEVICE_CLASSES = frozenset({"occupancy", "presence"})
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
for entity in entities:
if entity.domain == "climate":
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:
return "Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit."
return "Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit."

View File

@@ -5,8 +5,16 @@ 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 _sensor(entity_id: str, device_class: str | None = None) -> HaEntitySummary:
return HaEntitySummary(entity_id=entity_id, domain="sensor", device_class=device_class)
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:
@@ -16,11 +24,25 @@ def _climate(entity_id: str) -> HaEntitySummary:
def test_heating_rule_triggers() -> None:
rule = HeatingRule()
assert rule.matches([_climate("climate.living_room")])
assert rule.matches([_sensor("sensor.temperature_living")])
assert rule.matches([_sensor("sensor.temperature_living", device_class="temperature")])
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:
recommender = Recommender(rules=[HeatingRule()])
assert recommender.run([_climate("climate.living_room")]) == [
"Prüfe Heizungsregelung: Aktiviere energiesparenden Modus bei Abwesenheit."
]
]