fix v1.7.2: mobile 'configuration error' on cold start — register card as Lovelace resource (awaited before render) instead of only add_extra_js_url (raced); idempotent version update + YAML-mode fallback; _loadFromServer retries (up to 8 hass ticks) so card doesn't stick on onboarding if WS not ready yet

This commit is contained in:
JB
2026-07-04 15:12:27 +03:00
parent e5e02faa6c
commit aa4e3aed1d
10 changed files with 111 additions and 42 deletions
+50 -3
View File
@@ -63,12 +63,59 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.http.register_static_path(PLANS_URL, str(plans_path), cache_headers=True) hass.http.register_static_path(PLANS_URL, str(plans_path), cache_headers=True)
hass.http.register_static_path(FILES_URL, str(files_path), cache_headers=True) hass.http.register_static_path(FILES_URL, str(files_path), cache_headers=True)
if card_path.exists(): if not card_path.exists():
add_extra_js_url(hass, f"{FRONTEND_URL}?v={VERSION}")
else:
_LOGGER.warning("houseplan-card.js не найден рядом с интеграцией: %s", card_path) _LOGGER.warning("houseplan-card.js не найден рядом с интеграцией: %s", card_path)
return True return True
# Подключаем карточку. Предпочтительно — как Lovelace-ресурс (его фронтенд ДОЖИДАЕТСЯ
# перед рендером дашбордов, поэтому карточка доступна даже на холодном старте мобильного
# приложения). Если реестр ресурсов недоступен (YAML-режим Lovelace, старые версии) —
# откатываемся на extra_module_url.
module_url = f"{FRONTEND_URL}?v={VERSION}"
if not await _register_lovelace_resource(hass, module_url):
add_extra_js_url(hass, module_url)
return True
async def _register_lovelace_resource(hass: HomeAssistant, module_url: str) -> bool:
"""Зарегистрировать (или обновить) карточку в реестре Lovelace-ресурсов.
Возвращает True при успехе. Пишем идемпотентно: если ресурс с нашим путём уже есть —
обновляем URL при смене версии; отсутствует — создаём. Any-except → False (фолбэк на JS).
"""
try:
lovelace = hass.data.get("lovelace")
resources = getattr(lovelace, "resources", None)
if resources is None and isinstance(lovelace, dict):
resources = lovelace.get("resources")
if resources is None:
return False
# реестр ресурсов должен быть загружен
if hasattr(resources, "loaded") and not resources.loaded:
await resources.async_load()
resources.loaded = True
elif hasattr(resources, "async_get_info"):
await resources.async_get_info()
# только storage-режим позволяет создавать элементы
if not hasattr(resources, "async_create_item"):
return False
base = FRONTEND_URL
existing = [
item for item in resources.async_items()
if str(item.get("url", "")).split("?", 1)[0] == base
]
if existing:
item = existing[0]
if item.get("url") != module_url and hasattr(resources, "async_update_item"):
await resources.async_update_item(item["id"], {"url": module_url})
return True
await resources.async_create_item({"res_type": "module", "url": module_url})
_LOGGER.debug("House Plan card зарегистрирована как Lovelace-ресурс: %s", module_url)
return True
except Exception as err: # noqa: BLE001 — любой сбой → фолбэк
_LOGGER.debug("Не удалось зарегистрировать Lovelace-ресурс (%s), фолбэк на extra_module_url", err)
return False
async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
hass.data[DOMAIN]["entry"] = entry hass.data[DOMAIN]["entry"] = entry
+1 -1
View File
@@ -10,7 +10,7 @@ PLANS_DIR = "houseplan/plans" # относительно каталога ко
FILES_URL = "/houseplan_files/files" FILES_URL = "/houseplan_files/files"
FILES_DIR = "houseplan/files" FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only" CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.7.0" VERSION = "1.7.2"
DEFAULT_CONFIG: dict = { DEFAULT_CONFIG: dict = {
"spaces": [], "spaces": [],
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -15,5 +15,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"issue_tracker": "https://github.com/justbusiness/houseplan-card/issues", "issue_tracker": "https://github.com/justbusiness/houseplan-card/issues",
"requirements": [], "requirements": [],
"version": "1.7.0" "version": "1.7.2"
} }
+11 -11
View File
File diff suppressed because one or more lines are too long
+5 -3
View File
@@ -28,9 +28,11 @@ houseplan-card/
## Ключевые решения ## Ключевые решения
1. **Один репозиторий — интеграция + карточка.** Интеграция сама раздаёт JS 1. **Один репозиторий — интеграция + карточка.** Интеграция раздаёт JS
(`hass.http.async_register_static_paths` + `frontend.add_extra_js_url`), пользователю не (`async_register_static_paths`) и регистрирует его как **Lovelace-ресурс** (module) —
нужно прописывать ресурс Lovelace. Паттерн как у browser_mod/xiaomi_vacuum_map. фронтенд дожидается ресурсов перед рендером, поэтому карточка работает и на холодном старте
мобильного приложения (в отличие от `add_extra_js_url`, который остаётся фолбэком для
YAML-режима). Пользователю не нужно вручную добавлять ресурс.
2. **Раскладка иконок — на сервере.** `helpers.storage.Store(1, "houseplan.layout")` 2. **Раскладка иконок — на сервере.** `helpers.storage.Store(1, "houseplan.layout")`
`.storage/houseplan.layout`. Карточка читает/пишет через `hass.callWS` `.storage/houseplan.layout`. Карточка читает/пишет через `hass.callWS`
(`houseplan/layout/get|set|update`). Fallback — localStorage (если интеграции нет). (`houseplan/layout/get|set|update`). Fallback — localStorage (если интеграции нет).
+12
View File
@@ -1,5 +1,17 @@
# Changelog # Changelog
## v1.7.2 — 2026-07-04 (фикс мобильного «ошибка конфигурации»)
- ПРИЧИНА: карточка подключалась только через `add_extra_js_url` (extra_module_url), загрузку
которого фронтенд НЕ дожидается — на холодном старте мобильного приложения дашборд рисовался
раньше регистрации элемента → «ошибка конфигурации». (Все рабочие HACS-карточки — Lovelace-ресурсы.)
- ФИКС 1: интеграция регистрирует карточку как **Lovelace-ресурс** (module) — их фронтенд ждёт
перед рендером. Идемпотентно (обновляет URL при смене версии, без дублей); фолбэк на
extra_module_url для YAML-режима Lovelace. `_register_lovelace_resource` в __init__.py.
- ФИКС 2: `_loadFromServer` ретраит (до 8 попыток по обновлениям hass) — если hass пришёл раньше
готовности WS, карточка не застревает на онбординге, а дожидается конфига.
- Проверено: ресурс houseplan-card.js зарегистрирован, extra_module_url пуст (единый чистый
модуль), карточка рендерит план на свежей вкладке без ошибки.
## v1.7.0 — 2026-07-04 (аудит + рефакторинг + тесты) ## v1.7.0 — 2026-07-04 (аудит + рефакторинг + тесты)
- Удалён вшитый в бандл дом-образец (дача, ~245 КБ base64 планов + ROOMS/FLOOR_*): бандл - Удалён вшитый в бандл дом-образец (дача, ~245 КБ base64 планов + ROOMS/FLOOR_*): бандл
293 КБ → 83 КБ. Свежая установка показывает онбординг «Добавить пространство», а не чужой дом. 293 КБ → 83 КБ. Свежая установка показывает онбординг «Добавить пространство», а не чужой дом.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "houseplan-card", "name": "houseplan-card",
"version": "1.7.0", "version": "1.7.2",
"description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)", "description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
+13 -5
View File
@@ -13,7 +13,7 @@ import {
} from './logic'; } from './logic';
import './editor'; import './editor';
const CARD_VERSION = '1.7.0'; const CARD_VERSION = '1.7.2';
const LS_KEY = 'houseplan_card_layout_v1'; const LS_KEY = 'houseplan_card_layout_v1';
const NORM_W = 1000; // ширина рендер-пространства для нормированных конфигов const NORM_W = 1000; // ширина рендер-пространства для нормированных конфигов
@@ -121,7 +121,9 @@ class HouseplanCard extends LitElement {
private _edit = false; private _edit = false;
private _layout: Record<string, { x: number; y: number; s?: string }> = {}; private _layout: Record<string, { x: number; y: number; s?: string }> = {};
private _serverStorage = false; private _serverStorage = false;
private _loaded = false; private _loadOk = false;
private _loading = false;
private _loadTries = 0;
private _serverCfg: ServerConfig | null = null; private _serverCfg: ServerConfig | null = null;
private _cfgRev = 0; private _cfgRev = 0;
private _unsubCfg: (() => void) | null = null; private _unsubCfg: (() => void) | null = null;
@@ -321,8 +323,7 @@ class HouseplanCard extends LitElement {
protected willUpdate(changed: PropertyValues): void { protected willUpdate(changed: PropertyValues): void {
if (changed.has('hass') && this.hass) { if (changed.has('hass') && this.hass) {
if (!this._loaded) { if (!this._loadOk && !this._loading && this._loadTries < 8) {
this._loaded = true;
this._loadFromServer(); this._loadFromServer();
} }
this._maybeRebuildDevices(); this._maybeRebuildDevices();
@@ -332,11 +333,14 @@ class HouseplanCard extends LitElement {
// ================= сервер: конфиг + раскладка ================= // ================= сервер: конфиг + раскладка =================
private async _loadFromServer(): Promise<void> { private async _loadFromServer(): Promise<void> {
this._loading = true;
this._loadTries++;
try { try {
const [cfgResp, layResp] = await Promise.all([ const [cfgResp, layResp] = await Promise.all([
this.hass.callWS({ type: 'houseplan/config/get' }), this.hass.callWS({ type: 'houseplan/config/get' }),
this.hass.callWS({ type: 'houseplan/layout/get' }), this.hass.callWS({ type: 'houseplan/layout/get' }),
]); ]);
this._loadOk = true;
this._serverStorage = true; this._serverStorage = true;
const cfg = cfgResp?.config; const cfg = cfgResp?.config;
this._serverCfg = cfg && Array.isArray(cfg.spaces) ? cfg : null; this._serverCfg = cfg && Array.isArray(cfg.spaces) ? cfg : null;
@@ -352,6 +356,8 @@ class HouseplanCard extends LitElement {
this._space = this._model[0]?.id || this._space; this._space = this._model[0]?.id || this._space;
} }
} catch (e) { } catch (e) {
// не последняя попытка — молча ждём следующего обновления hass (прогрев WS)
if (this._loadTries >= 8) {
this._serverStorage = false; this._serverStorage = false;
this._serverCfg = null; this._serverCfg = null;
try { try {
@@ -359,7 +365,9 @@ class HouseplanCard extends LitElement {
} catch { } catch {
this._layout = {}; this._layout = {};
} }
this._showToast('Интеграция houseplan не найдена — позиции сохраняются локально'); }
} finally {
this._loading = false;
} }
this._regSignature = ''; this._regSignature = '';
this.requestUpdate(); this.requestUpdate();