Files
home-automation/apps/homekit
..
2025-11-17 10:18:27 +01:00
2025-11-17 10:18:27 +01:00
2025-11-17 10:18:27 +01:00
2025-11-17 10:18:27 +01:00

HomeKit Bridge

HomeKit Accessory Protocol (HAP) Bridge für das Home Automation System.

Überblick

Diese HomeKit-Bridge ermöglicht die Steuerung aller Geräte über Apple HomeKit / Home-App auf iPhone/iPad/Mac.

Architektur:

  • Läuft auf Raspberry Pi im lokalen Netzwerk
  • Kommuniziert ausschließlich über REST-API (kein direkter MQTT/Redis-Zugriff)
  • Verwendet HAP-Python für HomeKit-Protokoll
  • Unterstützt Echtzeit-Updates über Server-Sent Events (SSE)

Implementierungsanleitung für Copilot

Copilot-Aufgabe: Implementiere die HomeKit-Bridge (HAP-Python) für unser System

Kontext

  • Wir verwenden HAP-Python als HomeKit-Bridge.
  • Die Bridge läuft auf einem Raspberry Pi im gleichen LAN wie die iPhones.
  • Die Bridge kommuniziert AUSSCHLIESSLICH über die REST-API mit dem restlichen System (KEIN direkter MQTT-/Redis-Zugriff).
  • Geräte- und Raum-Infos kommen aus:
    • GET /devices
    • GET /layout und werden über device_id gejoint.
  • Die Abbildung von abstrakten Gerätetypen/Features auf HomeKit-Services/Characteristics steht in der Datei apps/homekit/homekit_mapping.md (Markdown-Tabelle). Bitte orientiere dich daran.

Ziel

Implementiere eine lauffähige HomeKit-Bridge in apps/homekit, die:

  • beim Start Geräte & Räume über die API lädt,
  • daraus anhand der Mapping-Tabelle HomeKit-Accessories erzeugt,
  • Zustandsänderungen über einen Realtime-Endpoint der API empfängt und in HomeKit spiegeln kann,
  • Set-Kommandos aus HomeKit (z.B. Licht an/aus, Helligkeit, Zieltemperatur) per POST /devices/{id}/set an die API sendet,
  • saubere Start-/Stop-Logs ausgibt,
  • einfache Test-/Startanweisungen für den Raspberry Pi und die Home-App (iPhone) in Kommentaren dokumentiert.

WICHTIG

  • Bitte benutze HAP-Python (pyhap).
  • Keine direkte MQTT- oder Redis-Kommunikation in der Bridge.
  • Nutze HTTP(S) zur API (z.B. mit requests oder httpx; gerne sync, HAP-Python ist selbst eventloop-orientiert).
  • Nutze die Mapping-Tabelle in apps/homekit/homekit_mapping.md als Leitlinie für die Accessory-Typen.

Paketstruktur

apps/homekit/
├── __init__.py
├── main.py                  # Einstiegspunkt
├── api_client.py            # REST-Client zur API
├── device_registry.py       # Join /devices + /layout → internes Device-Modell
├── accessories/
│   ├── base.py              # gemeinsame Basisklasse/n für Accessories
│   ├── light.py             # Light-Accessories
│   ├── thermostat.py
│   ├── contact.py
│   ├── sensor.py            # temp_humidity etc.
│   └── cover.py             # optional, Rollladen
└── tests/                   # rudimentäre Tests/Checks

Teil 1: Internes Gerätemodell / Device-Registry

1.1 Erstelle in apps/homekit/device_registry.py:

Dataklasse/Model Device:

  • device_id: str
  • type: str - "light","thermostat","outlet","contact","temp_humidity","cover",...
  • name: str - Kurzname aus GET /devices.name
  • friendly_name: str - title aus GET /layout (fallback name)
  • room: str | None - Raumname aus layout
  • features: dict[str, bool]
  • read_only: bool - heuristisch: Sensor-/Kontakt-Typen sind read_only

Klasse DeviceRegistry mit Funktionen:

  • def load_from_api(api: ApiClient) -> DeviceRegistry:
    • ruft GET /devices und GET /layout auf,
    • joint über device_id,
    • erstellt Device-Instanzen.
  • get_all(): list[Device]
  • get_by_id(device_id: str) -> Device | None

Akzeptanz:

  • load_from_api funktioniert mit der bestehenden Struktur von /devices und /layout:
    • /devices liefert mindestens {device_id, type, name, features}
    • /layout liefert eine Struktur, aus der device_id → room + title ableitbar ist
  • Der Join über device_id klappt; fehlende Layout-Einträge werden toleriert

