feat: copy spaces without room bindings

Issue: #456
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-05 09:25:08 +03:00
parent fe0c2e0c83
commit b286d7ba43
52 changed files with 1835 additions and 601 deletions
@@ -1,182 +1,182 @@
{
"schema": 1,
"fingerprint": "e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418",
"fingerprint": "814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Dzad_CGf.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 296719,
"initialViewGzipBytes": 297478,
"lazyFiles": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/de-Cr-89HP-.js",
"houseplan-assets/editor-CJNTjUIy.js",
"houseplan-assets/fr-DKZ8hWr9.js",
"houseplan-assets/houseplan-editor-runtime-CVoz67fJ.js",
"houseplan-assets/houseplan-onboarding-runtime-D3aymMlw.js",
"houseplan-assets/hp-zigbee-topology-overlay-DzOph_RS.js",
"houseplan-assets/live-interaction-runtime-eQAnQ-sE.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js",
"houseplan-assets/zigbee-topology-runtime-fETRrPi1.js"
"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"
],
"lazyGzipBytes": 240647,
"lazyGzipBytes": 244317,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/editor-CJNTjUIy.js",
"houseplan-assets/houseplan-editor-runtime-CVoz67fJ.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js"
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/editor-DsfH0y9C.js",
"houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
],
"lazyEditorGzipBytes": 178325,
"lazyEditorGzipBytes": 181158,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/houseplan-onboarding-runtime-D3aymMlw.js"
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js"
],
"lazyOnboardingGzipBytes": 14241,
"lazyOnboardingGzipBytes": 14317,
"lazyLocaleFiles": [
"houseplan-assets/de-Cr-89HP-.js",
"houseplan-assets/fr-DKZ8hWr9.js"
"houseplan-assets/de-CFOjLpp6.js",
"houseplan-assets/fr-D_LHTTjd.js"
],
"lazyLocaleGzipBytes": 49104,
"lazyLocaleGzipBytes": 49942,
"files": [
{
"path": "houseplan-assets/backdrop-pick-DMkLFHkR.js",
"sha256": "384450870956f8ca5a72cf2d8d07dc88d87f92c6881ae7e88dc18695d0aca575",
"rawBytes": 21417,
"gzipBytes": 7281,
"path": "houseplan-assets/backdrop-pick-j3BiTfHY.js",
"sha256": "2d2f927c8cb8b5e89217dbb4f4493250618eb85c52142ea97100dbd99159f298",
"rawBytes": 21592,
"gzipBytes": 7359,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-Cr-89HP-.js",
"sha256": "3424417404f8b253837205e5b78c734c9bfd5730d0f6c69b117a8412503ea256",
"rawBytes": 82182,
"gzipBytes": 24821,
"path": "houseplan-assets/de-CFOjLpp6.js",
"sha256": "80bf83a74e97e2715bde9b7a65c9202e013d7864ab24997855b2eff8eeb1e46c",
"rawBytes": 83907,
"gzipBytes": 25246,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CJNTjUIy.js",
"sha256": "ca584266dd1a4c030504fdfbf3b6e5fa685fd16843fcb3102d4ec43ac8e76a59",
"path": "houseplan-assets/editor-DsfH0y9C.js",
"sha256": "622afb272e44b005bb5070b8acb6f76c469069fbcd6e6399c0c1437425490017",
"rawBytes": 3831,
"gzipBytes": 1581,
"gzipBytes": 1583,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-DKZ8hWr9.js",
"sha256": "9e978437d010589c9f0c29889f27077bc2ac39501d955e0f455eccfc1fcc35b4",
"rawBytes": 84312,
"gzipBytes": 24283,
"path": "houseplan-assets/fr-D_LHTTjd.js",
"sha256": "1a5256323034c314188f078265e27fb7453af4b4f75c38c833eca3685c637fef",
"rawBytes": 85967,
"gzipBytes": 24696,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Dzad_CGf.js",
"sha256": "97483ed191736bdd71f6f7969afac2ce35cd8b9fdc11b7de453e0d5c135994df",
"rawBytes": 1053904,
"gzipBytes": 295926,
"path": "houseplan-assets/houseplan-card-5IGoG-3Y.js",
"sha256": "6bf3f534f13750e9a667c534e6b389314a6fe9332ae96ed4502c6ac8a6c72b12",
"rawBytes": 1058121,
"gzipBytes": 296682,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Cr-89HP-.js",
"houseplan-assets/editor-CJNTjUIy.js",
"houseplan-assets/fr-DKZ8hWr9.js",
"houseplan-assets/houseplan-editor-runtime-CVoz67fJ.js",
"houseplan-assets/houseplan-onboarding-runtime-D3aymMlw.js",
"houseplan-assets/hp-zigbee-topology-overlay-DzOph_RS.js",
"houseplan-assets/live-interaction-runtime-eQAnQ-sE.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"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-CVoz67fJ.js",
"sha256": "46fc38e58a35df471132386af3898a558d0f02e1c0350d2997edcdcdd0e32183",
"rawBytes": 622491,
"gzipBytes": 164620,
"path": "houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"sha256": "453615ec48b49e236b31662c34545f1ce10f7e564b9c176ddcefd4d021ebeb23",
"rawBytes": 630790,
"gzipBytes": 167369,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/houseplan-card-Dzad_CGf.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js"
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-fETRrPi1.js"
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-D3aymMlw.js",
"sha256": "85dfe77718c0ae7b1f8d9e23facc0f7d2670303237b4489af0b3455ce4fbdea8",
"path": "houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js",
"sha256": "162cf2cd86f30e0201a52c01e75347a12fda32b6b37c49985fec792aa9825cab",
"rawBytes": 28088,
"gzipBytes": 6960,
"gzipBytes": 6958,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-DzOph_RS.js",
"sha256": "44b33d85c3783c1317469c33979c9546f4b819065859a5e0bd95ac6a9ff5b4a0",
"path": "houseplan-assets/hp-zigbee-topology-overlay-BrSsp4IO.js",
"sha256": "6f88cb32ee1fe3080e55489e7363e32e9e0dac2954cc69de943f62bd57de988e",
"rawBytes": 6006,
"gzipBytes": 2371,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js",
"houseplan-assets/zigbee-topology-runtime-fETRrPi1.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-eQAnQ-sE.js",
"sha256": "20b7763e70a6839fc11ad95edae4032c6f6159d0fa408d7a5bfa50dfbd0ca1eb",
"path": "houseplan-assets/live-interaction-runtime-BLkoEJ76.js",
"sha256": "9b808162204030f58ff08fb21879d8b633fe701d1c1b5c6113d102093289a99e",
"rawBytes": 5196,
"gzipBytes": 2126,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-c40qIaLR.js",
"sha256": "6d9b329b0e56a525638ba11f388638ad583c94e61c361484d4ba6f85ad6c2732",
"path": "houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"sha256": "688604a9e2f9348c00619e20aeff3a6cb3024ca331fa655995cd714105ebba99",
"rawBytes": 12382,
"gzipBytes": 4843,
"gzipBytes": 4847,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-fETRrPi1.js",
"sha256": "1c93cc0b92ad7a44c9cbd6c9bab9fded6f21fbb4d6e194befe260eed3fb4c466",
"path": "houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js",
"sha256": "cf3c4a14ae1eecd545c2cf09fcce42fec057f9498ed761fe295eaa087c9e4466",
"rawBytes": 3941,
"gzipBytes": 1761,
"gzipBytes": 1762,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "26075a61cfd33425279f0906c3e9d2d4a6ca5992ebb58da65f7a83aa79ac79b1",
"sha256": "9412b5f3b0c7fd701d32528f84d3c74277e63c9f1ea0ff73edbc7de930af213e",
"rawBytes": 1183,
"gzipBytes": 793,
"gzipBytes": 796,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.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__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";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-Dzad_CGf.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__="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
.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
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";import{l as t,a$ as s,A as e,z as o,c as a,cj as l,ck as i,cl as h,cm as c,aY as n,cn as r,cs as p,co as d,cp as _,cq as u,cr as g,dj as m,aX as b,aZ as $,dk as v}from"./houseplan-card-Dzad_CGf.js";import{i as f,c as y,e as w,r as D,a as k,b as S,d as C,t as M}from"./backdrop-pick-DMkLFHkR.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";class L{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";import{l as t,a$ as s,A as e,z as o,c as a,cj as l,ck as i,cl as h,cm as c,aY as n,cn as r,cs as p,co as d,cp as _,cq as u,cr as g,dj as m,aX as b,aZ as $,dk as v}from"./houseplan-card-5IGoG-3Y.js";import{i as f,c as y,e as w,r as D,a as k,b as S,g as C,t as M}from"./backdrop-pick-j3BiTfHY.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";class L{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
.text=${o(h,l)} .ariaLabel=${o(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const o=l(e),a=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,p="none"===o.fill?{...a||h,a:0}:a;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:o.showBorders,showNames:o.showNames,zeroWallStyle:r(e),displayTouched:!0,hideDecor:o.hideDecor,hideOpenings:o.hideOpenings,roomColor:o.color,roomOpacity:o.opacity,fillMode:"none"===o.fill?"custom":o.fill,customFill:p,glowEnabled:o.glow,bgColor:o.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:o.tempMin,tempMax:o.tempMax,showLqi:o.showLqi??this.host._config?.show_signal??!0,cardFontScale:o.cardFontScale,labelTemp:o.labelTemp,labelHum:o.labelHum,labelLqi:o.labelLqi,labelLight:o.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=p(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...f(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:_,tempMax:d,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await y(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await w(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return D(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){const s=this.host._spaceDialog,e=s?.saved?.find(s=>s.name===t);if(!s||!e||e.used_by.length||e.url===s.planUrl)return;const o=await this.host._confirmDanger({key:"delete-plan",kind:"destructive",title:this.host._t("confirm.delete_plan_title"),message:this.host._t("confirm.delete_plan_body"),objectName:t,confirmLabel:this.host._t("btn.delete"),cancelLabel:this.host._t("btn.cancel")}),a=this.host._spaceDialog,l=a?.saved?.find(s=>s.name===t);if(o&&a&&l&&l.url===e.url&&l.modified===e.modified&&!l.used_by.length&&l.url!==a.planUrl)try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return a`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return a`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return a`<div class="savedplans">
${s.map(s=>{return a`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";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-Dzad_CGf.js";import{a,r as h,t as d}from"./zigbee-topology-c40qIaLR.js";import{zigbeeTopologyRuntimeSnapshot as p,subscribeZigbeeTopology as c}from"./zigbee-topology-runtime-fETRrPi1.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`
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}
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";import{n as t,b as n}from"./zigbee-topology-c40qIaLR.js";import{I as e}from"./houseplan-card-Dzad_CGf.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__="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};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";try{await import("./houseplan-assets/houseplan-card-Dzad_CGf.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__="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)}
+86 -86
View File
@@ -1,182 +1,182 @@
{
"schema": 1,
"fingerprint": "e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418",
"fingerprint": "814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Dzad_CGf.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 296719,
"initialViewGzipBytes": 297478,
"lazyFiles": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/de-Cr-89HP-.js",
"houseplan-assets/editor-CJNTjUIy.js",
"houseplan-assets/fr-DKZ8hWr9.js",
"houseplan-assets/houseplan-editor-runtime-CVoz67fJ.js",
"houseplan-assets/houseplan-onboarding-runtime-D3aymMlw.js",
"houseplan-assets/hp-zigbee-topology-overlay-DzOph_RS.js",
"houseplan-assets/live-interaction-runtime-eQAnQ-sE.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js",
"houseplan-assets/zigbee-topology-runtime-fETRrPi1.js"
"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"
],
"lazyGzipBytes": 240647,
"lazyGzipBytes": 244317,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/editor-CJNTjUIy.js",
"houseplan-assets/houseplan-editor-runtime-CVoz67fJ.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js"
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/editor-DsfH0y9C.js",
"houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
],
"lazyEditorGzipBytes": 178325,
"lazyEditorGzipBytes": 181158,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/houseplan-onboarding-runtime-D3aymMlw.js"
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js"
],
"lazyOnboardingGzipBytes": 14241,
"lazyOnboardingGzipBytes": 14317,
"lazyLocaleFiles": [
"houseplan-assets/de-Cr-89HP-.js",
"houseplan-assets/fr-DKZ8hWr9.js"
"houseplan-assets/de-CFOjLpp6.js",
"houseplan-assets/fr-D_LHTTjd.js"
],
"lazyLocaleGzipBytes": 49104,
"lazyLocaleGzipBytes": 49942,
"files": [
{
"path": "houseplan-assets/backdrop-pick-DMkLFHkR.js",
"sha256": "384450870956f8ca5a72cf2d8d07dc88d87f92c6881ae7e88dc18695d0aca575",
"rawBytes": 21417,
"gzipBytes": 7281,
"path": "houseplan-assets/backdrop-pick-j3BiTfHY.js",
"sha256": "2d2f927c8cb8b5e89217dbb4f4493250618eb85c52142ea97100dbd99159f298",
"rawBytes": 21592,
"gzipBytes": 7359,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-Cr-89HP-.js",
"sha256": "3424417404f8b253837205e5b78c734c9bfd5730d0f6c69b117a8412503ea256",
"rawBytes": 82182,
"gzipBytes": 24821,
"path": "houseplan-assets/de-CFOjLpp6.js",
"sha256": "80bf83a74e97e2715bde9b7a65c9202e013d7864ab24997855b2eff8eeb1e46c",
"rawBytes": 83907,
"gzipBytes": 25246,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CJNTjUIy.js",
"sha256": "ca584266dd1a4c030504fdfbf3b6e5fa685fd16843fcb3102d4ec43ac8e76a59",
"path": "houseplan-assets/editor-DsfH0y9C.js",
"sha256": "622afb272e44b005bb5070b8acb6f76c469069fbcd6e6399c0c1437425490017",
"rawBytes": 3831,
"gzipBytes": 1581,
"gzipBytes": 1583,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-DKZ8hWr9.js",
"sha256": "9e978437d010589c9f0c29889f27077bc2ac39501d955e0f455eccfc1fcc35b4",
"rawBytes": 84312,
"gzipBytes": 24283,
"path": "houseplan-assets/fr-D_LHTTjd.js",
"sha256": "1a5256323034c314188f078265e27fb7453af4b4f75c38c833eca3685c637fef",
"rawBytes": 85967,
"gzipBytes": 24696,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Dzad_CGf.js",
"sha256": "97483ed191736bdd71f6f7969afac2ce35cd8b9fdc11b7de453e0d5c135994df",
"rawBytes": 1053904,
"gzipBytes": 295926,
"path": "houseplan-assets/houseplan-card-5IGoG-3Y.js",
"sha256": "6bf3f534f13750e9a667c534e6b389314a6fe9332ae96ed4502c6ac8a6c72b12",
"rawBytes": 1058121,
"gzipBytes": 296682,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Cr-89HP-.js",
"houseplan-assets/editor-CJNTjUIy.js",
"houseplan-assets/fr-DKZ8hWr9.js",
"houseplan-assets/houseplan-editor-runtime-CVoz67fJ.js",
"houseplan-assets/houseplan-onboarding-runtime-D3aymMlw.js",
"houseplan-assets/hp-zigbee-topology-overlay-DzOph_RS.js",
"houseplan-assets/live-interaction-runtime-eQAnQ-sE.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"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-CVoz67fJ.js",
"sha256": "46fc38e58a35df471132386af3898a558d0f02e1c0350d2997edcdcdd0e32183",
"rawBytes": 622491,
"gzipBytes": 164620,
"path": "houseplan-assets/houseplan-editor-runtime-DcWBzfnw.js",
"sha256": "453615ec48b49e236b31662c34545f1ce10f7e564b9c176ddcefd4d021ebeb23",
"rawBytes": 630790,
"gzipBytes": 167369,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/houseplan-card-Dzad_CGf.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js"
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-fETRrPi1.js"
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-D3aymMlw.js",
"sha256": "85dfe77718c0ae7b1f8d9e23facc0f7d2670303237b4489af0b3455ce4fbdea8",
"path": "houseplan-assets/houseplan-onboarding-runtime-DCANv9te.js",
"sha256": "162cf2cd86f30e0201a52c01e75347a12fda32b6b37c49985fec792aa9825cab",
"rawBytes": 28088,
"gzipBytes": 6960,
"gzipBytes": 6958,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DMkLFHkR.js",
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/backdrop-pick-j3BiTfHY.js",
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-DzOph_RS.js",
"sha256": "44b33d85c3783c1317469c33979c9546f4b819065859a5e0bd95ac6a9ff5b4a0",
"path": "houseplan-assets/hp-zigbee-topology-overlay-BrSsp4IO.js",
"sha256": "6f88cb32ee1fe3080e55489e7363e32e9e0dac2954cc69de943f62bd57de988e",
"rawBytes": 6006,
"gzipBytes": 2371,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js",
"houseplan-assets/zigbee-topology-runtime-fETRrPi1.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-eQAnQ-sE.js",
"sha256": "20b7763e70a6839fc11ad95edae4032c6f6159d0fa408d7a5bfa50dfbd0ca1eb",
"path": "houseplan-assets/live-interaction-runtime-BLkoEJ76.js",
"sha256": "9b808162204030f58ff08fb21879d8b633fe701d1c1b5c6113d102093289a99e",
"rawBytes": 5196,
"gzipBytes": 2126,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-c40qIaLR.js",
"sha256": "6d9b329b0e56a525638ba11f388638ad583c94e61c361484d4ba6f85ad6c2732",
"path": "houseplan-assets/zigbee-topology-rpeu0cwQ.js",
"sha256": "688604a9e2f9348c00619e20aeff3a6cb3024ca331fa655995cd714105ebba99",
"rawBytes": 12382,
"gzipBytes": 4843,
"gzipBytes": 4847,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-fETRrPi1.js",
"sha256": "1c93cc0b92ad7a44c9cbd6c9bab9fded6f21fbb4d6e194befe260eed3fb4c466",
"path": "houseplan-assets/zigbee-topology-runtime-D_aGI6IY.js",
"sha256": "cf3c4a14ae1eecd545c2cf09fcce42fec057f9498ed761fe295eaa087c9e4466",
"rawBytes": 3941,
"gzipBytes": 1761,
"gzipBytes": 1762,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js",
"houseplan-assets/zigbee-topology-c40qIaLR.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js",
"houseplan-assets/zigbee-topology-rpeu0cwQ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "26075a61cfd33425279f0906c3e9d2d4a6ca5992ebb58da65f7a83aa79ac79b1",
"sha256": "9412b5f3b0c7fd701d32528f84d3c74277e63c9f1ea0ff73edbc7de930af213e",
"rawBytes": 1183,
"gzipBytes": 793,
"gzipBytes": 796,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Dzad_CGf.js"
"houseplan-assets/houseplan-card-5IGoG-3Y.js"
],
"dynamicImports": []
}
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,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";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-Dzad_CGf.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__="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
.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
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";import{l as t,a$ as s,A as e,z as o,c as a,cj as l,ck as i,cl as h,cm as c,aY as n,cn as r,cs as p,co as d,cp as _,cq as u,cr as g,dj as m,aX as b,aZ as $,dk as v}from"./houseplan-card-Dzad_CGf.js";import{i as f,c as y,e as w,r as D,a as k,b as S,d as C,t as M}from"./backdrop-pick-DMkLFHkR.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";class L{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";import{l as t,a$ as s,A as e,z as o,c as a,cj as l,ck as i,cl as h,cm as c,aY as n,cn as r,cs as p,co as d,cp as _,cq as u,cr as g,dj as m,aX as b,aZ as $,dk as v}from"./houseplan-card-5IGoG-3Y.js";import{i as f,c as y,e as w,r as D,a as k,b as S,g as C,t as M}from"./backdrop-pick-j3BiTfHY.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406";class L{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
.text=${o(h,l)} .ariaLabel=${o(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const o=l(e),a=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,p="none"===o.fill?{...a||h,a:0}:a;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:o.showBorders,showNames:o.showNames,zeroWallStyle:r(e),displayTouched:!0,hideDecor:o.hideDecor,hideOpenings:o.hideOpenings,roomColor:o.color,roomOpacity:o.opacity,fillMode:"none"===o.fill?"custom":o.fill,customFill:p,glowEnabled:o.glow,bgColor:o.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:o.tempMin,tempMax:o.tempMax,showLqi:o.showLqi??this.host._config?.show_signal??!0,cardFontScale:o.cardFontScale,labelTemp:o.labelTemp,labelHum:o.labelHum,labelLqi:o.labelLqi,labelLight:o.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=p(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...f(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:_,tempMax:d,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await y(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await w(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return D(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){const s=this.host._spaceDialog,e=s?.saved?.find(s=>s.name===t);if(!s||!e||e.used_by.length||e.url===s.planUrl)return;const o=await this.host._confirmDanger({key:"delete-plan",kind:"destructive",title:this.host._t("confirm.delete_plan_title"),message:this.host._t("confirm.delete_plan_body"),objectName:t,confirmLabel:this.host._t("btn.delete"),cancelLabel:this.host._t("btn.cancel")}),a=this.host._spaceDialog,l=a?.saved?.find(s=>s.name===t);if(o&&a&&l&&l.url===e.url&&l.modified===e.modified&&!l.used_by.length&&l.url!==a.planUrl)try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return a`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return a`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return a`<div class="savedplans">
${s.map(s=>{return a`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";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-Dzad_CGf.js";import{a,r as h,t as d}from"./zigbee-topology-c40qIaLR.js";import{zigbeeTopologyRuntimeSnapshot as p,subscribeZigbeeTopology as c}from"./zigbee-topology-runtime-fETRrPi1.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`
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}
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__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";import{n as t,b as n}from"./zigbee-topology-c40qIaLR.js";import{I as e}from"./houseplan-card-Dzad_CGf.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__="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};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e7c2adf422d4fedfe2d4b29c74727ae74dd46823436e7da9250a4777b1ba7418";try{await import("./houseplan-assets/houseplan-card-Dzad_CGf.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__="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)}
+6
View File
@@ -2,6 +2,12 @@
## Unreleased
- 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;
House Plan asks separately only when the whole plan must first be optimized
([#456](https://github.com/Matysh/houseplan-card/issues/456)).
## v1.72.0-beta.3 — 2026-09-05
- Large plans now keep their heavy room, wall, lighting, decor and device scene
+7
View File
@@ -8,6 +8,13 @@
## Не выпущено
- В настройках пространства теперь можно создать под новым именем копию без
комнат: стены, проёмы, колонны, элементы подложки, положение фонового плана,
масштаб и настройки отображения сохраняются, а размещение устройств не
переносится; отдельное подтверждение появляется только тогда, когда сначала
требуется оптимизировать весь план
([#456](https://github.com/Matysh/houseplan-card/issues/456)).
## v1.72.0-beta.3 — 2026-09-05
- На больших планах тяжёлая сцена комнат, стен, света, подложки и устройств
+10
View File
@@ -334,6 +334,16 @@ scale and which room metrics are visible.
Room cards are positioned and scaled on the plan. View renders only the metrics
enabled for that space.
In an existing space's settings, **Copy** creates a new space from its walls,
openings, columns, Background objects, image transform, scale and display
settings. The suggested name is the first free numbered copy, but duplicate
names are allowed. Rooms and device placements are deliberately not copied;
room walls become ordinary independent walls, ready for a different room
layout. House Plan opens the accepted copy in Plan with **Walls** selected. If
the saved plan must first be repaired, a separate warning explains that
**Optimize plans** will change the whole plan; cancelling that warning writes
nothing.
Deleting a space is blocked while any active device still points to the space,
one of its rooms or a saved position on it, provided another space remains.
Move or delete those devices first; then the confirmed delete removes the
+11
View File
@@ -401,6 +401,17 @@ desktop: для точного рисования, Resize, модификато
пространства без `cell_cm` остаётся compatibility fallback 5 см, без скрытой
миграции.
В настройках существующего пространства кнопка **«Копировать»** создаёт новое
пространство с теми же стенами, проёмами, колоннами, элементами подложки,
положением фонового плана, масштабом и настройками отображения. Предлагается
первое свободное имя с номером, но при необходимости можно оставить совпадающее
имя. Комнаты и размещение устройств намеренно не переносятся: контурные стены
комнат становятся обычными самостоятельными стенами, на которых можно собрать
другую планировку. После успешной записи House Plan открывает копию в редакторе
Плана с инструментом **«Стены»**. Если сохранённому плану сначала требуется
исправление, отдельное предупреждение сообщает, что **оптимизация изменит весь
план**; отмена этого предупреждения ничего не записывает.
Удаление пространства блокируется, пока хотя бы одно активное устройство
ссылается на это пространство, его комнату или сохранённую на нём позицию и при
этом остаётся другое пространство. Сначала перенесите или удалите такие
+4 -2
View File
@@ -30,7 +30,9 @@ export function checkCoordinateWriteBarriers(root = defaultRoot) {
const card = readFileSync(resolve(root, 'src/houseplan-card.ts'), 'utf8');
const editor = readFileSync(resolve(root, 'src/houseplan-editor-runtime.ts'), 'utf8')
.replaceAll('this.host.', 'this.');
const frontend = `${card}\n${editor}`;
const optimize = readFileSync(resolve(root, 'src/plan-optimize-write.ts'), 'utf8')
.replaceAll('host.', 'this.');
const frontend = `${card}\n${editor}\n${optimize}`;
const configWrites = occurrences(frontend, "type: 'houseplan/config/set'");
if (configWrites.length !== 1) errors.push(`frontend config writer inventory: ${configWrites.length}`);
@@ -63,7 +65,7 @@ export function checkCoordinateWriteBarriers(root = defaultRoot) {
if (optimizeWrites.length !== 1) errors.push(`frontend Optimize writer inventory: ${optimizeWrites.length}`);
for (const index of optimizeWrites) requireWindow(
errors, frontend, index,
/config: d\.config,[\s\S]*layout: d\.layout/,
/config,[\s\S]*layout,[\s\S]*expected_config_rev: this\._cfgRev/,
'frontend Optimize transaction', 300,
);
+115
View File
@@ -105,6 +105,121 @@ function relocateEditorPatch(patch, cardSource, editorSource) {
// `find` обязан встречаться в файле ровно один раз: патч, который ложится «куда
// попало», проверяет не то, что объявлен проверять. Это контролирует --check.
const MUTANT_DEFINITIONS = [
{
id: 'space-copy-title-always-reuses-two',
guard: 'node --test --test-name-pattern="first free numbered" test/space-copy.test.mjs',
because: 'copy naming must skip occupied suffixes instead of silently proposing a duplicate (#456 AC2)',
patches: [{
file: 'src/space-copy.ts',
find: ' while (names.has(`${base} (${suffix})`)) suffix++;',
replace: ' while (false && names.has(`${base} (${suffix})`)) suffix++;',
}],
},
{
id: 'space-copy-always-asks-to-optimize',
guard: 'node --test --test-name-pattern="no extra confirmation" test/space-copy-runtime.test.mjs',
because: 'an already clean plan must use the name submission as its only confirmation (#456 AC3)',
patches: [{
file: 'src/space-copy-runtime.ts',
find: ' if (optimized.changed) {',
replace: ' if (true) {',
}],
},
{
id: 'space-copy-skips-required-optimize',
guard: 'node --test --test-name-pattern="durable before the copy" test/space-copy-runtime.test.mjs',
because: 'the accepted whole-plan Optimize must become durable before the copy is derived or written (#456 AC4)',
patches: [{
file: 'src/space-copy-runtime.ts',
find: ' await commitPlanOptimization(host, optimized.config, optimized.layout);',
replace: ' await Promise.resolve();',
}],
},
{
id: 'space-copy-ignores-unsafe-optimize-preflight',
guard: 'node --test --test-name-pattern="unsafe Optimize" test/space-copy-runtime.test.mjs',
because: 'a failed whole-plan geometry proof must stop before confirmation or either write (#456 AC5)',
patches: [{
file: 'src/space-copy-runtime.ts',
find: ' if (!preflight.ok) {',
replace: ' if (false && !preflight.ok) {',
}],
},
{
id: 'space-copy-drops-existing-partitions',
guard: 'node --test --test-name-pattern="complete allowed physical surface" test/space-copy.test.mjs',
because: 'copying room contours alone loses independent walls already drawn in the source (#456 AC6)',
patches: [{
file: 'src/space-copy.ts',
find: ' const sourcePartitions = geometryList(source.partitions);',
replace: ' const sourcePartitions = [];',
}],
},
{
id: 'space-copy-keeps-opening-device-binding',
guard: 'node --test --test-name-pattern="complete allowed physical surface" test/space-copy.test.mjs',
because: 'copied openings are geometry only and must not control the source contact or lock (#456 AC7)',
patches: [{
file: 'src/space-copy.ts',
find: ' delete copied.contact;',
replace: '',
}],
},
{
id: 'space-copy-shares-settings-object',
guard: 'node --test --test-name-pattern="complete allowed physical surface" test/space-copy.test.mjs',
because: 'editing one copy must not mutate the source display or backdrop state through an alias (#456 AC8)',
patches: [{
file: 'src/space-copy.ts',
find: " if (key !== 'view_box' && own(source, key)) copiedSpace[key] = clone(source[key]);",
replace: " if (key !== 'view_box' && own(source, key)) copiedSpace[key] = source[key];",
}],
},
{
id: 'space-copy-leaks-room-drafts',
guard: 'node --test --test-name-pattern="complete allowed physical surface" test/space-copy.test.mjs',
because: 'the new floor must start without room identity or unfinished room chains (#456 AC9)',
patches: [{
file: 'src/space-copy.ts',
find: ' if (copiedPartitions.length) copiedSpace.partitions = copiedPartitions;',
replace: " if (own(source, 'room_drafts')) copiedSpace.room_drafts = clone(source.room_drafts);\n"
+ ' if (copiedPartitions.length) copiedSpace.partitions = copiedPartitions;',
}],
},
{
id: 'space-copy-ignores-wall-limit',
guard: 'node --test --test-name-pattern="collection boundaries" test/space-copy.test.mjs',
because: 'an oversized copy must fail before it can create an optimistic or server-side partial state (#456 AC10)',
patches: [{
file: 'src/space-copy.ts',
find: " if (partitionCount > SPACE_COPY_LIMITS.partitions) throw new SpaceCopyError('partitions_limit');",
replace: " if (false) throw new SpaceCopyError('partitions_limit');",
}],
},
{
id: 'space-copy-rejection-keeps-local-copy',
guard: 'node --test --test-name-pattern="keeps accepted Optimize" test/space-copy-runtime.test.mjs',
because: 'a rejected second write must restore server truth without rolling back the accepted Optimize (#456 AC11)',
patches: [{
file: 'src/space-copy-runtime.ts',
find: ' if (rollbackOptimistic(host, attempt, contentFingerprint)) invalidateConfig(host);',
replace: '',
}, {
file: 'src/space-copy-runtime.ts',
find: ' await host._reloadConfigOnly(true);',
replace: ' await Promise.resolve();',
}],
},
{
id: 'space-copy-does-not-select-result',
guard: 'node --test --test-name-pattern="one config write" test/space-copy-runtime.test.mjs',
because: 'after an accepted write the user must enter the clean copied space, not remain on the source (#456 AC12)',
patches: [{
file: 'src/space-copy-runtime.ts',
find: ' host._commitSpace(result.space.id, true);',
replace: '',
}],
},
{
id: 'render-invalidation-renders-irrelevant-ha',
guard: 'node demo/smoke_render_invalidation.mjs',
+2 -49
View File
@@ -107,6 +107,7 @@ import {
import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection';
import {
createEmptySpaceConfig, initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay,
type SpaceDialogState,
} from './space-dialog';
import { mdiHomeCityOutline } from '@mdi/js';
import {
@@ -2166,55 +2167,7 @@ export class HouseplanCard extends LitElement {
hideFromPlan: boolean; // 'space#area' for a virtual one
busy: boolean;
} | null = null;
private _spaceDialog: {
mode: 'edit' | 'create';
spaceId?: string;
title: string;
planUrl: string | null;
planFile: { ext: string; b64: string; aspect: number; name: string } | null;
/**
* The "already uploaded" list, its contents, and the aspect of whatever was
* picked from it. Plans are never deleted for being unreferenced, which is
* only a sane policy if they can be found again (docs/SCOPE.md).
*/
pickSaved?: boolean;
saved?: { name: string; url: string; size: number; modified: number; used_by: string[] }[] | null;
savedBusy?: boolean;
savedAspect?: number;
source: 'file' | 'draw'; // draw = no background image, hand-drawn rooms
showBorders: boolean;
showNames: boolean;
zeroWallStyle: ZeroWallStyle;
/** Create-only source-default guard; never persisted. Edit starts touched. */
displayTouched: boolean;
hideDecor: boolean; // the decorative layer is not drawn outside its editor
hideOpenings: boolean; // opening symbols are not drawn outside the plan editor
roomColor: string;
roomOpacity: number; // 0..1
bgColor: string | null; // background around the plan; null = inherit general
bgMode: 'static' | 'daynight' | null; // plan background mode; null = inherit (docs/SUN.md)
northDeg: number | null; // per-space compass override; null = inherit
sunRays: boolean | null; // per-space wedges override; null = inherit
fillMode: 'none' | 'lqi' | 'light' | 'temp' | 'custom';
/** null keeps the optional persisted field absent and uses product default. */
customFill: FillColorEntry | null;
glowEnabled: boolean;
tempMin: number;
tempMax: number;
showLqi: boolean;
cardFontScale: number;
labelTemp: boolean;
labelHum: boolean;
labelLqi: boolean;
labelLight: boolean;
cellCm: number; // real-world cm represented by one grid cell
/** Projected text is separate so an untouched imperial edit stays lossless. */
cellCmInput?: string;
cellCmTouched?: boolean;
/** Set only after Delete is activated with dependent active markers. */
deleteBlockers?: number;
busy: boolean;
} | null = null;
private _spaceDialog: SpaceDialogState | null = null;
private _keyHandler = (e: KeyboardEvent) => this._onKey(e);
/** DEV-B703-03 warm re-mount: the dead instance's viewport, and the two
* flags that keep the restore from being undone (the server load's centred
+25 -38
View File
@@ -119,8 +119,11 @@ import {
} from './initial-load';
import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection';
import {
createEmptySpaceConfig, initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay,
createEmptySpaceConfig, initialSpaceDisplayDraft, strictNumber, switchSpacePlanSource,
touchSpaceDisplay, type SpaceDialogState,
} from './space-dialog';
import { commitPlanOptimization } from './plan-optimize-write';
import { openSpaceCopyDialog, renderSpaceCopyDialog, saveSpaceCopy } from './space-copy-runtime';
import { mdiHomeCityOutline } from '@mdi/js';
import {
Affine, applyAffine, readVacTelemetry,
@@ -421,15 +424,6 @@ const BOOT_MAX_MS = 1200;
/** AUD-1552-02: post-reveal grace during which late chrome shifts glide
* (CSS height transition on the stage) instead of snapping. */
const BOOT_SOFT_MS = 1500;
/** Numeric editor fields must consume the whole value: `50abc` is invalid,
* not a surprisingly accepted 50. Decimal comma remains supported. */
const strictNumber = (value: string): number | null => {
const text = String(value ?? '').trim().replace(',', '.');
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text)) return null;
const parsed = Number(text);
return Number.isFinite(parsed) ? parsed : null;
};
type LruRead<V> = { hit: true; value: V } | { hit: false };
const lruRead = <K, V>(cache: Map<K, V>, key: K): LruRead<V> => {
if (!cache.has(key)) return { hit: false };
@@ -1109,7 +1103,7 @@ export interface HouseplanEditorHostPort {
_showToast: (msg: string) => void;
_signer: ContentSigner;
_space: string;
_spaceDialog: { mode: "edit" | "create"; spaceId?: string; title: string; planUrl: string | null; planFile: { ext: string; b64: string; aspect: number; name: string; } | null; pickSaved?: boolean; saved?: { name: string; url: string; size: number; modified: number; used_by: string[]; }[] | null; savedBusy?: boolean; savedAspect?: number; source: "file" | "draw"; showBorders: boolean; showNames: boolean; zeroWallStyle: ZeroWallStyle; displayTouched: boolean; hideDecor: boolean; hideOpenings: boolean; roomColor: string; roomOpacity: number; bgColor: string | null; bgMode: "static" | "daynight" | null; northDeg: number | null; sunRays: boolean | null; fillMode: "none" | "lqi" | "light" | "temp" | "custom"; customFill: FillColorEntry | null; glowEnabled: boolean; tempMin: number; tempMax: number; showLqi: boolean; cardFontScale: number; labelTemp: boolean; labelHum: boolean; labelLqi: boolean; labelLight: boolean; cellCm: number; cellCmInput?: string; cellCmTouched?: boolean; deleteBlockers?: number; busy: boolean; } | null;
_spaceDialog: SpaceDialogState | null;
_spaceH: number;
_spaceModel: () => SpaceModel | undefined;
_spaceModelById: (id: string | null | undefined) => SpaceModel | undefined;
@@ -8974,6 +8968,17 @@ public async _saveConfigNow(attempt: OptimisticAttempt<ServerConfig> | null = nu
}
}
public _saveSpaceCopy(): Promise<void> {
return saveSpaceCopy(this.host, {
clearGeometryGesture: () => this._clearGeometryGesture(),
optimizeReferenceContext: () => this._optimizeReferenceContext(false),
reportPreflightFailure: (result, config) => this._reportPreflightFailure(result, config),
saveConfigNow: (attempt) => this._saveConfigNow(attempt),
setMode: () => this._setMode('plan'),
showWallModelMigrationBlocked: (error) => this._showWallModelMigrationBlocked(error),
});
}
public _startImport(): void {
const dlg = this.host._importDialog;
if (!dlg) return;
@@ -9757,35 +9762,9 @@ public async _runAlignToGrid(): Promise<void> {
this._clearGeometryGesture();
this.host._alignDialog = { ...d, busy: true };
try {
if (this.host._saveConfigDebounced.pending()) this.host._saveConfigDebounced.flush();
await this.host._writeChain;
const resp = await this.host.hass.callWS({
type: 'houseplan/plan/optimize',
config: d.config,
layout: d.layout,
expected_config_rev: this.host._cfgRev,
expected_layout_rev: this.host._layoutRev,
});
this.host._serverCfg = d.config;
this.host._layout = d.layout;
this.host._geometryHistory.clear();
this.host._cancelDeviceDrag();
this.host._devicePositionHistory.clear();
this.host._cfgRev = resp?.config_rev ?? this.host._cfgRev + 1;
this.host._layoutRev = resp?.layout_rev ?? this.host._layoutRev + 1;
this.host._canOptimizeUndo = !!resp?.can_undo;
this.host._undoKind = resp?.can_undo ? 'optimize' : null;
this.host._dirtyPos.clear();
this.host._sentPos.clear();
this.host._cfgEpoch++;
this.host._modelCache = null;
this.host._frame = null;
this.host._regSignature = '';
this.host._maybeRebuildDevices();
this.host._cacheSnapshot();
await commitPlanOptimization(this.host, d.config, d.layout);
this.host._alignDialog = null;
this.host._preflightClipboardFallback = null;
this.host.requestUpdate();
this.host._showToast(this.host._t('gs.align_done', {
n: String(d.report.moved),
m: String(d.report.migrated + d.report.canonicalized
@@ -13846,6 +13825,7 @@ public _renderMarkerDialog(): TemplateResult {
public _renderSpaceDialog(): TemplateResult {
const d = this.host._spaceDialog!;
if (d.copy) return renderSpaceCopyDialog(this.host, () => { void this._saveSpaceCopy(); });
const progress = this.host._importTotal > 0 && d.mode === 'create'
? this.host._t('import.progress', {
i: this.host._importTotal - this.host._importQueue.length,
@@ -14111,6 +14091,13 @@ public _renderSpaceDialog(): TemplateResult {
: nothing}
</div>
<div class="row dialog-action-footer" slot="footer">
${d.mode === 'edit'
? html`<div class="dialog-action-group">
<button class="btn ghost" @click=${() => openSpaceCopyDialog(this.host)} ?disabled=${d.busy}>
<ha-icon icon="mdi:content-copy"></ha-icon>${this.host._t('btn.copy')}
</button>
</div>`
: nothing}
${d.mode === 'edit'
? html`<div class="dialog-action-group dialog-action-danger">
<button class="btn danger" @click=${() => this._deleteSpace()} ?disabled=${d.busy}>
+20
View File
@@ -64,6 +64,7 @@
"btn.save": "Speichern",
"btn.close": "Schließen",
"btn.delete": "Löschen",
"btn.copy": "Kopieren",
"btn.edit": "Bearbeiten",
"btn.open_in_ha": "In HA öffnen",
"btn.reset": "Zurücksetzen",
@@ -269,6 +270,24 @@
"marker.sub_group": "Gruppe",
"marker.sub_helper": "Helfer",
"space.new": "Neuer Bereich",
"space.copy_title": "Bereich kopieren",
"space.copy_name": "Name des neuen Bereichs",
"space.copy_fallback": "Bereich",
"space.copy_create": "Kopie erstellen",
"space.copy_optimize_title": "Vor dem Kopieren optimieren?",
"space.copy_optimize_body": "Vor dem Erstellen von „{name}“ muss House Plan den gesamten Plan einschließlich anderer Bereiche optimieren. Fortfahren?",
"space.copy_optimize_confirm": "Optimieren und kopieren",
"space.copy_error_changed": "Der Plan wurde während der Bestätigung geändert. Bitte prüfen und erneut versuchen.",
"space.copy_error_source_missing": "Der Quellbereich existiert nicht mehr. Öffnen Sie seine Einstellungen erneut.",
"space.copy_error_source_invalid": "House Plan konnte nicht alle Wände dieses Bereichs prüfen. Korrigieren Sie den Quellbereich.",
"space.copy_error_spaces_limit": "House Plan enthält bereits die maximale Anzahl an Bereichen.",
"space.copy_error_partitions_limit": "Dieser Bereich enthält zu viele Wände zum Kopieren.",
"space.copy_error_openings_limit": "Dieser Bereich enthält zu viele Öffnungen zum Kopieren.",
"space.copy_error_decor_limit": "Dieser Bereich enthält zu viele Hintergrundobjekte zum Kopieren.",
"space.copy_error_columns_limit": "Dieser Bereich enthält zu viele Säulen zum Kopieren.",
"space.copy_error_opening_host": "Eine Öffnung ist keiner Wand mehr zugeordnet. Korrigieren oder löschen Sie sie vor dem Kopieren.",
"space.copy_error_geometry": "Die Kopie würde eine unsichere Wandgeometrie erzeugen. Korrigieren Sie den Quellbereich.",
"space.copy_error_optimize_geometry": "Das Kopieren wurde gestoppt, weil die Optimierung des gesamten Plans für einen Bereich unsicher ist.",
"space.header": "Bereich",
"space.title_label": "Titel",
"space.title_ph": "z.B. Garage",
@@ -326,6 +345,7 @@
"toast.plan_required": "Grundriss hochladen — erforderlich",
"toast.space_added_onboard": "Bereich hinzugefügt. Räume einzeichnen: Rasterpunkte anklicken und die Kontur schließen.",
"toast.space_added": "Bereich hinzugefügt",
"toast.space_copied": "Bereich „{name}“ erstellt",
"toast.space_saved": "Bereich gespeichert",
"toast.space_deleted": "Bereich gelöscht",
"toast.delete_failed": "Löschen fehlgeschlagen: {err}",
+20
View File
@@ -64,6 +64,7 @@
"btn.save": "Save",
"btn.close": "Close",
"btn.delete": "Delete",
"btn.copy": "Copy",
"btn.edit": "Edit",
"btn.open_in_ha": "Open in HA",
"btn.reset": "Reset",
@@ -269,6 +270,24 @@
"marker.sub_group": "group",
"marker.sub_helper": "helper",
"space.new": "New space",
"space.copy_title": "Copy space",
"space.copy_name": "New space name",
"space.copy_fallback": "Space",
"space.copy_create": "Create copy",
"space.copy_optimize_title": "Optimize before copying?",
"space.copy_optimize_body": "Before creating “{name}”, House Plan must optimize the entire plan, including other spaces. Continue?",
"space.copy_optimize_confirm": "Optimize and copy",
"space.copy_error_changed": "The plan changed while confirmation was open. Review it and try again.",
"space.copy_error_source_missing": "The source space no longer exists. Reopen its settings and try again.",
"space.copy_error_source_invalid": "House Plan could not validate every wall in this space. Fix the source space and try again.",
"space.copy_error_spaces_limit": "House Plan already has the maximum number of spaces.",
"space.copy_error_partitions_limit": "This space has too many walls to copy.",
"space.copy_error_openings_limit": "This space has too many openings to copy.",
"space.copy_error_decor_limit": "This space has too many background objects to copy.",
"space.copy_error_columns_limit": "This space has too many columns to copy.",
"space.copy_error_opening_host": "One of the openings is no longer attached to a wall. Fix or delete it before copying.",
"space.copy_error_geometry": "The copy would create unsafe wall geometry. Fix the source space and try again.",
"space.copy_error_optimize_geometry": "Copy stopped because optimizing the entire plan is unsafe for one of the spaces.",
"space.header": "Space",
"space.title_label": "Title",
"space.title_ph": "e.g. Garage",
@@ -326,6 +345,7 @@
"toast.plan_required": "Upload a floor plan — it is required",
"toast.space_added_onboard": "Space added. Outline the rooms: click grid dots and close the contour.",
"toast.space_added": "Space added",
"toast.space_copied": "Space “{name}” created",
"toast.space_saved": "Space saved",
"toast.space_deleted": "Space deleted",
"toast.delete_failed": "Delete failed: {err}",
+20
View File
@@ -64,6 +64,7 @@
"btn.save": "Enregistrer",
"btn.close": "Fermer",
"btn.delete": "Supprimer",
"btn.copy": "Copier",
"btn.edit": "Modifier",
"btn.open_in_ha": "Ouvrir dans HA",
"btn.reset": "Réinitialiser",
@@ -269,6 +270,24 @@
"marker.sub_group": "groupe",
"marker.sub_helper": "assistant",
"space.new": "Nouvel espace",
"space.copy_title": "Copier l’espace",
"space.copy_name": "Nom du nouvel espace",
"space.copy_fallback": "Espace",
"space.copy_create": "Créer la copie",
"space.copy_optimize_title": "Optimiser avant de copier ?",
"space.copy_optimize_body": "Avant de créer « {name} », House Plan doit optimiser tout le plan, y compris les autres espaces. Continuer ?",
"space.copy_optimize_confirm": "Optimiser et copier",
"space.copy_error_changed": "Le plan a changé pendant la confirmation. Vérifiez-le et réessayez.",
"space.copy_error_source_missing": "L’espace source n’existe plus. Rouvrez ses réglages et réessayez.",
"space.copy_error_source_invalid": "House Plan n’a pas pu valider tous les murs de cet espace. Corrigez l’espace source.",
"space.copy_error_spaces_limit": "House Plan contient déjà le nombre maximal d’espaces.",
"space.copy_error_partitions_limit": "Cet espace contient trop de murs pour être copié.",
"space.copy_error_openings_limit": "Cet espace contient trop d’ouvertures pour être copié.",
"space.copy_error_decor_limit": "Cet espace contient trop d’objets d’arrière-plan pour être copié.",
"space.copy_error_columns_limit": "Cet espace contient trop de colonnes pour être copié.",
"space.copy_error_opening_host": "Une ouverture n’est plus rattachée à un mur. Corrigez-la ou supprimez-la avant la copie.",
"space.copy_error_geometry": "La copie créerait une géométrie de murs non sûre. Corrigez l’espace source.",
"space.copy_error_optimize_geometry": "La copie est arrêtée car l’optimisation de tout le plan n’est pas sûre pour un espace.",
"space.header": "Espace",
"space.title_label": "Titre",
"space.title_ph": "ex. Garage",
@@ -326,6 +345,7 @@
"toast.plan_required": "Importez un plan — il est obligatoire",
"toast.space_added_onboard": "Espace ajouté. Tracez les pièces : cliquez sur les points de la grille et fermez le contour.",
"toast.space_added": "Espace ajouté",
"toast.space_copied": "Espace « {name} » créé",
"toast.space_saved": "Espace enregistré",
"toast.space_deleted": "Espace supprimé",
"toast.delete_failed": "Échec de la suppression : {err}",
+20
View File
@@ -64,6 +64,7 @@
"btn.save": "Сохранить",
"btn.close": "Закрыть",
"btn.delete": "Удалить",
"btn.copy": "Копировать",
"btn.edit": "Редактировать",
"btn.open_in_ha": "Открыть в HA",
"btn.reset": "Сброс",
@@ -269,6 +270,24 @@
"marker.sub_group": "группа",
"marker.sub_helper": "хелпер",
"space.new": "Новое пространство",
"space.copy_title": "Копирование пространства",
"space.copy_name": "Название нового пространства",
"space.copy_fallback": "Пространство",
"space.copy_create": "Создать копию",
"space.copy_optimize_title": "Оптимизировать перед копированием?",
"space.copy_optimize_body": "Перед созданием «{name}» House Plan должен оптимизировать весь план, включая другие пространства. Продолжить?",
"space.copy_optimize_confirm": "Оптимизировать и скопировать",
"space.copy_error_changed": "План изменился, пока было открыто подтверждение. Проверьте его и повторите попытку.",
"space.copy_error_source_missing": "Исходного пространства больше нет. Снова откройте его настройки и повторите попытку.",
"space.copy_error_source_invalid": "House Plan не смог проверить все стены этого пространства. Исправьте исходное пространство и повторите попытку.",
"space.copy_error_spaces_limit": "В House Plan уже создано максимальное число пространств.",
"space.copy_error_partitions_limit": "В этом пространстве слишком много стен для копирования.",
"space.copy_error_openings_limit": "В этом пространстве слишком много проёмов для копирования.",
"space.copy_error_decor_limit": "В этом пространстве слишком много элементов подложки для копирования.",
"space.copy_error_columns_limit": "В этом пространстве слишком много колонн для копирования.",
"space.copy_error_opening_host": "Один из проёмов больше не привязан к стене. Исправьте или удалите его перед копированием.",
"space.copy_error_geometry": "Копия создаст небезопасную геометрию стен. Исправьте исходное пространство и повторите попытку.",
"space.copy_error_optimize_geometry": "Копирование остановлено: оптимизация всего плана небезопасна для одного из пространств.",
"space.header": "Пространство",
"space.title_label": "Название",
"space.title_ph": "Например: Гараж",
@@ -326,6 +345,7 @@
"toast.plan_required": "Загрузите подложку — план этажа обязателен",
"toast.space_added_onboard": "Пространство добавлено. Обведите комнаты: кликайте по точкам сетки и замкните контур.",
"toast.space_added": "Пространство добавлено",
"toast.space_copied": "Пространство «{name}» создано",
"toast.space_saved": "Пространство сохранено",
"toast.space_deleted": "Пространство удалено",
"toast.delete_failed": "Ошибка удаления: {err}",
+70
View File
@@ -0,0 +1,70 @@
import type { DeviceLayout } from './device-position-history';
import type { ServerConfig } from './types';
import { contentFingerprint } from './visual-continuity';
interface Clearable { clear(): void }
interface Flushable { pending(): boolean; flush(): void }
export interface PlanOptimizeWriteHost {
hass: { callWS(message: Record<string, unknown>): Promise<Record<string, unknown>> };
_saveConfigDebounced: Flushable;
_writeChain: Promise<void>;
_cfgRev: number;
_cfgContentFingerprint: string;
_layoutRev: number;
_serverCfg: ServerConfig | null;
_layout: DeviceLayout;
_geometryHistory: Clearable;
_devicePositionHistory: Clearable;
_cancelDeviceDrag(): boolean;
_canOptimizeUndo: boolean;
_undoKind: 'optimize' | 'import' | null;
_dirtyPos: Set<string>;
_sentPos: Map<string, DeviceLayout[string] | null>;
_cfgEpoch: number;
_modelCache: unknown;
_frame: unknown;
_regSignature: string;
_maybeRebuildDevices(): void;
_cacheSnapshot(): void;
requestUpdate(): void;
}
const finiteRevision = (value: unknown, fallback: number): number =>
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
/** The one adoption path shared by the Optimize dialog and Copy space. */
export async function commitPlanOptimization(
host: PlanOptimizeWriteHost,
config: ServerConfig,
layout: DeviceLayout,
): Promise<void> {
if (host._saveConfigDebounced.pending()) host._saveConfigDebounced.flush();
await host._writeChain;
const response = await host.hass.callWS({
type: 'houseplan/plan/optimize',
config,
layout,
expected_config_rev: host._cfgRev,
expected_layout_rev: host._layoutRev,
});
host._serverCfg = config;
host._cfgContentFingerprint = contentFingerprint(config);
host._layout = layout;
host._geometryHistory.clear();
host._cancelDeviceDrag();
host._devicePositionHistory.clear();
host._cfgRev = finiteRevision(response.config_rev, host._cfgRev + 1);
host._layoutRev = finiteRevision(response.layout_rev, host._layoutRev + 1);
host._canOptimizeUndo = response.can_undo === true;
host._undoKind = response.can_undo === true ? 'optimize' : null;
host._dirtyPos.clear();
host._sentPos.clear();
host._cfgEpoch++;
host._modelCache = null;
host._frame = null;
host._regSignature = '';
host._maybeRebuildDevices();
host._cacheSnapshot();
host.requestUpdate();
}
+253
View File
@@ -0,0 +1,253 @@
import { html, type TemplateResult } from 'lit';
import type { HouseplanEditorHostPort } from './houseplan-editor-runtime';
import type { I18nKey } from './i18n';
import { optimizePlans } from './plan-optimizer';
import { commitPlanOptimization } from './plan-optimize-write';
import {
createSpaceCopyCandidate, newSpaceCopySeed, nextSpaceCopyTitle,
SpaceCopyError, type SpaceCopyErrorCode,
} from './space-copy';
import type { OptimizeGeometryPreflightResult } from './plan-geometry-preflight';
import type { SpaceReferenceRepairContext } from './space-reference-repair';
import { optimisticAttempt, rollbackOptimistic, type OptimisticAttempt } from './serialized-write-queue';
import type { ServerConfig } from './types';
import { contentFingerprint } from './visual-continuity';
export interface SpaceCopyRuntimeServices {
clearGeometryGesture(): void;
optimizeReferenceContext(): SpaceReferenceRepairContext;
reportPreflightFailure(result: OptimizeGeometryPreflightResult, config: ServerConfig): void;
saveConfigNow(attempt: OptimisticAttempt<ServerConfig>): Promise<void>;
setMode(mode: 'plan'): void;
showWallModelMigrationBlocked(error: unknown): void;
optimize?: typeof optimizePlans;
newSeed?: () => string;
}
const ERROR_KEYS: Record<SpaceCopyErrorCode, I18nKey> = {
source_missing: 'space.copy_error_source_missing',
source_invalid: 'space.copy_error_source_invalid',
spaces_limit: 'space.copy_error_spaces_limit',
partitions_limit: 'space.copy_error_partitions_limit',
openings_limit: 'space.copy_error_openings_limit',
decor_limit: 'space.copy_error_decor_limit',
columns_limit: 'space.copy_error_columns_limit',
opening_host_missing: 'space.copy_error_opening_host',
opening_host_unknown: 'space.copy_error_opening_host',
geometry_unsafe: 'space.copy_error_geometry',
};
let operationSequence = 0;
function setBusy(host: HouseplanEditorHostPort, token: number, busy: boolean): boolean {
const dialog = host._spaceDialog;
if (!dialog?.copy || dialog.copy.token !== token) return false;
host._spaceDialog = { ...dialog, busy, copy: { ...dialog.copy, busy } };
return true;
}
function invalidateConfig(host: HouseplanEditorHostPort): void {
host._cfgEpoch++;
host._modelCache = null;
host._frame = null;
host._regSignature = '';
host._maybeRebuildDevices();
host.requestUpdate();
}
export function openSpaceCopyDialog(host: HouseplanEditorHostPort): void {
const dialog = host._spaceDialog;
if (!dialog || dialog.mode !== 'edit' || dialog.busy || !host._serverCfg) return;
const source = host._serverCfg.spaces.find((space) => space?.id === dialog.spaceId);
if (!source) return;
host._spaceDialog = {
...dialog,
copy: {
title: nextSpaceCopyTitle(source.title, host._serverCfg.spaces, host._t('space.copy_fallback')),
busy: false,
token: 0,
},
};
}
export function closeSpaceCopyDialog(host: HouseplanEditorHostPort): void {
const dialog = host._spaceDialog;
if (!dialog?.copy || dialog.busy || dialog.copy.busy) return;
host._spaceDialog = { ...dialog, copy: undefined };
}
export function renderSpaceCopyDialog(
host: HouseplanEditorHostPort,
submit: () => void,
): TemplateResult {
const dialog = host._spaceDialog!;
const copy = dialog.copy!;
const close = () => closeSpaceCopyDialog(host);
return html`<hp-dialog .hass=${host.hass} .title=${host._t('space.copy_title')}
icon="mdi:content-copy" @hp-close=${close}>
<div class="body">
<label for="space-copy-name">${host._t('space.copy_name')}</label>
<input id="space-copy-name" class="namein" type="text" autofocus
.value=${copy.title}
@focus=${(event: FocusEvent) => (event.currentTarget as HTMLInputElement).select()}
@input=${(event: Event) => {
const current = host._spaceDialog;
if (current?.copy && !current.busy) {
host._spaceDialog = {
...current,
copy: { ...current.copy, title: (event.target as HTMLInputElement).value },
};
}
}}
@keydown=${(event: KeyboardEvent) => {
if (event.key === 'Enter' && copy.title.trim() && !copy.busy) {
event.preventDefault();
submit();
}
}} />
</div>
<div class="row dialog-action-footer" slot="footer">
<span class="spacer"></span>
<button class="btn ghost" @click=${close} ?disabled=${copy.busy}>
${host._t('btn.cancel')}
</button>
<button class="btn on" @click=${submit} ?disabled=${!copy.title.trim() || copy.busy}>
<ha-icon icon="mdi:content-copy"></ha-icon>${copy.busy ? '…' : host._t('space.copy_create')}
</button>
</div>
</hp-dialog>`;
}
function showCopyError(host: HouseplanEditorHostPort, error: unknown): void {
if (error instanceof SpaceCopyError) {
host._showToast(host._t(ERROR_KEYS[error.code]));
return;
}
const code = (error as { code?: unknown } | null)?.code;
if (code === 'wall_model_client_outdated') {
host._showToast(host._t('toast.wall_model_client_outdated'));
return;
}
host._showToast(host._t('toast.error', { err: host._errText(error) }));
}
/** Submit one guarded two-step operation: optional Optimize, then config Copy. */
export async function saveSpaceCopy(
host: HouseplanEditorHostPort,
services: SpaceCopyRuntimeServices,
): Promise<void> {
const initial = host._spaceDialog;
if (!initial?.copy || initial.mode !== 'edit' || initial.busy || initial.copy.busy
|| !initial.copy.title.trim() || !initial.spaceId || !host._serverCfg) return;
const token = ++operationSequence;
const sourceId = initial.spaceId;
const title = initial.copy.title.trim();
host._spaceDialog = {
...initial,
busy: true,
copy: { ...initial.copy, title, busy: true, token },
};
let optimizeAccepted = false;
try {
if (host._saveConfigDebounced.pending()) host._saveConfigDebounced.flush();
await host._writeChain;
if (!setBusy(host, token, true) || !host._serverCfg) return;
const configRevision = host._cfgRev;
const layoutRevision = host._layoutRev;
const configFingerprint = contentFingerprint(host._serverCfg);
const layoutFingerprint = contentFingerprint(host._layout);
let optimized;
try {
optimized = (services.optimize ?? optimizePlans)(
host._serverCfg, host._layout, services.optimizeReferenceContext(),
);
} catch (error) {
setBusy(host, token, false);
services.showWallModelMigrationBlocked(error);
return;
}
if (optimized.changed) {
const preflight = host._checkOptimizeGeometry(optimized.config);
services.reportPreflightFailure(preflight, optimized.config);
if (!preflight.ok) {
setBusy(host, token, false);
host._showToast(host._t('space.copy_error_optimize_geometry'));
return;
}
const accepted = await host._confirmDanger({
key: `copy-space:${sourceId}`,
kind: 'warning',
title: host._t('space.copy_optimize_title'),
message: host._t('space.copy_optimize_body', { name: title }),
objectName: title,
confirmLabel: host._t('space.copy_optimize_confirm'),
cancelLabel: host._t('btn.cancel'),
icon: 'mdi:broom',
});
if (!setBusy(host, token, accepted)) return;
if (!accepted) return;
if (host._cfgRev !== configRevision || host._layoutRev !== layoutRevision
|| !host._serverCfg || contentFingerprint(host._serverCfg) !== configFingerprint
|| contentFingerprint(host._layout) !== layoutFingerprint) {
setBusy(host, token, false);
host._showToast(host._t('space.copy_error_changed'));
return;
}
services.clearGeometryGesture();
await commitPlanOptimization(host, optimized.config, optimized.layout);
optimizeAccepted = true;
}
if (!setBusy(host, token, true) || !host._serverCfg) return;
const previous = host._serverCfg;
const result = createSpaceCopyCandidate(
previous, sourceId, title, services.newSeed?.() ?? newSpaceCopySeed(),
);
let safe = false;
try { safe = host._checkSpacePhysicalGeometry(result.config, result.space.id).ok; } catch { safe = false; }
if (!safe || host._junctionLimitViolations(result.config, result.space.id).length) {
throw new SpaceCopyError('geometry_unsafe');
}
const attempt = optimisticAttempt(
previous, result.config, host._cfgContentFingerprint, host._cfgRev, contentFingerprint,
);
host._serverCfg = result.config;
invalidateConfig(host);
try {
await services.saveConfigNow(attempt);
} catch (error) {
if (rollbackOptimistic(host, attempt, contentFingerprint)) invalidateConfig(host);
if ((error as { code?: unknown } | null)?.code !== 'conflict') {
await host._reloadConfigOnly(true);
}
throw error;
}
host._canOptimizeUndo = false;
host._undoKind = null;
services.clearGeometryGesture();
host._selId = null;
host._physicalSel = null;
delete host._resumeDraftBySpace[result.space.id];
host._spaceDialog = null;
host._commitSpace(result.space.id, true);
services.setMode('plan');
host._tool = 'draw';
host._path = [];
host._cursorPt = null;
host._activeDraftId = null;
host._primeDrawWallField();
host._saveNav();
host.requestUpdate();
host._showToast(host._t('toast.space_copied', { name: title }));
} catch (error) {
if ((error as { code?: unknown } | null)?.code === 'conflict') {
await Promise.all([host._reloadConfigOnly(true), host._reloadLayoutOnly()]);
}
setBusy(host, token, false);
showCopyError(host, error);
if (optimizeAccepted) host.requestUpdate();
}
}
+188
View File
@@ -0,0 +1,188 @@
import type { ServerConfig } from './types';
export const SPACE_COPY_LIMITS = Object.freeze({
spaces: 50,
partitions: 2000,
openings: 500,
decor: 1000,
columns: 500,
});
export type SpaceCopyErrorCode =
| 'source_missing'
| 'source_invalid'
| 'spaces_limit'
| 'partitions_limit'
| 'openings_limit'
| 'decor_limit'
| 'columns_limit'
| 'opening_host_missing'
| 'opening_host_unknown'
| 'geometry_unsafe';
export class SpaceCopyError extends Error {
public constructor(public readonly code: SpaceCopyErrorCode) {
super(code);
this.name = 'SpaceCopyError';
}
}
type SpaceRecord = Record<string, unknown>;
type GeometryRecord = Record<string, unknown> & { id: string };
export interface SpaceCopyResult {
config: ServerConfig;
space: SpaceRecord & { id: string; title: string };
sourceIndex: number;
}
const own = (value: object, key: PropertyKey): boolean =>
Object.prototype.hasOwnProperty.call(value, key);
const clone = <T>(value: T): T => value === undefined
? value : JSON.parse(JSON.stringify(value)) as T;
const record = (value: unknown): SpaceRecord | null =>
value !== null && typeof value === 'object' && !Array.isArray(value)
? value as SpaceRecord : null;
function geometryList(value: unknown, required = false): GeometryRecord[] {
if (value === undefined && !required) return [];
if (!Array.isArray(value)) throw new SpaceCopyError('source_invalid');
return value.map((item) => {
const row = record(item);
if (!row || typeof row.id !== 'string' || !row.id) {
throw new SpaceCopyError('source_invalid');
}
return row as GeometryRecord;
});
}
const point = (value: unknown): value is [number, number] => Array.isArray(value)
&& value.length === 2 && value.every((coordinate) => Number.isFinite(coordinate));
/** Same visible convention as import: the first free "Name (N)". */
export function nextSpaceCopyTitle(
sourceTitle: unknown,
spaces: readonly unknown[],
fallback: string,
): string {
const base = String(sourceTitle ?? '').trim() || fallback;
const names = new Set(spaces.map((item) => {
const candidate = record(item);
return String(candidate?.title ?? '').trim();
}));
let suffix = 2;
while (names.has(`${base} (${suffix})`)) suffix++;
return `${base} (${suffix})`;
}
/** One bounded seed for a space id and every geometry id in one copy. */
export function newSpaceCopySeed(now = Date.now(), random = Math.random()): string {
const entropy = Math.floor(Math.max(0, Math.min(0.999999999, random)) * 0x1000000)
.toString(36).padStart(5, '0');
return `${Math.max(0, Math.floor(now)).toString(36)}${entropy}`;
}
const COPY_KEYS = [
'settings', 'zero_wall_style', 'cell_cm', 'view_box',
'plan_url', 'plan_aspect', 'plan_x', 'plan_y', 'plan_scale',
'plan_scale_x', 'plan_scale_y', 'plan_angle',
] as const;
/**
* Build an immutable full-config candidate. Room-owned walls become
* independent partitions; markers and layout live outside the copied space.
*/
export function createSpaceCopyCandidate(
input: ServerConfig,
sourceId: string,
title: string,
seed: string,
): SpaceCopyResult {
const spaces = input.spaces as SpaceRecord[];
if (!title.trim()) throw new SpaceCopyError('source_invalid');
if (spaces.length >= SPACE_COPY_LIMITS.spaces) throw new SpaceCopyError('spaces_limit');
const sourceIndex = spaces.findIndex((item) => item.id === sourceId);
if (sourceIndex < 0) throw new SpaceCopyError('source_missing');
const source = spaces[sourceIndex];
if (!Array.isArray(source.view_box)) throw new SpaceCopyError('source_invalid');
const wallSegments = geometryList(source.wall_segments, true);
const sourcePartitions = geometryList(source.partitions);
const openings = geometryList(source.openings);
const decor = geometryList(source.decor);
const columns = geometryList(source.wall_columns);
const partitionCount = wallSegments.length + sourcePartitions.length;
if (partitionCount > SPACE_COPY_LIMITS.partitions) throw new SpaceCopyError('partitions_limit');
if (openings.length > SPACE_COPY_LIMITS.openings) throw new SpaceCopyError('openings_limit');
if (decor.length > SPACE_COPY_LIMITS.decor) throw new SpaceCopyError('decor_limit');
if (columns.length > SPACE_COPY_LIMITS.columns) throw new SpaceCopyError('columns_limit');
let serial = 0;
const nextId = (prefix: string): string => `${prefix}${seed}${(serial++).toString(36)}`;
const wallMap = new Map<string, string>();
const partitionMap = new Map<string, string>();
const copiedPartitions: SpaceRecord[] = [];
for (const wall of wallSegments) {
if (wallMap.has(wall.id) || !point(wall.a) || !point(wall.b)
|| typeof wall.cm !== 'number' || !Number.isFinite(wall.cm)) {
throw new SpaceCopyError('source_invalid');
}
const id = nextId('cw');
wallMap.set(wall.id, id);
copiedPartitions.push({ id, a: clone(wall.a), b: clone(wall.b), cm: wall.cm });
}
for (const partition of sourcePartitions) {
if (partitionMap.has(partition.id) || !point(partition.a) || !point(partition.b)
|| typeof partition.cm !== 'number' || !Number.isFinite(partition.cm)) {
throw new SpaceCopyError('source_invalid');
}
const id = nextId('cp');
partitionMap.set(partition.id, id);
copiedPartitions.push({ id, a: clone(partition.a), b: clone(partition.b), cm: partition.cm });
}
const copiedOpenings = openings.map((opening) => {
const host = record(opening.host);
if (!host || typeof host.id !== 'string' || typeof host.kind !== 'string') {
throw new SpaceCopyError('opening_host_missing');
}
const mapped = host.kind === 'wall'
? wallMap.get(host.id)
: host.kind === 'partition' ? partitionMap.get(host.id) : undefined;
if (!mapped) throw new SpaceCopyError('opening_host_unknown');
const copied: SpaceRecord = {
...clone(opening),
id: nextId('co'),
host: { ...clone(host), kind: 'partition', id: mapped },
};
delete copied.contact;
delete copied.lock;
return copied;
});
const copiedDecor = decor.map((item) => ({ ...clone(item), id: nextId('cd') }));
const copiedColumns = columns.map((item) => ({ ...clone(item), id: nextId('cc') }));
const newSpaceId = `s${seed}`;
if (spaces.some((item) => item.id === newSpaceId)) throw new SpaceCopyError('source_invalid');
const copiedSpace: SpaceRecord & { id: string; title: string } = {
id: newSpaceId,
title: title.trim(),
rooms: [],
wall_segments: [],
view_box: clone(source.view_box),
};
for (const key of COPY_KEYS) {
if (key !== 'view_box' && own(source, key)) copiedSpace[key] = clone(source[key]);
}
if (copiedPartitions.length) copiedSpace.partitions = copiedPartitions;
if (copiedOpenings.length) copiedSpace.openings = copiedOpenings;
if (copiedDecor.length) copiedSpace.decor = copiedDecor;
if (copiedColumns.length) copiedSpace.wall_columns = copiedColumns;
const config = clone(input);
config.spaces.splice(sourceIndex + 1, 0, copiedSpace);
return { config, space: copiedSpace, sourceIndex };
}
+62
View File
@@ -1,5 +1,59 @@
import type { FillColorEntry } from './logic';
import type { ZeroWallStyle } from './types';
export type SpacePlanSource = 'file' | 'draw';
export interface SpaceCopyDraft {
title: string;
busy: boolean;
token: number;
}
export interface SpaceDialogState {
mode: 'edit' | 'create';
spaceId?: string;
title: string;
planUrl: string | null;
planFile: { ext: string; b64: string; aspect: number; name: string } | null;
/** Server-side plan picker state; asset files are never inferred garbage. */
pickSaved?: boolean;
saved?: Array<{
name: string; url: string; size: number; modified: number; used_by: string[];
}> | null;
savedBusy?: boolean;
savedAspect?: number;
source: SpacePlanSource;
showBorders: boolean;
showNames: boolean;
zeroWallStyle: ZeroWallStyle;
displayTouched: boolean;
hideDecor: boolean;
hideOpenings: boolean;
roomColor: string;
roomOpacity: number;
bgColor: string | null;
bgMode: 'static' | 'daynight' | null;
northDeg: number | null;
sunRays: boolean | null;
fillMode: 'none' | 'lqi' | 'light' | 'temp' | 'custom';
customFill: FillColorEntry | null;
glowEnabled: boolean;
tempMin: number;
tempMax: number;
showLqi: boolean;
cardFontScale: number;
labelTemp: boolean;
labelHum: boolean;
labelLqi: boolean;
labelLight: boolean;
cellCm: number;
cellCmInput?: string;
cellCmTouched?: boolean;
deleteBlockers?: number;
copy?: SpaceCopyDraft;
busy: boolean;
}
export interface SpaceDisplayDraft {
source: SpacePlanSource;
showBorders: boolean;
@@ -57,3 +111,11 @@ export function touchSpaceDisplay<T extends SpaceDisplayDraft>(
): T {
return { ...draft, [field]: value, displayTouched: true };
}
/** Numeric dialog fields consume the complete value; decimal comma is valid. */
export function strictNumber(value: string): number | null {
const text = String(value ?? '').trim().replace(',', '.');
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text)) return null;
const parsed = Number(text);
return Number.isFinite(parsed) ? parsed : null;
}
+1 -1
View File
@@ -20,7 +20,7 @@ test('the canonical config writer cannot drop its expected revision (#340)', ()
try {
mkdirSync(resolve(fixtureRoot, 'src'), { recursive: true });
mkdirSync(resolve(fixtureRoot, 'custom_components/houseplan'), { recursive: true });
for (const name of ['houseplan-card.ts', 'houseplan-editor-runtime.ts']) {
for (const name of ['houseplan-card.ts', 'houseplan-editor-runtime.ts', 'plan-optimize-write.ts']) {
writeFileSync(
resolve(fixtureRoot, 'src', name),
readFileSync(resolve(repoRoot, 'src', name), 'utf8'),
+241
View File
@@ -0,0 +1,241 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
closeSpaceCopyDialog, openSpaceCopyDialog, saveSpaceCopy,
} from '../test-build/space-copy-runtime.js';
import { contentFingerprint } from '../test-build/visual-continuity.js';
const configFixture = () => ({
model_version: 9,
spaces: [{
id: 'source', title: 'Floor', view_box: [0, 0, 1, 1], rooms: [], wall_segments: [],
}],
markers: [],
settings: {},
});
function runtimeHarness(overrides = {}) {
const config = configFixture();
const events = [];
const toasts = [];
const host = {
_spaceDialog: {
mode: 'edit', spaceId: 'source', title: 'unsaved settings title', busy: false,
copy: { title: 'Floor (2)', busy: false, token: 0 },
},
_serverCfg: config,
_layout: {},
_cfgRev: 5,
_layoutRev: 8,
_cfgContentFingerprint: contentFingerprint(config),
_saveConfigDebounced: { pending: () => false, flush: () => events.push('flush-config') },
_writeChain: Promise.resolve(),
_geometryHistory: { clear: () => events.push('clear-geometry-history') },
_devicePositionHistory: { clear: () => events.push('clear-device-history') },
_cancelDeviceDrag: () => false,
_canOptimizeUndo: false,
_undoKind: null,
_dirtyPos: new Set(),
_sentPos: new Map(),
_cfgEpoch: 0,
_modelCache: {},
_frame: {},
_regSignature: 'old',
_maybeRebuildDevices: () => events.push('rebuild'),
_cacheSnapshot: () => events.push('snapshot'),
requestUpdate: () => events.push('render'),
hass: {
callWS: async (message) => {
events.push(message.type);
if (message.type !== 'houseplan/plan/optimize') throw new Error('unexpected WS');
return { config_rev: 6, layout_rev: 9, can_undo: true };
},
},
_checkOptimizeGeometry: () => ({ fingerprint: 'safe', spaces: [], failures: [], ok: true }),
_checkSpacePhysicalGeometry: () => ({ ok: true }),
_junctionLimitViolations: () => [],
_confirmDanger: async () => { events.push('confirm'); return true; },
_reloadConfigOnly: async () => { events.push('reload-config'); },
_reloadLayoutOnly: async () => { events.push('reload-layout'); },
_showToast: (message) => toasts.push(message),
_t: (key, vars) => vars?.name ? `${key}:${vars.name}` : key,
_errText: (error) => error instanceof Error ? error.message : String(error),
_selId: 'selected-device',
_physicalSel: { kind: 'partition', id: 'selected-wall' },
_resumeDraftBySpace: { source: 'old-draft', sseed: 'must-be-cleared' },
_space: 'source',
_commitSpace: (id) => { events.push(`space:${id}`); host._space = id; return true; },
_tool: 'select',
_path: [[0, 0]],
_cursorPt: [0.5, 0.5],
_activeDraftId: 'draft',
_primeDrawWallField: () => events.push('prime-draw'),
_saveNav: () => events.push('save-nav'),
...overrides,
};
const services = {
clearGeometryGesture: () => {
events.push('clear-gesture');
host._activeDraftId = null;
host._path = [];
},
optimizeReferenceContext: () => ({}),
reportPreflightFailure: () => events.push('report-preflight'),
saveConfigNow: async () => {
events.push('config-write');
host._cfgRev++;
host._cfgContentFingerprint = contentFingerprint(host._serverCfg);
},
setMode: () => events.push('mode:plan'),
showWallModelMigrationBlocked: () => events.push('migration-blocked'),
optimize: (currentConfig, currentLayout) => ({
config: structuredClone(currentConfig), layout: structuredClone(currentLayout),
changed: false, report: {},
}),
newSeed: () => 'seed',
};
return { host, services, events, toasts };
}
test('#456 opens a numbered name dialog and Cancel restores the settings dialog', () => {
const { host } = runtimeHarness();
host._spaceDialog.copy = undefined;
host._serverCfg.spaces.push({
id: 'copy-2', title: 'Floor (2)', view_box: [0, 0, 1, 1], rooms: [], wall_segments: [],
});
openSpaceCopyDialog(host);
assert.equal(host._spaceDialog.copy.title, 'Floor (3)');
closeSpaceCopyDialog(host);
assert.equal(host._spaceDialog.copy, undefined);
assert.equal(host._serverCfg.spaces.length, 2, 'opening and cancelling are read-only');
});
test('#456 clean input performs one config write with no extra confirmation', async () => {
const { host, services, events, toasts } = runtimeHarness();
await saveSpaceCopy(host, services);
assert.equal(events.includes('confirm'), false);
assert.equal(events.includes('houseplan/plan/optimize'), false);
assert.equal(events.filter((event) => event === 'config-write').length, 1);
assert.deepEqual(host._serverCfg.spaces.map((space) => space.id), ['source', 'sseed']);
assert.equal(host._space, 'sseed');
assert.equal(host._spaceDialog, null);
assert.equal(host._tool, 'draw');
assert.deepEqual(host._path, []);
assert.equal(host._activeDraftId, null);
assert.equal(host._selId, null);
assert.equal(host._physicalSel, null);
assert.equal(toasts.at(-1), 'toast.space_copied:Floor (2)');
});
test('#456 a manually entered duplicate title is accepted but an empty title is inert', async () => {
const duplicate = runtimeHarness();
duplicate.host._spaceDialog.copy.title = ' Floor ';
await saveSpaceCopy(duplicate.host, duplicate.services);
assert.deepEqual(duplicate.host._serverCfg.spaces.map((space) => space.title), ['Floor', 'Floor']);
const empty = runtimeHarness();
empty.host._spaceDialog.copy.title = ' ';
await saveSpaceCopy(empty.host, empty.services);
assert.equal(empty.events.includes('config-write'), false);
assert.equal(empty.host._serverCfg.spaces.length, 1);
});
test('#456 required Optimize is explicitly confirmed and Cancel performs zero writes', async () => {
const { host, services, events } = runtimeHarness({
_confirmDanger: async () => { events.push('confirm'); return false; },
});
services.optimize = (currentConfig, currentLayout) => ({
config: structuredClone(currentConfig), layout: structuredClone(currentLayout),
changed: true, report: {},
});
await saveSpaceCopy(host, services);
assert.deepEqual(events.filter((event) => event === 'houseplan/plan/optimize' || event === 'config-write'), []);
assert.equal(host._spaceDialog.copy.title, 'Floor (2)');
assert.equal(host._spaceDialog.copy.busy, false);
assert.equal(host._spaceDialog.busy, false);
});
test('#456 a concurrent plan change invalidates an open Optimize confirmation', async () => {
const harness = runtimeHarness();
harness.services.optimize = (currentConfig, currentLayout) => ({
config: structuredClone(currentConfig), layout: structuredClone(currentLayout),
changed: true, report: {},
});
harness.host._confirmDanger = async () => {
harness.events.push('confirm');
harness.host._cfgRev++;
return true;
};
await saveSpaceCopy(harness.host, harness.services);
assert.equal(harness.events.includes('houseplan/plan/optimize'), false);
assert.equal(harness.events.includes('config-write'), false);
assert.equal(harness.host._spaceDialog.copy.busy, false);
assert.equal(harness.toasts.at(-1), 'space.copy_error_changed');
});
test('#456 accepted Optimize is durable before the copy candidate is written', async () => {
const { host, services, events } = runtimeHarness();
services.optimize = (currentConfig, currentLayout) => {
const optimized = structuredClone(currentConfig);
optimized.spaces[0].partitions = [{ id: 'optimized-wall', a: [0, 0], b: [1, 0], cm: 10 }];
return { config: optimized, layout: structuredClone(currentLayout), changed: true, report: {} };
};
await saveSpaceCopy(host, services);
assert.ok(events.indexOf('confirm') < events.indexOf('houseplan/plan/optimize'));
assert.ok(events.indexOf('houseplan/plan/optimize') < events.indexOf('config-write'));
assert.equal(host._serverCfg.spaces[1].partitions.length, 1);
assert.notEqual(host._serverCfg.spaces[1].partitions[0].id, 'optimized-wall');
assert.equal(host._canOptimizeUndo, false, 'the following config write consumes Optimize undo');
});
test('#456 unsafe Optimize reports the existing diagnostic and performs zero writes', async () => {
const { host, services, events, toasts } = runtimeHarness({
_checkOptimizeGeometry: () => ({
fingerprint: 'unsafe', spaces: [], failures: [{ status: 'failed' }], ok: false,
}),
});
services.optimize = (currentConfig, currentLayout) => ({
config: structuredClone(currentConfig), layout: structuredClone(currentLayout),
changed: true, report: {},
});
await saveSpaceCopy(host, services);
assert.ok(events.includes('report-preflight'));
assert.equal(events.includes('confirm'), false);
assert.equal(events.includes('houseplan/plan/optimize'), false);
assert.equal(events.includes('config-write'), false);
assert.equal(toasts.at(-1), 'space.copy_error_optimize_geometry');
});
test('#456 a rejected copy rolls back only the copy and keeps accepted Optimize', async () => {
const optimized = configFixture();
optimized.spaces[0].settings = { optimized: true };
const { host, services, events } = runtimeHarness({
_reloadConfigOnly: async () => {
events.push('reload-config');
host._serverCfg = structuredClone(optimized);
host._cfgContentFingerprint = contentFingerprint(host._serverCfg);
},
});
services.optimize = (_currentConfig, currentLayout) => ({
config: structuredClone(optimized), layout: structuredClone(currentLayout),
changed: true, report: {},
});
services.saveConfigNow = async () => {
events.push('config-write');
throw new Error('offline');
};
await saveSpaceCopy(host, services);
assert.deepEqual(host._serverCfg, optimized);
assert.equal(host._serverCfg.spaces.length, 1, 'no partial copy remains');
assert.equal(host._canOptimizeUndo, true, 'accepted Optimize remains undoable after a failed copy');
assert.equal(host._space, 'source');
assert.equal(host._spaceDialog.copy.busy, false);
assert.ok(events.includes('reload-config'));
});
+158
View File
@@ -0,0 +1,158 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createSpaceCopyCandidate, newSpaceCopySeed, nextSpaceCopyTitle, SpaceCopyError,
} from '../test-build/space-copy.js';
import { optimizePlans } from '../test-build/plan-optimizer.js';
import { makeLargeHouseFixture } from '../demo/fixtures/large-house.mjs';
const fixture = () => ({
model_version: 9,
spaces: [
{ id: 'before', title: 'Before', rooms: [], wall_segments: [], view_box: [0, 0, 1, 1] },
{
id: 'source', title: 'First floor', view_box: [0.1, 0.2, 0.7, 0.6], cell_cm: 1,
rooms: [{ id: 'room', name: 'Kitchen', poly: [[0, 0], [1, 0], [1, 1]] }],
wall_segments: [
{ id: 'wall-a', a: [0, 0], b: [1, 0], cm: 20, owners: ['room'] },
{ id: 'wall-b', a: [1, 0], b: [1, 1], cm: 0, owners: ['room'] },
],
partitions: [{ id: 'part-a', a: [0.5, 0], b: [0.5, 1], cm: 10, style: { keep: true } }],
openings: [
{
id: 'door', type: 'door', x: 0.25, y: 0, angle: 0, length: 0.2,
host: { kind: 'wall', id: 'wall-a', t: 0.25 },
contact: 'binary_sensor.door', lock: 'lock.door', invert: true,
},
{
id: 'passage', type: 'passage', x: 0.5, y: 0.6, angle: 90, length: 0.3,
host: { kind: 'partition', id: 'part-a', t: 0.6 }, flip_h: true,
},
],
decor: [{ id: 'decor-a', kind: 'rect', x: 0.2, y: 0.3, style: { color: '#123456' } }],
wall_columns: [{ id: 'column-a', x: 0.7, y: 0.8, cm: 20, shape: 'square' }],
room_drafts: [{ id: 'draft-a', path: [[0, 0], [0.2, 0.2]] }],
open_spans: [{ a: [0, 0], b: [0.1, 0] }],
walls: [{ key: 'legacy', cm: 15 }],
settings: { show_names: false, nested: { keep: true } },
zero_wall_style: 'solid',
plan_url: '/api/houseplan/plan/source.svg', plan_aspect: 1.5,
plan_x: 0.1, plan_y: 0.2, plan_scale: 1.2,
plan_scale_x: -1, plan_scale_y: 0.9, plan_angle: 12,
future_room_bound_field: { must_not_copy: true },
},
{ id: 'after', title: 'After', rooms: [], wall_segments: [], view_box: [0, 0, 1, 1] },
],
markers: [{ id: 'device', space: 'source', room_id: 'room', vacuum: { map_routes: [{ space: 'source' }] } }],
settings: { global: true },
});
test('#456 chooses the first free numbered copy title', () => {
const spaces = [{ title: 'Floor' }, { title: 'Floor (2)' }, { title: 'Floor (4)' }];
assert.equal(nextSpaceCopyTitle(' Floor ', spaces, 'Space'), 'Floor (3)');
assert.equal(nextSpaceCopyTitle('', [{ title: 'Space (2)' }], 'Space'), 'Space (3)');
});
test('#456 copies the complete allowed physical surface without room or device ownership', () => {
const input = fixture();
const before = structuredClone(input);
const result = createSpaceCopyCandidate(input, 'source', ' My copy ', 'seed');
const copy = result.space;
assert.deepEqual(input, before, 'preview must be immutable');
assert.deepEqual(result.config.spaces.map((space) => space.id), ['before', 'source', 'sseed', 'after']);
assert.equal(copy.title, 'My copy');
assert.deepEqual(copy.rooms, []);
assert.deepEqual(copy.wall_segments, []);
assert.deepEqual(copy.partitions.map(({ id, a, b, cm }) => ({ id, a, b, cm })), [
{ id: 'cwseed0', a: [0, 0], b: [1, 0], cm: 20 },
{ id: 'cwseed1', a: [1, 0], b: [1, 1], cm: 0 },
{ id: 'cpseed2', a: [0.5, 0], b: [0.5, 1], cm: 10 },
]);
assert.equal(Object.hasOwn(copy.partitions[2], 'style'), false, 'unknown wall metadata is not copied');
assert.deepEqual(copy.openings.map((opening) => ({
id: opening.id, host: opening.host, contact: opening.contact, lock: opening.lock,
})), [
{ id: 'coseed3', host: { kind: 'partition', id: 'cwseed0', t: 0.25 }, contact: undefined, lock: undefined },
{ id: 'coseed4', host: { kind: 'partition', id: 'cpseed2', t: 0.6 }, contact: undefined, lock: undefined },
]);
assert.equal(copy.openings[0].invert, true);
assert.equal(copy.openings[1].flip_h, true);
assert.equal(copy.decor[0].id, 'cdseed5');
assert.equal(copy.wall_columns[0].id, 'ccseed6');
for (const key of [
'settings', 'zero_wall_style', 'cell_cm', 'view_box', 'plan_url', 'plan_aspect',
'plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle',
]) assert.deepEqual(copy[key], before.spaces[1][key], key);
for (const key of ['room_drafts', 'open_spans', 'walls', 'future_room_bound_field']) {
assert.equal(Object.hasOwn(copy, key), false, key);
}
assert.deepEqual(result.config.markers, before.markers, 'markers and vacuum routes stay untouched');
assert.deepEqual(result.config.settings, before.settings, 'global settings stay untouched');
copy.settings.nested.keep = false;
copy.decor[0].style.color = '#ffffff';
assert.equal(input.spaces[1].settings.nested.keep, true, 'settings are deep-copied');
assert.equal(input.spaces[1].decor[0].style.color, '#123456', 'decor is deep-copied');
});
test('#456 rejects missing and unknown opening hosts instead of losing an opening', () => {
const missing = fixture();
delete missing.spaces[1].openings[0].host;
assert.throws(
() => createSpaceCopyCandidate(missing, 'source', 'Copy', 'seed'),
(error) => error instanceof SpaceCopyError && error.code === 'opening_host_missing',
);
const unknown = fixture();
unknown.spaces[1].openings[0].host.id = 'gone';
assert.throws(
() => createSpaceCopyCandidate(unknown, 'source', 'Copy', 'seed'),
(error) => error instanceof SpaceCopyError && error.code === 'opening_host_unknown',
);
});
test('#456 enforces backend collection boundaries before producing a candidate', () => {
const atSpaceLimit = fixture();
atSpaceLimit.spaces.push(...Array.from({ length: 47 }, (_, index) => ({
id: `extra-${index}`, title: `Extra ${index}`, rooms: [], wall_segments: [], view_box: [0, 0, 1, 1],
})));
assert.throws(
() => createSpaceCopyCandidate(atSpaceLimit, 'source', 'Copy', 'seed'),
(error) => error instanceof SpaceCopyError && error.code === 'spaces_limit',
);
const tooManyWalls = fixture();
tooManyWalls.spaces[1].wall_segments = Array.from({ length: 2001 }, (_, index) => ({
id: `w-${index}`, a: [index / 3000, 0], b: [index / 3000, 1], cm: 10,
}));
tooManyWalls.spaces[1].openings = [];
tooManyWalls.spaces[1].partitions = [];
assert.throws(
() => createSpaceCopyCandidate(tooManyWalls, 'source', 'Copy', 'seed'),
(error) => error instanceof SpaceCopyError && error.code === 'partitions_limit',
);
});
test('#456 generated seeds are bounded, deterministic at the test seam and title-free', () => {
assert.equal(newSpaceCopySeed(123456789, 0.5), '21i3v94zsow');
assert.match(newSpaceCopySeed(), /^[a-z0-9]+$/);
});
test('#456 the large-house floor keeps every wall and opening after Optimize', () => {
const fixture = makeLargeHouseFixture();
const optimized = optimizePlans(fixture.config, fixture.layout);
const source = optimized.config.spaces[0];
const result = createSpaceCopyCandidate(
optimized.config, source.id, `${source.title} copy`, 'large',
);
assert.equal(result.space.rooms.length, 0);
assert.equal(result.space.wall_segments.length, 0);
assert.equal(
result.space.partitions.length,
source.wall_segments.length + (source.partitions?.length || 0),
);
assert.equal(result.space.openings.length, source.openings.length);
assert.ok(result.space.openings.every((opening) => opening.host.kind === 'partition'));
});
+10
View File
@@ -1,5 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
createEmptySpaceConfig,
@@ -72,3 +73,12 @@ test('source and display transitions are immutable and preserve unrelated fields
assert.equal(touched.title, 'Ground');
assert.equal(touched.cellCm, 7.5);
});
test('#456 Copy is an edit-settings action and is absent from onboarding', () => {
const runtime = readFileSync(new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8');
const onboarding = readFileSync(new URL('../src/houseplan-onboarding-runtime.ts', import.meta.url), 'utf8');
assert.match(runtime, /d\.mode === 'edit'[\s\S]*openSpaceCopyDialog\(this\.host\)/);
assert.match(runtime, /<div class="dialog-action-group">[\s\S]*btn\.copy/);
assert.doesNotMatch(runtime, /dialog-action-danger[\s\S]{0,300}btn\.copy/);
assert.doesNotMatch(onboarding, /btn\.copy|openSpaceCopyDialog|space\.copy_/);
});
+33
View File
@@ -326,6 +326,39 @@ def test_config_schema_defaults_and_extra():
assert "spaces" in out2
def test_issue_456_roomless_copy_with_rehosted_openings_is_valid():
"""The frontend Copy result is a supported v9 document, not a UI-only shape."""
copied = {
"model_version": 9,
"spaces": [{
"id": "copy",
"title": "Floor copy",
"view_box": [0, 0, 1, 1],
"rooms": [],
"wall_segments": [],
"partitions": [
{"id": "wall-copy", "a": [0, 0], "b": [1, 0], "cm": 20},
{"id": "partition-copy", "a": [0.5, 0], "b": [0.5, 1], "cm": 10},
],
"openings": [
{
"id": "door-copy", "type": "door", "x": 0.25, "y": 0,
"angle": 0, "length": 0.2, "invert": True,
"host": {"kind": "partition", "id": "wall-copy", "t": 0.25},
},
{
"id": "passage-copy", "type": "passage", "x": 0.5, "y": 0.7,
"angle": 90, "length": 0.2,
"host": {"kind": "partition", "id": "partition-copy", "t": 0.7},
},
],
}],
"markers": [],
"settings": {},
}
assert v.CONFIG_SCHEMA(copied)["spaces"][0]["rooms"] == []
def test_config_schema_full_roundtrip():
cfg = {
"spaces": [{
+1 -1
View File
@@ -32,7 +32,7 @@
"src/coordinate-canonicalization.ts",
"src/serialized-write-queue.ts",
"src/align-grid.ts",
"src/plan-optimizer.ts", "src/space-reference-repair.ts", "src/space-deletion.ts",
"src/plan-optimizer.ts", "src/plan-optimize-write.ts", "src/space-reference-repair.ts", "src/space-deletion.ts", "src/space-copy.ts", "src/space-copy-runtime.ts",
"src/plan-geometry-preflight.ts",
"src/furniture.ts", "src/decor-assets.ts",
"src/floating-surface.ts", "src/floating-surface-controller.ts", "src/help-behavior.ts", "src/support-feedback.ts",