Fix WS API: read lovelace config from .storage file directly
CI / Identify a merged version PR (push) Canceled after 0s
CI / Rust checks, rendering, and native package (push) Canceled after 0s
CI / Publish release and Homebrew feed (push) Canceled after 0s
Release PR / Maintain Version Packages PR (push) Canceled after 0s

HA 2026.10 stores dashboard configs as .storage/lovelace.{url_path}
with hyphens replaced by underscores. Direct file read is more reliable
than internal hass.data access which varies across versions.

Also added error logging to async_setup_entry for WS registration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kaloyan DanchevandClaude Opus 4.6 committed 2026-10-25 13:37:40 +02:00
1 parent d681f20884
commit 483b19f286
2 files changed
+35 -16

No files matched your search

@@ -1,10 +1,12 @@
"""Luma Home — config bridge between HA dashboards and Luma TV app.""" """Luma Home — config bridge between HA dashboards and Luma TV app."""
import logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN from .const import DOMAIN
from . import websocket_api
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup(hass: HomeAssistant, config: dict) -> bool:
@@ -19,7 +21,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
"dashboard_url_path": entry.data.get("dashboard_url_path", "luma-tv"), "dashboard_url_path": entry.data.get("dashboard_url_path", "luma-tv"),
} }
try:
from . import websocket_api
websocket_api.async_register_commands(hass) websocket_api.async_register_commands(hass)
_LOGGER.info("Luma Home WS commands registered successfully")
except Exception:
_LOGGER.exception("Failed to register Luma Home WS commands")
return True return True
@@ -9,15 +9,12 @@ from __future__ import annotations
import json import json
import logging import logging
import os
import secrets
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, DEFAULT_DASHBOARD_URL_PATH, PAIRING_DIR from .const import DOMAIN, DEFAULT_DASHBOARD_URL_PATH, PAIRING_DIR
@@ -195,17 +192,10 @@ async def handle_get_config(
"""Return the Luma v2 config derived from a Lovelace dashboard.""" """Return the Luma v2 config derived from a Lovelace dashboard."""
url_path = msg.get("dashboard_url_path") or _get_dashboard_path(hass) url_path = msg.get("dashboard_url_path") or _get_dashboard_path(hass)
try: config = await _load_lovelace_config(hass, url_path)
config = await hass.data["lovelace"]["dashboards"][url_path].async_load(False)
except KeyError: if config is None:
# Try the default dashboard connection.send_error(msg["id"], "not_found", f"Dashboard '{url_path}' not found")
try:
config = await hass.data["lovelace"]["dashboards"][None].async_load(False)
_LOGGER.warning(
"Dashboard '%s' not found, falling back to default", url_path
)
except (KeyError, Exception) as err:
connection.send_error(msg["id"], "not_found", f"Dashboard not found: {err}")
return return
views = config.get("views", []) views = config.get("views", [])
@@ -219,6 +209,28 @@ async def handle_get_config(
connection.send_result(msg["id"], result) connection.send_result(msg["id"], result)
async def _load_lovelace_config(hass: HomeAssistant, url_path: str) -> dict | None:
"""Load a Lovelace dashboard config by reading the .storage file."""
storage_key = url_path.replace("-", "_")
storage_path = Path(hass.config.path(f".storage/lovelace.{storage_key}"))
_LOGGER.debug("Looking for dashboard at %s", storage_path)
try:
if not await hass.async_add_executor_job(storage_path.exists):
_LOGGER.warning("Dashboard storage file not found: %s", storage_path)
return None
raw = await hass.async_add_executor_job(storage_path.read_text, "utf-8")
stored = json.loads(raw)
config = stored.get("data", {}).get("config")
if config is None:
config = stored.get("data", {})
_LOGGER.debug("Loaded dashboard with %d views", len(config.get("views", [])))
return config
except Exception as err:
_LOGGER.error("Failed to load dashboard '%s': %s", url_path, err)
return None
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
vol.Required("type"): "luma/pair", vol.Required("type"): "luma/pair",