s`
${this._targetText(e.target)}
`)}
${d.remoteCount?s`
{let n=t.get(e);return n||(n={painted:null,pending:null,raf:0},t.set(e,n)),n},o=e=>`${e.x} ${e.y} ${e.w} ${e.h}`;const r=e=>0===e.translateXPercent&&0===e.translateYPercent&&1===e.scaleX&&1===e.scaleY,a=e=>[e.x,e.y,e.w,e.h].every(Number.isFinite)&&e.w>0&&e.h>0,s=(e,t)=>{if(!t)return e.style.removeProperty("transform"),e.style.removeProperty("transform-origin"),void e.style.removeProperty("will-change");e.style.transformOrigin="0 0",e.style.willChange="transform",e.style.transform=`translate(${t.translateXPercent}%,${t.translateYPercent}%) scale(${t.scaleX},${t.scaleY})`};function i(e,t,n){if(!a(t.view)||!a(n.view)||!a(n.floor))return;const i=o(n.view),l=o(n.floor);for(const t of e.querySelectorAll('[data-hp-live-viewbox="camera"]'))t.setAttribute("viewBox",i);for(const t of e.querySelectorAll('[data-hp-live-viewbox="floor"]'))t.setAttribute("viewBox",l);const c=(f=t.view,u=n.view,{translateXPercent:(f.x-u.x)/u.w*100,translateYPercent:(f.y-u.y)/u.h*100,scaleX:f.w/u.w,scaleY:f.h/u.h});var f,u;for(const t of e.querySelectorAll('[data-hp-live-layer="camera"]'))s(t,r(c)?null:c);const d=e.querySelector("[data-hp-live-zoom]");if(d){d.hidden=n.zoom<=1;const e=d.querySelector("[data-hp-live-zoom-value]");e&&(e.textContent=`${Math.round(100*n.zoom)}%`)}}const l=e=>{const t=e._viewOr(e._baseVb());return{view:{...t},floor:{...e._floorView(t)},zoom:e._zoom}};const c=new WeakMap,f=(e,t,n,o)=>{if(!n)return;const r=document.createElement("span");if(r.className="m",t){r.append(`${t} `);const e=document.createElement("b");e.textContent=n,o&&(e.style.color=o),r.append(e)}else r.textContent=n;e.append(r)},u=(e,t,n)=>{const o=e.querySelector(t);o&&(o.setAttribute("d",n),o.toggleAttribute("hidden",!n))};function d(t){const n=t,o=n.renderRoot;if(!o)return;((t,n)=>{const o=n.querySelector("[data-hp-live-tip]");if(!o)return;const r=t._tip;if(!r)return o.hidden=!0,void o.replaceChildren();o.style.left=`${r.x+12}px`,o.style.top=`${r.y+12}px`,o.replaceChildren();const a=document.createElement("b");a.textContent=r.title,o.append(a),f(o,"",r.meta),f(o,t._t("tip.temp_avg"),null==r.temp?"":`${r.temp}°`),f(o,t._t("tip.hum_avg"),null==r.hum?"":`${r.hum}%`),f(o,t._t("tip.lqi"),null==r.lqi?"":String(r.lqi),null==r.lqi?void 0:e(r.lqi)),o.hidden=!1})(n,o);const r=n._hoverRoom,a=c.get(t),s=r?.space||"",i=r?.room||null;if(a?.room===i&&a?.space===s)return;c.set(t,{room:i,space:s});const l=r&&n._spaceModel(),d=l?n._roomHoverPaths(l):null;u(o,'[data-hp-live-room-hover="fill"]',d?.fillD||""),u(o,'[data-hp-live-room-hover="halo"]',d?.outlineD||""),u(o,'[data-hp-live-room-hover="outline"]',d?.outlineD||"")}function m(e){c.delete(e)}class p{constructor(e){this.host=e,this.deferredHass=!1}hass(e,t,n,o){const r=function(e,t,n){if(e===t)return"none";if(!e||!t||!n)return"structural";if(e.connection!==t.connection||e.entities!==t.entities||e.devices!==t.devices||e.areas!==t.areas||e.themes!==t.themes||e.user!==t.user||e.config!==t.config||e.floors!==t.floors||e.services!==t.services||e.panels!==t.panels||e.language!==t.language||e.locale?.language!==t.locale?.language||e.locale?.number_format!==t.locale?.number_format||e.locale?.time_format!==t.locale?.time_format||e.locale?.date_format!==t.locale?.date_format)return"structural";const o=new Set(["states","connection","entities","devices","areas","themes","user","config","floors","services","panels","language","locale"]);for(const n of new Set([...Object.keys(e),...Object.keys(t)]))if(!o.has(n)&&e[n]!==t[n])return"structural";for(const o of n.entityIds)if(e.states?.[o]!==t.states?.[o])return"state";return"none"}(e,t,n),a="state"===r&&this.active();a&&(this.deferredHass=!0);const s="none"!==r&&!a;return s||this.host._renderLife.observe(e,t,n,o),s}clear(){this.deferredHass=!1}take(){const e=this.deferredHass;return this.deferredHass=!1,e}viewport(e=!1){!function(e,t=!1){const o=e,r=n(e),a=l(o);if(t)return r.raf&&"function"==typeof cancelAnimationFrame&&cancelAnimationFrame(r.raf),r.raf=0,r.pending=null,r.painted||(r.painted=a),void i(o.renderRoot,r.painted,a);r.pending=a,r.raf||"function"!=typeof requestAnimationFrame||(r.raf=requestAnimationFrame(()=>{r.raf=0;const e=r.pending;r.pending=null;const t=o.renderRoot;e&&t&&(r.painted||(r.painted=e),i(t,r.painted,e))}))}(this.host,e)}hover(){d(this.host)}active(){const e=this.host;return e._pointers.size>0||e._cameraTransition.active||!!e._deviceDrag||!!e._physicalDrag||!!e._physicalRotate||!!e._decorMove||!!e._decorDraft||!!e._dtDrag||!!e._bdDrag||!!e._opDrag||!!e._resize?.dragging}commit(){!function(e){const t=e,o=n(e);o.raf&&"function"==typeof cancelAnimationFrame&&cancelAnimationFrame(o.raf),o.raf=0,o.pending=null,o.painted=l(t);const r=t.renderRoot;if(r){for(const e of r.querySelectorAll('[data-hp-live-layer="camera"]'))s(e,null);i(r,o.painted,o.painted)}}(this.host),m(this.host),d(this.host)}dispose(){!function(e){const n=t.get(e);n?.raf&&"function"==typeof cancelAnimationFrame&&cancelAnimationFrame(n.raf),t.delete(e)}(this.host),m(this.host)}}export{p as LiveRuntime};
+globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b2baa797361b836964adc96221eabf4f8e9123b02ae55fe32b18f4d4662a8e9c";import{dx as e}from"./houseplan-card-BPjCRsTo.js";const t=new WeakMap,n=e=>{let n=t.get(e);return n||(n={painted:null,pending:null,raf:0},t.set(e,n)),n},o=e=>`${e.x} ${e.y} ${e.w} ${e.h}`;const r=e=>0===e.translateXPercent&&0===e.translateYPercent&&1===e.scaleX&&1===e.scaleY,a=e=>[e.x,e.y,e.w,e.h].every(Number.isFinite)&&e.w>0&&e.h>0,s=(e,t)=>{if(!t)return e.style.removeProperty("transform"),e.style.removeProperty("transform-origin"),void e.style.removeProperty("will-change");e.style.transformOrigin="0 0",e.style.willChange="transform",e.style.transform=`translate(${t.translateXPercent}%,${t.translateYPercent}%) scale(${t.scaleX},${t.scaleY})`};function i(e,t,n){if(!a(t.view)||!a(n.view)||!a(n.floor))return;const i=o(n.view),l=o(n.floor);for(const t of e.querySelectorAll('[data-hp-live-viewbox="camera"]'))t.setAttribute("viewBox",i);for(const t of e.querySelectorAll('[data-hp-live-viewbox="floor"]'))t.setAttribute("viewBox",l);const c=(f=t.view,u=n.view,{translateXPercent:(f.x-u.x)/u.w*100,translateYPercent:(f.y-u.y)/u.h*100,scaleX:f.w/u.w,scaleY:f.h/u.h});var f,u;for(const t of e.querySelectorAll('[data-hp-live-layer="camera"]'))s(t,r(c)?null:c);const d=e.querySelector("[data-hp-live-zoom]");if(d){d.hidden=n.zoom<=1;const e=d.querySelector("[data-hp-live-zoom-value]");e&&(e.textContent=`${Math.round(100*n.zoom)}%`)}}const l=e=>{const t=e._viewOr(e._baseVb());return{view:{...t},floor:{...e._floorView(t)},zoom:e._zoom}};const c=new WeakMap,f=(e,t,n,o)=>{if(!n)return;const r=document.createElement("span");if(r.className="m",t){r.append(`${t} `);const e=document.createElement("b");e.textContent=n,o&&(e.style.color=o),r.append(e)}else r.textContent=n;e.append(r)},u=(e,t,n)=>{const o=e.querySelector(t);o&&(o.setAttribute("d",n),o.toggleAttribute("hidden",!n))};function d(t){const n=t,o=n.renderRoot;if(!o)return;((t,n)=>{const o=n.querySelector("[data-hp-live-tip]");if(!o)return;const r=t._tip;if(!r)return o.hidden=!0,void o.replaceChildren();o.style.left=`${r.x+12}px`,o.style.top=`${r.y+12}px`,o.replaceChildren();const a=document.createElement("b");a.textContent=r.title,o.append(a),f(o,"",r.meta),f(o,t._t("tip.temp_avg"),null==r.temp?"":`${r.temp}°`),f(o,t._t("tip.hum_avg"),null==r.hum?"":`${r.hum}%`),f(o,t._t("tip.lqi"),null==r.lqi?"":String(r.lqi),null==r.lqi?void 0:e(r.lqi)),o.hidden=!1})(n,o);const r=n._hoverRoom,a=c.get(t),s=r?.space||"",i=r?.room||null;if(a?.room===i&&a?.space===s)return;c.set(t,{room:i,space:s});const l=r&&n._spaceModel(),d=l?n._roomHoverPaths(l):null;u(o,'[data-hp-live-room-hover="fill"]',d?.fillD||""),u(o,'[data-hp-live-room-hover="halo"]',d?.outlineD||""),u(o,'[data-hp-live-room-hover="outline"]',d?.outlineD||"")}function m(e){c.delete(e)}class p{constructor(e){this.host=e,this.deferredHass=!1}hass(e,t,n,o){const r=function(e,t,n){if(e===t)return"none";if(!e||!t||!n)return"structural";if(e.connection!==t.connection||e.entities!==t.entities||e.devices!==t.devices||e.areas!==t.areas||e.themes!==t.themes||e.user!==t.user||e.config!==t.config||e.floors!==t.floors||e.services!==t.services||e.panels!==t.panels||e.language!==t.language||e.locale?.language!==t.locale?.language||e.locale?.number_format!==t.locale?.number_format||e.locale?.time_format!==t.locale?.time_format||e.locale?.date_format!==t.locale?.date_format)return"structural";const o=new Set(["states","connection","entities","devices","areas","themes","user","config","floors","services","panels","language","locale"]);for(const n of new Set([...Object.keys(e),...Object.keys(t)]))if(!o.has(n)&&e[n]!==t[n])return"structural";for(const o of n.entityIds)if(e.states?.[o]!==t.states?.[o])return"state";return"none"}(e,t,n),a="state"===r&&this.active();a&&(this.deferredHass=!0);const s="none"!==r&&!a;return s||this.host._renderLife.observe(e,t,n,o),s}clear(){this.deferredHass=!1}take(){const e=this.deferredHass;return this.deferredHass=!1,e}viewport(e=!1){!function(e,t=!1){const o=e,r=n(e),a=l(o);if(t)return r.raf&&"function"==typeof cancelAnimationFrame&&cancelAnimationFrame(r.raf),r.raf=0,r.pending=null,r.painted||(r.painted=a),void i(o.renderRoot,r.painted,a);r.pending=a,r.raf||"function"!=typeof requestAnimationFrame||(r.raf=requestAnimationFrame(()=>{r.raf=0;const e=r.pending;r.pending=null;const t=o.renderRoot;e&&t&&(r.painted||(r.painted=e),i(t,r.painted,e))}))}(this.host,e)}hover(){d(this.host)}active(){const e=this.host;return e._pointers.size>0||e._cameraTransition.active||!!e._deviceDrag||!!e._physicalDrag||!!e._physicalRotate||!!e._decorMove||!!e._decorDraft||!!e._dtDrag||!!e._bdDrag||!!e._opDrag||!!e._resize?.dragging}commit(){!function(e){const t=e,o=n(e);o.raf&&"function"==typeof cancelAnimationFrame&&cancelAnimationFrame(o.raf),o.raf=0,o.pending=null,o.painted=l(t);const r=t.renderRoot;if(r){for(const e of r.querySelectorAll('[data-hp-live-layer="camera"]'))s(e,null);i(r,o.painted,o.painted)}}(this.host),m(this.host),d(this.host)}dispose(){!function(e){const n=t.get(e);n?.raf&&"function"==typeof cancelAnimationFrame&&cancelAnimationFrame(n.raf),t.delete(e)}(this.host),m(this.host)}}export{p as LiveRuntime};
diff --git a/dist/houseplan-assets/zigbee-topology-tpWwm6RD.js b/dist/houseplan-assets/zigbee-topology-OesQErJ9.js
similarity index 50%
rename from dist/houseplan-assets/zigbee-topology-tpWwm6RD.js
rename to dist/houseplan-assets/zigbee-topology-OesQErJ9.js
index 5f6bc96d..500d0b0f 100644
--- a/dist/houseplan-assets/zigbee-topology-tpWwm6RD.js
+++ b/dist/houseplan-assets/zigbee-topology-OesQErJ9.js
@@ -1 +1 @@
-globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="4ee1af967e8c303dcc691e9d85991cfec23c0787b69a7a4790eea2084797a225";import{s as e,I as n}from"./houseplan-card-C7v6ZaXI.js";var t={title:"Zigbee links",toggle:"Show Zigbee links when hovering over a device",hint:"Shows observed direct neighbours only. Nothing is fetched while you hover.",admin_only:"Topology data is available to Home Assistant administrators only.",save_first:"Save this setting before loading topology data.",zha:"ZHA",zha_read:"Read ZHA data",zha_hint:"Reads ZHA's cached topology; it does not start a radio scan.",z2m:"Zigbee2MQTT",z2m_topics:"Base topics (one per line)",z2m_update:"Update map",z2m_warning:"A network-map scan can take 10 seconds to 2 minutes and may temporarily reduce Zigbee responsiveness.",status_idle:"Data not loaded",status_loading:"Loading…",status_ready:"Received {time}",status_stale:"Received {time} · stale",status_partial:"Received {time} · some nodes were omitted",status_no_links:"Received {time} · no observed links found",error_permission:"Administrator permission is required.",error_unsupported:"This provider or Home Assistant API is unavailable.",error_timeout:"The provider did not respond in time.",error_invalid_topic:"Check the Zigbee2MQTT base topic.",error_invalid_payload:"The provider returned unsupported topology data.",error_provider:"Topology data could not be loaded.",remote_count:"+{n} in other spaces",route_device_not_on_plan:"device is not on the plan",route_coordinator_not_on_plan:"coordinator is not on the plan",route_other_space:"another space"};const i={en:t,ru:{title:"Связи Zigbee",toggle:"Показывать связи Zigbee при наведении на устройство",hint:"Показываются только наблюдаемые прямые соседи. Наведение ничего не запрашивает.",admin_only:"Данные топологии доступны только администраторам Home Assistant.",save_first:"Сохраните настройку перед загрузкой данных топологии.",zha:"ZHA",zha_read:"Прочитать данные ZHA",zha_hint:"Читает сохранённую топологию ZHA и не запускает радио-сканирование.",z2m:"Zigbee2MQTT",z2m_topics:"Базовые топики (по одному в строке)",z2m_update:"Обновить карту",z2m_warning:"Сканирование карты сети занимает от 10 секунд до 2 минут и временно может снизить отзывчивость Zigbee.",status_idle:"Данные не загружены",status_loading:"Загрузка…",status_ready:"Получено: {time}",status_stale:"Получено: {time} · данные устарели",status_partial:"Получено: {time} · часть узлов не показана",status_no_links:"Получено: {time} · наблюдаемые связи не найдены",error_permission:"Нужны права администратора.",error_unsupported:"Провайдер или API Home Assistant недоступен.",error_timeout:"Провайдер не ответил вовремя.",error_invalid_topic:"Проверьте базовый топик Zigbee2MQTT.",error_invalid_payload:"Провайдер вернул неподдерживаемые данные топологии.",error_provider:"Не удалось загрузить данные топологии.",remote_count:"+{n} в других пространствах",route_device_not_on_plan:"устройства нет на плане",route_coordinator_not_on_plan:"координатора нет на плане",route_other_space:"другое пространство"},de:{title:"Zigbee-Verbindungen",toggle:"Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",hint:"Es werden nur beobachtete direkte Nachbarn angezeigt. Beim Darüberfahren werden keine Daten abgerufen.",admin_only:"Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",save_first:"Speichere diese Einstellung, bevor Topologiedaten geladen werden.",zha:"ZHA",zha_read:"ZHA-Daten lesen",zha_hint:"Liest den ZHA-Topologiecache; es wird kein Funkscan gestartet.",z2m:"Zigbee2MQTT",z2m_topics:"Basistopics (eines pro Zeile)",z2m_update:"Karte aktualisieren",z2m_warning:"Ein Netzwerkscan kann 10 Sekunden bis 2 Minuten dauern und Zigbee vorübergehend verlangsamen.",status_idle:"Daten nicht geladen",status_loading:"Wird geladen…",status_ready:"Empfangen: {time}",status_stale:"Empfangen: {time} · veraltet",status_partial:"Empfangen: {time} · einige Knoten fehlen",status_no_links:"Empfangen: {time} · keine beobachteten Verbindungen gefunden",error_permission:"Administratorrechte sind erforderlich.",error_unsupported:"Dieser Anbieter oder die Home-Assistant-API ist nicht verfügbar.",error_timeout:"Der Anbieter hat nicht rechtzeitig geantwortet.",error_invalid_topic:"Prüfe das Zigbee2MQTT-Basistopic.",error_invalid_payload:"Der Anbieter hat nicht unterstützte Topologiedaten geliefert.",error_provider:"Topologiedaten konnten nicht geladen werden.",remote_count:"+{n} in anderen Bereichen",route_device_not_on_plan:"Gerät ist nicht im Plan",route_coordinator_not_on_plan:"Koordinator ist nicht im Plan",route_other_space:"anderer Bereich"},fr:{title:"Liens Zigbee",toggle:"Afficher les liens Zigbee au survol d’un appareil",hint:"Seuls les voisins directs observés sont affichés. Le survol ne lance aucune requête.",admin_only:"Les données de topologie sont réservées aux administrateurs Home Assistant.",save_first:"Enregistrez ce réglage avant de charger les données de topologie.",zha:"ZHA",zha_read:"Lire les données ZHA",zha_hint:"Lit la topologie en cache de ZHA sans lancer d’analyse radio.",z2m:"Zigbee2MQTT",z2m_topics:"Topics de base (un par ligne)",z2m_update:"Actualiser la carte",z2m_warning:"L’analyse de la carte réseau peut durer de 10 secondes à 2 minutes et réduire temporairement la réactivité de Zigbee.",status_idle:"Données non chargées",status_loading:"Chargement…",status_ready:"Reçu : {time}",status_stale:"Reçu : {time} · obsolète",status_partial:"Reçu : {time} · certains nœuds sont omis",status_no_links:"Reçu : {time} · aucun lien observé trouvé",error_permission:"Les droits administrateur sont requis.",error_unsupported:"Ce fournisseur ou l’API Home Assistant n’est pas disponible.",error_timeout:"Le fournisseur n’a pas répondu à temps.",error_invalid_topic:"Vérifiez le topic de base Zigbee2MQTT.",error_invalid_payload:"Le fournisseur a renvoyé des données de topologie non prises en charge.",error_provider:"Impossible de charger les données de topologie.",remote_count:"+{n} dans d’autres espaces",route_device_not_on_plan:"appareil absent du plan",route_coordinator_not_on_plan:"coordinateur absent du plan",route_other_space:"autre espace"}};function o(n,o,r){return e(i[n]?.[o]??t[o]??o,r)}const r=3e5,s=2097152,a=6e3;function d(e){return null!==e&&"object"==typeof e?e:null}function c(e){if("string"!=typeof e&&"number"!=typeof e)return null;let n=String(e).trim().toLowerCase();return n=/^0x[0-9a-f]{16}$/.test(n)?n.slice(2):n.replace(/[:-]/g,""),/^[0-9a-f]{16}$/.test(n)?n:null}function l(e,n,t){return`${e}:${n}:${t}`}function u(e){const n=String(e||"").toLowerCase().replace(/[ _-]/g,"");return n.includes("coordinator")?"coordinator":n.includes("router")?"router":n.includes("enddevice")||"end"===n?"end":"unknown"}const p=["parent","child","sibling","none","previous_child"];function _(e){const n=d(e),t=function(e){const n=Number(e);return Number.isFinite(n)&&n>=0&&n<=255?Math.round(n):void 0}(n?.lqi??n?.linkquality??n?.link_quality),i=function(e){return"number"==typeof e?p[e]:"string"==typeof e&&e.trim().toLowerCase().slice(0,40)||void 0}(n?.relationship);return{...void 0===t?{}:{lqi:t},...i?{relationship:i}:{}}}function g(e,n){return Array.isArray(e)&&e.length<=n?e:null}function f(e){if("string"==typeof e)return e.length<=s;try{return JSON.stringify(e).length<=s}catch{return!1}}function h(e,n,t,i,o){if(t===i)return void n.push({code:"self_link",nodeKey:t});const r=t=a){t.push({code:"invalid_payload"});break}const n=d(e),i=c(n?.ieee??n?.ieee_address);if(!i){t.push({code:"invalid_payload",nodeKey:s});continue}const p=l("zha","zha",i);o.has(p)||o.set(p,{key:p,ieee:i,role:u(n?.device_type)}),h(r,t,s,p,e)}else null!=n?.neighbors&&t.push({code:"invalid_payload",nodeKey:s})}return{provider:"zha",instanceId:"zha",obtainedAt:n,freshness:"provider-cache",nodes:[...o.values()],links:[...r.values()],warnings:t}}function b(e,n){const t=d(e),i=c(t?.ieeeAddr??t?.ieee_address??t?.ieee??e);if(i)return i;for(const i of[t?.id,t?.networkAddress,t?.network_address,e])if(null!=i){const e=n.get(String(i));if(e)return e}return null}function v(e,t,i=Date.now()){const o=[],r=n(t)||"invalid",p=f(e)?function(e){let n=e;if("string"==typeof n){if(n.length>s)return null;try{n=JSON.parse(n)}catch{return null}}const t=d(n),i=d(t?.data);if(n=i?.value??t?.value??t?.data??n,"string"==typeof n){if(n.length>s)return null;try{n=JSON.parse(n)}catch{return null}}return n}(e):null,_=d(p),m=g(_?.nodes,1e3),v=g(_?.links,a);if(!m||!v)return{provider:"z2m",instanceId:r,obtainedAt:i,freshness:"fresh-scan",nodes:[],links:[],warnings:[{code:"invalid_payload"}]};const y=new Map,k=new Map;for(const e of m){const n=d(e),t=c(n?.ieeeAddr??n?.ieee_address??n?.ieee);if(!t){o.push({code:"invalid_payload"});continue}const i=l("z2m",r,t);y.set(i,{key:i,ieee:t,role:u(n?.type??n?.device_type),..."boolean"==typeof n?.failed?{available:!n.failed}:{}});for(const e of[n?.id,n?.networkAddress,n?.network_address])null!=e&&k.set(String(e),t)}const z=new Map;for(const e of v){const n=d(e),t=c(n?.sourceIeeeAddr)??b(n?.source,k),i=c(n?.targetIeeeAddr)??b(n?.target,k);if(!t||!i){o.push({code:"invalid_payload"});continue}const s=l("z2m",r,t),a=l("z2m",r,i);y.has(s)||y.set(s,{key:s,ieee:t,role:"unknown"}),y.has(a)||y.set(a,{key:a,ieee:i,role:"unknown"}),h(z,o,s,a,e)}return{provider:"z2m",instanceId:r,obtainedAt:i,freshness:"fresh-scan",nodes:[...y.values()],links:[...z.values()],warnings:o}}function y(e){if("string"!=typeof e)return null;const n=c(e);if(n)return n;const t=e.toLowerCase().match(/^zigbee2mqtt(?:_bridge)?_(.+)$/);return t?c(t[1]):null}function k(e,n){if(e.deviceId&&n.devices[e.deviceId])return[e.deviceId];const t=new Set;for(const[i,o]of Object.entries(n.devices)){const n=d(o);(Array.isArray(n?.identifiers)?n.identifiers:[]).some(n=>Array.isArray(n)&&y(n[1])===e.ieee)&&t.add(i)}if(t.size)return[...t];for(const i of Object.values(n.entities)){const n=d(i);"string"==typeof n?.device_id&&"string"==typeof n.unique_id&&(y(n.unique_id)===e.ieee&&t.add(n.device_id))}return[...t]}function z(e){return!e.hidden&&!e.virtual&&"ha_disabled"!==e.bindingStatus?.kind&&"orphaned"!==e.bindingStatus?.kind&&"unverified"!==e.bindingStatus?.kind}function w(e){const n=new Map,t=new Map,i=e.nodes.filter(e=>"coordinator"===e.role).map(e=>e.key).sort();if(1!==i.length)return{distances:n,parents:t};const o=i[0],r=function(e){const n=new Set(e.nodes.map(e=>e.key)),t=new Map;for(const e of[...n].sort())t.set(e,[]);for(const i of[...e.links].sort((e,n)=>`${e.a}\0${e.b}`.localeCompare(`${n.a}\0${n.b}`)))n.has(i.a)&&n.has(i.b)&&(t.get(i.a).push({neighborKey:i.b,observation:i.aToB}),t.get(i.b).push({neighborKey:i.a,observation:i.bToA}));for(const e of t.values())e.sort((e,n)=>e.neighborKey.localeCompare(n.neighborKey));return t}(e);if(!r.has(o))return{distances:n,parents:t};n.set(o,0);const s=[o];for(let e=0;en.get(e)===i-1).sort((e,n)=>{const t="parent"===e.observation?.relationship?1:0,i="parent"===n.observation?.relationship?1:0;if(t!==i)return i-t;const o=e.observation?.lqi??-1;return(n.observation?.lqi??-1)-o||e.neighborKey.localeCompare(n.neighborKey)});o[0]&&t.set(e,o[0].neighborKey)}return{coordinatorKey:o,distances:n,parents:t}}function A(e,n,t){const i=new Map,o=[];for(const r of e.nodes){if(!1===r.available){o.push({code:"provider_scan_failure",nodeKey:r.key});continue}const e=k(r,t);if(1!==e.length){o.push({code:"unmatched_device",nodeKey:r.key});continue}const s=e[0];let a=n.filter(e=>z(e)&&"device"===e.bindingKind&&e.bindingRef===s);a.length||(a=n.filter(e=>z(e)&&"entity"===e.bindingKind&&!!e.bindingRef&&t.entities[e.bindingRef]?.device_id===s)),1===a.length?i.set(r.key,{markerId:a[0].id,space:a[0].space}):o.push({code:a.length>1?"ambiguous_placement":"unmatched_device",nodeKey:r.key})}return{placements:i,warnings:o}}function T(e,n,t){return e.map(e=>({topology:e,placements:A(e,n,t).placements,routes:w(e)}))}function Z(e,n,t){const i=new Map,o=new Set,r=new Map;let s=0;for(const{topology:a,placements:d,routes:c}of e){const e=new Set([...d].filter(([,e])=>e.markerId===t&&e.space===n).map(([e])=>e));if(e.size){for(const t of e){const e=c.parents.get(t);if(!e)continue;const i=d.get(e);if(i?.space===n)continue;let o;o=i?{kind:"remote-space",spaceId:i.space}:"coordinator"===a.nodes.find(n=>n.key===e)?.role?{kind:"unplaced-coordinator"}:{kind:"unplaced-device"};const s="remote-space"===o.kind?`${o.kind}:${o.spaceId}`:o.kind;r.set(s,o)}for(const r of a.links){const a=e.has(r.a),l=e.has(r.b);if(!a&&!l)continue;const u=a?r.a:r.b,p=a?r.b:r.a,_=d.get(p);if(!_||_.markerId===t){s++;continue}const g=c.parents.get(u)===p;if(_.space!==n){g||o.add(_.markerId);continue}const f=a?r.aToB:r.bToA,h=g?"toward-neighbor":c.parents.get(p)===u?"toward-origin":void 0,m=i.get(_.markerId);m?(void 0===m.line.lqi&&void 0!==f?.lqi&&(m.line.lqi=f.lqi),h&&m.directions.add(h)):i.set(_.markerId,{line:{neighborMarkerId:_.markerId,lqi:f?.lqi},directions:new Set(h?[h]:[])})}}}return{lines:[...i.values()].map(({line:e,directions:n})=>1===n.size?{...e,routeDirection:[...n][0]}:e),remoteCount:o.size,omittedCount:s,parentTargets:[...r.values()]}}export{r as T,T as a,v as b,A as m,m as n,Z as r,o as t};
+globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b2baa797361b836964adc96221eabf4f8e9123b02ae55fe32b18f4d4662a8e9c";import{s as e,I as n}from"./houseplan-card-BPjCRsTo.js";var t={title:"Zigbee links",toggle:"Show Zigbee links when hovering over a device",hint:"Shows observed direct neighbours only. Nothing is fetched while you hover.",admin_only:"Topology data is available to Home Assistant administrators only.",save_first:"Save this setting before loading topology data.",zha:"ZHA",zha_read:"Read ZHA data",zha_hint:"Reads ZHA's cached topology; it does not start a radio scan.",z2m:"Zigbee2MQTT",z2m_topics:"Base topics (one per line)",z2m_update:"Update map",z2m_warning:"A network-map scan can take 10 seconds to 2 minutes and may temporarily reduce Zigbee responsiveness.",status_idle:"Data not loaded",status_loading:"Loading…",status_ready:"Received {time}",status_stale:"Received {time} · stale",status_partial:"Received {time} · some nodes were omitted",status_no_links:"Received {time} · no observed links found",error_permission:"Administrator permission is required.",error_unsupported:"This provider or Home Assistant API is unavailable.",error_timeout:"The provider did not respond in time.",error_invalid_topic:"Check the Zigbee2MQTT base topic.",error_invalid_payload:"The provider returned unsupported topology data.",error_provider:"Topology data could not be loaded.",remote_count:"+{n} in other spaces",route_device_not_on_plan:"device is not on the plan",route_coordinator_not_on_plan:"coordinator is not on the plan",route_other_space:"another space"};const i={en:t,ru:{title:"Связи Zigbee",toggle:"Показывать связи Zigbee при наведении на устройство",hint:"Показываются только наблюдаемые прямые соседи. Наведение ничего не запрашивает.",admin_only:"Данные топологии доступны только администраторам Home Assistant.",save_first:"Сохраните настройку перед загрузкой данных топологии.",zha:"ZHA",zha_read:"Прочитать данные ZHA",zha_hint:"Читает сохранённую топологию ZHA и не запускает радио-сканирование.",z2m:"Zigbee2MQTT",z2m_topics:"Базовые топики (по одному в строке)",z2m_update:"Обновить карту",z2m_warning:"Сканирование карты сети занимает от 10 секунд до 2 минут и временно может снизить отзывчивость Zigbee.",status_idle:"Данные не загружены",status_loading:"Загрузка…",status_ready:"Получено: {time}",status_stale:"Получено: {time} · данные устарели",status_partial:"Получено: {time} · часть узлов не показана",status_no_links:"Получено: {time} · наблюдаемые связи не найдены",error_permission:"Нужны права администратора.",error_unsupported:"Провайдер или API Home Assistant недоступен.",error_timeout:"Провайдер не ответил вовремя.",error_invalid_topic:"Проверьте базовый топик Zigbee2MQTT.",error_invalid_payload:"Провайдер вернул неподдерживаемые данные топологии.",error_provider:"Не удалось загрузить данные топологии.",remote_count:"+{n} в других пространствах",route_device_not_on_plan:"устройства нет на плане",route_coordinator_not_on_plan:"координатора нет на плане",route_other_space:"другое пространство"},de:{title:"Zigbee-Verbindungen",toggle:"Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",hint:"Es werden nur beobachtete direkte Nachbarn angezeigt. Beim Darüberfahren werden keine Daten abgerufen.",admin_only:"Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",save_first:"Speichere diese Einstellung, bevor Topologiedaten geladen werden.",zha:"ZHA",zha_read:"ZHA-Daten lesen",zha_hint:"Liest den ZHA-Topologiecache; es wird kein Funkscan gestartet.",z2m:"Zigbee2MQTT",z2m_topics:"Basistopics (eines pro Zeile)",z2m_update:"Karte aktualisieren",z2m_warning:"Ein Netzwerkscan kann 10 Sekunden bis 2 Minuten dauern und Zigbee vorübergehend verlangsamen.",status_idle:"Daten nicht geladen",status_loading:"Wird geladen…",status_ready:"Empfangen: {time}",status_stale:"Empfangen: {time} · veraltet",status_partial:"Empfangen: {time} · einige Knoten fehlen",status_no_links:"Empfangen: {time} · keine beobachteten Verbindungen gefunden",error_permission:"Administratorrechte sind erforderlich.",error_unsupported:"Dieser Anbieter oder die Home-Assistant-API ist nicht verfügbar.",error_timeout:"Der Anbieter hat nicht rechtzeitig geantwortet.",error_invalid_topic:"Prüfe das Zigbee2MQTT-Basistopic.",error_invalid_payload:"Der Anbieter hat nicht unterstützte Topologiedaten geliefert.",error_provider:"Topologiedaten konnten nicht geladen werden.",remote_count:"+{n} in anderen Bereichen",route_device_not_on_plan:"Gerät ist nicht im Plan",route_coordinator_not_on_plan:"Koordinator ist nicht im Plan",route_other_space:"anderer Bereich"},fr:{title:"Liens Zigbee",toggle:"Afficher les liens Zigbee au survol d’un appareil",hint:"Seuls les voisins directs observés sont affichés. Le survol ne lance aucune requête.",admin_only:"Les données de topologie sont réservées aux administrateurs Home Assistant.",save_first:"Enregistrez ce réglage avant de charger les données de topologie.",zha:"ZHA",zha_read:"Lire les données ZHA",zha_hint:"Lit la topologie en cache de ZHA sans lancer d’analyse radio.",z2m:"Zigbee2MQTT",z2m_topics:"Topics de base (un par ligne)",z2m_update:"Actualiser la carte",z2m_warning:"L’analyse de la carte réseau peut durer de 10 secondes à 2 minutes et réduire temporairement la réactivité de Zigbee.",status_idle:"Données non chargées",status_loading:"Chargement…",status_ready:"Reçu : {time}",status_stale:"Reçu : {time} · obsolète",status_partial:"Reçu : {time} · certains nœuds sont omis",status_no_links:"Reçu : {time} · aucun lien observé trouvé",error_permission:"Les droits administrateur sont requis.",error_unsupported:"Ce fournisseur ou l’API Home Assistant n’est pas disponible.",error_timeout:"Le fournisseur n’a pas répondu à temps.",error_invalid_topic:"Vérifiez le topic de base Zigbee2MQTT.",error_invalid_payload:"Le fournisseur a renvoyé des données de topologie non prises en charge.",error_provider:"Impossible de charger les données de topologie.",remote_count:"+{n} dans d’autres espaces",route_device_not_on_plan:"appareil absent du plan",route_coordinator_not_on_plan:"coordinateur absent du plan",route_other_space:"autre espace"}};function o(n,o,r){return e(i[n]?.[o]??t[o]??o,r)}const r=3e5,s=2097152,a=6e3;function d(e){return null!==e&&"object"==typeof e?e:null}function c(e){if("string"!=typeof e&&"number"!=typeof e)return null;let n=String(e).trim().toLowerCase();return n=/^0x[0-9a-f]{16}$/.test(n)?n.slice(2):n.replace(/[:-]/g,""),/^[0-9a-f]{16}$/.test(n)?n:null}function l(e,n,t){return`${e}:${n}:${t}`}function u(e){const n=String(e||"").toLowerCase().replace(/[ _-]/g,"");return n.includes("coordinator")?"coordinator":n.includes("router")?"router":n.includes("enddevice")||"end"===n?"end":"unknown"}const p=["parent","child","sibling","none","previous_child"];function _(e){const n=d(e),t=function(e){const n=Number(e);return Number.isFinite(n)&&n>=0&&n<=255?Math.round(n):void 0}(n?.lqi??n?.linkquality??n?.link_quality),i=function(e){if("number"==typeof e)return p[e];if("string"!=typeof e)return;const n=e.trim().toLowerCase().replace(/[\s_-]+/g,"");return"previouschild"===n?"previous_child":n.slice(0,40)||void 0}(n?.relationship);return{...void 0===t?{}:{lqi:t},...i?{relationship:i}:{}}}function g(e,n){return Array.isArray(e)&&e.length<=n?e:null}function f(e){if("string"==typeof e)return e.length<=s;try{return JSON.stringify(e).length<=s}catch{return!1}}function h(e,n,t,i,o){if(t===i)return void n.push({code:"self_link",nodeKey:t});const r=t=a){t.push({code:"invalid_payload"});break}const n=d(e),i=c(n?.ieee??n?.ieee_address);if(!i){t.push({code:"invalid_payload",nodeKey:s});continue}const p=l("zha","zha",i);o.has(p)||o.set(p,{key:p,ieee:i,role:u(n?.device_type)}),h(r,t,s,p,e)}else null!=n?.neighbors&&t.push({code:"invalid_payload",nodeKey:s})}return{provider:"zha",instanceId:"zha",obtainedAt:n,freshness:"provider-cache",nodes:[...o.values()],links:[...r.values()],warnings:t}}function b(e,n){const t=d(e),i=c(t?.ieeeAddr??t?.ieee_address??t?.ieee??e);if(i)return i;for(const i of[t?.id,t?.networkAddress,t?.network_address,e])if(null!=i){const e=n.get(String(i));if(e)return e}return null}function v(e,t,i=Date.now()){const o=[],r=n(t)||"invalid",p=f(e)?function(e){let n=e;if("string"==typeof n){if(n.length>s)return null;try{n=JSON.parse(n)}catch{return null}}const t=d(n),i=d(t?.data);if(n=i?.value??t?.value??t?.data??n,"string"==typeof n){if(n.length>s)return null;try{n=JSON.parse(n)}catch{return null}}return n}(e):null,_=d(p),m=g(_?.nodes,1e3),v=g(_?.links,a);if(!m||!v)return{provider:"z2m",instanceId:r,obtainedAt:i,freshness:"fresh-scan",nodes:[],links:[],warnings:[{code:"invalid_payload"}]};const y=new Map,k=new Map;for(const e of m){const n=d(e),t=c(n?.ieeeAddr??n?.ieee_address??n?.ieee);if(!t){o.push({code:"invalid_payload"});continue}const i=l("z2m",r,t);y.set(i,{key:i,ieee:t,role:u(n?.type??n?.device_type),..."boolean"==typeof n?.failed?{available:!n.failed}:{}});for(const e of[n?.id,n?.networkAddress,n?.network_address])null!=e&&k.set(String(e),t)}const z=new Map;for(const e of v){const n=d(e),t=c(n?.sourceIeeeAddr)??b(n?.source,k),i=c(n?.targetIeeeAddr)??b(n?.target,k);if(!t||!i){o.push({code:"invalid_payload"});continue}const s=l("z2m",r,t),a=l("z2m",r,i);y.has(s)||y.set(s,{key:s,ieee:t,role:"unknown"}),y.has(a)||y.set(a,{key:a,ieee:i,role:"unknown"}),h(z,o,s,a,e)}return{provider:"z2m",instanceId:r,obtainedAt:i,freshness:"fresh-scan",nodes:[...y.values()],links:[...z.values()],warnings:o}}function y(e){if("string"!=typeof e)return null;const n=c(e);if(n)return n;const t=e.toLowerCase().match(/^zigbee2mqtt(?:_bridge)?_(.+)$/);return t?c(t[1]):null}function k(e,n){if(e.deviceId&&n.devices[e.deviceId])return[e.deviceId];const t=new Set;for(const[i,o]of Object.entries(n.devices)){const n=d(o);(Array.isArray(n?.identifiers)?n.identifiers:[]).some(n=>Array.isArray(n)&&y(n[1])===e.ieee)&&t.add(i)}if(t.size)return[...t];for(const i of Object.values(n.entities)){const n=d(i);"string"==typeof n?.device_id&&"string"==typeof n.unique_id&&(y(n.unique_id)===e.ieee&&t.add(n.device_id))}return[...t]}function z(e){return!e.hidden&&!e.virtual&&"ha_disabled"!==e.bindingStatus?.kind&&"orphaned"!==e.bindingStatus?.kind&&"unverified"!==e.bindingStatus?.kind}function w(e){const n=new Map,t=new Map,i=e.nodes.filter(e=>"coordinator"===e.role).map(e=>e.key).sort();if(1!==i.length)return{distances:n,parents:t};const o=i[0],r=function(e){const n=new Set(e.nodes.map(e=>e.key)),t=new Map;for(const e of[...n].sort())t.set(e,[]);for(const i of[...e.links].sort((e,n)=>`${e.a}\0${e.b}`.localeCompare(`${n.a}\0${n.b}`)))n.has(i.a)&&n.has(i.b)&&(t.get(i.a).push({neighborKey:i.b,observation:i.aToB}),t.get(i.b).push({neighborKey:i.a,observation:i.bToA}));for(const e of t.values())e.sort((e,n)=>e.neighborKey.localeCompare(n.neighborKey));return t}(e);if(!r.has(o))return{distances:n,parents:t};n.set(o,0);const s=[o];for(let e=0;en.get(e)===i-1).sort((e,n)=>{const t="parent"===e.observation?.relationship?1:0,i="parent"===n.observation?.relationship?1:0;if(t!==i)return i-t;const o=e.observation?.lqi??-1;return(n.observation?.lqi??-1)-o||e.neighborKey.localeCompare(n.neighborKey)});o[0]&&t.set(e,o[0].neighborKey)}return{coordinatorKey:o,distances:n,parents:t}}function A(e,n,t){const i=new Map,o=[];for(const r of e.nodes){if(!1===r.available){o.push({code:"provider_scan_failure",nodeKey:r.key});continue}const e=k(r,t);if(1!==e.length){o.push({code:"unmatched_device",nodeKey:r.key});continue}const s=e[0];let a=n.filter(e=>z(e)&&"device"===e.bindingKind&&e.bindingRef===s);a.length||(a=n.filter(e=>z(e)&&"entity"===e.bindingKind&&!!e.bindingRef&&t.entities[e.bindingRef]?.device_id===s)),1===a.length?i.set(r.key,{markerId:a[0].id,space:a[0].space}):o.push({code:a.length>1?"ambiguous_placement":"unmatched_device",nodeKey:r.key})}return{placements:i,warnings:o}}function T(e,n,t){return e.map(e=>({topology:e,placements:A(e,n,t).placements,routes:w(e)}))}function Z(e,n,t){const i=new Map,o=new Set,r=new Map;let s=0;for(const{topology:a,placements:d,routes:c}of e){const e=new Set([...d].filter(([,e])=>e.markerId===t&&e.space===n).map(([e])=>e));if(e.size){for(const t of e){const e=c.parents.get(t);if(!e)continue;const i=d.get(e);if(i?.space===n)continue;let o;o=i?{kind:"remote-space",spaceId:i.space}:"coordinator"===a.nodes.find(n=>n.key===e)?.role?{kind:"unplaced-coordinator"}:{kind:"unplaced-device"};const s="remote-space"===o.kind?`${o.kind}:${o.spaceId}`:o.kind;r.set(s,o)}for(const r of a.links){const a=e.has(r.a),l=e.has(r.b);if(!a&&!l)continue;const u=a?r.a:r.b,p=a?r.b:r.a,_=d.get(p);if(!_||_.markerId===t){s++;continue}const g=c.parents.get(u)===p;if(_.space!==n){g||o.add(_.markerId);continue}const f=a?r.aToB:r.bToA,h=g?"toward-neighbor":c.parents.get(p)===u?"toward-origin":void 0,m=i.get(_.markerId);m?(void 0===m.line.lqi&&void 0!==f?.lqi&&(m.line.lqi=f.lqi),h&&m.directions.add(h)):i.set(_.markerId,{line:{neighborMarkerId:_.markerId,lqi:f?.lqi},directions:new Set(h?[h]:[])})}}}return{lines:[...i.values()].map(({line:e,directions:n})=>1===n.size?{...e,routeDirection:[...n][0]}:e),remoteCount:o.size,omittedCount:s,parentTargets:[...r.values()]}}export{r as T,T as a,v as b,A as m,m as n,Z as r,o as t};
diff --git a/dist/houseplan-assets/zigbee-topology-runtime-Dvx229IG.js b/dist/houseplan-assets/zigbee-topology-runtime-D1hCsInw.js
similarity index 93%
rename from dist/houseplan-assets/zigbee-topology-runtime-Dvx229IG.js
rename to dist/houseplan-assets/zigbee-topology-runtime-D1hCsInw.js
index 64b6d557..0fe07e32 100644
--- a/dist/houseplan-assets/zigbee-topology-runtime-Dvx229IG.js
+++ b/dist/houseplan-assets/zigbee-topology-runtime-D1hCsInw.js
@@ -1 +1 @@
-globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="4ee1af967e8c303dcc691e9d85991cfec23c0787b69a7a4790eea2084797a225";import{n as t,b as n}from"./zigbee-topology-tpWwm6RD.js";import{I as e}from"./houseplan-card-C7v6ZaXI.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};
+globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b2baa797361b836964adc96221eabf4f8e9123b02ae55fe32b18f4d4662a8e9c";import{n as t,b as n}from"./zigbee-topology-OesQErJ9.js";import{I as e}from"./houseplan-card-BPjCRsTo.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};
diff --git a/dist/houseplan-card.js b/dist/houseplan-card.js
index 05138b1c..fd7e791f 100644
--- a/dist/houseplan-card.js
+++ b/dist/houseplan-card.js
@@ -1 +1 @@
-globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="4ee1af967e8c303dcc691e9d85991cfec23c0787b69a7a4790eea2084797a225";try{await import("./houseplan-assets/houseplan-card-C7v6ZaXI.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__="b2baa797361b836964adc96221eabf4f8e9123b02ae55fe32b18f4d4662a8e9c";try{await import("./houseplan-assets/houseplan-card-BPjCRsTo.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)}
diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs
index 690acbb0..7bb9efe4 100644
--- a/scripts/mutation-gate.mjs
+++ b/scripts/mutation-gate.mjs
@@ -5837,6 +5837,17 @@ const MUTANT_DEFINITIONS = [
replace: ' try { void unsubscribe; } catch { /* cleanup is best effort */ }',
}],
},
+ {
+ id: 'zigbee-route-relationship-separators-normalized',
+ guard: 'node --test --test-name-pattern="Z2M relationship strings" test/zigbee-topology.test.mjs',
+ because: 'provider relationship strings must ignore separators before the parent tie-break '
+ + 'uses them (#457 section 6.3 and code-review r1)',
+ patches: [{
+ file: 'src/zigbee-topology.ts',
+ find: " const compact = value.trim().toLowerCase().replace(/[\\s_-]+/g, '');",
+ replace: " const compact = value.trim().toLowerCase();",
+ }],
+ },
{
id: 'zigbee-route-parent-keeps-bfs-level',
guard: 'node --test --test-name-pattern="uplink tree is deterministic" test/zigbee-topology.test.mjs',
diff --git a/src/zigbee-topology.ts b/src/zigbee-topology.ts
index 12de6865..fce9eb27 100644
--- a/src/zigbee-topology.ts
+++ b/src/zigbee-topology.ts
@@ -111,8 +111,9 @@ const Z2M_RELATIONSHIPS = ['parent', 'child', 'sibling', 'none', 'previous_child
function relationshipOf(value: unknown): string | undefined {
if (typeof value === 'number') return Z2M_RELATIONSHIPS[value];
- return typeof value === 'string'
- ? value.trim().toLowerCase().slice(0, 40) || undefined : undefined;
+ if (typeof value !== 'string') return undefined;
+ const compact = value.trim().toLowerCase().replace(/[\s_-]+/g, '');
+ return compact === 'previouschild' ? 'previous_child' : compact.slice(0, 40) || undefined;
}
function observation(value: unknown): ZigbeeDirectionalObservation {
diff --git a/test/zigbee-topology.test.mjs b/test/zigbee-topology.test.mjs
index 1bb7a8ac..9d64c147 100644
--- a/test/zigbee-topology.test.mjs
+++ b/test/zigbee-topology.test.mjs
@@ -112,6 +112,23 @@ test('Z2M normalization keeps snake_case compatibility and prefers flat link IEE
assert.ok(topology.warnings.some((item) => item.code === 'self_link'));
});
+test('Z2M relationship strings ignore case and separators', () => {
+ const topology = normalizeZ2mTopology({ data: { value: JSON.stringify({
+ nodes: [
+ { ieeeAddr: '00124b0000000001', type: 'Coordinator' },
+ { ieeeAddr: '00124b0000000002', type: 'Router' },
+ { ieeeAddr: '00124b0000000003', type: 'EndDevice' },
+ ],
+ links: [
+ { sourceIeeeAddr: '00124b0000000001', targetIeeeAddr: '00124b0000000002',
+ relationship: ' PREVIOUS-child ' },
+ { sourceIeeeAddr: '00124b0000000002', targetIeeeAddr: '00124b0000000003',
+ relationship: ' P_a-r ent ' },
+ ],
+ }) } }, 'zigbee2mqtt');
+ assert.deepEqual(topology.links.map((link) => link.aToB.relationship), ['previous_child', 'parent']);
+});
+
test('exact device/entity mapping yields local lines and a deduplicated remote count', () => {
const topology = normalizeZhaTopology([
{ ieee: '00124b0000000001', device_reg_id: 'da', neighbors: [