Teil 2: API-Client

2.1 Erstelle in apps/homekit/api_client.py:

Klasse ApiClient mit:

  • __init__(self, base_url: str, token: str | None = None, timeout: int = 5)
  • Methoden:
    • get_devices() -> list[dict]: GET /devices
    • get_layout() -> dict: GET /layout
    • get_device_state(device_id: str) -> dict: GET /devices/{id}/state
    • post_device_set(device_id: str, type: str, payload: dict) -> None: POST /devices/{id}/set
    • stream_realtime() -> Iterator[dict]:
      • GET /realtime als SSE, yield jedes Event als dict: {"type":"state","device_id":...,"payload":{...},"ts":...}

Auth:

  • Wenn ein API-Token via ENV HOMEKIT_API_TOKEN gesetzt ist, nutze HTTP-Header: Authorization: Bearer <token>

Akzeptanz:

  • ApiClient ist robust:
    • bei Netzwerkfehlern gibt es sinnvolle Exceptions/Logs,
    • stream_realtime behandelt Reconnect (z.B. einfache Endlosschleife mit Backoff).
  • Es werden keine MQTT-Details verwendet, nur HTTP.

Teil 3: HomeKit-Accessory-Klassen (HAP-Python)

3.1 Erstelle in apps/homekit/accessories/base.py:

Basisklasse BaseDeviceAccessory(Accessory) mit:

  • Referenz auf Device (aus DeviceRegistry)
  • Referenz auf ApiClient
  • Methoden zum:
    • Registrieren von HAP-Characteristics und Set-Handlern
    • Aktualisieren von Characteristics bei eingehenden Events
  • Logging

3.2 Erstelle spezifische Accessory-Klassen basierend auf homekit_mapping.md:

LightAccessories (apps/homekit/accessories/light.py):

  • On/Off (nur power)
  • Dimmable (power + brightness)
  • Color (power + brightness + color_hsb)

ThermostatAccessory:

  • CurrentTemperature, TargetTemperature, Mode (so weit in Mapping definiert)

ContactAccessory:

  • ContactSensorState (open/closed)

TempHumidityAccessory:

  • TemperatureSensor (CurrentTemperature)
  • HumiditySensor (CurrentRelativeHumidity)

OutletAccessory:

  • On/Off

CoverAccessory (optional):

  • WindowCovering mit CurrentPosition/TargetPosition

Die Mapping-Tabelle in homekit_mapping.md ist die normative Referenz:

  • Bitte lies die Tabelle und mappe abstract_type + Features → passende Accessory-Klasse und Characteristics
  • Wo die Tabelle Status=TODO/REVIEW hat:
    • Implementiere nur das, was eindeutig ist,
    • lasse TODO-Kommentare an den entsprechenden Stellen im Code.

Akzeptanz:

  • Für die abstrakten Typen, die bereits in devices.yaml und Mapping-Tabelle klar definiert sind (z.B. light, thermostat, contact, temp_humidity), existieren passende Accessory-Klassen.
  • Set-Operationen erzeugen korrekte Payloads für POST /devices/{id}/set:
    • Light: {"type":"light","payload":{"power":"on"/"off", "brightness":..., "hue":..., "sat":...}}
    • Thermostat: {"type":"thermostat","payload":{"target":...}}
    • Contact: read_only → keine Set-Handler
    • Temp/Humidity: read_only → keine Set-Handler

Teil 4: Bridge-Setup mit HAP-Python

4.1 Implementiere in apps/homekit/main.py:

env-Konfiguration:

  • HOMEKIT_NAME (default: "Home Automation Bridge")
  • HOMEKIT_PIN (z.B. "031-45-154")
  • HOMEKIT_PORT (default 51826)
  • API_BASE (z.B. "http://api:8001" oder extern)
  • HOMEKIT_API_TOKEN (optional)

Funktion build_bridge(driver, api_client: ApiClient) -> Bridge:

  • DeviceRegistry via load_from_api(api_client) laden.
  • Für jedes Device anhand Mapping-Tabelle die passende Accessory-Klasse instanziieren.
  • Einen Bridge-Accessory (pyhap.accessory.Bridge) erstellen.
  • Alle Device-Accessories der Bridge hinzufügen.

