# HomeKit-Bridge API-Modell: Analyse der bestehenden Implementierung **Analysedatum:** 17. November 2025 **Analysierte Dateien:** - `apps/api/main.py` - `apps/api/routes/groups_scenes.py` - `config/devices.yaml` --- ## Zusammenfassung Die bestehende API-Implementierung erfüllt **~60%** der Anforderungen des HomeKit-Bridge Ziel-Modells. Die meisten Kernfunktionalitäten sind vorhanden, aber es fehlen wichtige Metadaten-Felder und ein dedizierter State-Endpoint. --- ## 1. GET /devices ### Status: ✅ **VORHANDEN** mit Abweichungen ### Implementierung (apps/api/main.py:325-343) ```python @app.get("/devices") async def get_devices() -> list[DeviceInfo]: devices = load_devices() return [ DeviceInfo( device_id=device["device_id"], type=device["type"], name=device.get("name", device["device_id"]), features=device.get("features", {}) ) for device in devices ] ``` ### Response-Modell (DeviceInfo) ```python class DeviceInfo(BaseModel): device_id: str type: str name: str features: dict[str, Any] = {} ``` ### Abweichungen vom Ziel-Modell | Feld | Ziel-Modell | Ist-Zustand | Status | |------|-------------|-------------|---------| | `device_id` | ✅ Erforderlich | ✅ Vorhanden | OK | | `type` | ✅ Erforderlich | ✅ Vorhanden | OK | | `cap_version` | ✅ Erforderlich | ❌ **FEHLT** | FEHLT | | `room` | ✅ Erforderlich | ❌ **FEHLT** | FEHLT | | `friendly_name` | ✅ Erforderlich | ⚠️ Heißt `name` | UMBENENNUNG | | `technology` | ✅ Erforderlich | ❌ **FEHLT** | FEHLT | | `features` | ✅ Erforderlich | ✅ Vorhanden | OK | | `read_only` | ✅ Erforderlich | ❌ **FEHLT** | FEHLT | | `tags` | Optional | ❌ **FEHLT** | FEHLT | ### Details zu fehlenden Feldern #### ❌ `cap_version` - **Vorhanden in devices.yaml:** Ja, als `cap_version` (z.B. `"light@1.2.0"`) - **Problem:** Wird von `load_devices()` geladen, aber nicht in `DeviceInfo` exponiert - **Lösung:** Feld zu `DeviceInfo` hinzufügen und aus `device["cap_version"]` befüllen #### ❌ `room` - **Vorhanden in layout.yaml:** Ja, indirekt über Raum-Zuordnung - **Problem:** Aktuell nur über separaten Endpoint `/devices/{device_id}/room` verfügbar - **Lösung:** Room-Mapping in `/devices` integrieren (Resolver bereits vorhanden in `apps/api/resolvers.py`) #### ⚠️ `friendly_name` vs. `name` - **Vorhanden in devices.yaml:** Ja, als `metadata.friendly_name` - **Problem:** Aktuell wird `device.get("name", device["device_id"])` verwendet, nicht `metadata.friendly_name` - **Lösung:** Priorisierung: `metadata.friendly_name` > `name` > `device_id` #### ❌ `technology` - **Vorhanden in devices.yaml:** Ja, als `technology` (z.B. `"zigbee2mqtt"`) - **Problem:** Wird nicht in Response exponiert - **Lösung:** Feld zu `DeviceInfo` hinzufügen #### ❌ `read_only` - **Implizit vorhanden:** Ja, über `topics.set` (wenn fehlt → read-only) - **Problem:** Muss berechnet werden - **Lösung:** `read_only = "set" not in device.get("topics", {})` #### ❌ `tags` - **Vorhanden in devices.yaml:** Nein - **Status:** Nicht kritisch, kann später ergänzt werden --- ## 2. GET /devices/{device_id} ### Status: ❌ **FEHLT KOMPLETT** ### Aktuell vorhanden - `/devices/{device_id}/room` (liefert nur `{"device_id": str, "room": str | None}`) ### Erforderlich Ein Endpoint, der das gleiche Schema wie ein Eintrag aus `/devices` zurückgibt: ```python @app.get("/devices/{device_id}") async def get_device(device_id: str) -> DeviceInfo: # Load device, enrich with room, return DeviceInfo ``` ### Implementierung - Device aus `load_devices()` filtern - Mit `get_room(device_id)` anreichern - Als `DeviceInfo` zurückgeben - 404 bei nicht gefunden --- ## 3. GET /devices/{device_id}/state ### Status: ❌ **FEHLT KOMPLETT** ### Aktuell vorhanden - `/devices/states` (liefert **alle** Device-States als Dict) ```python @app.get("/devices/states") async def get_device_states() -> dict[str, dict[str, Any]]: return device_states # In-memory cache ``` ### Ziel-Format ```json { "device_id": "thermostat_wolfgang", "type": "thermostat", "room": "Schlafzimmer", "payload": { "current": 19.5, "target": 21.0, "mode": "heat" }, "ts": "2025-11-17T14:23:45.123Z" } ``` ### Erforderlich ```python @app.get("/devices/{device_id}/state") async def get_device_state(device_id: str) -> DeviceStateResponse: # Get from device_states cache # Enrich with metadata (type, room) # Add timestamp # Return structured response ``` ### Problem - Aktuell wird nur `payload` im Cache gespeichert - Timestamp fehlt im Cache (müsste bei SSE-Updates mitgespeichert werden) - Metadaten (type, room) müssen aus devices.yaml/layout.yaml ergänzt werden --- ## 4. SSE-Endpoint /realtime ### Status: ✅ **VORHANDEN** mit kleineren Abweichungen ### Implementierung (apps/api/main.py:608-637) ```python @app.get("/realtime") async def realtime_events(request: Request) -> StreamingResponse: return StreamingResponse( event_generator(request), media_type="text/event-stream", # ... headers ) ``` ### Aktuelles Event-Format (aus Redis) ```json { "type": "state", "device_id": "thermostat_wolfgang", "payload": { "current": 19.5, "target": 21.0 } } ``` ### Ziel-Format ```json { "type": "state", "device_id": "thermostat_wolfgang", "device_type": "thermostat", // ← FEHLT "room": "Schlafzimmer", // ← FEHLT "payload": { "current": 19.5, "target": 21.0 }, "ts": "2025-11-17T14:23:45.123Z", // ← FEHLT "source": "zigbee2mqtt" // ← FEHLT (optional) } ``` ### Abweichungen | Feld | Ziel-Modell | Ist-Zustand | Status | |------|-------------|-------------|---------| | `type` | ✅ | ✅ | OK | | `device_id` | ✅ | ✅ | OK | | `device_type` | ✅ | ❌ **FEHLT** | FEHLT | | `room` | ✅ | ❌ **FEHLT** | FEHLT | | `payload` | ✅ | ✅ | OK | | `ts` | ✅ | ❌ **FEHLT** | FEHLT | | `source` | Optional | ❌ **FEHLT** | FEHLT | ### Problem Events werden direkt aus Redis weitergeleitet ohne Enrichment. ### Lösungsansätze **Option A: Enrichment im SSE-Generator** ```python # Im event_generator() nach JSON-Parse: state_data = json.loads(data) if state_data.get("type") == "state": # Enrich with metadata device_id = state_data["device_id"] device = get_device_from_cache(device_id) state_data["device_type"] = device["type"] state_data["room"] = get_room(device_id) if "ts" not in state_data: state_data["ts"] = datetime.utcnow().isoformat() data = json.dumps(state_data) ``` **Option B: Enrichment im Publisher (apps/abstraction)** - Besser: Events bereits vollständig beim Publizieren - Würde auch `/devices/{id}/state` helfen --- ## 5. POST /devices/{device_id}/set ### Status: ✅ **VORHANDEN** mit kleinen Abweichungen ### Implementierung (apps/api/main.py:406-504) ```python @app.post("/devices/{device_id}/set", status_code=status.HTTP_202_ACCEPTED) async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str]: # Validierung, MQTT publish ``` ### Request-Modell ```python class SetDeviceRequest(BaseModel): type: str payload: dict[str, Any] ``` ### Vergleich mit Ziel-Modell | Aspekt | Ziel-Modell | Ist-Zustand | Status | |--------|-------------|-------------|---------| | Body-Format | `{type, payload}` | `{type, payload}` | ✅ OK | | Type-Validierung | ✅ Erforderlich | ✅ Vorhanden | OK | | Payload-Validierung | ✅ Per Device-Type | ✅ Vorhanden | OK | | Read-Only Check | ✅ → 405 | ✅ → 405 | OK | | Response Code | 200/202 | 202 | OK | ### Validierungs-Details **✅ Gut implementiert:** - Type-spezifische Pydantic-Validierung (LightState, ThermostatState, etc.) - Whitelist für erlaubte Felder bei Thermostaten - Read-only Detection über `topics.set` - Proper HTTP Status Codes (404, 405, 422) **⚠️ Kleine Abweichung:** - Thermostat-Validierung erlaubt nur `{mode, target}` beim SET - Ziel-Modell erwähnt dies nicht explizit - **Bewertung:** Ist sinnvolle Einschränkung, kein Problem ### MQTT-Publishing ```python topic = f"home/{request.type}/{device_id}/set" mqtt_payload = { "type": request.type, "payload": request.payload } await publish_mqtt(topic, mqtt_payload) ``` ✅ Korrekt implementiert --- ## 6. Zusätzliche Endpoints (nicht im Ziel-Modell) ### ℹ️ Vorhanden, aber nicht gefordert - **GET /spec** - Capability-Versionen - **GET /devices/states** - Alle States (könnte nützlich für Bridge sein) - **GET /layout** - UI-spezifisch - **GET /devices/{device_id}/room** - Wird obsolet wenn `/devices` `room` hat - **GET /groups**, **POST /groups/{id}/set** - Gruppen-Feature - **GET /scenes**, **POST /scenes/{id}/run** - Szenen-Feature **Bewertung:** Nicht störend, können bleiben. Bridge muss diese nicht nutzen. --- ## 7. Datenquellen-Analyse ### devices.yaml **✅ Enthält alle benötigten Felder:** ```yaml - device_id: leselampe_esszimmer type: light cap_version: "light@1.2.0" # ← Vorhanden technology: zigbee2mqtt # ← Vorhanden features: power: true brightness: true topics: state: "..." set: "..." # ← Für read_only Detection metadata: friendly_name: "Leselampe Esszimmer" # ← Vorhanden ieee_address: "..." model: "LED1842G3" vendor: "IKEA" ``` ### layout.yaml **✅ Enthält Room-Mapping:** ```yaml rooms: - name: "Schlafzimmer" devices: - device_id: thermostat_wolfgang ``` **✅ Resolver bereits vorhanden:** `apps/api/resolvers.py::get_room(device_id)` --- ## 8. Priorisierte To-Do-Liste ### 🔴 **Kritisch** (Bridge funktioniert nicht ohne) 1. **GET /devices: Fehlende Felder ergänzen** - `cap_version` aus devices.yaml - `room` via `get_room()` - `friendly_name` aus `metadata.friendly_name` - `technology` aus devices.yaml - `read_only` berechnen 2. **GET /devices/{device_id}/state implementieren** - Neuer Endpoint - State aus Cache + Metadaten - Timestamp hinzufügen ### 🟡 **Wichtig** (Bridge funktioniert, aber eingeschränkt) 3. **SSE /realtime: Events enrichen** - `device_type` hinzufügen - `room` hinzufügen - `ts` sicherstellen 4. **GET /devices/{device_id} implementieren** - Einzelgerät-Abfrage - Gleiche Struktur wie `/devices`-Eintrag ### 🟢 **Nice-to-have** 5. **State-Cache mit Timestamps erweitern** - Aktuell: `device_states[id] = payload` - Ziel: `device_states[id] = {payload, ts}` 6. **SSE: source-Feld hinzufügen** - Aus `device["technology"]` ableiten --- ## 9. Implementierungs-Reihenfolge ### Phase 1: GET /devices erweitern **Dateien:** - `apps/api/main.py` (DeviceInfo-Modell, get_devices()) **Änderungen:** ```python class DeviceInfo(BaseModel): device_id: str type: str cap_version: str room: str | None friendly_name: str technology: str features: dict[str, Any] read_only: bool tags: list[str] | None = None @app.get("/devices") async def get_devices() -> list[DeviceInfo]: devices = load_devices() return [ DeviceInfo( device_id=device["device_id"], type=device["type"], cap_version=device["cap_version"], room=get_room(device["device_id"]), friendly_name=device.get("metadata", {}).get("friendly_name", device["device_id"]), technology=device["technology"], features=device.get("features", {}), read_only="set" not in device.get("topics", {}), tags=device.get("tags") ) for device in devices ] ``` ### Phase 2: GET /devices/{device_id}/state **Dateien:** - `apps/api/main.py` **Neues Modell:** ```python class DeviceStateResponse(BaseModel): device_id: str type: str room: str | None payload: dict[str, Any] ts: str @app.get("/devices/{device_id}/state") async def get_device_state(device_id: str) -> DeviceStateResponse: if device_id not in device_states: raise HTTPException(404, f"No state for {device_id}") devices = load_devices() device = next((d for d in devices if d["device_id"] == device_id), None) if not device: raise HTTPException(404, f"Device {device_id} not found") return DeviceStateResponse( device_id=device_id, type=device["type"], room=get_room(device_id), payload=device_states[device_id], ts=datetime.utcnow().isoformat() + "Z" ) ``` ### Phase 3: SSE Enrichment **Dateien:** - `apps/api/main.py` (event_generator()) **Im event_generator() nach JSON-Parse:** ```python if message and message["type"] == "message": data = message["data"] state_data = json.loads(data) # Enrich events if state_data.get("type") == "state" and state_data.get("device_id"): device_id = state_data["device_id"] devices = load_devices() device = next((d for d in devices if d["device_id"] == device_id), None) if device: state_data["device_type"] = device["type"] state_data["room"] = get_room(device_id) if "ts" not in state_data: state_data["ts"] = datetime.utcnow().isoformat() + "Z" state_data["source"] = device.get("technology") data = json.dumps(state_data) yield f"event: message\ndata: {data}\n\n" ``` ### Phase 4: GET /devices/{device_id} **Dateien:** - `apps/api/main.py` ```python @app.get("/devices/{device_id}") async def get_device(device_id: str) -> DeviceInfo: devices = load_devices() device = next((d for d in devices if d["device_id"] == device_id), None) if not device: raise HTTPException(404, f"Device {device_id} not found") return DeviceInfo( device_id=device["device_id"], type=device["type"], cap_version=device["cap_version"], room=get_room(device["device_id"]), friendly_name=device.get("metadata", {}).get("friendly_name", device["device_id"]), technology=device["technology"], features=device.get("features", {}), read_only="set" not in device.get("topics", {}), tags=device.get("tags") ) ``` --- ## 10. Zusammenfassung der Abweichungen ### ✅ Bereits konform (40%) - POST /devices/{id}/set - Vollständig implementiert - SSE /realtime - Grundfunktion vorhanden - GET /devices - Grundstruktur vorhanden ### ⚠️ Teilweise konform (40%) - GET /devices - Fehlen wichtige Felder (cap_version, room, friendly_name, technology, read_only) - SSE /realtime - Events ohne device_type, room, ts ### ❌ Nicht vorhanden (20%) - GET /devices/{device_id}/state - Komplett fehlend - GET /devices/{device_id} - Komplett fehlend --- ## 11. Risiko-Bewertung ### 🟢 **Geringes Risiko** - Alle Daten sind in devices.yaml/layout.yaml vorhanden - Resolver-Funktionen existieren bereits - Pydantic-Modelle sind etabliert - Keine Breaking Changes an bestehenden Endpoints nötig ### 🟡 **Mittleres Risiko** - SSE-Enrichment könnte Performance beeinflussen (load_devices() bei jedem Event) - **Mitigation:** Device-Lookup cachen - Timestamp-Handling muss konsistent sein - **Mitigation:** UTC + ISO8601 + "Z" Suffix ### 🔴 **Kein hohes Risiko identifiziert** --- ## 12. Nächste Schritte 1. **Freigabe einholen:** Sollen wir mit Phase 1 (GET /devices erweitern) starten? 2. **Testing-Strategie:** Sollen Tests für die neuen Endpoints geschrieben werden? 3. **Backward Compatibility:** GET /devices ändert Response-Struktur - ist das OK? (Vermutlich ja, da UI diese Felder ignorieren kann) 4. **Performance:** Device-Lookup-Cache implementieren vor SSE-Enrichment? --- **Ende der Analyse**