feat: add the House Plan sidebar panel

Issue: #486
User-Visible: yes
This commit is contained in:
Matysh
2026-09-08 00:31:25 +03:00
parent bf4c1bb674
commit 8a97d4e30d
104 changed files with 3643 additions and 564 deletions
+16 -3
View File
@@ -22,6 +22,10 @@ from .frontend_registration import (
async_setup_frontend_registration,
)
from .geometry_migration import migrate_config, migrate_layout, pending_from_config
from .panel_registration import (
async_setup_panel_registration,
remove_panel_registration,
)
from .plans import collect_attachments, collect_plans, sweep_upload_temps
from .repairs import async_check_plan_files
from .store import (
@@ -267,19 +271,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) ->
entry.async_on_unload(
async_track_time_interval(hass, _sweep, timedelta(hours=24))
)
# The application panel is intentionally the final setup step. It is a
# discoverability surface over the already-working stores and APIs, so a
# missing/colliding/broken panel remains fail-soft and cannot strand a
# sidebar entry after a migration, repair or initial housekeeping failure.
panel_path = Path(__file__).parent / "frontend" / "houseplan-panel.js"
await async_setup_panel_registration(hass, entry, panel_path)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) -> bool:
rec = hass.data.get(DOMAIN, {}).pop("trail_recorder", None)
if rec:
rec.teardown()
"""Unload the entry.
WS commands and the HTTP view are global (async_setup) and stay registered —
their handlers resolve runtime data per call and answer `not_ready` while no
entry is loaded. Static paths cannot be unregistered by design.
"""
remove_panel_registration(hass)
rec = hass.data.get(DOMAIN, {}).pop("trail_recorder", None)
if rec:
rec.teardown()
return True
@@ -287,4 +299,5 @@ async def async_remove_entry(
hass: HomeAssistant, entry: HouseplanConfigEntry
) -> None:
"""Clean up frontend registration and notice on integration removal."""
remove_panel_registration(hass)
await async_remove_frontend_registration(hass, entry)
+6 -1
View File
@@ -14,7 +14,12 @@ class HouseplanConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None):
if user_input is not None:
return self.async_create_entry(title="House Plan", data={}, options=user_input)
return self.async_create_entry(
title="House Plan",
data={},
options=user_input,
description="panel_ready",
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Optional(CONF_ADMIN_ONLY, default=True): bool}),
+6
View File
@@ -8,6 +8,12 @@ STORAGE_VERSION = 1
STORAGE_MINOR_VERSION = 2
FRONTEND_URL = "/houseplan_files/houseplan-card.js"
FRONTEND_ASSETS_URL = "/houseplan_files/houseplan-assets"
PANEL_FRONTEND_URL = "/houseplan_files/houseplan-panel.js"
PANEL_URL_PATH = "houseplan"
PANEL_URL = f"/{PANEL_URL_PATH}"
PANEL_COMPONENT_NAME = "houseplan-panel"
PANEL_TITLE = "House Plan"
PANEL_ICON = "mdi:floor-plan"
PLANS_URL = "/houseplan_files/plans"
PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
FILES_URL = "/houseplan_files/files"
@@ -1,251 +1,273 @@
{
"schema": 1,
"fingerprint": "b0d09c9516d591f6cbedc31941c27ca6d60536efbfe6d6a581d652f892af314c",
"fingerprint": "3555cb7791d3391b320376d4bcba14d217a5718d018ca955eb741df1762804b5",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DnOXDKzw.js",
"houseplan-assets/houseplan-card-ZsbfBSl1.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 289578,
"initialViewGzipBytes": 289934,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-ZsbfBSl1.js",
"houseplan-card.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 291864,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 1930,
"lazyFiles": [
"houseplan-assets/backdrop-pick-B35mi_yP.js",
"houseplan-assets/de-BtNP3xgK.js",
"houseplan-assets/editor-CbKgPNqk.js",
"houseplan-assets/fr-f560b583.js",
"houseplan-assets/furniture-plan-art.generated-CP7GBt2L.js",
"houseplan-assets/guard-B_l7xd7s.js",
"houseplan-assets/houseplan-editor-runtime-rdfalUon.js",
"houseplan-assets/houseplan-onboarding-runtime-Bvx5Dld7.js",
"houseplan-assets/hp-zigbee-topology-overlay-XlIYV874.js",
"houseplan-assets/iso-scene-render-CMZR5dLh.js",
"houseplan-assets/live-interaction-runtime-BaHUiHhO.js",
"houseplan-assets/pdf-export-Dgp0UwWM.js",
"houseplan-assets/zigbee-topology-DESIftME.js",
"houseplan-assets/zigbee-topology-runtime-D0VNHB73.js"
"houseplan-assets/backdrop-pick-B8uJVpYl.js",
"houseplan-assets/de-pA3tnOax.js",
"houseplan-assets/editor-CJSkbmJl.js",
"houseplan-assets/fr-BjBWP2nl.js",
"houseplan-assets/furniture-plan-art.generated-CUlu3GpV.js",
"houseplan-assets/guard-Dk7ygy6z.js",
"houseplan-assets/houseplan-editor-runtime-B3O-Y3rk.js",
"houseplan-assets/houseplan-onboarding-runtime-lwyMxFqY.js",
"houseplan-assets/hp-zigbee-topology-overlay-f-40j--2.js",
"houseplan-assets/iso-scene-render-C5A4O41t.js",
"houseplan-assets/live-interaction-runtime-iZ4G2R9J.js",
"houseplan-assets/pdf-export-Cg8R9f0g.js",
"houseplan-assets/zigbee-topology-DjTDqsI9.js",
"houseplan-assets/zigbee-topology-runtime-CEUb94-9.js"
],
"lazyGzipBytes": 383149,
"lazyGzipBytes": 383241,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-B35mi_yP.js",
"houseplan-assets/editor-CbKgPNqk.js",
"houseplan-assets/furniture-plan-art.generated-CP7GBt2L.js",
"houseplan-assets/guard-B_l7xd7s.js",
"houseplan-assets/houseplan-editor-runtime-rdfalUon.js",
"houseplan-assets/zigbee-topology-DESIftME.js"
"houseplan-assets/backdrop-pick-B8uJVpYl.js",
"houseplan-assets/editor-CJSkbmJl.js",
"houseplan-assets/furniture-plan-art.generated-CUlu3GpV.js",
"houseplan-assets/guard-Dk7ygy6z.js",
"houseplan-assets/houseplan-editor-runtime-B3O-Y3rk.js",
"houseplan-assets/zigbee-topology-DjTDqsI9.js"
],
"lazyEditorGzipBytes": 195228,
"lazyEditorGzipBytes": 195231,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-B35mi_yP.js",
"houseplan-assets/houseplan-onboarding-runtime-Bvx5Dld7.js"
"houseplan-assets/backdrop-pick-B8uJVpYl.js",
"houseplan-assets/houseplan-onboarding-runtime-lwyMxFqY.js"
],
"lazyOnboardingGzipBytes": 14326,
"lazyOnboardingGzipBytes": 14330,
"lazyLocaleFiles": [
"houseplan-assets/de-BtNP3xgK.js",
"houseplan-assets/fr-f560b583.js"
"houseplan-assets/de-pA3tnOax.js",
"houseplan-assets/fr-BjBWP2nl.js"
],
"lazyLocaleGzipBytes": 50165,
"lazyLocaleGzipBytes": 50249,
"lazyIsometricFiles": [
"houseplan-assets/guard-B_l7xd7s.js",
"houseplan-assets/iso-scene-render-CMZR5dLh.js"
"houseplan-assets/guard-Dk7ygy6z.js",
"houseplan-assets/iso-scene-render-C5A4O41t.js"
],
"lazyIsometricGzipBytes": 13455,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-CP7GBt2L.js"
"houseplan-assets/furniture-plan-art.generated-CUlu3GpV.js"
],
"lazyFurnitureArtGzipBytes": 10254,
"lazyFurnitureArtGzipBytes": 10250,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-CP7GBt2L.js",
"houseplan-assets/pdf-export-Dgp0UwWM.js"
"houseplan-assets/furniture-plan-art.generated-CUlu3GpV.js",
"houseplan-assets/pdf-export-Cg8R9f0g.js"
],
"lazyPdfGzipBytes": 120334,
"lazyPdfGzipBytes": 120330,
"files": [
{
"path": "houseplan-assets/backdrop-pick-B35mi_yP.js",
"sha256": "f37ed3523b1539ba22edc28e78aad8d583d4016ed24f6fb33cf2db8af8df244d",
"path": "houseplan-assets/backdrop-pick-B8uJVpYl.js",
"sha256": "2068520cc9eff1a9d016ff3d466d1fcc5bb92a81d738b81eebc2787552b9bfea",
"rawBytes": 21592,
"gzipBytes": 7358,
"gzipBytes": 7360,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-BtNP3xgK.js",
"sha256": "f09bb93057b3dc41fc2cfe63857f8908520654f33048e132eae760f9ee21e4ad",
"rawBytes": 84140,
"gzipBytes": 25332,
"path": "houseplan-assets/de-pA3tnOax.js",
"sha256": "24f7756f5e74d37fb84570bdafe56c7038e4429a5e873efca18cefa098ac7f07",
"rawBytes": 84250,
"gzipBytes": 25378,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CbKgPNqk.js",
"sha256": "054b29de0a57ba66916130b081d0681daae8698bfe7252d4911652eacbd746a8",
"path": "houseplan-assets/editor-CJSkbmJl.js",
"sha256": "d075ee42e36c88cd90da708c6f8f8870e18749b4e39f0409a0151d17a0e47308",
"rawBytes": 3837,
"gzipBytes": 1589,
"gzipBytes": 1590,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-f560b583.js",
"sha256": "1372b351794b3b7ca41eaec35ce55cce6fe0b466fae6d59526ae1b272004b97a",
"rawBytes": 86167,
"gzipBytes": 24833,
"path": "houseplan-assets/fr-BjBWP2nl.js",
"sha256": "800d1cc7dc98f729dff9a8683dc7b5d4bdf890f901a0e1a953b047751e150694",
"rawBytes": 86269,
"gzipBytes": 24871,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-CP7GBt2L.js",
"sha256": "7fbcc130b0f270b677dbd89e6d7c7f1c337ebb553d82a204e9aeda2345da2c4b",
"path": "houseplan-assets/furniture-plan-art.generated-CUlu3GpV.js",
"sha256": "3fc4d19f8fb5320c9edc15079cbe62f031ba25f2c35cc10411028ccf90ff8da1",
"rawBytes": 33655,
"gzipBytes": 10254,
"gzipBytes": 10250,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/guard-B_l7xd7s.js",
"sha256": "1dc329a4458f65305eaaec5d04c0db25db6978dcec9d926a2d306344c7c32505",
"path": "houseplan-assets/guard-Dk7ygy6z.js",
"sha256": "9a4f82565f03a902724d6d58b596becce76d82095233e7f125ee64e67445a0ee",
"rawBytes": 518,
"gzipBytes": 371,
"gzipBytes": 370,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DnOXDKzw.js",
"sha256": "c3d7a3e8135ed9ca0f8251d7935784716deb13d21a041ac7c3074105670d260f",
"rawBytes": 1040786,
"gzipBytes": 288781,
"path": "houseplan-assets/houseplan-card-ZsbfBSl1.js",
"sha256": "15f7c3dc17e2fbfb4a0c61ef15498da70d2d01ed9f2831bb08103fa5c3c0d13c",
"rawBytes": 1042320,
"gzipBytes": 289130,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-BtNP3xgK.js",
"houseplan-assets/editor-CbKgPNqk.js",
"houseplan-assets/fr-f560b583.js",
"houseplan-assets/furniture-plan-art.generated-CP7GBt2L.js",
"houseplan-assets/houseplan-editor-runtime-rdfalUon.js",
"houseplan-assets/houseplan-onboarding-runtime-Bvx5Dld7.js",
"houseplan-assets/hp-zigbee-topology-overlay-XlIYV874.js",
"houseplan-assets/iso-scene-render-CMZR5dLh.js",
"houseplan-assets/live-interaction-runtime-BaHUiHhO.js",
"houseplan-assets/pdf-export-Dgp0UwWM.js"
"houseplan-assets/de-pA3tnOax.js",
"houseplan-assets/editor-CJSkbmJl.js",
"houseplan-assets/fr-BjBWP2nl.js",
"houseplan-assets/furniture-plan-art.generated-CUlu3GpV.js",
"houseplan-assets/houseplan-editor-runtime-B3O-Y3rk.js",
"houseplan-assets/houseplan-onboarding-runtime-lwyMxFqY.js",
"houseplan-assets/hp-zigbee-topology-overlay-f-40j--2.js",
"houseplan-assets/iso-scene-render-C5A4O41t.js",
"houseplan-assets/live-interaction-runtime-iZ4G2R9J.js",
"houseplan-assets/pdf-export-Cg8R9f0g.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-rdfalUon.js",
"sha256": "f62646d7e7cada6577c780ac28840715409a39de13d9fa20b9c803bd506265b0",
"path": "houseplan-assets/houseplan-editor-runtime-B3O-Y3rk.js",
"sha256": "f203e37165f45f1c286d49937077cddeb3744f74e7b5fe0b78438cb88f0d72c2",
"rawBytes": 631884,
"gzipBytes": 168230,
"gzipBytes": 168234,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-B35mi_yP.js",
"houseplan-assets/furniture-plan-art.generated-CP7GBt2L.js",
"houseplan-assets/guard-B_l7xd7s.js",
"houseplan-assets/houseplan-card-DnOXDKzw.js",
"houseplan-assets/zigbee-topology-DESIftME.js"
"houseplan-assets/backdrop-pick-B8uJVpYl.js",
"houseplan-assets/furniture-plan-art.generated-CUlu3GpV.js",
"houseplan-assets/guard-Dk7ygy6z.js",
"houseplan-assets/houseplan-card-ZsbfBSl1.js",
"houseplan-assets/zigbee-topology-DjTDqsI9.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-D0VNHB73.js"
"houseplan-assets/zigbee-topology-runtime-CEUb94-9.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-Bvx5Dld7.js",
"sha256": "ceb90eed1045fe6b704f460efa571a1cea57c5e5eefaf450f6834119c940bd9f",
"path": "houseplan-assets/houseplan-onboarding-runtime-lwyMxFqY.js",
"sha256": "ce8ffc4f4fd388849286b785baae152f82c96dcfe71def236e392cd7a9c41a66",
"rawBytes": 28071,
"gzipBytes": 6968,
"gzipBytes": 6970,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-B35mi_yP.js",
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/backdrop-pick-B8uJVpYl.js",
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-XlIYV874.js",
"sha256": "43d26fcaf7101cde05ab6accdd5d0327cc32b979c19e279c070e17bbd98193d6",
"path": "houseplan-assets/hp-zigbee-topology-overlay-f-40j--2.js",
"sha256": "c2f4e19a3e42a124b336d949d51a9c70c10560b36a66c700b59c7ba8e5be02a6",
"rawBytes": 11034,
"gzipBytes": 3733,
"gzipBytes": 3732,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DnOXDKzw.js",
"houseplan-assets/zigbee-topology-DESIftME.js",
"houseplan-assets/zigbee-topology-runtime-D0VNHB73.js"
"houseplan-assets/houseplan-card-ZsbfBSl1.js",
"houseplan-assets/zigbee-topology-DjTDqsI9.js",
"houseplan-assets/zigbee-topology-runtime-CEUb94-9.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-CMZR5dLh.js",
"sha256": "ee7ccae62a0ad5d41d33aab199e1911d48381de5c5209e524c194681e7d433bf",
"path": "houseplan-assets/iso-scene-render-C5A4O41t.js",
"sha256": "d8feaf167a8a23a407336c75e1dc55b24bf0e6538001f7556ec83df2fd0fe2f5",
"rawBytes": 38562,
"gzipBytes": 13084,
"gzipBytes": 13085,
"isEntry": false,
"imports": [
"houseplan-assets/guard-B_l7xd7s.js",
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/guard-Dk7ygy6z.js",
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-BaHUiHhO.js",
"sha256": "94ac6883195623a7dcdb804e41303a7d4e31f4be33b828c3aaeceffe760954a5",
"path": "houseplan-assets/live-interaction-runtime-iZ4G2R9J.js",
"sha256": "6983fe8ce1e011f26b2c35d28199d111ef460e53c8617e37a99cf70d687c9495",
"rawBytes": 5196,
"gzipBytes": 2127,
"gzipBytes": 2129,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-Dgp0UwWM.js",
"sha256": "554a94b6d11541274f1cac489c21c1e752dff60662c68ce68863caa831e0e3d4",
"path": "houseplan-assets/pdf-export-Cg8R9f0g.js",
"sha256": "12dfd49e241e1d2e05848601c0afe258edf244c41b34bdd7a8eb2640fdc8a784",
"rawBytes": 237163,
"gzipBytes": 110080,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-CP7GBt2L.js",
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/furniture-plan-art.generated-CUlu3GpV.js",
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-DESIftME.js",
"sha256": "7185ad273c06229ef74466759785333f099fa50c09fb0dff60854d2bbae1071a",
"path": "houseplan-assets/zigbee-topology-DjTDqsI9.js",
"sha256": "a4cc005467f8000796a9ebcd8eaa1c181059ee501edb49c92ed093f2ce3868f9",
"rawBytes": 18961,
"gzipBytes": 7426,
"gzipBytes": 7427,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-D0VNHB73.js",
"sha256": "3f68ec926fdb0e45cf0ff35cacaf5b96e356abc2304b1a508c40dc8be70f203e",
"path": "houseplan-assets/zigbee-topology-runtime-CEUb94-9.js",
"sha256": "b48d6eaa60ee40fb8e48782194d4e9ddffea2e206cb5d8b920427a2fd38e5cd3",
"rawBytes": 3941,
"gzipBytes": 1764,
"gzipBytes": 1765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DnOXDKzw.js",
"houseplan-assets/zigbee-topology-DESIftME.js"
"houseplan-assets/houseplan-card-ZsbfBSl1.js",
"houseplan-assets/zigbee-topology-DjTDqsI9.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "be883fac6a50c6bf61b283196e5cf57d4f110ce3652017d4405ffc559dac63c5",
"rawBytes": 1183,
"gzipBytes": 797,
"sha256": "ba995a027242c56f3033f47778a21f24cab36ef8cc0809658c6a2479619ebfd3",
"rawBytes": 1205,
"gzipBytes": 804,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DnOXDKzw.js"
"houseplan-assets/houseplan-card-ZsbfBSl1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "7593c0dc42ba3a4c49b40995d91b63f348320d8b2094318601f51a6f03fb61cb",
"rawBytes": 5119,
"gzipBytes": 1930,
"isEntry": true,
"imports": [
"houseplan-card.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b0d09c9516d591f6cbedc31941c27ca6d60536efbfe6d6a581d652f892af314c";import{t as e,l as o,x as t,dX as s,A as a,dY as i,dZ as l,d_ as n,dM as r,b as c}from"./houseplan-card-DnOXDKzw.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="3555cb7791d3391b320376d4bcba14d217a5718d018ca955eb741df1762804b5";import{t as e,l as o,x as t,dX as s,A as a,dY as i,dZ as l,d_ as n,dM as r,b as c}from"./houseplan-card-ZsbfBSl1.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},_=this._schema,f=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
${d(_.slice(0,3))}
${f?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
${t(s,"editor.default_floor_missing",{id:f})}
</div>`:a}
${d(f.slice(3))}
${d(_.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b0d09c9516d591f6cbedc31941c27ca6d60536efbfe6d6a581d652f892af314c";import{dK as r,dL as t,dM as e}from"./houseplan-card-DnOXDKzw.js";const s={},a=r(class extends t{constructor(){super(...arguments),this.ot=s}render(r,t){return t()}update(r,[t,s]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return e}else if(this.ot===t)return e;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{a as i};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="3555cb7791d3391b320376d4bcba14d217a5718d018ca955eb741df1762804b5";import{dK as r,dL as t,dM as s}from"./houseplan-card-ZsbfBSl1.js";const e={},a=r(class extends t{constructor(){super(...arguments),this.ot=e}render(r,t){return t()}update(r,[t,e]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return s}else if(this.ot===t)return s;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,e)}});export{a as i};
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b0d09c9516d591f6cbedc31941c27ca6d60536efbfe6d6a581d652f892af314c";import{l as t,a_ as s,A as e,x as o,b as a,ci as l,cj as i,ck as h,cl as c,aX as n,cm as r,cr as p,cn as d,co as _,cp as u,cq as g,di as m,aW as b,aY as $,dj as v}from"./houseplan-card-DnOXDKzw.js";import{i as f,c as y,e as w,r as D,a as k,b as S,g as C,t as M}from"./backdrop-pick-B35mi_yP.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="b0d09c9516d591f6cbedc31941c27ca6d60536efbfe6d6a581d652f892af314c";class L{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="3555cb7791d3391b320376d4bcba14d217a5718d018ca955eb741df1762804b5";import{l as t,a_ as s,A as e,x as o,b as a,ci as l,cj as i,ck as h,cl as c,aX as n,cm as r,cr as p,cn as d,co as _,cp as u,cq as g,di as m,aW as b,aY as $,dj as v}from"./houseplan-card-ZsbfBSl1.js";import{i as f,c as y,e as w,r as D,a as k,b as S,g as C,t as M}from"./backdrop-pick-B8uJVpYl.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="3555cb7791d3391b320376d4bcba14d217a5718d018ca955eb741df1762804b5";class L{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
.text=${o(h,l)} .ariaLabel=${o(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const o=l(e),a=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,p="none"===o.fill?{...a||h,a:0}:a;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:o.showBorders,showNames:o.showNames,zeroWallStyle:r(e),displayTouched:!0,hideDecor:o.hideDecor,hideOpenings:o.hideOpenings,roomColor:o.color,roomOpacity:o.opacity,fillMode:"none"===o.fill?"custom":o.fill,customFill:p,glowEnabled:o.glow,bgColor:o.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:o.tempMin,tempMax:o.tempMax,showLqi:o.showLqi??this.host._config?.show_signal??!0,cardFontScale:o.cardFontScale,labelTemp:o.labelTemp,labelHum:o.labelHum,labelLqi:o.labelLqi,labelLight:o.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=p(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...f(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:_,tempMax:d,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await y(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await w(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return D(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){const s=this.host._spaceDialog,e=s?.saved?.find(s=>s.name===t);if(!s||!e||e.used_by.length||e.url===s.planUrl)return;const o=await this.host._confirmDanger({key:"delete-plan",kind:"destructive",title:this.host._t("confirm.delete_plan_title"),message:this.host._t("confirm.delete_plan_body"),objectName:t,confirmLabel:this.host._t("btn.delete"),cancelLabel:this.host._t("btn.cancel")}),a=this.host._spaceDialog,l=a?.saved?.find(s=>s.name===t);if(o&&a&&l&&l.url===e.url&&l.modified===e.modified&&!l.used_by.length&&l.url!==a.planUrl)try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return a`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return a`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return a`<div class="savedplans">
${s.map(s=>{return a`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b0d09c9516d591f6cbedc31941c27ca6d60536efbfe6d6a581d652f892af314c";import{n as t,b as n}from"./zigbee-topology-DESIftME.js";import{F as e}from"./houseplan-card-DnOXDKzw.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function f(t){if(!0!==t?.user?.is_admin)throw a("permission")}function p(n){const e=i(n);return e?c(e,"zha",async()=>{if(f(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(f(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,p=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{p=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const f=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&p?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof f&&b.push(f),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{p as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="3555cb7791d3391b320376d4bcba14d217a5718d018ca955eb741df1762804b5";import{n as t,b as n}from"./zigbee-topology-DjTDqsI9.js";import{F as e}from"./houseplan-card-ZsbfBSl1.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b0d09c9516d591f6cbedc31941c27ca6d60536efbfe6d6a581d652f892af314c";try{await import("./houseplan-assets/houseplan-card-DnOXDKzw.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="3555cb7791d3391b320376d4bcba14d217a5718d018ca955eb741df1762804b5";try{await import("./houseplan-assets/houseplan-card-ZsbfBSl1.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale houseplan-card.js: the implementation chunk is unavailable",e)}
File diff suppressed because one or more lines are too long
@@ -30,12 +30,13 @@ _NOTICE_TITLE_KEY = "component.houseplan.issues.frontend_reload_notice.title"
_NOTICE_DESCRIPTION_KEY = (
"component.houseplan.issues.frontend_reload_notice.description"
)
_NOTICE_TITLE_FALLBACK = "House Plan card connected"
_NOTICE_TITLE_FALLBACK = "House Plan is ready"
_NOTICE_DESCRIPTION_FALLBACK = (
"The House Plan card is connected. Fully reload this page to use the latest "
"frontend (`Ctrl+F5` on Windows/Linux or `Cmd+Shift+R` on macOS). If you "
"manage dashboard resources manually in storage mode, open Settings → "
"Dashboards → Resources."
"Open House Plan from the sidebar. After an update, fully reload the page "
"to use the latest frontend (`Ctrl+F5` on Windows/Linux or `Cmd+Shift+R` "
"on macOS). The dashboard card remains available; if you manage its "
"resource manually in storage mode, open Settings → Dashboards → "
"Resources. If the sidebar item is missing, check House Plan in System Health."
)
RegistrationStatus = Literal[
@@ -69,6 +70,14 @@ class RegistrationOutcome:
last_error: str | None = None
@dataclass(frozen=True, slots=True)
class StaticPathRegistrationOutcome:
"""Result of registering one exact public frontend URL."""
registered: bool
last_error: str | None = None
@dataclass(slots=True)
class FrontendRegistrationState:
"""Observable state and lifecycle handles for the current config-entry run."""
@@ -110,6 +119,64 @@ def _safe_error(phase: str, err: BaseException) -> str:
return f"{phase}:{type(err).__name__}"
def _static_registered_urls(hass: HomeAssistant) -> set[str]:
"""Return the exact static URLs registered for this Core process.
Before the panel entry existed this state was a boolean. A legacy ``True``
proves only that the card URL was registered; treating it as a generic
truthy flag would incorrectly skip the panel route after an integration
reload.
"""
domain_data = hass.data.setdefault(DOMAIN, {})
value = domain_data.get(FRONTEND_STATIC_REGISTERED_KEY)
if value is True:
urls = {FRONTEND_URL}
domain_data[FRONTEND_STATIC_REGISTERED_KEY] = urls
return urls
if isinstance(value, set):
return value
if isinstance(value, dict):
urls = {url for url, registered in value.items() if registered is True}
domain_data[FRONTEND_STATIC_REGISTERED_KEY] = urls
return urls
return set()
async def async_register_frontend_static_path(
hass: HomeAssistant,
frontend_url: str,
file_path: Path,
) -> StaticPathRegistrationOutcome:
"""Register one exact bundle entry across supported Home Assistant APIs."""
registered_urls = _static_registered_urls(hass)
if frontend_url in registered_urls:
return StaticPathRegistrationOutcome(True)
try:
try:
from homeassistant.components.http import StaticPathConfig
except ImportError: # Home Assistant versions before the async API
hass.http.register_static_path(
frontend_url, str(file_path), cache_headers=False
)
else:
await hass.http.async_register_static_paths(
[StaticPathConfig(frontend_url, str(file_path), cache_headers=False)]
)
except asyncio.CancelledError:
raise
except Exception as err: # noqa: BLE001 - callers intentionally fail soft
return StaticPathRegistrationOutcome(
False, _safe_error("static_path", err)
)
registered_urls.add(frontend_url)
hass.data.setdefault(DOMAIN, {})[
FRONTEND_STATIC_REGISTERED_KEY
] = registered_urls
return StaticPathRegistrationOutcome(True)
def _lovelace_resources(hass: HomeAssistant):
"""Return the Lovelace resource collection across supported HA versions."""
lovelace = hass.data.get("lovelace")
@@ -536,28 +603,17 @@ async def async_setup_frontend_registration(
domain_data[FRONTEND_REGISTRATION_KEY] = state
entry.async_on_unload(state.cancel)
if domain_data.get(FRONTEND_STATIC_REGISTERED_KEY):
state.static_path_registered = True
elif card_file_present:
try:
try:
from homeassistant.components.http import StaticPathConfig
except ImportError: # Home Assistant versions before the async API
hass.http.register_static_path(
FRONTEND_URL, str(card_path), cache_headers=False
)
else:
await hass.http.async_register_static_paths(
[StaticPathConfig(FRONTEND_URL, str(card_path), cache_headers=False)]
)
except asyncio.CancelledError:
raise
except Exception as err: # noqa: BLE001 - setup continues without frontend
state.last_error = _safe_error("static_path", err)
_LOGGER.warning("Could not register the House Plan static path", exc_info=True)
else:
domain_data[FRONTEND_STATIC_REGISTERED_KEY] = True
state.static_path_registered = True
if card_file_present:
static_outcome = await async_register_frontend_static_path(
hass, FRONTEND_URL, card_path
)
state.static_path_registered = static_outcome.registered
if not static_outcome.registered:
state.last_error = static_outcome.last_error
_LOGGER.warning(
"Could not register the House Plan card static path (%s)",
static_outcome.last_error or "static_path:UnknownError",
)
if not card_file_present:
_LOGGER.warning("houseplan-card.js was not found next to the integration")
@@ -11,6 +11,7 @@
"dependencies": [
"http",
"frontend",
"panel_custom",
"websocket_api"
],
"documentation": "https://github.com/Matysh/houseplan-card",
@@ -0,0 +1,241 @@
"""Register the House Plan application panel with strict lifecycle ownership."""
from __future__ import annotations
import asyncio
import logging
from collections.abc import Mapping
from dataclasses import dataclass, field
from functools import partial
from pathlib import Path
from typing import Literal
from homeassistant.components import frontend, panel_custom
from homeassistant.core import HomeAssistant, callback
from .const import (
DOMAIN,
PANEL_COMPONENT_NAME,
PANEL_FRONTEND_URL,
PANEL_ICON,
PANEL_TITLE,
PANEL_URL,
PANEL_URL_PATH,
VERSION,
)
from .frontend_registration import async_register_frontend_static_path
from .store import HouseplanConfigEntry
_LOGGER = logging.getLogger(__name__)
PANEL_REGISTRATION_KEY = "panel_registration"
PANEL_GENERATION_KEY = "panel_generation"
PanelStatus = Literal[
"not_attempted",
"registered",
"missing_asset",
"collision",
"static_error",
"registration_error",
"ownership_error",
"removed",
]
@dataclass(slots=True)
class PanelRegistrationState:
"""Observable state for one config-entry setup generation."""
panel_file_present: bool
generation: int
panel_static_path_registered: bool = False
panel_status: PanelStatus = "not_attempted"
panel_url: str | None = None
panel_module_url: str | None = None
panel_error: str | None = None
_owned_panel: object | None = field(default=None, repr=False)
def _safe_error(phase: str, err: BaseException) -> str:
"""Return a support-safe fingerprint without an exception message."""
return f"{phase}:{type(err).__name__}"
def _panel_registry(hass: HomeAssistant) -> Mapping[str, object] | None:
"""Read HA's in-memory panel registry without mutating private storage."""
key = getattr(frontend, "DATA_PANELS", None)
if not isinstance(key, str):
return None
panels = hass.data.get(key)
return panels if isinstance(panels, Mapping) else None
def _is_current(hass: HomeAssistant, state: PanelRegistrationState) -> bool:
"""Return whether state still owns the current setup generation."""
domain_data = hass.data.get(DOMAIN, {})
return (
domain_data.get(PANEL_REGISTRATION_KEY) is state
and domain_data.get(PANEL_GENERATION_KEY) == state.generation
)
@callback
def _remove_owned_panel(hass: HomeAssistant, state: PanelRegistrationState) -> None:
"""Remove only the exact registry object owned by this generation."""
if not _is_current(hass, state) or state._owned_panel is None:
return
panels = _panel_registry(hass)
if panels is None:
# An unknown registry shape is not authority to remove a route by name.
return
if panels.get(PANEL_URL_PATH) is not state._owned_panel:
# The route was removed or replaced outside this integration. It is no
# longer ours, and a later cleanup must not reconsider that identity.
state._owned_panel = None
state.panel_status = "removed"
state.panel_url = None
state.panel_module_url = None
return
try:
# Keep the HA 2024.6-compatible two-argument call. In particular, do
# not pass the newer warn_if_unknown keyword.
frontend.async_remove_panel(hass, PANEL_URL_PATH)
except Exception as err: # noqa: BLE001 - unload remains fail-soft
fingerprint = _safe_error("remove", err)
state.panel_error = fingerprint
_LOGGER.warning("Could not remove the owned House Plan panel (%s)", fingerprint)
return
state._owned_panel = None
state.panel_status = "removed"
state.panel_url = None
state.panel_module_url = None
@callback
def remove_panel_registration(hass: HomeAssistant) -> None:
"""Idempotently clean up the current owned panel, if one still exists."""
state = get_panel_registration_state(hass)
if state is not None:
_remove_owned_panel(hass, state)
async def async_setup_panel_registration(
hass: HomeAssistant,
entry: HouseplanConfigEntry,
panel_path: Path,
) -> PanelRegistrationState:
"""Register the panel last without making integration setup depend on it."""
domain_data = hass.data.setdefault(DOMAIN, {})
previous = domain_data.get(PANEL_REGISTRATION_KEY)
if isinstance(previous, PanelRegistrationState):
# Normally entry unload already did this. Keeping setup self-contained
# also makes a direct repeated setup deterministic and duplicate-free.
_remove_owned_panel(hass, previous)
old_generation = domain_data.get(PANEL_GENERATION_KEY)
generation = old_generation + 1 if isinstance(old_generation, int) else 1
domain_data[PANEL_GENERATION_KEY] = generation
panel_file_present = panel_path.is_file()
state = PanelRegistrationState(
panel_file_present=panel_file_present,
generation=generation,
)
domain_data[PANEL_REGISTRATION_KEY] = state
if not panel_file_present:
state.panel_status = "missing_asset"
_LOGGER.warning("houseplan-panel.js was not found next to the integration")
return state
static_outcome = await async_register_frontend_static_path(
hass, PANEL_FRONTEND_URL, panel_path
)
state.panel_static_path_registered = static_outcome.registered
if not static_outcome.registered:
state.panel_status = "static_error"
state.panel_error = static_outcome.last_error
_LOGGER.warning(
"Could not register the House Plan panel static path (%s)",
static_outcome.last_error or "static_path:UnknownError",
)
return state
module_url = f"{PANEL_FRONTEND_URL}?v={VERSION}"
try:
await panel_custom.async_register_panel(
hass=hass,
frontend_url_path=PANEL_URL_PATH,
webcomponent_name=PANEL_COMPONENT_NAME,
sidebar_title=PANEL_TITLE,
sidebar_icon=PANEL_ICON,
module_url=module_url,
embed_iframe=False,
trust_external=False,
require_admin=False,
)
except asyncio.CancelledError:
raise
except ValueError as err:
state.panel_error = _safe_error("registration", err)
panels = _panel_registry(hass)
if panels is not None and PANEL_URL_PATH in panels:
state.panel_status = "collision"
_LOGGER.warning(
"House Plan cannot register /%s because that panel route is "
"already in use; open House Plan in System Health for details",
PANEL_URL_PATH,
)
else:
state.panel_status = "registration_error"
_LOGGER.warning(
"Could not register the House Plan panel (%s)", state.panel_error
)
return state
except Exception as err: # noqa: BLE001 - the card/backend remain available
state.panel_status = "registration_error"
state.panel_error = _safe_error("registration", err)
_LOGGER.warning(
"Could not register the House Plan panel (%s)", state.panel_error
)
return state
panels = _panel_registry(hass)
owned_panel = panels.get(PANEL_URL_PATH) if panels is not None else None
if owned_panel is None:
# The public register API has no ownership return value. If the exact
# object cannot be captured now, leaving a route until a later blind
# cleanup would be unsafe. Remove the just-created route immediately.
state.panel_status = "ownership_error"
state.panel_error = "ownership:Unavailable"
try:
frontend.async_remove_panel(hass, PANEL_URL_PATH)
except Exception as err: # noqa: BLE001 - setup must still succeed
_LOGGER.warning(
"Could not clean up an unowned House Plan panel (%s)",
_safe_error("ownership_remove", err),
)
_LOGGER.warning(
"House Plan panel ownership could not be verified; the panel was removed"
)
return state
state._owned_panel = owned_panel
state.panel_status = "registered"
state.panel_url = PANEL_URL
state.panel_module_url = module_url
entry.async_on_unload(partial(_remove_owned_panel, hass, state))
return state
@callback
def get_panel_registration_state(
hass: HomeAssistant,
) -> PanelRegistrationState | None:
"""Return the current panel state for System Health."""
state = hass.data.get(DOMAIN, {}).get(PANEL_REGISTRATION_KEY)
return state if isinstance(state, PanelRegistrationState) else None
+12 -3
View File
@@ -6,6 +6,9 @@
"data": { "admin_only": "Only administrators may edit the layout" }
}
},
"create_entry": {
"panel_ready": "Open House Plan from the sidebar when the item is available. A desktop browser is recommended for editing. If the item is missing, use the House Plan dashboard card and check System Health."
},
"abort": { "single_instance_allowed": "Already configured — only one entry is allowed." }
},
"options": {
@@ -21,8 +24,8 @@
"description": "The plan file `{file}` for space `{space}` was not found in `config/houseplan/plans/`. Open the space settings in the House Plan card and upload the plan again."
},
"frontend_reload_notice": {
"title": "House Plan card connected",
"description": "The House Plan card is connected. Fully reload this page to use the latest frontend (`Ctrl+F5` on Windows/Linux or `Cmd+Shift+R` on macOS). If you manage dashboard resources manually in storage mode, open Settings → Dashboards → Resources."
"title": "House Plan is ready",
"description": "Open House Plan from the sidebar. After an update, fully reload the page to use the latest frontend (`Ctrl+F5` on Windows/Linux or `Cmd+Shift+R` on macOS). The dashboard card remains available; if you manage its resource manually in storage mode, open Settings → Dashboards → Resources. If the sidebar item is missing, check House Plan in System Health."
}
},
"system_health": {
@@ -34,7 +37,13 @@
"resource_url": "Frontend resource URL",
"resource_retry": "Resource registration retry",
"resource_error": "Frontend registration error",
"first_reload_notice": "First reload notice"
"first_reload_notice": "First reload notice",
"panel_file": "House Plan panel file",
"panel_static_path": "Panel static frontend path",
"panel_status": "Panel registration status",
"panel_url": "House Plan panel URL",
"panel_module_url": "Panel module URL",
"panel_error": "Panel registration error"
}
}
}
+56 -24
View File
@@ -7,6 +7,7 @@ from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
from .frontend_registration import get_frontend_registration_state
from .panel_registration import get_panel_registration_state
from .store import get_data
@@ -33,6 +34,36 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"markers": len(config.get("markers", [])),
"layout_entries": len(layout_raw.get("layout", {})),
}
panel = get_panel_registration_state(hass)
if panel is None:
result.update(
{
"panel_file": "missing",
"panel_static_path": "not_registered",
"panel_status": "not_attempted",
"panel_url": "unavailable",
"panel_module_url": "unavailable",
"panel_error": "none",
}
)
else:
result.update(
{
"panel_file": (
"present" if panel.panel_file_present else "missing"
),
"panel_static_path": (
"registered"
if panel.panel_static_path_registered
else "not_registered"
),
"panel_status": panel.panel_status,
"panel_url": panel.panel_url or "unavailable",
"panel_module_url": panel.panel_module_url or "unavailable",
"panel_error": panel.panel_error or "none",
}
)
frontend = get_frontend_registration_state(hass)
if frontend is None:
result.update(
@@ -47,28 +78,29 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"first_reload_notice": "pending_frontend",
}
)
return result
result.update(
{
"card_file": "present" if frontend.card_file_present else "missing",
"static_path": (
"registered"
if frontend.static_path_registered
else "not_registered"
),
"resource_status": frontend.resource_status,
"resource_loader": frontend.loader,
"resource_url": frontend.module_url or "unavailable",
"resource_retry": (
"pending"
if frontend.retry_pending
else "attempted"
if frontend.retry_attempted
else "not_needed"
),
"resource_error": frontend.last_error or "none",
"first_reload_notice": frontend.first_reload_notice,
}
)
else:
result.update(
{
"card_file": (
"present" if frontend.card_file_present else "missing"
),
"static_path": (
"registered"
if frontend.static_path_registered
else "not_registered"
),
"resource_status": frontend.resource_status,
"resource_loader": frontend.loader,
"resource_url": frontend.module_url or "unavailable",
"resource_retry": (
"pending"
if frontend.retry_pending
else "attempted"
if frontend.retry_attempted
else "not_needed"
),
"resource_error": frontend.last_error or "none",
"first_reload_notice": frontend.first_reload_notice,
}
)
return result
@@ -8,6 +8,9 @@
}
}
},
"create_entry": {
"panel_ready": "Öffnen Sie House Plan über die Seitenleiste, wenn der Eintrag verfügbar ist. Für die Bearbeitung wird ein Desktop-Browser empfohlen. Fehlt der Eintrag, verwenden Sie die House-Plan-Dashboard-Karte und prüfen Sie die Systeminformationen."
},
"abort": {
"single_instance_allowed": "Bereits konfiguriert – es ist nur eine Instanz zulässig."
}
@@ -27,8 +30,8 @@
"description": "Die Plandatei `{file}` für den Bereich `{space}` wurde in `config/houseplan/plans/` nicht gefunden. In den Bereichseinstellungen der House-Plan-Karte den Plan erneut hochladen."
},
"frontend_reload_notice": {
"title": "House-Plan-Karte verbunden",
"description": "Die House-Plan-Karte ist verbunden. Laden Sie diese Seite vollständig neu, um die aktuelle Benutzeroberfläche zu verwenden (`Ctrl+F5` unter Windows/Linux oder `Cmd+Shift+R` unter macOS). Wenn Sie Dashboard-Ressourcen im Speichermodus manuell verwalten, öffnen Sie Einstellungen → Dashboards → Ressourcen."
"title": "House Plan ist bereit",
"description": "Öffnen Sie House Plan über die Seitenleiste. Laden Sie die Seite nach einem Update vollständig neu, um die aktuelle Benutzeroberfläche zu verwenden (`Ctrl+F5` unter Windows/Linux oder `Cmd+Shift+R` unter macOS). Die Dashboard-Karte bleibt verfügbar; wenn Sie deren Ressource im Speichermodus manuell verwalten, öffnen Sie Einstellungen → Dashboards → Ressourcen. Fehlt der Seitenleisteneintrag, prüfen Sie House Plan in den Systeminformationen."
}
},
"system_health": {
@@ -40,7 +43,13 @@
"resource_url": "URL der Frontend-Ressource",
"resource_retry": "Wiederholung der Ressourcenregistrierung",
"resource_error": "Fehler der Frontend-Registrierung",
"first_reload_notice": "Erster Hinweis zum Neuladen"
"first_reload_notice": "Erster Hinweis zum Neuladen",
"panel_file": "Datei des House-Plan-Panels",
"panel_static_path": "Statischer Frontend-Pfad des Panels",
"panel_status": "Status der Panel-Registrierung",
"panel_url": "URL des House-Plan-Panels",
"panel_module_url": "Modul-URL des Panels",
"panel_error": "Fehler der Panel-Registrierung"
}
}
}
@@ -8,6 +8,9 @@
}
}
},
"create_entry": {
"panel_ready": "Open House Plan from the sidebar when the item is available. A desktop browser is recommended for editing. If the item is missing, use the House Plan dashboard card and check System Health."
},
"abort": {
"single_instance_allowed": "Already configured — only one entry is allowed."
}
@@ -27,8 +30,8 @@
"description": "The plan file `{file}` for space `{space}` was not found in `config/houseplan/plans/`. Open the space settings in the House Plan card and upload the plan again."
},
"frontend_reload_notice": {
"title": "House Plan card connected",
"description": "The House Plan card is connected. Fully reload this page to use the latest frontend (`Ctrl+F5` on Windows/Linux or `Cmd+Shift+R` on macOS). If you manage dashboard resources manually in storage mode, open Settings → Dashboards → Resources."
"title": "House Plan is ready",
"description": "Open House Plan from the sidebar. After an update, fully reload the page to use the latest frontend (`Ctrl+F5` on Windows/Linux or `Cmd+Shift+R` on macOS). The dashboard card remains available; if you manage its resource manually in storage mode, open Settings → Dashboards → Resources. If the sidebar item is missing, check House Plan in System Health."
}
},
"system_health": {
@@ -40,7 +43,13 @@
"resource_url": "Frontend resource URL",
"resource_retry": "Resource registration retry",
"resource_error": "Frontend registration error",
"first_reload_notice": "First reload notice"
"first_reload_notice": "First reload notice",
"panel_file": "House Plan panel file",
"panel_static_path": "Panel static frontend path",
"panel_status": "Panel registration status",
"panel_url": "House Plan panel URL",
"panel_module_url": "Panel module URL",
"panel_error": "Panel registration error"
}
}
}
@@ -8,6 +8,9 @@
}
}
},
"create_entry": {
"panel_ready": "Ouvrez House Plan depuis la barre latérale lorsque l’entrée est disponible. Un navigateur sur ordinateur est recommandé pour la modification. Si l’entrée est absente, utilisez la carte House Plan du tableau de bord et consultez les informations système."
},
"abort": {
"single_instance_allowed": "Déjà configuré — une seule entrée est autorisée."
}
@@ -27,8 +30,8 @@
"description": "Le fichier de plan `{file}` pour l’espace `{space}` est introuvable dans `config/houseplan/plans/`. Ouvrez les paramètres de l’espace dans la carte House Plan et téléversez de nouveau le plan."
},
"frontend_reload_notice": {
"title": "Carte House Plan connectée",
"description": "La carte House Plan est connectée. Rechargez complètement cette page pour utiliser l’interface la plus récente (`Ctrl+F5` sous Windows/Linux ou `Cmd+Shift+R` sous macOS). Si vous gérez manuellement les ressources du tableau de bord en mode stockage, ouvrez Paramètres → Tableaux de bord → Ressources."
"title": "House Plan est prêt",
"description": "Ouvrez House Plan depuis la barre latérale. Après une mise à jour, rechargez complètement la page pour utiliser l’interface la plus récente (`Ctrl+F5` sous Windows/Linux ou `Cmd+Shift+R` sous macOS). La carte de tableau de bord reste disponible ; si vous gérez manuellement sa ressource en mode stockage, ouvrez Paramètres → Tableaux de bord → Ressources. Si l’entrée de la barre latérale est absente, consultez House Plan dans les informations système."
}
},
"system_health": {
@@ -40,7 +43,13 @@
"resource_url": "URL de la ressource de l’interface",
"resource_retry": "Nouvelle tentative d’enregistrement",
"resource_error": "Erreur d’enregistrement de l’interface",
"first_reload_notice": "Premier avis de rechargement"
"first_reload_notice": "Premier avis de rechargement",
"panel_file": "Fichier du panneau House Plan",
"panel_static_path": "Chemin statique du panneau",
"panel_status": "État de l’enregistrement du panneau",
"panel_url": "URL du panneau House Plan",
"panel_module_url": "URL du module du panneau",
"panel_error": "Erreur d’enregistrement du panneau"
}
}
}
@@ -8,6 +8,9 @@
}
}
},
"create_entry": {
"panel_ready": "Откройте House Plan в боковом меню, если пункт появился. Для редактирования рекомендуется браузер на компьютере. Если пункта нет, используйте карточку House Plan на панели и проверьте информацию о системе."
},
"abort": {
"single_instance_allowed": "Уже настроено — допускается одна запись."
}
@@ -27,8 +30,8 @@
"description": "Файл плана `{file}` пространства `{space}` не найден в `config/houseplan/plans/`. Откройте настройки пространства в карточке House Plan и загрузите план заново."
},
"frontend_reload_notice": {
"title": "Карточка House Plan подключена",
"description": "Карточка House Plan подключена. Полностью перезагрузите эту страницу, чтобы использовать актуальный интерфейс (`Ctrl+F5` в Windows/Linux или `Cmd+Shift+R` в macOS). Если вы вручную управляете ресурсами панели в режиме хранения, откройте Настройки → Панели → Ресурсы."
"title": "House Plan готов к работе",
"description": "Откройте House Plan в боковом меню. После обновления полностью перезагрузите страницу, чтобы использовать актуальный интерфейс (`Ctrl+F5` в Windows/Linux или `Cmd+Shift+R` в macOS). Карточка для панели остаётся доступна; если вы управляете её ресурсом вручную в режиме хранения, откройте Настройки → Панели → Ресурсы. Если пункта в боковом меню нет, проверьте House Plan в информации о системе."
}
},
"system_health": {
@@ -40,7 +43,13 @@
"resource_url": "URL ресурса интерфейса",
"resource_retry": "Повтор регистрации ресурса",
"resource_error": "Ошибка регистрации интерфейса",
"first_reload_notice": "Первое уведомление о перезагрузке"
"first_reload_notice": "Первое уведомление о перезагрузке",
"panel_file": "Файл панели House Plan",
"panel_static_path": "Статический путь панели",
"panel_status": "Статус регистрации панели",
"panel_url": "URL панели House Plan",
"panel_module_url": "URL модуля панели",
"panel_error": "Ошибка регистрации панели"
}
}
}