{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:1.414),e.onerror=()=>t(1.414),e.src=r});URL.revokeObjectURL(r),this._spaceDialog={...this._spaceDialog,planFile:{ext:s,b64:o,aspect:l,name:i.name}}}async _saveSpaceDialog(){const t=this._spaceDialog;if(t&&!t.busy&&t.title.trim()){this._spaceDialog={...t,busy:!0};try{const e=this._serverCfg;let i;if("create"===t.mode?(i={id:"s"+Date.now().toString(36),title:t.title.trim(),plan_url:null,aspect:1.414,view_box:[0,0,1,1],rooms:[],segments:[]},e.spaces.push(i)):(i=e.spaces.find(e=>e.id===t.spaceId),i.title=t.title.trim()),t.planFile){const e=await this.hass.callWS({type:"houseplan/plan/set",space_id:i.id,ext:t.planFile.ext,data:t.planFile.b64});i.plan_url=e.url,i.aspect=t.planFile.aspect}await this._saveConfigNow(),this._spaceDialog=null,"create"===t.mode&&(this._space=i.id),this._regSignature="",this._maybeRebuildDevices(),this._showToast("create"===t.mode?"Пространство добавлено":"Пространство сохранено")}catch(t){this._spaceDialog={...this._spaceDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||t))}}}async _deleteSpace(){const t=this._spaceDialog;if(!t||"edit"!==t.mode)return;const e=this._serverCfg.spaces.find(e=>e.id===t.spaceId);if(confirm(`Удалить пространство «${e.title}» со всеми комнатами и разметкой?`)){this._serverCfg.spaces=this._serverCfg.spaces.filter(e=>e.id!==t.spaceId);try{await this._saveConfigNow(),this._spaceDialog=null,this._space===t.spaceId&&(this._space=this._serverCfg.spaces[0]?.id||""),this._regSignature="",this._maybeRebuildDevices(),this._showToast("Пространство удалено")}catch(t){this._showToast("Ошибка удаления: "+(t?.message||t))}}}async _saveConfigNow(){const t=await this.hass.callWS({type:"houseplan/config/set",config:this._serverCfg,expected_rev:this._cfgRev});this._cfgRev=t?.rev??this._cfgRev+1}render(){if(!this._config||!this.hass)return B;const t=this._model;if(!t.length)return F`
${this._config.title||"План дома"}
@@ -65,12 +65,12 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
@@ -89,19 +89,19 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
`:B}
${this._toast?F`${this._toast}
`:B}
- `}_renderDevice(t,e){const i=this._pos(t),s=(i.x-e[0])/e[2]*100,a=(i.y-e[1])/e[3]*100,n=this._stateClass(t),r=this._liveTemp(t),o=this._config?.show_signal&&!t.virtual?this._lqiFor(t.entities):null;return F`this._clickDevice(e,t)}
- @mousemove=${e=>this._showTip(e,t.name,t.model+(null!=r?" · "+r+"°":"")+(null!=o?" · LQI "+o:""))}
+ @mousemove=${e=>this._showTip(e,t.name,t.model+(null!=o?" · "+o+"°":"")+(null!=r?" · LQI "+r:""))}
@mouseleave=${()=>this._tip=null}
@pointerdown=${e=>this._pointerDown(e,t)}
@pointermove=${e=>this._pointerMove(e,t)}
@pointerup=${e=>this._pointerUp(e,t)}
>
- ${null!=r?F`${r}°`:B}
- ${null!=o?F`${o}`:B}
+ ${null!=o?F`${o}°`:B}
+ ${null!=r?F`${r}`:B}
`}_roomCenter(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}_renderMarkupDefs(t){const e=this._gridPitch,i=.14*e;return L`
@@ -986,4 +986,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
z-index: 120;
max-width: 90vw;
}
- `,customElements.get("houseplan-card")||customElements.define("houseplan-card",xt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Интерактивный план дома: пространства, комнаты, устройства с живыми состояниями и drag-раскладкой."}),console.info("%c HOUSEPLAN-CARD %c v1.7.0 ","background:#3ea6ff;color:#04121f;font-weight:700","");
+ `,customElements.get("houseplan-card")||customElements.define("houseplan-card",xt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Интерактивный план дома: пространства, комнаты, устройства с живыми состояниями и drag-раскладкой."}),console.info("%c HOUSEPLAN-CARD %c v1.7.2 ","background:#3ea6ff;color:#04121f;font-weight:700","");
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 6d3ed36..3d2ad6f 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -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 (если интеграции нет).
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index fc3b89b..1e76b92 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -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 КБ. Свежая установка показывает онбординг «Добавить пространство», а не чужой дом.
diff --git a/package.json b/package.json
index 4b0764c..a7546fd 100755
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts
index 715949d..14bc7f5 100755
--- a/src/houseplan-card.ts
+++ b/src/houseplan-card.ts
@@ -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 = {};
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 {
+ 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();