From f685f73512dc121c226b0b221996938c0dde8f67 Mon Sep 17 00:00:00 2001 From: Stream Date: Wed, 15 Apr 2026 14:40:01 +0300 Subject: [PATCH] auto-sync: 2026-04-15 14:40:01 --- .../appdaemon/availability.py | 140 ++++++++++-------- 1 file changed, 79 insertions(+), 61 deletions(-) diff --git a/tasks/ha-availability-dashboard/appdaemon/availability.py b/tasks/ha-availability-dashboard/appdaemon/availability.py index dcf04a7..0be0d95 100644 --- a/tasks/ha-availability-dashboard/appdaemon/availability.py +++ b/tasks/ha-availability-dashboard/appdaemon/availability.py @@ -102,7 +102,6 @@ class Availability(hass.Hass): """Fetch area registry via WebSocket and build entity_id -> area mapping. HA registry APIs are only available via WebSocket, not REST. - We use a synchronous websocket-client to fetch the data. """ try: import websocket @@ -112,83 +111,102 @@ class Availability(hass.Hass): self._areas_cache = {} return {} - token = self._get_token() + # Use HA long-lived access token for WebSocket auth + token = self.args.get("ha_token", "") + if not token: + token = self._get_token() if not token: self._areas_cache = {} return {} - # Connect to HA WebSocket - # From addon, use homeassistant:8123 (not supervisor proxy) ws_url = "ws://homeassistant:8123/api/websocket" try: - ws = websocket.create_connection(ws_url, timeout=10, - header=["Authorization: Bearer " + token]) + ws = websocket.create_connection(ws_url, timeout=15) except Exception as e: self.log(f"WebSocket connect failed: {e}", level="WARNING") self._areas_cache = {} return {} - def _ws_call(msg_type): - """Send a WebSocket command and return the result.""" - # Auth - result = _json.loads(ws.recv()) - if result.get("type") == "auth_required": + try: + # Step 1: Auth handshake + auth_msg = _json.loads(ws.recv()) + if auth_msg.get("type") == "auth_required": ws.send(_json.dumps({"type": "auth", "access_token": token})) - result = _json.loads(ws.recv()) - if result.get("type") != "auth_ok": - return None - # Send command - ws.send(_json.dumps({"id": msg_type.__hash__() % 10000, "type": msg_type})) - result = _json.loads(ws.recv()) - return result.get("result") if result.get("success") else None + auth_result = _json.loads(ws.recv()) + if auth_result.get("type") != "auth_ok": + self.log(f"WS auth failed: {auth_result.get('message','')}", level="ERROR") + ws.close() + self._areas_cache = {} + return {} + self.log("WS auth OK") + + # Step 2: Fetch area registry + ws.send(_json.dumps({"id": 1, "type": "config/area_registry/list"})) + areas_result = _json.loads(ws.recv()) + if not areas_result.get("success"): + self.log(f"Area registry failed: {areas_result.get('error','?')}", level="WARNING") + ws.close() + self._areas_cache = {} + return {} + areas_data = areas_result.get("result", []) + + # Build area_id -> area_name map + area_map = {} + if isinstance(areas_data, list): + for area in areas_data: + area_map[area["area_id"]] = area.get("name", "Без комнаты") + self.log(f"WS: {len(area_map)} areas fetched") + + # Step 3: Fetch device registry + ws.send(_json.dumps({"id": 2, "type": "config/device_registry/list"})) + dev_result = _json.loads(ws.recv()) + dev_area = {} + if dev_result.get("success"): + dev_data = dev_result.get("result", []) + if isinstance(dev_data, list): + for dev in dev_data: + dev_id = dev.get("id") + a_id = dev.get("area_id") + if dev_id and a_id and a_id in area_map: + dev_area[dev_id] = area_map[a_id] + self.log(f"WS: {len(dev_area)} device-area mappings") + + # Step 4: Fetch entity registry + ws.send(_json.dumps({"id": 3, "type": "config/entity_registry/list"})) + ent_result = _json.loads(ws.recv()) + eid_to_area = {} + if ent_result.get("success"): + ent_data = ent_result.get("result", []) + # HA returns either a list directly or a dict with 'entities' key + if isinstance(ent_data, dict): + ent_list = ent_data.get("entities", []) + else: + ent_list = ent_data + for entry in ent_list: + eid = entry.get("entity_id", "") + a_id = entry.get("area_id") + dev_id = entry.get("device_id") + if a_id and a_id in area_map: + eid_to_area[eid] = area_map[a_id] + elif dev_id and dev_id in dev_area: + eid_to_area[eid] = dev_area[dev_id] + else: + eid_to_area[eid] = "Без комнаты" + self.log(f"WS: {len(eid_to_area)} entity-area mappings") - # Fetch area registry - areas_data = _ws_call("config/area_registry/list") - if not areas_data: - self.log("Failed to fetch area registry via WebSocket", level="WARNING") ws.close() + self._areas_cache = eid_to_area + return eid_to_area + + except Exception as e: + self.log(f"WebSocket area fetch error: {e}", level="WARNING") + try: + ws.close() + except: + pass self._areas_cache = {} return {} - # Build area_id -> area_name map - area_map = {} - for area in areas_data: - area_map[area["area_id"]] = area.get("name", "Без комнаты") - - # Fetch device registry - dev_data = _ws_call("config/device_registry/list") - dev_area = {} - if dev_data and isinstance(dev_data, dict): - dev_data = dev_data.get("devices", dev_data) if isinstance(dev_data, dict) else dev_data - if dev_data and isinstance(dev_data, list): - for dev in dev_data: - dev_id = dev.get("id") - a_id = dev.get("area_id") - if dev_id and a_id and a_id in area_map: - dev_area[dev_id] = area_map[a_id] - - # Fetch entity registry - ent_data = _ws_call("config/entity_registry/list") - eid_to_area = {} - if ent_data and isinstance(ent_data, dict): - ent_data = ent_data.get("entities", ent_data) - if ent_data and isinstance(ent_data, list): - for entry in ent_data: - eid = entry.get("entity_id", "") - a_id = entry.get("area_id") - dev_id = entry.get("device_id") - if a_id and a_id in area_map: - eid_to_area[eid] = area_map[a_id] - elif dev_id and dev_id in dev_area: - eid_to_area[eid] = dev_area[dev_id] - else: - eid_to_area[eid] = "Без комнаты" - - ws.close() - self._areas_cache = eid_to_area - self.log(f"Fetched {len(area_map)} areas, {len(dev_area)} device-areas, {len(eid_to_area)} entity-areas") - return eid_to_area - # ── History Fetch ── def _fetch_history(self, entity_ids: list, period_start: datetime, period_end: datetime):