Realtime-Event-Loop:

  • In einem Hintergrund-Thread oder ThreadPool:
    • api_client.stream_realtime() iterieren,
    • für jedes Event device_id → Accessory finden,
    • Characteristics updaten.
  • Thread wird beim Shutdown sauber beendet.

main():

  • Logging einrichten.
  • ApiClient erstellen.
  • AccessoryDriver(port=HOMEKIT_PORT, persist_file="homekit.state") erstellen.
  • Bridge via build_bridge(driver, api_client) bauen.
  • Bridge dem Driver hinzufügen.
  • Realtime-Thread starten.
  • driver.start() aufrufen.
  • Auf KeyboardInterrupt reagieren und sauber stoppen.

Akzeptanz:

  • Beim Start loggt die Bridge:
    • Anzahl Devices,
    • auf welchem Port sie als HomeKit-Bridge lauscht,
    • welches API_BASE verwendet wird.
  • Die Datei homekit.state wird im Arbeitsverzeichnis bzw. einem konfigurierbaren Ordner abgelegt (um Pairing-Info persistent zu halten).
  • Die Bridge übersteht API-Neustarts (Realtime-Loop reconnectet) und Netzwerkflaps.

Teil 5: Tests & Testanweisungen

5.1 Lege in apps/homekit/tests/ einfache Tests/Checks an:

Unit-Tests (pytest), soweit ohne echtes HomeKit möglich:

  • Test für DeviceRegistry.load_from_api() mit Mock-Antworten aus /devices und /layout:
    • Korrekte Join-Logik,
    • Korrekte room/friendly_name-Zuordnung.
  • Test für set-Payload-Erzeugung pro Accessory:
    • z.B. LightAccessory: On=True → POST /devices/{id}/set wird mit korrektem Payload aufgerufen (über Mock ApiClient).

Allgemein

  • Nutze möglichst sinnvolle Typannotationen und Docstrings.
  • Hinterlasse TODO-Kommentare an Stellen, wo die Mapping-Tabelle explizit Status=TODO/REVIEW hat.
  • Ändere KEINE bestehenden API-Endpunkte; verlasse dich nur auf deren aktuelles Verhalten (GET /devices, GET /layout, /realtime, POST /devices/{id}/set).

Installation & Setup

Voraussetzungen

  • Python 3.9+
  • Raspberry Pi im gleichen LAN wie iPhone/iPad
  • API-Server erreichbar (z.B. http://api:8001)

Installation

cd apps/homekit
pip install -r requirements.txt

Umgebungsvariablen

Erstelle eine .env Datei oder setze folgende Variablen:

export API_BASE="http://YOUR_API_IP:8001"
export HOMEKIT_API_TOKEN="your-token-if-needed"  # optional
export HOMEKIT_PIN="031-45-154"
export HOMEKIT_NAME="Home Automation Bridge"
export HOMEKIT_PORT="51826"
export HOMEKIT_PERSIST_FILE="homekit.state"

Start

python -m apps.homekit.main

Erwartete Logs:

Loading devices from API...
Loaded X devices from API
Bridge built with Y accessories
HomeKit Bridge started on port 51826
Starting realtime event loop...

Pairing mit iPhone (Home-App)

  1. Voraussetzungen:

    • iPhone im gleichen WLAN wie Raspberry Pi
    • Bridge läuft und zeigt "started on port 51826"
  2. Home-App öffnen:

    • Öffne die Home-App auf dem iPhone
    • Tippe auf "+" → "Gerät hinzufügen"
    • Wähle "Weitere Optionen..." oder "Code fehlt oder kann nicht gescannt werden"
  3. Bridge auswählen:

    • Die Bridge sollte in der Nähe-Liste erscheinen (z.B. "Home Automation Bridge")
    • Tippe auf die Bridge
  4. PIN eingeben:

    • Gib den PIN ein: 031-45-154 (oder dein HOMEKIT_PIN)
    • Format: XXX-XX-XXX
  5. Konfiguration abschließen:

    • Geräte werden geladen
    • Räume werden automatisch aus layout.yaml übernommen
    • Geräte können nun über Home-App gesteuert werden

Funktionstests

Test 1: Licht einschalten

Aktion: Lampe in Home-App antippen (On)

Erwartung:

  • API-Log zeigt: POST /devices/{id}/set mit {"type":"light","payload":{"power":"on"}}
  • Physische Lampe oder Simulator schaltet ein

Test 2: Helligkeit ändern (dimmbare Lampe)

Aktion: Helligkeits-Slider bewegen (z.B. 75%)

Erwartung:

  • POST /devices/{id}/set mit brightness ca. 75
  • Lampe dimmt entsprechend

Test 3: Farbe ändern (Farblampe)

Aktion: Farbe in Home-App wählen

Erwartung:

  • POST /devices/{id}/set mit hue/sat Werten
  • Lampe wechselt Farbe

Test 4: Thermostat Zieltemperatur

Aktion: Zieltemperatur auf 22°C setzen

Erwartung:

  • POST /devices/{id}/set mit target=22
  • CurrentTemperature wird über /realtime aktualisiert

Test 5: Kontaktsensor (read-only)

Aktion: Fenster physisch öffnen/schließen

Erwartung:

  • /realtime sendet Event
  • Home-App zeigt "Offen" oder "Geschlossen"

Test 6: Temperatur-/Feuchtigkeitssensor

Aktion: Werte ändern sich (z.B. Heizung an)

Erwartung:

  • /realtime Events aktualisieren Werte
  • Home-App zeigt aktuelle Temperatur/Luftfeuchtigkeit

Troubleshooting

Bridge erscheint nicht in Home-App

  • Netzwerk prüfen: iPhone und RPi im gleichen Subnet?
  • Firewall prüfen: Port 51826 muss erreichbar sein
  • Logs prüfen: Fehler beim Start?
  • mDNS/Bonjour: Funktioniert Bonjour im Netzwerk?

Geräte reagieren nicht

  • API-Logs prüfen: Kommen POST-Requests an?
  • Realtime-Verbindung: Läuft /realtime Event-Loop? (Log-Meldungen)
  • API-Endpoints testen: Manuell mit curl testen

Pairing schlägt fehl

  • State-Datei löschen: rm homekit.state und Bridge neu starten
  • PIN-Format prüfen: Muss XXX-XX-XXX Format haben
  • Alte Pairings löschen: In Home-App unter "Home-Einstellungen" → "HomeKit-Geräte zurücksetzen"

Realtime-Updates funktionieren nicht

  • SSE-Verbindung prüfen: Logs zeigen "Starting realtime event loop..."?
  • API-Endpoint testen: curl -N http://api:8001/realtime
  • Firewall/Proxy: Blockiert etwas SSE-Streams?

Docker Deployment

Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "-m", "apps.homekit.main"]

