auto-sync: 2026-04-15 14:30:01
This commit is contained in:
@@ -21,6 +21,14 @@ HA_URL = "http://supervisor/core/api"
|
||||
|
||||
class Availability(hass.Hass):
|
||||
def initialize(self):
|
||||
# Ensure websocket-client is available for registry queries
|
||||
try:
|
||||
import websocket
|
||||
except ImportError:
|
||||
import subprocess
|
||||
subprocess.check_call(["pip", "install", "websocket-client", "-q"])
|
||||
self.log("Installed websocket-client")
|
||||
|
||||
self._token = None
|
||||
self._entities_cache = []
|
||||
self._areas_cache = {}
|
||||
@@ -91,63 +99,94 @@ class Availability(hass.Hass):
|
||||
return entities
|
||||
|
||||
def _fetch_areas(self):
|
||||
"""Fetch area registry and build entity_id -> area mapping."""
|
||||
data = self._api_get("/config/area_registry")
|
||||
if not data:
|
||||
"""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
|
||||
import json as _json
|
||||
except ImportError:
|
||||
self.log("websocket-client not available, areas will be unavailable", level="WARNING")
|
||||
self._areas_cache = {}
|
||||
return {}
|
||||
|
||||
# Build device_id -> area map
|
||||
device_areas = {}
|
||||
for area in data:
|
||||
area_id = area.get("area_id")
|
||||
area_name = area.get("name", "Без комнаты")
|
||||
device_areas[area_id] = area_name
|
||||
|
||||
# Now get entity registry to map entity -> device -> area
|
||||
reg = self._api_get("/config/entity_registry")
|
||||
if not reg:
|
||||
token = self._get_token()
|
||||
if not token:
|
||||
self._areas_cache = {}
|
||||
return {}
|
||||
|
||||
eid_to_area = {}
|
||||
for entry in reg:
|
||||
eid = entry.get("entity_id", "")
|
||||
device_id = entry.get("device_id")
|
||||
area_id = entry.get("area_id")
|
||||
# Use entity-level area if set, otherwise device-level
|
||||
if area_id and area_id in device_areas:
|
||||
eid_to_area[eid] = device_areas[area_id]
|
||||
elif device_id:
|
||||
# Try to find device's area via device registry
|
||||
dev_data = self._api_get(f"/config/device_registry")
|
||||
# This is expensive; fallback to simple mapping
|
||||
pass
|
||||
else:
|
||||
eid_to_area[eid] = "Без комнаты"
|
||||
# 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])
|
||||
except Exception as e:
|
||||
self.log(f"WebSocket connect failed: {e}", level="WARNING")
|
||||
self._areas_cache = {}
|
||||
return {}
|
||||
|
||||
# Fill from device registry if available
|
||||
dev_reg = self._api_get("/config/device_registry")
|
||||
if dev_reg:
|
||||
dev_area_map = {}
|
||||
for dev in dev_reg:
|
||||
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":
|
||||
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
|
||||
|
||||
# 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 = {}
|
||||
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 device_areas:
|
||||
dev_area_map[dev_id] = device_areas[a_id]
|
||||
if dev_id and a_id and a_id in area_map:
|
||||
dev_area[dev_id] = area_map[a_id]
|
||||
|
||||
for entry in reg:
|
||||
# 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", "")
|
||||
device_id = entry.get("device_id")
|
||||
area_id = entry.get("area_id")
|
||||
if area_id and area_id in device_areas:
|
||||
eid_to_area[eid] = device_areas[area_id]
|
||||
elif device_id and device_id in dev_area_map:
|
||||
eid_to_area[eid] = dev_area_map[device_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] = eid_to_area.get(eid, "Без комнаты")
|
||||
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 ──
|
||||
@@ -176,7 +215,7 @@ class Availability(hass.Hass):
|
||||
self._set_progress(progress)
|
||||
|
||||
if i < total_chunks - 1:
|
||||
self.sleep(BATCH_DELAY)
|
||||
import time; time.sleep(BATCH_DELAY)
|
||||
|
||||
self._set_progress("idle")
|
||||
return all_history
|
||||
|
||||
@@ -58,10 +58,23 @@ def sanitize_entity_id(entity_id: str) -> str:
|
||||
return entity_id.replace(".", "_")
|
||||
|
||||
|
||||
# Cyrillic transliteration table
|
||||
_CYRILLIC_MAP = {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
|
||||
'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
|
||||
'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
|
||||
'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch',
|
||||
'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
|
||||
}
|
||||
|
||||
|
||||
def sanitize_area_name(name: str) -> str:
|
||||
"""Convert area name to safe sensor suffix."""
|
||||
"""Convert area name to safe sensor suffix (Latin only)."""
|
||||
s = name.lower().strip()
|
||||
s = re.sub(r"[^a-zа-яё0-9_]", "_", s)
|
||||
# Transliterate Cyrillic to Latin
|
||||
s = ''.join(_CYRILLIC_MAP.get(ch, ch) for ch in s)
|
||||
# Keep only a-z, 0-9, _; replace everything else
|
||||
s = re.sub(r"[^a-z0-9_]", "_", s)
|
||||
s = re.sub(r"_+", "_", s)
|
||||
return s.strip("_")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user