feat: show Zigbee routes to coordinator

Issue: #457
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-05 10:50:19 +03:00
parent d13ecf1237
commit 793d06c75e
48 changed files with 932 additions and 377 deletions
@@ -1,182 +1,182 @@
{
"schema": 1,
"fingerprint": "814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406",
"fingerprint": "533869cf64f877c1c61b0449fdb3106eecd4ce3c08188e9e7694ee3c11478520",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/houseplan-card-BfEhiIMd.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 297478,
"initialViewGzipBytes": 297613,
"lazyFiles": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/de-CFOjLpp6.js",
"houseplan-assets/editor-DsfH0y9C.js",
"houseplan-assets/fr-D_LHTTjd.js",
"houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js",
"houseplan-assets/hp-zigbee-topology-overlay-BrSsp4IO.js",
"houseplan-assets/live-interaction-runtime-BLkoEJ76.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/de-BcyCnUvK.js",
"houseplan-assets/editor-BmoXysmF.js",
"houseplan-assets/fr-BQv8XYpX.js",
"houseplan-assets/houseplan-editor-runtime-OM_-QLOC.js",
"houseplan-assets/houseplan-onboarding-runtime-C6c_QeHE.js",
"houseplan-assets/hp-zigbee-topology-overlay-Q8RXHre6.js",
"houseplan-assets/live-interaction-runtime-D_Cpramq.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js",
"houseplan-assets/zigbee-topology-runtime-B2u6gOwO.js"
],
"lazyGzipBytes": 244317,
"lazyGzipBytes": 246104,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/editor-DsfH0y9C.js",
"houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/editor-BmoXysmF.js",
"houseplan-assets/houseplan-editor-runtime-OM_-QLOC.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js"
],
"lazyEditorGzipBytes": 181158,
"lazyEditorGzipBytes": 182097,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/houseplan-onboarding-runtime-C6c_QeHE.js"
],
"lazyOnboardingGzipBytes": 14317,
"lazyOnboardingGzipBytes": 14314,
"lazyLocaleFiles": [
"houseplan-assets/de-CFOjLpp6.js",
"houseplan-assets/fr-D_LHTTjd.js"
"houseplan-assets/de-BcyCnUvK.js",
"houseplan-assets/fr-BQv8XYpX.js"
],
"lazyLocaleGzipBytes": 49942,
"lazyLocaleGzipBytes": 49944,
"files": [
{
"path": "houseplan-assets/backdrop-pick-j3BiTfHY.js",
"sha256": "2d2f927c8cb8b5e89217dbb4f4493250618eb85c52142ea97100dbd99159f298",
"path": "houseplan-assets/backdrop-pick-CqP55HFV.js",
"sha256": "c756470e82ef38e899c948896a49dd42cfc830edf400e345cacf6dd6ae3b3708",
"rawBytes": 21592,
"gzipBytes": 7359,
"gzipBytes": 7356,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CFOjLpp6.js",
"sha256": "80bf83a74e97e2715bde9b7a65c9202e013d7864ab24997855b2eff8eeb1e46c",
"path": "houseplan-assets/de-BcyCnUvK.js",
"sha256": "8895e7e4ae7e81197a335d4f27ed2d7576a233c9df3d0ed8532775af9b719302",
"rawBytes": 83907,
"gzipBytes": 25246,
"gzipBytes": 25247,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DsfH0y9C.js",
"sha256": "622afb272e44b005bb5070b8acb6f76c469069fbcd6e6399c0c1437425490017",
"path": "houseplan-assets/editor-BmoXysmF.js",
"sha256": "eb7e71dffa805f11f3ec6d44263a9719a310ec26585602e67517e3ec4b97aa75",
"rawBytes": 3831,
"gzipBytes": 1583,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-D_LHTTjd.js",
"sha256": "1a5256323034c314188f078265e27fb7453af4b4f75c38c833eca3685c637fef",
"path": "houseplan-assets/fr-BQv8XYpX.js",
"sha256": "772df4e7681d72395c6dfb16a99d53b738857792f92f158bf929dc717c01a33f",
"rawBytes": 85967,
"gzipBytes": 24696,
"gzipBytes": 24697,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-5IGoG-3Y.js",
"sha256": "6bf3f534f13750e9a667c534e6b389314a6fe9332ae96ed4502c6ac8a6c72b12",
"rawBytes": 1058121,
"gzipBytes": 296682,
"path": "houseplan-assets/houseplan-card-BfEhiIMd.js",
"sha256": "ec413ccf39fb459a872cad19feba2a26bb5e4918aed30231b4226be29a13279d",
"rawBytes": 1058523,
"gzipBytes": 296817,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CFOjLpp6.js",
"houseplan-assets/editor-DsfH0y9C.js",
"houseplan-assets/fr-D_LHTTjd.js",
"houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js",
"houseplan-assets/hp-zigbee-topology-overlay-BrSsp4IO.js",
"houseplan-assets/live-interaction-runtime-BLkoEJ76.js"
"houseplan-assets/de-BcyCnUvK.js",
"houseplan-assets/editor-BmoXysmF.js",
"houseplan-assets/fr-BQv8XYpX.js",
"houseplan-assets/houseplan-editor-runtime-OM_-QLOC.js",
"houseplan-assets/houseplan-onboarding-runtime-C6c_QeHE.js",
"houseplan-assets/hp-zigbee-topology-overlay-Q8RXHre6.js",
"houseplan-assets/live-interaction-runtime-D_Cpramq.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"sha256": "453615ec48b49e236b31662c34545f1ce10f7e564b9c176ddcefd4d021ebeb23",
"path": "houseplan-assets/houseplan-editor-runtime-OM_-QLOC.js",
"sha256": "f33eacfaeb67ebd970cd896a5c735546544ab681b9a8d70db3292e3e5f381c47",
"rawBytes": 630790,
"gzipBytes": 167369,
"gzipBytes": 167372,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/houseplan-card-BfEhiIMd.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
"houseplan-assets/zigbee-topology-runtime-B2u6gOwO.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js",
"sha256": "162cf2cd86f30e0201a52c01e75347a12fda32b6b37c49985fec792aa9825cab",
"path": "houseplan-assets/houseplan-onboarding-runtime-C6c_QeHE.js",
"sha256": "166dadd4a99098e6e87ed060612f18acaae35bfb9151a9b48320aa47ca0ffdff",
"rawBytes": 28088,
"gzipBytes": 6958,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-BrSsp4IO.js",
"sha256": "6f88cb32ee1fe3080e55489e7363e32e9e0dac2954cc69de943f62bd57de988e",
"rawBytes": 6006,
"gzipBytes": 2371,
"path": "houseplan-assets/hp-zigbee-topology-overlay-Q8RXHre6.js",
"sha256": "3b63bfe2f80678fa073e0ef48d5a240270b3a88048991604a5cef42a21d3da2b",
"rawBytes": 8593,
"gzipBytes": 3217,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js",
"houseplan-assets/zigbee-topology-runtime-B2u6gOwO.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-BLkoEJ76.js",
"sha256": "9b808162204030f58ff08fb21879d8b633fe701d1c1b5c6113d102093289a99e",
"path": "houseplan-assets/live-interaction-runtime-D_Cpramq.js",
"sha256": "3ee5dbdc02acc76aa11e1320914fef02e6c71bf919c7720b6cfe7bfe04555306",
"rawBytes": 5196,
"gzipBytes": 2126,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"sha256": "688604a9e2f9348c00619e20aeff3a6cb3024ca331fa655995cd714105ebba99",
"rawBytes": 12382,
"gzipBytes": 4847,
"path": "houseplan-assets/zigbee-topology-Bvg-A1og.js",
"sha256": "c3242a28cb4ca5e141cef9b25ef6f26811bd484bb1277ddfbfacae138d52413b",
"rawBytes": 15252,
"gzipBytes": 5786,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js",
"sha256": "cf3c4a14ae1eecd545c2cf09fcce42fec057f9498ed761fe295eaa087c9e4466",
"path": "houseplan-assets/zigbee-topology-runtime-B2u6gOwO.js",
"sha256": "3a4346efc23ebe2ad722bc61dbccaa21531da9baadb23e052dbcded193b07458",
"rawBytes": 3941,
"gzipBytes": 1762,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "9412b5f3b0c7fd701d32528f84d3c74277e63c9f1ea0ff73edbc7de930af213e",
"sha256": "0dd55163b547b8bcc1caff73c34f7a8cb6c8c4cc66fed7f5d7b8a7eada664cfb",
"rawBytes": 1183,
"gzipBytes": 796,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";import{v as e,l as o,z as t,dt as s,A as a,du as i,dv as l,dw as n,E as r,c}from"./houseplan-card-5IGoG-3Y.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__="533869cf64f877c1c61b0449fdb3106eecd4ce3c08188e9e7694ee3c11478520";import{v as e,l as o,z as t,dt as s,A as a,du as i,dv as l,dw as n,E as r,c}from"./houseplan-card-BfEhiIMd.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
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
@@ -1,36 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";import{v as e,A as t,dx as i,w as s,c as o,l as r,u as n}from"./houseplan-card-5IGoG-3Y.js";import{a,r as h,t as d}from"./zigbee-topology-rpeu0cwQ.js";import{zigbeeTopologyRuntimeSnapshot as p,subscribeZigbeeTopology as c}from"./zigbee-topology-runtime-D_aGI6IY.js";const l={revision:0,topologies:[],states:{}};class v extends e{constructor(){super(...arguments),this.devices=[],this.currentSpace="",this._runtime=l,this._hovered="",this._pointerOver=e=>{if(!this._mouseAllowed(e))return void this._clear();const t=this._deviceFromEvent(e),i=t?.dataset.id||"";i&&i!==this._hovered&&(this._hovered=i,this.requestUpdate())},this._pointerOut=e=>{const t=this._deviceFromEvent(e);if(!t||t.dataset.id!==this._hovered)return;const i=e.relatedTarget instanceof Element?e.relatedTarget.closest('[data-hp="device"]'):null;i?.dataset.id!==this._hovered&&this._clear()},this._pointerDown=e=>{"mouse"!==e.pointerType&&this._clear()}}connectedCallback(){super.connectedCallback();const e=this.getRootNode();e.host&&"undefined"!=typeof MutationObserver&&(this._hoverGateObserver=new MutationObserver(()=>{e.host.hasAttribute("data-pointer-hover")||this._clear()}),this._hoverGateObserver.observe(e.host,{attributes:!0,attributeFilter:["data-pointer-hover"]})),queueMicrotask(()=>this._connectParent())}disconnectedCallback(){this._release?.(),this._release=void 0,this._disconnectParent(),this._hoverGateObserver?.disconnect(),this._hoverGateObserver=void 0,this._hovered="",super.disconnectedCallback()}updated(e){e.has("hass")&&(this._release?.(),this._runtime=p(this.hass),this._mappedMemo=void 0,this._release=c(this.hass,()=>{this._runtime=p(this.hass),this._mappedMemo=void 0,this.requestUpdate()})),(e.has("currentSpace")||e.has("devices")||e.has("registry"))&&(this._mappedMemo=void 0,this.devices.some(e=>e.id===this._hovered&&e.space===this.currentSpace)||(this._hovered=""))}_connectParent(){const e=this.parentElement;e&&e!==this._parent&&(this._disconnectParent(),this._parent=e,e.addEventListener("pointerover",this._pointerOver),e.addEventListener("pointerout",this._pointerOut),e.addEventListener("pointerdown",this._pointerDown,!0))}_disconnectParent(){this._parent?.removeEventListener("pointerover",this._pointerOver),this._parent?.removeEventListener("pointerout",this._pointerOut),this._parent?.removeEventListener("pointerdown",this._pointerDown,!0),this._parent=void 0}_deviceFromEvent(e){for(const t of e.composedPath())if(t instanceof HTMLElement&&"device"===t.dataset.hp)return t;return null}_mouseAllowed(e){const t=this.getRootNode();return"mouse"===e.pointerType&&t.host?.hasAttribute?.("data-pointer-hover")}_clear(){this._hovered&&(this._hovered="",this.requestUpdate())}_position(e){const t=[...this._parent?.querySelectorAll(".dev[data-id]")||[]].find(t=>t.dataset.id===e);if(!t)return null;const i=Number.parseFloat(t.style.left),s=Number.parseFloat(t.style.top),o=t.getBoundingClientRect();return Number.isFinite(i)&&Number.isFinite(s)?{x:i,y:s,width:o.width,height:o.height}:null}render(){if(!this._hovered||!this.registry||!this._runtime.topologies.length)return t;const e=this._mappedMemo,n=e&&e.runtime===this._runtime&&e.devices===this.devices&&e.registry===this.registry?e.mapped:a(this._runtime.topologies,this.devices,this.registry);e&&n===e.mapped||(this._mappedMemo={runtime:this._runtime,devices:this.devices,registry:this.registry,mapped:n});const p=h(n,this.currentSpace,this._hovered),c=this._position(this._hovered);if(!c)return t;const l=p.lines.map(e=>({...e,point:this._position(e.neighborMarkerId)})).filter(e=>!!e.point);return o`
${l.length?s`<svg viewBox="0 0 100 100" preserveAspectRatio="none"
aria-hidden="true" data-hp="zigbee-topology-lines">
${l.map(e=>s`<line x1=${c.x} y1=${c.y}
x2=${e.point.x} y2=${e.point.y}
stroke=${void 0===e.lqi?"rgba(145,155,165,.85)":i(e.lqi)}
stroke-width=${void 0===e.lqi?2:2.2}
stroke-dasharray=${void 0===e.lqi?"5 5":t} opacity=".9"></line>`)}
</svg>`:t}
${l.map(e=>o`<div class="halo" data-hp="zigbee-topology-neighbor"
data-id=${e.neighborMarkerId} style="left:${e.point.x}%;top:${e.point.y}%;width:${1.22*e.point.width}px;height:${1.22*e.point.height}px"></div>`)}
${p.remoteCount?o`<div class="remote" data-hp="zigbee-topology-remote"
style="left:${c.x}%;top:${c.y}%">${d(r(this.hass),"remote_count",{n:p.remoteCount})}</div>`:t}
`}}v.properties={hass:{attribute:!1},devices:{attribute:!1},registry:{attribute:!1},currentSpace:{type:String,attribute:"current-space"},viewKey:{attribute:!1}},v.styles=n`
:host { position: absolute; inset: 0; z-index: 5; display: block; pointer-events: none; }
svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
line { vector-effect: non-scaling-stroke; stroke-linecap: round; }
.halo {
position: absolute; width: calc(var(--device-base-size, 4cqw) * 1.22);
height: calc(var(--device-base-size, 4cqw) * 1.22); transform: translate(-50%, -50%);
box-sizing: border-box; border: 2px solid rgba(120, 190, 220, .82); border-radius: 50%;
box-shadow: 0 0 0 4px rgba(120, 190, 220, .16);
}
.remote {
position: absolute; transform: translate(12px, calc(-100% - 12px));
border: 1px solid rgba(255,255,255,.7); border-radius: 999px;
padding: 3px 7px; color: #fff; background: rgba(28,31,36,.88);
box-shadow: 0 2px 7px rgba(0,0,0,.28); white-space: nowrap;
font: 600 11px/1.2 system-ui, sans-serif;
}
@media (forced-colors: active) {
line { stroke: Highlight !important; }
.halo { border-color: Highlight; box-shadow: none; }
.remote { color: CanvasText; background: Canvas; border-color: CanvasText; }
}
`,customElements.get("hp-zigbee-topology-overlay")||customElements.define("hp-zigbee-topology-overlay",v);export{v as HpZigbeeTopologyOverlay};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";import{n as t,b as n}from"./zigbee-topology-rpeu0cwQ.js";import{I as e}from"./houseplan-card-5IGoG-3Y.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__="533869cf64f877c1c61b0449fdb3106eecd4ce3c08188e9e7694ee3c11478520";import{n as t,b as n}from"./zigbee-topology-Bvg-A1og.js";import{I as e}from"./houseplan-card-BfEhiIMd.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__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";try{await import("./houseplan-assets/houseplan-card-5IGoG-3Y.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__="533869cf64f877c1c61b0449fdb3106eecd4ce3c08188e9e7694ee3c11478520";try{await import("./houseplan-assets/houseplan-card-BfEhiIMd.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)}
+77 -21
View File
@@ -4,11 +4,11 @@ const { page, browser } = await launch();
const result = await page.evaluate(async () => {
const card = window.__card;
const root = () => card.shadowRoot || card.renderRoot;
const wait = (predicate, timeout = 3000) => new Promise((resolve, reject) => {
const wait = (predicate, label, timeout = 3000) => new Promise((resolve, reject) => {
const started = performance.now();
const tick = () => {
if (predicate()) return resolve();
if (performance.now() - started > timeout) return reject(new Error('topology smoke timeout'));
if (performance.now() - started > timeout) return reject(new Error(`topology smoke timeout: ${label}`));
setTimeout(tick, 20);
};
tick();
@@ -29,37 +29,46 @@ const result = await page.evaluate(async () => {
if (message.type !== 'zha/devices') return originalHass.callWS(message);
zhaCalls++;
return [
{ ieee: '00124b0000000001', device_reg_id: 'd_light1', neighbors: [
{ ieee: '00124b0000000002', lqi: 180 },
{ ieee: '00124b0000000003', lqi: 80 },
{ ieee: '00124b0000000001', device_reg_id: 'd_light1', device_type: 'Router', neighbors: [
{ ieee: '00124b0000000002', lqi: 180, relationship: 'Parent' },
{ ieee: '00124b0000000003', lqi: 80, relationship: 'Child' },
] },
{ ieee: '00124b0000000002', device_reg_id: 'd_lamp', neighbors: [] },
{ ieee: '00124b0000000003', device_reg_id: 'd_mower', neighbors: [] },
{ ieee: '00124b0000000004', device_reg_id: 'd_temp', neighbors: [
{ ieee: '00124b0000000005', lqi: 20 },
{ ieee: '00124b0000000002', device_reg_id: 'd_lamp', device_type: 'Coordinator', neighbors: [
{ ieee: '00124b0000000001', lqi: 170, relationship: 'Child' },
{ ieee: '00124b0000000006', lqi: 140, relationship: 'Child' },
] },
{ ieee: '00124b0000000003', device_reg_id: 'd_mower', device_type: 'EndDevice', neighbors: [
{ ieee: '00124b0000000001', lqi: 75, relationship: 'Parent' },
] },
{ ieee: '00124b0000000004', device_reg_id: 'd_temp', device_type: 'EndDevice', neighbors: [
{ ieee: '00124b0000000006', lqi: 20, relationship: 'Parent' },
] },
{ ieee: '00124b0000000005', device_reg_id: 'd_tv', device_type: 'EndDevice', neighbors: [] },
{ ieee: '00124b0000000006', device_reg_id: 'not_on_plan', device_type: 'Router', neighbors: [
{ ieee: '00124b0000000002', lqi: 130, relationship: 'Parent' },
{ ieee: '00124b0000000004', lqi: 25, relationship: 'Child' },
] },
{ ieee: '00124b0000000005', device_reg_id: 'd_tv', neighbors: [] },
];
} };
await card._ensureEditorRuntime();
card._openSettingsDialog();
await card.updateComplete;
let settings = root().querySelector('hp-zigbee-topology-settings');
await wait(() => !!settings?.shadowRoot);
await wait(() => !!settings?.shadowRoot, 'settings mounted');
settings._emit({ enabled: true, z2mBaseTopics: [] });
await card.updateComplete;
await card._saveSettingsDialog();
await card.updateComplete;
await wait(() => !!root().querySelector('hp-zigbee-topology-overlay'));
await wait(() => !!root().querySelector('hp-zigbee-topology-overlay'), 'overlay mounted');
out.settingPersists = card._serverCfg.settings.zigbee_topology?.enabled === true;
out.enableDoesNotFetch = zhaCalls === 0;
card._openSettingsDialog();
await card.updateComplete;
settings = root().querySelector('hp-zigbee-topology-settings');
await wait(() => !!settings?.shadowRoot?.querySelector('button'));
await wait(() => !!settings?.shadowRoot?.querySelector('button'), 'settings button');
await settings._readZha();
await wait(() => settings._snapshot?.states?.zha?.phase === 'ready');
await wait(() => settings._snapshot?.states?.zha?.phase === 'ready', 'ZHA ready');
out.explicitZhaRead = zhaCalls === 1;
card._settingsDialog = null;
card.requestUpdate();
@@ -68,25 +77,72 @@ const result = await page.evaluate(async () => {
const source = root().querySelector('.dev[data-id="d_light1"]');
source.dispatchEvent(mouse('pointerover'));
await wait(() => root().querySelector('hp-zigbee-topology-overlay')?.shadowRoot
?.querySelector('[data-hp="zigbee-topology-lines"]'));
?.querySelector('[data-hp="zigbee-topology-lines"]'), 'local route');
const overlay = root().querySelector('hp-zigbee-topology-overlay');
out.incidentOnly = overlay.shadowRoot.querySelectorAll('line').length === 1
&& overlay.shadowRoot.querySelectorAll('[data-hp="zigbee-topology-neighbor"]').length === 1;
const routeLine = overlay.shadowRoot.querySelector('line[data-direction="toward-neighbor"]');
const routeArrow = overlay.shadowRoot.querySelector('[data-hp="zigbee-topology-arrow"]');
const lineBox = routeLine?.getBoundingClientRect();
const arrowBox = routeArrow?.getBoundingClientRect();
out.localRouteArrow = !!routeLine && routeArrow?.getAttribute('data-direction') === 'toward-neighbor'
&& !!lineBox && !!arrowBox && arrowBox.width > 3 && arrowBox.height > 3;
out.crossSpaceCount = overlay.shadowRoot.querySelector('[data-hp="zigbee-topology-remote"]')
?.textContent.trim() === '+1 in other spaces';
out.pointerTransparent = getComputedStyle(overlay).pointerEvents === 'none'
&& getComputedStyle(overlay.shadowRoot.querySelector('[data-hp="zigbee-topology-remote"]')).pointerEvents === 'none';
source.dispatchEvent(mouse('pointerout', root().querySelector('.stage')));
card._commitSpace('garden', true);
card.requestUpdate();
await card.updateComplete;
out.leaveClears = !overlay.shadowRoot.querySelector('line');
source.dispatchEvent(mouse('pointerover'));
await wait(() => overlay.shadowRoot.querySelector('line'));
await wait(() => root().querySelector('.dev[data-id="d_mower"]'), 'garden marker');
const remoteChild = root().querySelector('.dev[data-id="d_mower"]');
remoteChild.dispatchEvent(mouse('pointerover'));
await wait(() => root().querySelector('hp-zigbee-topology-overlay')?.shadowRoot
?.querySelector('[data-hp="zigbee-topology-parent-bubble"]'), 'remote parent bubble');
let activeOverlay = root().querySelector('hp-zigbee-topology-overlay');
const firstTitle = card._serverCfg.spaces.find((space) => space.id === 'f1')?.title;
out.remoteParentBubble = activeOverlay.shadowRoot
.querySelector('[data-hp="zigbee-topology-parent-bubble"]')?.textContent.trim() === firstTitle
&& activeOverlay.shadowRoot.querySelector('[data-hp="zigbee-topology-parent-bubble"]')
?.getAttribute('data-kind') === 'remote-space'
&& !!activeOverlay.shadowRoot.querySelector('[data-hp="zigbee-topology-parent-arrow"]')
&& !activeOverlay.shadowRoot.querySelector('[data-hp="zigbee-topology-remote"]');
card._commitSpace('f1', true);
card.requestUpdate();
await card.updateComplete;
await wait(() => root().querySelector('.dev[data-id="d_temp"]'), 'temperature marker');
const temperature = root().querySelector('.dev[data-id="d_temp"]');
temperature.dispatchEvent(mouse('pointerover'));
await wait(() => root().querySelector('hp-zigbee-topology-overlay')?.shadowRoot
?.querySelector('[data-kind="unplaced-device"]'), 'unplaced device bubble');
activeOverlay = root().querySelector('hp-zigbee-topology-overlay');
out.unplacedDeviceBubble = activeOverlay.shadowRoot.querySelector('[data-kind="unplaced-device"]')
?.textContent.trim() === 'device is not on the plan';
activeOverlay.devices = activeOverlay.devices.map((device) => (
device.id === 'd_lamp' ? { ...device, hidden: true } : device
));
await activeOverlay.updateComplete;
const router = root().querySelector('.dev[data-id="d_light1"]');
router.dispatchEvent(mouse('pointerover'));
await wait(() => activeOverlay.shadowRoot.querySelector('[data-kind="unplaced-coordinator"]'),
'unplaced coordinator bubble');
out.unplacedCoordinatorBubble = activeOverlay.shadowRoot
.querySelector('[data-kind="unplaced-coordinator"]')?.textContent.trim()
=== 'coordinator is not on the plan';
router.dispatchEvent(mouse('pointerout', root().querySelector('.stage')));
await activeOverlay.updateComplete;
out.leaveClears = !activeOverlay.shadowRoot.querySelector('line');
router.dispatchEvent(mouse('pointerover'));
await wait(() => activeOverlay.shadowRoot.querySelector('line'), 'route restored');
root().querySelector('.stage').dispatchEvent(new PointerEvent('pointermove', {
pointerType: 'touch', bubbles: true, composed: true, pointerId: 7,
}));
await wait(() => !overlay.shadowRoot.querySelector('line'));
out.touchClears = !overlay.shadowRoot.querySelector('line');
await wait(() => !activeOverlay.shadowRoot.querySelector('line'), 'touch clears');
out.touchClears = !activeOverlay.shadowRoot.querySelector('line');
card._setMode('plan');
await card.updateComplete;
+81 -81
View File
@@ -1,182 +1,182 @@
{
"schema": 1,
"fingerprint": "814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406",
"fingerprint": "533869cf64f877c1c61b0449fdb3106eecd4ce3c08188e9e7694ee3c11478520",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/houseplan-card-BfEhiIMd.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 297478,
"initialViewGzipBytes": 297613,
"lazyFiles": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/de-CFOjLpp6.js",
"houseplan-assets/editor-DsfH0y9C.js",
"houseplan-assets/fr-D_LHTTjd.js",
"houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js",
"houseplan-assets/hp-zigbee-topology-overlay-BrSsp4IO.js",
"houseplan-assets/live-interaction-runtime-BLkoEJ76.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/de-BcyCnUvK.js",
"houseplan-assets/editor-BmoXysmF.js",
"houseplan-assets/fr-BQv8XYpX.js",
"houseplan-assets/houseplan-editor-runtime-OM_-QLOC.js",
"houseplan-assets/houseplan-onboarding-runtime-C6c_QeHE.js",
"houseplan-assets/hp-zigbee-topology-overlay-Q8RXHre6.js",
"houseplan-assets/live-interaction-runtime-D_Cpramq.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js",
"houseplan-assets/zigbee-topology-runtime-B2u6gOwO.js"
],
"lazyGzipBytes": 244317,
"lazyGzipBytes": 246104,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/editor-DsfH0y9C.js",
"houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/editor-BmoXysmF.js",
"houseplan-assets/houseplan-editor-runtime-OM_-QLOC.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js"
],
"lazyEditorGzipBytes": 181158,
"lazyEditorGzipBytes": 182097,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/houseplan-onboarding-runtime-C6c_QeHE.js"
],
"lazyOnboardingGzipBytes": 14317,
"lazyOnboardingGzipBytes": 14314,
"lazyLocaleFiles": [
"houseplan-assets/de-CFOjLpp6.js",
"houseplan-assets/fr-D_LHTTjd.js"
"houseplan-assets/de-BcyCnUvK.js",
"houseplan-assets/fr-BQv8XYpX.js"
],
"lazyLocaleGzipBytes": 49942,
"lazyLocaleGzipBytes": 49944,
"files": [
{
"path": "houseplan-assets/backdrop-pick-j3BiTfHY.js",
"sha256": "2d2f927c8cb8b5e89217dbb4f4493250618eb85c52142ea97100dbd99159f298",
"path": "houseplan-assets/backdrop-pick-CqP55HFV.js",
"sha256": "c756470e82ef38e899c948896a49dd42cfc830edf400e345cacf6dd6ae3b3708",
"rawBytes": 21592,
"gzipBytes": 7359,
"gzipBytes": 7356,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CFOjLpp6.js",
"sha256": "80bf83a74e97e2715bde9b7a65c9202e013d7864ab24997855b2eff8eeb1e46c",
"path": "houseplan-assets/de-BcyCnUvK.js",
"sha256": "8895e7e4ae7e81197a335d4f27ed2d7576a233c9df3d0ed8532775af9b719302",
"rawBytes": 83907,
"gzipBytes": 25246,
"gzipBytes": 25247,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DsfH0y9C.js",
"sha256": "622afb272e44b005bb5070b8acb6f76c469069fbcd6e6399c0c1437425490017",
"path": "houseplan-assets/editor-BmoXysmF.js",
"sha256": "eb7e71dffa805f11f3ec6d44263a9719a310ec26585602e67517e3ec4b97aa75",
"rawBytes": 3831,
"gzipBytes": 1583,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-D_LHTTjd.js",
"sha256": "1a5256323034c314188f078265e27fb7453af4b4f75c38c833eca3685c637fef",
"path": "houseplan-assets/fr-BQv8XYpX.js",
"sha256": "772df4e7681d72395c6dfb16a99d53b738857792f92f158bf929dc717c01a33f",
"rawBytes": 85967,
"gzipBytes": 24696,
"gzipBytes": 24697,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-5IGoG-3Y.js",
"sha256": "6bf3f534f13750e9a667c534e6b389314a6fe9332ae96ed4502c6ac8a6c72b12",
"rawBytes": 1058121,
"gzipBytes": 296682,
"path": "houseplan-assets/houseplan-card-BfEhiIMd.js",
"sha256": "ec413ccf39fb459a872cad19feba2a26bb5e4918aed30231b4226be29a13279d",
"rawBytes": 1058523,
"gzipBytes": 296817,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CFOjLpp6.js",
"houseplan-assets/editor-DsfH0y9C.js",
"houseplan-assets/fr-D_LHTTjd.js",
"houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js",
"houseplan-assets/hp-zigbee-topology-overlay-BrSsp4IO.js",
"houseplan-assets/live-interaction-runtime-BLkoEJ76.js"
"houseplan-assets/de-BcyCnUvK.js",
"houseplan-assets/editor-BmoXysmF.js",
"houseplan-assets/fr-BQv8XYpX.js",
"houseplan-assets/houseplan-editor-runtime-OM_-QLOC.js",
"houseplan-assets/houseplan-onboarding-runtime-C6c_QeHE.js",
"houseplan-assets/hp-zigbee-topology-overlay-Q8RXHre6.js",
"houseplan-assets/live-interaction-runtime-D_Cpramq.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"sha256": "453615ec48b49e236b31662c34545f1ce10f7e564b9c176ddcefd4d021ebeb23",
"path": "houseplan-assets/houseplan-editor-runtime-OM_-QLOC.js",
"sha256": "f33eacfaeb67ebd970cd896a5c735546544ab681b9a8d70db3292e3e5f381c47",
"rawBytes": 630790,
"gzipBytes": 167369,
"gzipBytes": 167372,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/houseplan-card-BfEhiIMd.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
"houseplan-assets/zigbee-topology-runtime-B2u6gOwO.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js",
"sha256": "162cf2cd86f30e0201a52c01e75347a12fda32b6b37c49985fec792aa9825cab",
"path": "houseplan-assets/houseplan-onboarding-runtime-C6c_QeHE.js",
"sha256": "166dadd4a99098e6e87ed060612f18acaae35bfb9151a9b48320aa47ca0ffdff",
"rawBytes": 28088,
"gzipBytes": 6958,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/backdrop-pick-CqP55HFV.js",
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-BrSsp4IO.js",
"sha256": "6f88cb32ee1fe3080e55489e7363e32e9e0dac2954cc69de943f62bd57de988e",
"rawBytes": 6006,
"gzipBytes": 2371,
"path": "houseplan-assets/hp-zigbee-topology-overlay-Q8RXHre6.js",
"sha256": "3b63bfe2f80678fa073e0ef48d5a240270b3a88048991604a5cef42a21d3da2b",
"rawBytes": 8593,
"gzipBytes": 3217,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js",
"houseplan-assets/zigbee-topology-runtime-B2u6gOwO.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-BLkoEJ76.js",
"sha256": "9b808162204030f58ff08fb21879d8b633fe701d1c1b5c6113d102093289a99e",
"path": "houseplan-assets/live-interaction-runtime-D_Cpramq.js",
"sha256": "3ee5dbdc02acc76aa11e1320914fef02e6c71bf919c7720b6cfe7bfe04555306",
"rawBytes": 5196,
"gzipBytes": 2126,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"sha256": "688604a9e2f9348c00619e20aeff3a6cb3024ca331fa655995cd714105ebba99",
"rawBytes": 12382,
"gzipBytes": 4847,
"path": "houseplan-assets/zigbee-topology-Bvg-A1og.js",
"sha256": "c3242a28cb4ca5e141cef9b25ef6f26811bd484bb1277ddfbfacae138d52413b",
"rawBytes": 15252,
"gzipBytes": 5786,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js",
"sha256": "cf3c4a14ae1eecd545c2cf09fcce42fec057f9498ed761fe295eaa087c9e4466",
"path": "houseplan-assets/zigbee-topology-runtime-B2u6gOwO.js",
"sha256": "3a4346efc23ebe2ad722bc61dbccaa21531da9baadb23e052dbcded193b07458",
"rawBytes": 3941,
"gzipBytes": 1762,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
"houseplan-assets/houseplan-card-BfEhiIMd.js",
"houseplan-assets/zigbee-topology-Bvg-A1og.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "9412b5f3b0c7fd701d32528f84d3c74277e63c9f1ea0ff73edbc7de930af213e",
"sha256": "0dd55163b547b8bcc1caff73c34f7a8cb6c8c4cc66fed7f5d7b8a7eada664cfb",
"rawBytes": 1183,
"gzipBytes": 796,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
"houseplan-assets/houseplan-card-BfEhiIMd.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,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";import{v as e,l as o,z as t,dt as s,A as a,du as i,dv as l,dw as n,E as r,c}from"./houseplan-card-5IGoG-3Y.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__="533869cf64f877c1c61b0449fdb3106eecd4ce3c08188e9e7694ee3c11478520";import{v as e,l as o,z as t,dt as s,A as a,du as i,dv as l,dw as n,E as r,c}from"./houseplan-card-BfEhiIMd.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
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,36 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";import{v as e,A as t,dx as i,w as s,c as o,l as r,u as n}from"./houseplan-card-5IGoG-3Y.js";import{a,r as h,t as d}from"./zigbee-topology-rpeu0cwQ.js";import{zigbeeTopologyRuntimeSnapshot as p,subscribeZigbeeTopology as c}from"./zigbee-topology-runtime-D_aGI6IY.js";const l={revision:0,topologies:[],states:{}};class v extends e{constructor(){super(...arguments),this.devices=[],this.currentSpace="",this._runtime=l,this._hovered="",this._pointerOver=e=>{if(!this._mouseAllowed(e))return void this._clear();const t=this._deviceFromEvent(e),i=t?.dataset.id||"";i&&i!==this._hovered&&(this._hovered=i,this.requestUpdate())},this._pointerOut=e=>{const t=this._deviceFromEvent(e);if(!t||t.dataset.id!==this._hovered)return;const i=e.relatedTarget instanceof Element?e.relatedTarget.closest('[data-hp="device"]'):null;i?.dataset.id!==this._hovered&&this._clear()},this._pointerDown=e=>{"mouse"!==e.pointerType&&this._clear()}}connectedCallback(){super.connectedCallback();const e=this.getRootNode();e.host&&"undefined"!=typeof MutationObserver&&(this._hoverGateObserver=new MutationObserver(()=>{e.host.hasAttribute("data-pointer-hover")||this._clear()}),this._hoverGateObserver.observe(e.host,{attributes:!0,attributeFilter:["data-pointer-hover"]})),queueMicrotask(()=>this._connectParent())}disconnectedCallback(){this._release?.(),this._release=void 0,this._disconnectParent(),this._hoverGateObserver?.disconnect(),this._hoverGateObserver=void 0,this._hovered="",super.disconnectedCallback()}updated(e){e.has("hass")&&(this._release?.(),this._runtime=p(this.hass),this._mappedMemo=void 0,this._release=c(this.hass,()=>{this._runtime=p(this.hass),this._mappedMemo=void 0,this.requestUpdate()})),(e.has("currentSpace")||e.has("devices")||e.has("registry"))&&(this._mappedMemo=void 0,this.devices.some(e=>e.id===this._hovered&&e.space===this.currentSpace)||(this._hovered=""))}_connectParent(){const e=this.parentElement;e&&e!==this._parent&&(this._disconnectParent(),this._parent=e,e.addEventListener("pointerover",this._pointerOver),e.addEventListener("pointerout",this._pointerOut),e.addEventListener("pointerdown",this._pointerDown,!0))}_disconnectParent(){this._parent?.removeEventListener("pointerover",this._pointerOver),this._parent?.removeEventListener("pointerout",this._pointerOut),this._parent?.removeEventListener("pointerdown",this._pointerDown,!0),this._parent=void 0}_deviceFromEvent(e){for(const t of e.composedPath())if(t instanceof HTMLElement&&"device"===t.dataset.hp)return t;return null}_mouseAllowed(e){const t=this.getRootNode();return"mouse"===e.pointerType&&t.host?.hasAttribute?.("data-pointer-hover")}_clear(){this._hovered&&(this._hovered="",this.requestUpdate())}_position(e){const t=[...this._parent?.querySelectorAll(".dev[data-id]")||[]].find(t=>t.dataset.id===e);if(!t)return null;const i=Number.parseFloat(t.style.left),s=Number.parseFloat(t.style.top),o=t.getBoundingClientRect();return Number.isFinite(i)&&Number.isFinite(s)?{x:i,y:s,width:o.width,height:o.height}:null}render(){if(!this._hovered||!this.registry||!this._runtime.topologies.length)return t;const e=this._mappedMemo,n=e&&e.runtime===this._runtime&&e.devices===this.devices&&e.registry===this.registry?e.mapped:a(this._runtime.topologies,this.devices,this.registry);e&&n===e.mapped||(this._mappedMemo={runtime:this._runtime,devices:this.devices,registry:this.registry,mapped:n});const p=h(n,this.currentSpace,this._hovered),c=this._position(this._hovered);if(!c)return t;const l=p.lines.map(e=>({...e,point:this._position(e.neighborMarkerId)})).filter(e=>!!e.point);return o`
${l.length?s`<svg viewBox="0 0 100 100" preserveAspectRatio="none"
aria-hidden="true" data-hp="zigbee-topology-lines">
${l.map(e=>s`<line x1=${c.x} y1=${c.y}
x2=${e.point.x} y2=${e.point.y}
stroke=${void 0===e.lqi?"rgba(145,155,165,.85)":i(e.lqi)}
stroke-width=${void 0===e.lqi?2:2.2}
stroke-dasharray=${void 0===e.lqi?"5 5":t} opacity=".9"></line>`)}
</svg>`:t}
${l.map(e=>o`<div class="halo" data-hp="zigbee-topology-neighbor"
data-id=${e.neighborMarkerId} style="left:${e.point.x}%;top:${e.point.y}%;width:${1.22*e.point.width}px;height:${1.22*e.point.height}px"></div>`)}
${p.remoteCount?o`<div class="remote" data-hp="zigbee-topology-remote"
style="left:${c.x}%;top:${c.y}%">${d(r(this.hass),"remote_count",{n:p.remoteCount})}</div>`:t}
`}}v.properties={hass:{attribute:!1},devices:{attribute:!1},registry:{attribute:!1},currentSpace:{type:String,attribute:"current-space"},viewKey:{attribute:!1}},v.styles=n`
:host { position: absolute; inset: 0; z-index: 5; display: block; pointer-events: none; }
svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
line { vector-effect: non-scaling-stroke; stroke-linecap: round; }
.halo {
position: absolute; width: calc(var(--device-base-size, 4cqw) * 1.22);
height: calc(var(--device-base-size, 4cqw) * 1.22); transform: translate(-50%, -50%);
box-sizing: border-box; border: 2px solid rgba(120, 190, 220, .82); border-radius: 50%;
box-shadow: 0 0 0 4px rgba(120, 190, 220, .16);
}
.remote {
position: absolute; transform: translate(12px, calc(-100% - 12px));
border: 1px solid rgba(255,255,255,.7); border-radius: 999px;
padding: 3px 7px; color: #fff; background: rgba(28,31,36,.88);
box-shadow: 0 2px 7px rgba(0,0,0,.28); white-space: nowrap;
font: 600 11px/1.2 system-ui, sans-serif;
}
@media (forced-colors: active) {
line { stroke: Highlight !important; }
.halo { border-color: Highlight; box-shadow: none; }
.remote { color: CanvasText; background: Canvas; border-color: CanvasText; }
}
`,customElements.get("hp-zigbee-topology-overlay")||customElements.define("hp-zigbee-topology-overlay",v);export{v as HpZigbeeTopologyOverlay};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";import{n as t,b as n}from"./zigbee-topology-rpeu0cwQ.js";import{I as e}from"./houseplan-card-5IGoG-3Y.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__="533869cf64f877c1c61b0449fdb3106eecd4ce3c08188e9e7694ee3c11478520";import{n as t,b as n}from"./zigbee-topology-Bvg-A1og.js";import{I as e}from"./houseplan-card-BfEhiIMd.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
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";try{await import("./houseplan-assets/houseplan-card-5IGoG-3Y.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__="533869cf64f877c1c61b0449fdb3106eecd4ce3c08188e9e7694ee3c11478520";try{await import("./houseplan-assets/houseplan-card-BfEhiIMd.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)}
+4
View File
@@ -2,6 +2,10 @@
## Unreleased
- Zigbee topology links now show arrows towards the coordinator on hover;
incoming arrows identify devices behind a router, while a short bubble
truthfully ends the path when the next node is in another space or is not
placed ([#457](https://github.com/Matysh/houseplan-card/issues/457)).
- Space settings can now create a room-free copy of the current space under a
new name, preserving walls, openings, columns, Background objects, image
transform, scale and display settings while leaving device placement behind;
+4
View File
@@ -8,6 +8,10 @@
## Не выпущено
- На линиях топологии Zigbee при наведении теперь видны стрелки в сторону
координатора; входящие стрелки показывают устройства за роутером, а если
следующий узел находится на другом плане или не размещён, короткая подпись
честно завершает путь ([#457](https://github.com/Matysh/houseplan-card/issues/457)).
- В настройках пространства теперь можно создать под новым именем копию без
комнат: стены, проёмы, колонны, элементы подложки, положение фонового плана,
масштаб и настройки отображения сохраняются, а размещение устройств не
+6
View File
@@ -73,6 +73,12 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
subset, stale controls never fall back, secure targets are no-op, and legacy
`cover`/absent light defaults round-trip losslessly. **Lights toggle by
default** (v1.39); other devices still default to the House Plan card.
- **Contextual Zigbee topology** (#54, refined by #457 on current dev): an
opt-in admin-only mouse hover shows direct neighbours without scanning. A
deterministic per-provider uplink tree now adds arrows towards the
coordinator while keeping non-route neighbour lines; a short bubble names a
remote space or explains that the next device/coordinator is not on the plan.
Touch, kiosk, editors and the static card remain unchanged.
- **Plan geometry**: polyline split (v1.32), island rooms w/ evenodd holes
(v1.34), smart guides + 45° angle badge (v1.40), opening hover preview.
Openings support doors, windows and compact wide gates; gates retain door
+12 -3
View File
@@ -211,9 +211,18 @@ may take 10 seconds to 2 minutes and can temporarily slow the Zigbee network.
After data is loaded, moving a real mouse over a mapped Zigbee marker shows
only its observed direct neighbours. Links to markers on the current space are
lines; drawable neighbours on other spaces are summarized as a temporary
count. This is a snapshot, not the route currently used by every packet. The
layer does not appear on touch/pen, in kiosk, in editors or in the static card,
and hovering never starts a scan.
count. An arrow on a line shows the next step towards the coordinator: an
ordinary device points to its parent, while arrows pointing into a router show
devices whose path goes through it. Neighbour links outside the derived path
tree remain plain lines.
If the next step is in another space, a short bubble names that space. If the
needed router or coordinator is not placed on the plan, the bubble says so. If
the snapshot has no coordinator or the graph is disconnected, House Plan does
not invent a direction and leaves the link without an arrow. This is a stable
path approximation derived from the neighbour snapshot, not the route used by
every current packet. The layer does not appear on touch/pen, in kiosk, in
editors or in the static card, and hovering never starts a scan.
Each editor has a stable primary toolbar. Tool parameters and selected-object
actions appear in a context tray over the top of the canvas. On a narrow screen
+13 -3
View File
@@ -223,9 +223,19 @@ Zigbee2MQTT для каждого указанного базового топи
После загрузки данных наведение реальной мыши на сопоставленный Zigbee-маркер
показывает только его наблюдаемых прямых соседей. Соседи в текущем пространстве
соединяются линиями, а доступные маркеры в других пространствах учитываются во
временном счётчике. Это снимок соседства, а не маршрут каждого текущего пакета.
На touch/pen, в киоске, редакторах и статической карточке слой не появляется;
само наведение никогда не запускает сканирование.
временном счётчике. Стрелка на линии показывает следующий шаг в сторону
координатора: у обычного устройства она направлена к родителю, а входящие
стрелки к роутеру показывают устройства, чей путь проходит через него. Связи,
не вошедшие в вычисленное дерево пути, остаются линиями без стрелок.
Если следующий шаг находится в другом пространстве, рядом с устройством
появляется его название. Если нужный роутер или координатор не размещён на
плане, короткая подпись сообщает об этом. При отсутствии координатора в снимке
или разрыве графа House Plan не выдумывает направление и оставляет линию без
стрелки. Это устойчивое приближение пути по снимку соседства, а не фактический
маршрут каждого текущего пакета. На touch/pen, в киоске, редакторах и
статической карточке слой не появляется; само наведение никогда не запускает
сканирование.
В редакторах сетка продолжается по всему рабочему холсту. В режиме просмотра сетка не показывается.
+33
View File
@@ -5837,6 +5837,39 @@ const MUTANT_DEFINITIONS = [
replace: ' try { void unsubscribe; } catch { /* cleanup is best effort */ }',
}],
},
{
id: 'zigbee-route-parent-keeps-bfs-level',
guard: 'node --test --test-name-pattern="uplink tree is deterministic" test/zigbee-topology.test.mjs',
because: 'every parent must be one BFS level nearer the coordinator; accepting a same-level '
+ 'neighbour can create a cycle and break the defining #457 AC1 invariant',
patches: [{
file: 'src/zigbee-topology.ts',
find: ' .filter(({ neighborKey }) => distances.get(neighborKey) === distance - 1)',
replace: ' .filter(({ neighborKey }) => distances.get(neighborKey) === distance)',
}],
},
{
id: 'zigbee-route-local-arrow-not-inverted',
guard: 'node --test --test-name-pattern="hover projects local route directions" test/zigbee-topology.test.mjs',
because: 'the arrow on the hovered device uplink must point to its parent rather than away '
+ 'from the coordinator (#457 AC4)',
patches: [{
file: 'src/zigbee-topology.ts',
find: " const direction = isParent ? 'toward-neighbor'",
replace: " const direction = isParent ? 'toward-origin'",
}],
},
{
id: 'zigbee-route-parent-not-counted-twice',
guard: 'node --test --test-name-pattern="hover projects local route directions" test/zigbee-topology.test.mjs',
because: 'a remote parent already has a named route bubble and must not also inflate the '
+ 'legacy cross-space neighbour count (#457 AC6)',
patches: [{
file: 'src/zigbee-topology.ts',
find: ' if (!isParent) remote.add(other.markerId);',
replace: ' remote.add(other.markerId);',
}],
},
{
id: 'double-fit-free-background-owner-removed',
guard: 'node --test --test-name-pattern="#449 only" test/room-fit.test.mjs',
+1 -1
View File
@@ -11779,7 +11779,7 @@ export class HouseplanCard extends LitElement {
<svg class="iso-walls-svg" data-hp-live-viewbox="camera" viewBox="${view.x} ${view.y} ${view.w} ${view.h}"
preserveAspectRatio="xMidYMid meet" aria-hidden="true" pointer-events="none">
${this._renderIsoWalls(isoLayers, isoPanels)}
</svg>` : nothing}${renderZigbeeTopologyOverlay({ hass: this.hass, settings: this._settings, devices: this._renderDevices, registry: this._haRegistry, currentSpace: space.id, viewKey: view, view: this._mode === 'view', kiosk: this._kiosk })}
</svg>` : nothing}${renderZigbeeTopologyOverlay({ hass: this.hass, settings: this._settings, devices: this._renderDevices, registry: this._haRegistry, currentSpace: space.id, spaces: this._serverCfg?.spaces || [], viewKey: view, view: this._mode === 'view', kiosk: this._kiosk })}
${''/* docs/CANVAS.md §6: an icon is a percentage of the PLAN and
scales with it when you zoom — the behaviour the card always
had, restored by the owner. `iconCqw` is `iconPct * iconUnit
+74 -14
View File
@@ -3,8 +3,10 @@ import { langOf } from './i18n';
import { lqiColor } from './logic';
import { topologyT } from './i18n/topology';
import {
mapTopologies, resolveMappedTopologyHover, type ZigbeeHoverResolution, type ZigbeeMappedTopology,
mapTopologies, resolveMappedTopologyHover, type ZigbeeMappedTopology,
type ZigbeeParentTarget,
} from './zigbee-topology';
import { zigbeeArrowGeometry, type ZigbeePixelPoint } from './zigbee-topology-geometry';
import {
subscribeZigbeeTopology, zigbeeTopologyRuntimeSnapshot,
type ZigbeeTopologyHass, type ZigbeeTopologyRuntimeSnapshot,
@@ -13,7 +15,7 @@ import type { HaRegistrySnapshot } from './ha-binding-status';
import type { DevItem } from './types';
const EMPTY_RUNTIME: ZigbeeTopologyRuntimeSnapshot = { revision: 0, topologies: [], states: {} };
const EMPTY_HOVER: ZigbeeHoverResolution = { lines: [], remoteCount: 0, omittedCount: 0 };
type MarkerPosition = ZigbeePixelPoint & { width: number; height: number };
export class HpZigbeeTopologyOverlay extends LitElement {
static properties = {
@@ -21,6 +23,7 @@ export class HpZigbeeTopologyOverlay extends LitElement {
devices: { attribute: false },
registry: { attribute: false },
currentSpace: { type: String, attribute: 'current-space' },
spaceTitles: { attribute: false },
viewKey: { attribute: false },
};
@@ -28,6 +31,7 @@ export class HpZigbeeTopologyOverlay extends LitElement {
devices: readonly DevItem[] = [];
registry!: HaRegistrySnapshot;
currentSpace = '';
spaceTitles: Readonly<Record<string, string>> = {};
viewKey: unknown;
private _runtime = EMPTY_RUNTIME;
private _hovered = '';
@@ -41,23 +45,27 @@ export class HpZigbeeTopologyOverlay extends LitElement {
:host { position: absolute; inset: 0; z-index: 5; display: block; pointer-events: none; }
svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
line { vector-effect: non-scaling-stroke; stroke-linecap: round; }
.route-arrow { vector-effect: non-scaling-stroke; }
.halo {
position: absolute; width: calc(var(--device-base-size, 4cqw) * 1.22);
height: calc(var(--device-base-size, 4cqw) * 1.22); transform: translate(-50%, -50%);
box-sizing: border-box; border: 2px solid rgba(120, 190, 220, .82); border-radius: 50%;
box-shadow: 0 0 0 4px rgba(120, 190, 220, .16);
}
.remote {
.remote, .parent-bubble {
position: absolute; transform: translate(12px, calc(-100% - 12px));
border: 1px solid rgba(255,255,255,.7); border-radius: 999px;
padding: 3px 7px; color: #fff; background: rgba(28,31,36,.88);
box-shadow: 0 2px 7px rgba(0,0,0,.28); white-space: nowrap;
font: 600 11px/1.2 system-ui, sans-serif;
}
.parent-bubble.right { transform: translate(0, -50%); }
.parent-bubble.left { transform: translate(-100%, -50%); }
@media (forced-colors: active) {
line { stroke: Highlight !important; }
.route-arrow { fill: Highlight !important; }
.halo { border-color: Highlight; box-shadow: none; }
.remote { color: CanvasText; background: Canvas; border-color: CanvasText; }
.remote, .parent-bubble { color: CanvasText; background: Canvas; border-color: CanvasText; }
}
`;
@@ -157,7 +165,7 @@ export class HpZigbeeTopologyOverlay extends LitElement {
this.requestUpdate();
}
private _position(id: string): { x: number; y: number; width: number; height: number } | null {
private _position(id: string, width: number, height: number): MarkerPosition | null {
const marker = [...(this._parent?.querySelectorAll<HTMLElement>('.dev[data-id]') || [])]
.find((item) => item.dataset.id === id);
if (!marker) return null;
@@ -165,7 +173,24 @@ export class HpZigbeeTopologyOverlay extends LitElement {
const y = Number.parseFloat(marker.style.top);
const rect = marker.getBoundingClientRect();
return Number.isFinite(x) && Number.isFinite(y)
? { x, y, width: rect.width, height: rect.height } : null;
? { x: x * width / 100, y: y * height / 100, width: rect.width, height: rect.height } : null;
}
private _targetText(target: ZigbeeParentTarget): string {
if (target.kind === 'remote-space') {
return this.spaceTitles[target.spaceId]?.trim()
|| topologyT(langOf(this.hass), 'route_other_space');
}
return topologyT(langOf(this.hass), target.kind === 'unplaced-coordinator'
? 'route_coordinator_not_on_plan' : 'route_device_not_on_plan');
}
private _markerClearance(position: MarkerPosition): number {
return Math.max(position.width, position.height) * 0.61 + 3;
}
private _points(points: readonly ZigbeePixelPoint[]): string {
return points.map((point) => `${point.x.toFixed(2)},${point.y.toFixed(2)}`).join(' ');
}
protected render() {
@@ -175,23 +200,58 @@ export class HpZigbeeTopologyOverlay extends LitElement {
&& memo.registry === this.registry ? memo.mapped : mapTopologies(this._runtime.topologies, this.devices, this.registry);
if (!memo || mapped !== memo.mapped) this._mappedMemo = { runtime: this._runtime, devices: this.devices, registry: this.registry, mapped };
const hover = resolveMappedTopologyHover(mapped, this.currentSpace, this._hovered);
const origin = this._position(this._hovered);
const width = this.clientWidth || this._parent?.clientWidth || 1;
const height = this.clientHeight || this._parent?.clientHeight || 1;
const origin = this._position(this._hovered, width, height);
if (!origin) return nothing;
const lines = hover.lines.map((line) => ({ ...line, point: this._position(line.neighborMarkerId) }))
.filter((line) => !!line.point);
const lines = hover.lines.map((line) => ({
...line,
point: this._position(line.neighborMarkerId, width, height),
})).filter((line) => !!line.point).map((line) => {
const color = line.lqi === undefined ? 'rgba(145,155,165,.85)' : lqiColor(line.lqi);
const arrow = line.routeDirection ? zigbeeArrowGeometry(
origin, line.point!, this._markerClearance(origin), this._markerClearance(line.point!),
line.routeDirection,
) : null;
return { ...line, color, arrow };
});
const placeLeft = origin.x > width / 2;
const bubbleGap = this._markerClearance(origin) + 18;
const bubbles = hover.parentTargets.map((target, index) => {
const offset = (index - (hover.parentTargets.length - 1) / 2) * 30;
const point = {
x: origin.x + (placeLeft ? -bubbleGap : bubbleGap),
y: Math.max(14, Math.min(height - 14, origin.y + offset)),
};
return { target, point, arrow: zigbeeArrowGeometry(
origin, point, this._markerClearance(origin), 0, 'toward-neighbor',
) };
});
return html`
${lines.length ? svg`<svg viewBox="0 0 100 100" preserveAspectRatio="none"
${lines.length || bubbles.length ? svg`<svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none"
aria-hidden="true" data-hp="zigbee-topology-lines">
${lines.map((line) => svg`<line x1=${origin.x} y1=${origin.y}
x2=${line.point!.x} y2=${line.point!.y}
stroke=${line.lqi === undefined ? 'rgba(145,155,165,.85)' : lqiColor(line.lqi)}
stroke=${line.color} data-direction=${line.routeDirection || 'none'}
stroke-width=${line.lqi === undefined ? 2 : 2.2}
stroke-dasharray=${line.lqi === undefined ? '5 5' : nothing} opacity=".9"></line>`)}
stroke-dasharray=${line.lqi === undefined ? '5 5' : nothing} opacity=".9"></line>
${line.arrow ? svg`<polygon class="route-arrow" data-hp="zigbee-topology-arrow"
data-direction=${line.routeDirection} points=${this._points(line.arrow.points)}
fill=${line.color}></polygon>` : nothing}`)}
${bubbles.map((bubble) => svg`<line class="parent-route" x1=${origin.x} y1=${origin.y}
x2=${bubble.point.x} y2=${bubble.point.y} stroke="rgba(145,155,165,.92)"
stroke-width="2" opacity=".9"></line>
${bubble.arrow ? svg`<polygon class="route-arrow" data-hp="zigbee-topology-parent-arrow"
data-direction="toward-neighbor" points=${this._points(bubble.arrow.points)}
fill="rgba(145,155,165,.92)"></polygon>` : nothing}`)}
</svg>` : nothing}
${lines.map((line) => html`<div class="halo" data-hp="zigbee-topology-neighbor"
data-id=${line.neighborMarkerId} style="left:${line.point!.x}%;top:${line.point!.y}%;width:${line.point!.width * 1.22}px;height:${line.point!.height * 1.22}px"></div>`)}
data-id=${line.neighborMarkerId} style="left:${line.point!.x}px;top:${line.point!.y}px;width:${line.point!.width * 1.22}px;height:${line.point!.height * 1.22}px"></div>`)}
${bubbles.map((bubble) => html`<div class="parent-bubble ${placeLeft ? 'left' : 'right'}"
data-hp="zigbee-topology-parent-bubble" data-kind=${bubble.target.kind}
style="left:${bubble.point.x}px;top:${bubble.point.y}px">${this._targetText(bubble.target)}</div>`)}
${hover.remoteCount ? html`<div class="remote" data-hp="zigbee-topology-remote"
style="left:${origin.x}%;top:${origin.y}%">${topologyT(
style="left:${origin.x}px;top:${origin.y}px">${topologyT(
langOf(this.hass), 'remote_count', { n: hover.remoteCount },
)}</div>` : nothing}
`;
+4 -1
View File
@@ -23,5 +23,8 @@
"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"
"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"
}
+4 -1
View File
@@ -23,5 +23,8 @@
"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"
"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"
}
+4 -1
View File
@@ -23,5 +23,8 @@
"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"
"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"
}
+4 -1
View File
@@ -23,5 +23,8 @@
"error_invalid_topic": "Проверьте базовый топик Zigbee2MQTT.",
"error_invalid_payload": "Провайдер вернул неподдерживаемые данные топологии.",
"error_provider": "Не удалось загрузить данные топологии.",
"remote_count": "+{n} в других пространствах"
"remote_count": "+{n} в других пространствах",
"route_device_not_on_plan": "устройства нет на плане",
"route_coordinator_not_on_plan": "координатора нет на плане",
"route_other_space": "другое пространство"
}
+45
View File
@@ -0,0 +1,45 @@
export type ZigbeePixelPoint = { x: number; y: number };
export type ZigbeeArrowGeometry = {
tip: ZigbeePixelPoint;
points: [ZigbeePixelPoint, ZigbeePixelPoint, ZigbeePixelPoint];
};
/** A screen-pixel arrowhead whose tip stops before the destination marker or bubble. */
export function zigbeeArrowGeometry(
origin: ZigbeePixelPoint,
neighbor: ZigbeePixelPoint,
originClearance: number,
neighborClearance: number,
direction: 'toward-neighbor' | 'toward-origin',
preferredLength = 9,
preferredHalfWidth = 4.5,
): ZigbeeArrowGeometry | null {
const dx = neighbor.x - origin.x;
const dy = neighbor.y - origin.y;
const distance = Math.hypot(dx, dy);
const usable = distance - Math.max(0, originClearance) - Math.max(0, neighborClearance);
if (!Number.isFinite(distance) || distance <= 0 || usable < 4) return null;
const ux = dx / distance;
const uy = dy / distance;
const towardNeighbor = direction === 'toward-neighbor';
const tip = towardNeighbor
? { x: neighbor.x - ux * Math.max(0, neighborClearance),
y: neighbor.y - uy * Math.max(0, neighborClearance) }
: { x: origin.x + ux * Math.max(0, originClearance),
y: origin.y + uy * Math.max(0, originClearance) };
const length = Math.min(preferredLength, Math.max(4, usable * 0.45));
const halfWidth = Math.min(preferredHalfWidth, length * 0.5);
const backSign = towardNeighbor ? -1 : 1;
const base = { x: tip.x + ux * length * backSign, y: tip.y + uy * length * backSign };
const nx = -uy;
const ny = ux;
return {
tip,
points: [
tip,
{ x: base.x + nx * halfWidth, y: base.y + ny * halfWidth },
{ x: base.x - nx * halfWidth, y: base.y - ny * halfWidth },
],
};
}
+18 -1
View File
@@ -4,19 +4,36 @@ import type { HaRegistrySnapshot } from './ha-binding-status';
import type { DevItem } from './types';
import type { ZigbeeTopologyHass } from './zigbee-topology-runtime';
const spaceTitleMemo = new WeakMap<object, { signature: string; titles: Readonly<Record<string, string>> }>();
function spaceTitlesOf(spaces: readonly { id?: unknown; title?: unknown }[]): Readonly<Record<string, string>> {
const signature = spaces.map((space) => `${String(space.id)}\u0000${String(space.title)}`).join('\u0001');
const cached = spaceTitleMemo.get(spaces);
if (cached?.signature === signature) return cached.titles;
const projected = Object.fromEntries(spaces.flatMap((space) => (
typeof space.id === 'string' && typeof space.title === 'string'
? [[space.id, space.title] as const] : []
)));
spaceTitleMemo.set(spaces, { signature, titles: projected });
return projected;
}
export function renderZigbeeTopologyOverlay(input: {
hass: ZigbeeTopologyHass;
settings: unknown;
devices: readonly DevItem[];
registry: HaRegistrySnapshot;
currentSpace: string;
spaces: readonly { id?: unknown; title?: unknown }[];
viewKey: unknown;
view: boolean;
kiosk: boolean;
}): TemplateResult | typeof nothing {
if (!input.view || input.kiosk || input.hass?.user?.is_admin !== true
|| !zigbeeTopologySettingsOf(input.settings).enabled) return nothing;
const spaceTitles = spaceTitlesOf(input.spaces);
void import('./hp-zigbee-topology-overlay');
return html`<hp-zigbee-topology-overlay data-hp-live-layer="camera" .hass=${input.hass} .devices=${input.devices}
.registry=${input.registry} .currentSpace=${input.currentSpace} .viewKey=${input.viewKey}></hp-zigbee-topology-overlay>`;
.registry=${input.registry} .currentSpace=${input.currentSpace} .spaceTitles=${spaceTitles}
.viewKey=${input.viewKey}></hp-zigbee-topology-overlay>`;
}
+135 -11
View File
@@ -52,12 +52,25 @@ export interface ZigbeeTopology {
export interface ZigbeeHoverLine {
neighborMarkerId: string;
lqi?: number;
routeDirection?: 'toward-neighbor' | 'toward-origin';
}
export type ZigbeeParentTarget =
| { kind: 'remote-space'; spaceId: string }
| { kind: 'unplaced-device' }
| { kind: 'unplaced-coordinator' };
export interface ZigbeeRouteTree {
coordinatorKey?: string;
distances: Map<string, number>;
parents: Map<string, string>;
}
export interface ZigbeeHoverResolution {
lines: ZigbeeHoverLine[];
remoteCount: number;
omittedCount: number;
parentTargets: ZigbeeParentTarget[];
}
export const TOPOLOGY_STALE_MS = 5 * 60 * 1000;
@@ -94,11 +107,26 @@ function lqiOf(value: unknown): number | undefined {
return Number.isFinite(n) && n >= 0 && n <= 255 ? Math.round(n) : undefined;
}
const Z2M_RELATIONSHIPS: Readonly<Record<number, string>> = {
0: 'parent', 1: 'child', 2: 'sibling', 3: 'none', 4: 'previous_child',
};
function relationshipOf(value: unknown): string | undefined {
if (typeof value === 'number' && Number.isInteger(value)) return Z2M_RELATIONSHIPS[value];
if (typeof value !== 'string') return undefined;
const compact = value.trim().toLowerCase().replace(/[\s_-]+/g, '');
if (!compact) return undefined;
if (compact === 'previouschild') return 'previous_child';
if (compact === 'parent' || compact === 'child' || compact === 'sibling' || compact === 'none') {
return compact;
}
return value.trim().toLowerCase().slice(0, 40) || undefined;
}
function observation(value: unknown): ZigbeeDirectionalObservation {
const record = recordOf(value);
const lqi = lqiOf(record?.lqi ?? record?.linkquality ?? record?.link_quality);
const relationship = typeof record?.relationship === 'string'
? record.relationship.slice(0, 40) : undefined;
const relationship = relationshipOf(record?.relationship);
return { ...(lqi === undefined ? {} : { lqi }), ...(relationship ? { relationship } : {}) };
}
@@ -270,7 +298,69 @@ function drawable(device: DevItem): boolean {
}
export type ZigbeeNodePlacement = { markerId: string; space: string };
export type ZigbeeMappedTopology = { topology: ZigbeeTopology; placements: Map<string, ZigbeeNodePlacement> };
export type ZigbeeMappedTopology = {
topology: ZigbeeTopology;
placements: Map<string, ZigbeeNodePlacement>;
routes: ZigbeeRouteTree;
};
type ZigbeeAdjacentLink = { neighborKey: string; observation?: ZigbeeDirectionalObservation };
function adjacencyOf(topology: ZigbeeTopology): Map<string, ZigbeeAdjacentLink[]> {
const nodeKeys = new Set(topology.nodes.map((node) => node.key));
const adjacency = new Map<string, ZigbeeAdjacentLink[]>();
for (const key of [...nodeKeys].sort()) adjacency.set(key, []);
for (const link of [...topology.links].sort((left, right) => (
`${left.a}\u0000${left.b}`.localeCompare(`${right.a}\u0000${right.b}`)
))) {
if (!nodeKeys.has(link.a) || !nodeKeys.has(link.b)) continue;
adjacency.get(link.a)!.push({ neighborKey: link.b, observation: link.aToB });
adjacency.get(link.b)!.push({ neighborKey: link.a, observation: link.bToA });
}
for (const neighbors of adjacency.values()) {
neighbors.sort((left, right) => left.neighborKey.localeCompare(right.neighborKey));
}
return adjacency;
}
/** Build one deterministic shortest-path uplink tree without trusting stale provider direction. */
export function buildZigbeeRouteTree(topology: ZigbeeTopology): ZigbeeRouteTree {
const distances = new Map<string, number>();
const parents = new Map<string, string>();
const coordinators = topology.nodes.filter((node) => node.role === 'coordinator')
.map((node) => node.key).sort();
if (coordinators.length !== 1) return { distances, parents };
const coordinatorKey = coordinators[0];
const adjacency = adjacencyOf(topology);
if (!adjacency.has(coordinatorKey)) return { distances, parents };
distances.set(coordinatorKey, 0);
const queue = [coordinatorKey];
for (let index = 0; index < queue.length; index++) {
const current = queue[index];
const nextDistance = distances.get(current)! + 1;
for (const { neighborKey } of adjacency.get(current) || []) {
if (distances.has(neighborKey)) continue;
distances.set(neighborKey, nextDistance);
queue.push(neighborKey);
}
}
for (const nodeKey of [...distances.keys()].sort()) {
const distance = distances.get(nodeKey)!;
if (distance === 0) continue;
const candidates = (adjacency.get(nodeKey) || [])
.filter(({ neighborKey }) => distances.get(neighborKey) === distance - 1)
.sort((left, right) => {
const leftParent = left.observation?.relationship === 'parent' ? 1 : 0;
const rightParent = right.observation?.relationship === 'parent' ? 1 : 0;
if (leftParent !== rightParent) return rightParent - leftParent;
const leftLqi = left.observation?.lqi ?? -1;
const rightLqi = right.observation?.lqi ?? -1;
return rightLqi - leftLqi || left.neighborKey.localeCompare(right.neighborKey);
});
if (candidates[0]) parents.set(nodeKey, candidates[0].neighborKey);
}
return { coordinatorKey, distances, parents };
}
/** Exact registry binding only; names, models and friendly names are never identities. */
export function mapTopologyNodes(
@@ -300,36 +390,70 @@ export function mapTopologies(
topologies: readonly ZigbeeTopology[], devices: readonly DevItem[], registry: HaRegistrySnapshot,
): ZigbeeMappedTopology[] {
return topologies.map((topology) => ({
topology, placements: mapTopologyNodes(topology, devices, registry).placements,
topology,
placements: mapTopologyNodes(topology, devices, registry).placements,
routes: buildZigbeeRouteTree(topology),
}));
}
export function resolveMappedTopologyHover(
mappedTopologies: readonly ZigbeeMappedTopology[], currentSpace: string, hoveredMarkerId: string,
): ZigbeeHoverResolution {
const lines = new Map<string, ZigbeeHoverLine>();
const lines = new Map<string, { line: ZigbeeHoverLine;
directions: Set<NonNullable<ZigbeeHoverLine['routeDirection']>> }>();
const remote = new Set<string>();
const parentTargets = new Map<string, ZigbeeParentTarget>();
let omittedCount = 0;
for (const { topology, placements } of mappedTopologies) {
for (const { topology, placements, routes } of mappedTopologies) {
const hoveredNodes = new Set([...placements]
.filter(([, placement]) => placement.markerId === hoveredMarkerId && placement.space === currentSpace)
.map(([key]) => key));
if (!hoveredNodes.size) continue;
for (const hoveredNode of hoveredNodes) {
const parentKey = routes.parents.get(hoveredNode);
if (!parentKey) continue;
const parentPlacement = placements.get(parentKey);
if (parentPlacement?.space === currentSpace) continue;
let target: ZigbeeParentTarget;
if (parentPlacement) target = { kind: 'remote-space', spaceId: parentPlacement.space };
else target = topology.nodes.find((node) => node.key === parentKey)?.role === 'coordinator'
? { kind: 'unplaced-coordinator' } : { kind: 'unplaced-device' };
const targetKey = target.kind === 'remote-space' ? `${target.kind}:${target.spaceId}` : target.kind;
parentTargets.set(targetKey, target);
}
for (const link of topology.links) {
const fromA = hoveredNodes.has(link.a);
const fromB = hoveredNodes.has(link.b);
if (!fromA && !fromB) continue;
const other = placements.get(fromA ? link.b : link.a);
const hoveredNode = fromA ? link.a : link.b;
const otherKey = fromA ? link.b : link.a;
const other = placements.get(otherKey);
if (!other || other.markerId === hoveredMarkerId) { omittedCount++; continue; }
if (other.space !== currentSpace) { remote.add(other.markerId); continue; }
const isParent = routes.parents.get(hoveredNode) === otherKey;
if (other.space !== currentSpace) {
if (!isParent) remote.add(other.markerId);
continue;
}
const obs = fromA ? link.aToB : link.bToA;
const direction = isParent ? 'toward-neighbor'
: routes.parents.get(otherKey) === hoveredNode ? 'toward-origin' : undefined;
const existing = lines.get(other.markerId);
if (!existing || (existing.lqi === undefined && obs?.lqi !== undefined)) {
lines.set(other.markerId, { neighborMarkerId: other.markerId, lqi: obs?.lqi });
if (!existing) {
lines.set(other.markerId, { line: { neighborMarkerId: other.markerId, lqi: obs?.lqi },
directions: new Set(direction ? [direction] : []) });
} else {
if (existing.line.lqi === undefined && obs?.lqi !== undefined) existing.line.lqi = obs.lqi;
if (direction) existing.directions.add(direction);
}
}
}
return { lines: [...lines.values()], remoteCount: remote.size, omittedCount };
return {
lines: [...lines.values()].map(({ line, directions }) => directions.size === 1
? { ...line, routeDirection: [...directions][0] } : line),
remoteCount: remote.size,
omittedCount,
parentTargets: [...parentTargets.values()],
};
}
/** Resolve only the edges incident to the marker currently under the mouse. */
+143 -2
View File
@@ -2,9 +2,10 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
mapTopologyNodes, normalizeIeee, normalizeZ2mTopology,
buildZigbeeRouteTree, mapTopologyNodes, normalizeIeee, normalizeZ2mTopology,
normalizeZhaTopology, resolveTopologyHover,
} from '../test-build/zigbee-topology.js';
import { zigbeeArrowGeometry } from '../test-build/zigbee-topology-geometry.js';
import {
normalizeZ2mBaseTopic, writeZigbeeTopologySettings, zigbeeTopologySettingsOf,
} from '../test-build/zigbee-topology-settings.js';
@@ -82,6 +83,9 @@ test('Z2M normalization accepts a real anonymized camelCase raw network map', ()
topology.links.map((link) => link.aToB?.lqi ?? link.bToA?.lqi).sort((a, b) => a - b),
[97, 182],
);
assert.ok(topology.links.some((link) => (
link.aToB?.relationship === 'sibling' || link.bToA?.relationship === 'sibling'
)));
assert.deepEqual(topology.warnings, []);
});
@@ -123,9 +127,11 @@ test('exact device/entity mapping yields local lines and a deduplicated remote c
]);
assert.deepEqual(resolveTopologyHover([topology], devices, registry, 'one', 'ma'), {
lines: [{ neighborMarkerId: 'mb', lqi: 180 }], remoteCount: 1, omittedCount: 0,
parentTargets: [],
});
assert.deepEqual(resolveTopologyHover([topology], devices, registry, 'one', 'mb'), {
lines: [{ neighborMarkerId: 'ma', lqi: undefined }], remoteCount: 0, omittedCount: 0,
parentTargets: [],
});
});
@@ -138,10 +144,145 @@ test('hidden and ambiguous placements fail closed', () => {
assert.equal(mapTopologyNodes(topology, ambiguous, registry).placements.has(topology.nodes[0].key), false);
const hidden = devices.map((item) => item.id === 'mb' ? { ...item, hidden: true } : item);
assert.deepEqual(resolveTopologyHover([topology], hidden, registry, 'one', 'ma'), {
lines: [], remoteCount: 0, omittedCount: 1,
lines: [], remoteCount: 0, omittedCount: 1, parentTargets: [],
});
});
test('uplink tree is deterministic, acyclic and always reaches the sole coordinator', () => {
const topology = {
provider: 'zha', instanceId: 'zha', obtainedAt: 1, freshness: 'provider-cache', warnings: [],
nodes: [
{ key: 'c', ieee: '0000000000000001', role: 'coordinator' },
{ key: 'a', ieee: '0000000000000002', role: 'router' },
{ key: 'b', ieee: '0000000000000003', role: 'router' },
{ key: 'd', ieee: '0000000000000004', role: 'end' },
{ key: 'x', ieee: '0000000000000005', role: 'router' },
],
links: [
{ a: 'c', b: 'a', bToA: { lqi: 90 } },
{ a: 'c', b: 'b', bToA: { lqi: 220 } },
{ a: 'a', b: 'b', aToB: { relationship: 'parent', lqi: 255 } },
{ a: 'a', b: 'd', bToA: { relationship: 'parent', lqi: 40 } },
{ a: 'b', b: 'd', bToA: { relationship: 'sibling', lqi: 240 } },
],
};
const tree = buildZigbeeRouteTree(topology);
assert.equal(tree.coordinatorKey, 'c');
assert.deepEqual(Object.fromEntries(tree.distances), { c: 0, a: 1, b: 1, d: 2 });
assert.deepEqual(Object.fromEntries(tree.parents), { a: 'c', b: 'c', d: 'a' });
assert.equal(tree.parents.has('x'), false);
for (const key of tree.parents.keys()) {
const visited = new Set();
let current = key;
while (current !== tree.coordinatorKey) {
assert.equal(visited.has(current), false, `cycle from ${key}`);
visited.add(current);
const parent = tree.parents.get(current);
assert.ok(parent, `missing parent from ${current}`);
assert.equal(tree.distances.get(parent), tree.distances.get(current) - 1);
current = parent;
}
}
const permuted = buildZigbeeRouteTree({
...topology, nodes: [...topology.nodes].reverse(), links: [...topology.links].reverse(),
});
assert.deepEqual(Object.fromEntries(permuted.parents), Object.fromEntries(tree.parents));
});
test('uplink parent tie-break uses direct LQI then stable key and ambiguous roots fail closed', () => {
const base = {
provider: 'zha', instanceId: 'zha', obtainedAt: 1, freshness: 'provider-cache', warnings: [],
nodes: [
{ key: 'c', ieee: '0000000000000001', role: 'coordinator' },
{ key: 'a', ieee: '0000000000000002', role: 'router' },
{ key: 'b', ieee: '0000000000000003', role: 'router' },
{ key: 'd', ieee: '0000000000000004', role: 'end' },
],
links: [
{ a: 'c', b: 'a' }, { a: 'c', b: 'b' },
{ a: 'a', b: 'd', bToA: { lqi: 100 } },
{ a: 'b', b: 'd', bToA: { lqi: 150 } },
],
};
assert.equal(buildZigbeeRouteTree(base).parents.get('d'), 'b');
const tied = { ...base, links: base.links.map((link) => (
link.a === 'b' && link.b === 'd' ? { ...link, bToA: { lqi: 100 } } : link
)) };
assert.equal(buildZigbeeRouteTree(tied).parents.get('d'), 'a');
assert.equal(buildZigbeeRouteTree({ ...base, nodes: base.nodes.filter((node) => node.key !== 'c') })
.parents.size, 0);
assert.equal(buildZigbeeRouteTree({
...base, nodes: [...base.nodes, { key: 'c2', ieee: '0000000000000005', role: 'coordinator' }],
}).parents.size, 0);
});
test('hover projects local route directions and keeps remote children in the old count', () => {
const topology = normalizeZhaTopology([
{ ieee: '00124b0000000001', device_reg_id: 'da', device_type: 'Coordinator',
neighbors: [{ ieee: '00124b0000000002', lqi: 180 }] },
{ ieee: '00124b0000000002', device_reg_id: 'db', device_type: 'Router', neighbors: [
{ ieee: '00124b0000000001', lqi: 150, relationship: 'Parent' },
{ ieee: '00124b0000000003', lqi: 70, relationship: 'Child' },
] },
{ ieee: '00124b0000000003', device_reg_id: 'dc', device_type: 'EndDevice',
neighbors: [{ ieee: '00124b0000000002', lqi: 60, relationship: 'Parent' }] },
]);
assert.deepEqual(resolveTopologyHover([topology], devices, registry, 'one', 'mb'), {
lines: [{ neighborMarkerId: 'ma', lqi: 150, routeDirection: 'toward-neighbor' }],
remoteCount: 1, omittedCount: 0, parentTargets: [],
});
assert.deepEqual(resolveTopologyHover([topology], devices, registry, 'one', 'ma'), {
lines: [{ neighborMarkerId: 'mb', lqi: 180, routeDirection: 'toward-origin' }],
remoteCount: 0, omittedCount: 0, parentTargets: [],
});
assert.deepEqual(resolveTopologyHover([topology], devices, registry, 'two', 'mc'), {
lines: [], remoteCount: 0, omittedCount: 0,
parentTargets: [{ kind: 'remote-space', spaceId: 'one' }],
});
});
test('hover distinguishes an unplaced coordinator from an unplaced router and never bubbles a child', () => {
const topology = normalizeZhaTopology([
{ ieee: '00124b0000000001', device_reg_id: 'missing-coordinator', device_type: 'Coordinator',
neighbors: [{ ieee: '00124b0000000002', lqi: 180 }] },
{ ieee: '00124b0000000002', device_reg_id: 'missing-router', device_type: 'Router', neighbors: [
{ ieee: '00124b0000000001', lqi: 150, relationship: 'parent' },
{ ieee: '00124b0000000003', lqi: 70 },
] },
{ ieee: '00124b0000000003', device_reg_id: 'dc', device_type: 'EndDevice',
neighbors: [{ ieee: '00124b0000000002', lqi: 60, relationship: 'parent' }] },
]);
const routerDevice = { ...devices[1], bindingKind: 'device', bindingRef: 'missing-router' };
const localRegistry = { ...registry, devices: {
...registry.devices,
'missing-coordinator': { id: 'missing-coordinator' },
'missing-router': { id: 'missing-router' },
} };
assert.deepEqual(resolveTopologyHover([topology], [routerDevice, devices[2]], localRegistry, 'one', 'mb'), {
lines: [], remoteCount: 1, omittedCount: 1,
parentTargets: [{ kind: 'unplaced-coordinator' }],
});
assert.deepEqual(resolveTopologyHover([topology], [devices[2]], localRegistry, 'two', 'mc'), {
lines: [], remoteCount: 0, omittedCount: 1,
parentTargets: [{ kind: 'unplaced-device' }],
});
});
test('screen-pixel arrow geometry points at the requested endpoint and respects clearance', () => {
const origin = { x: 0, y: 20 };
const neighbor = { x: 100, y: 20 };
const outgoing = zigbeeArrowGeometry(origin, neighbor, 10, 12, 'toward-neighbor');
assert.deepEqual(outgoing?.tip, { x: 88, y: 20 });
assert.equal(outgoing?.points[1].x, 79);
assert.equal(outgoing?.points[2].x, 79);
const incoming = zigbeeArrowGeometry(origin, neighbor, 10, 12, 'toward-origin');
assert.deepEqual(incoming?.tip, { x: 10, y: 20 });
assert.equal(incoming?.points[1].x, 19);
const diagonal = zigbeeArrowGeometry({ x: 10, y: 10 }, { x: 70, y: 90 }, 5, 7, 'toward-neighbor');
assert.ok(diagonal && diagonal.tip.x < 70 && diagonal.tip.y < 90);
assert.equal(zigbeeArrowGeometry(origin, { x: 20, y: 20 }, 10, 8, 'toward-neighbor'), null);
});
test('ZHA runtime is explicit, admin-only and deduplicates concurrent reads', async () => {
let calls = 0;
let release;
+1 -1
View File
@@ -13,7 +13,7 @@
"src/color.ts", "src/styles.ts", "src/styles/*.styles.ts", "src/logic.ts", "src/i18n.ts", "src/i18n/registry.ts", "src/i18n/language-runtime.ts", "src/glow-blend.ts", "src/grid-scale.ts", "src/device-visual.ts", "src/device-pulse.ts", "src/device-presentation.ts", "src/device-marker-geometry.ts",
"src/device-face.ts", "src/device-toggle.ts", "src/marker-toggle-entity.ts", "src/activity-runtime.ts", "src/device-area-relocation.ts",
"src/ha-binding-status.ts",
"src/zigbee-topology-settings.ts", "src/zigbee-topology.ts", "src/zigbee-topology-runtime.ts",
"src/zigbee-topology-settings.ts", "src/zigbee-topology.ts", "src/zigbee-topology-geometry.ts", "src/zigbee-topology-runtime.ts",
"src/integration-provider.ts", "src/vacuum.ts", "src/vacuum-routes.ts", "src/vacuum-route-edit.ts",
"src/sun.ts",
"src/light-visibility.ts", "src/glow-scene.ts", "src/space-render.ts",