docker-compose.yml

services:
  homekit:
    build: ./apps/homekit
    environment:
      - API_BASE=http://api:8001
      - HOMEKIT_PIN=031-45-154
      - HOMEKIT_NAME=Home Automation Bridge
    ports:
      - "51826:51826"
    volumes:
      - ./data/homekit:/data
    network_mode: "host"  # Wichtig für mDNS/Bonjour Discovery
    restart: unless-stopped

Wichtig: network_mode: "host" ist erforderlich für mDNS/Bonjour Discovery, damit die Bridge im lokalen Netzwerk gefunden werden kann.

Architektur

┌─────────────┐
│  iPhone/    │
│  Home-App   │
└──────┬──────┘
       │ HomeKit (HAP)
       │ Port 51826
┌──────▼──────────────────┐
│  HomeKit Bridge         │
│  (HAP-Python)           │
│  - Device Registry      │
│  - Accessory Mapping    │
│  - SSE Event Loop       │
└──────┬──────────────────┘
       │ HTTP REST API
       │ (GET /devices, POST /set, SSE /realtime)
┌──────▼──────────────────┐
│  API Server             │
│  (FastAPI)              │
└──────┬──────────────────┘
       │ MQTT
┌──────▼──────────────────┐
│  Abstraction Layer      │
│  (Zigbee2MQTT, MAX!)    │
└─────────────────────────┘

Weitere Dokumentation

  • API-Mapping: Siehe homekit_mapping.md für Device-Type → HomeKit-Service Mapping
  • API-Dokumentation: Siehe API-Server README für Endpoint-Dokumentation
  • HAP-Python Docs: https://github.com/ikalchev/HAP-python

Entwicklung

Tests ausführen

pytest apps/homekit/tests/

Logs

Die Bridge gibt detaillierte Logs aus:

  • INFO: Normale Betriebsmeldungen (Start, Device-Anzahl, etc.)
  • DEBUG: Detaillierte State-Updates und API-Calls
  • ERROR: Fehler bei API-Kommunikation oder Accessory-Updates

Log-Level über Environment-Variable steuern:

export LOG_LEVEL=DEBUG

Lizenz

Siehe Hauptprojekt-Lizenz.