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,13 +63,60 @@ 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(FILES_URL, str(files_path), cache_headers=True)
if card_path.exists():
add_extra_js_url(hass, f"{FRONTEND_URL}?v={VERSION}")
else:
if not card_path.exists():
_LOGGER.warning("houseplan-card.js не найден рядом с интеграцией: %s", card_path)
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:
hass.data[DOMAIN]["entry"] = entry
+1 -1
View File
@@ -10,7 +10,7 @@ PLANS_DIR = "houseplan/plans" # относительно каталога ко
FILES_URL = "/houseplan_files/files"
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.7.0"
VERSION = "1.7.2"
DEFAULT_CONFIG: dict = {
"spaces": [],
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -15,5 +15,5 @@
"iot_class": "local_push",
"issue_tracker": "https://github.com/justbusiness/houseplan-card/issues",
"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
(`hass.http.async_register_static_paths` + `frontend.add_extra_js_url`), пользователю не
нужно прописывать ресурс Lovelace. Паттерн как у browser_mod/xiaomi_vacuum_map.
1. **Один репозиторий — интеграция + карточка.** Интеграция раздаёт JS
(`async_register_static_paths`) и регистрирует его как **Lovelace-ресурс** (module) —
фронтенд дожидается ресурсов перед рендером, поэтому карточка работает и на холодном старте
мобильного приложения (в отличие от `add_extra_js_url`, который остаётся фолбэком для
YAML-режима). Пользователю не нужно вручную добавлять ресурс.
2. **Раскладка иконок — на сервере.** `helpers.storage.Store(1, "houseplan.layout")`
`.storage/houseplan.layout`. Карточка читает/пишет через `hass.callWS`
(`houseplan/layout/get|set|update`). Fallback — localStorage (если интеграции нет).
+12
View File
@@ -1,5 +1,17 @@
# 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 (аудит + рефакторинг + тесты)
- Удалён вшитый в бандл дом-образец (дача, ~245 КБ base64 планов + ROOMS/FLOOR_*): бандл
293 КБ → 83 КБ. Свежая установка показывает онбординг «Добавить пространство», а не чужой дом.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.7.0",
"version": "1.7.2",
"description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)",
"license": "MIT",
"type": "module",
+19 -11
View File
@@ -13,7 +13,7 @@ import {
} from './logic';
import './editor';
const CARD_VERSION = '1.7.0';
const CARD_VERSION = '1.7.2';
const LS_KEY = 'houseplan_card_layout_v1';
const NORM_W = 1000; // ширина рендер-пространства для нормированных конфигов
@@ -121,7 +121,9 @@ class HouseplanCard extends LitElement {
private _edit = false;
private _layout: Record<string, { x: number; y: number; s?: string }> = {};
private _serverStorage = false;
private _loaded = false;
private _loadOk = false;
private _loading = false;
private _loadTries = 0;
private _serverCfg: ServerConfig | null = null;
private _cfgRev = 0;
private _unsubCfg: (() => void) | null = null;
@@ -321,8 +323,7 @@ class HouseplanCard extends LitElement {
protected willUpdate(changed: PropertyValues): void {
if (changed.has('hass') && this.hass) {
if (!this._loaded) {
this._loaded = true;
if (!this._loadOk && !this._loading && this._loadTries < 8) {
this._loadFromServer();
}
this._maybeRebuildDevices();
@@ -332,11 +333,14 @@ class HouseplanCard extends LitElement {
// ================= сервер: конфиг + раскладка =================
private async _loadFromServer(): Promise<void> {
this._loading = true;
this._loadTries++;
try {
const [cfgResp, layResp] = await Promise.all([
this.hass.callWS({ type: 'houseplan/config/get' }),
this.hass.callWS({ type: 'houseplan/layout/get' }),
]);
this._loadOk = true;
this._serverStorage = true;
const cfg = cfgResp?.config;
this._serverCfg = cfg && Array.isArray(cfg.spaces) ? cfg : null;
@@ -352,14 +356,18 @@ class HouseplanCard extends LitElement {
this._space = this._model[0]?.id || this._space;
}
} catch (e) {
this._serverStorage = false;
this._serverCfg = null;
try {
this._layout = JSON.parse(localStorage.getItem(LS_KEY) || '{}') || {};
} catch {
this._layout = {};
// не последняя попытка — молча ждём следующего обновления hass (прогрев WS)
if (this._loadTries >= 8) {
this._serverStorage = false;
this._serverCfg = null;
try {
this._layout = JSON.parse(localStorage.getItem(LS_KEY) || '{}') || {};
} catch {
this._layout = {};
}
}
this._showToast('Интеграция houseplan не найдена — позиции сохраняются локально');
} finally {
this._loading = false;
}
this._regSignature = '';
this.requestUpdate();