feat: smooth vacuum trails (#209)

Issue: #209
User-Visible: yes
This commit is contained in:
Matysh
2026-08-28 23:16:58 +03:00
parent 2890b9a1ab
commit 9d2ef12c96
43 changed files with 578 additions and 268 deletions
@@ -1,111 +1,111 @@
{
"schema": 1,
"fingerprint": "20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019",
"fingerprint": "dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
"houseplan-assets/houseplan-card-B_i3R4iG.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 271455,
"initialViewGzipBytes": 271846,
"lazyFiles": [
"houseplan-assets/de-DRFSDdUU.js",
"houseplan-assets/editor-Rs89b855.js",
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/de-W0NvUh84.js",
"houseplan-assets/editor-CyHGDaga.js",
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"lazyGzipBytes": 165917,
"lazyGzipBytes": 165918,
"lazyEditorFiles": [
"houseplan-assets/editor-Rs89b855.js",
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/editor-CyHGDaga.js",
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"lazyEditorGzipBytes": 136808,
"lazyEditorGzipBytes": 136811,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"lazyOnboardingGzipBytes": 7195,
"lazyOnboardingGzipBytes": 7199,
"lazyLocaleFiles": [
"houseplan-assets/de-DRFSDdUU.js"
"houseplan-assets/de-W0NvUh84.js"
],
"lazyLocaleGzipBytes": 22465,
"lazyLocaleGzipBytes": 22460,
"files": [
{
"path": "houseplan-assets/de-DRFSDdUU.js",
"sha256": "93319bdb17d7b9902672342eb6daa15f32ab09658dd6415deb157decb0edf613",
"path": "houseplan-assets/de-W0NvUh84.js",
"sha256": "37aaf04bdd37c2780782d25e6167ce5e61e2022f1c30c8febfa01fb63bf7eb0a",
"rawBytes": 73919,
"gzipBytes": 22465,
"gzipBytes": 22460,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Rs89b855.js",
"sha256": "93bd34541456059f9dd5369dd9a51a5bd8a39f99089927d610bf6fe6cfbf87c5",
"path": "houseplan-assets/editor-CyHGDaga.js",
"sha256": "059234eb256c8ffe07feba0342796637b8b71e49068bc9f5c338a8e3033d96d2",
"rawBytes": 3831,
"gzipBytes": 1582,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
"houseplan-assets/houseplan-card-B_i3R4iG.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Cjdt1t7f.js",
"sha256": "10d36979e2f624b0dc5e98a445ea0c8dedec6469857b12dbd99c84f8dded4ee5",
"rawBytes": 957284,
"gzipBytes": 270703,
"path": "houseplan-assets/houseplan-card-B_i3R4iG.js",
"sha256": "00bfdc2f641863996f10c6d32da364da10d45d5af2c0dc2dc755e929da771d55",
"rawBytes": 958245,
"gzipBytes": 271094,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-DRFSDdUU.js",
"houseplan-assets/editor-Rs89b855.js",
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js"
"houseplan-assets/de-W0NvUh84.js",
"houseplan-assets/editor-CyHGDaga.js",
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
"sha256": "ea20c56b4b365bd0accc4cd07950322b5b9dda70bc15a17b5d69c14df0ed0a55",
"path": "houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
"sha256": "c90c6f8d2160755c7931cf80aaedfe988ed5bb5822aa1432eab8944dc700a063",
"rawBytes": 520292,
"gzipBytes": 134675,
"gzipBytes": 134677,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/houseplan-card-B_i3R4iG.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
"sha256": "f18f2ec9af6091f8f731a2c8d969f05cdd8e34dec1c76c35a11857f7ab1e9d3a",
"path": "houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
"sha256": "a1fafac0f5ffc1839c18f66398c7f397c38feeda8296b6aad2a06d34cf7dedce",
"rawBytes": 26356,
"gzipBytes": 6644,
"gzipBytes": 6647,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/houseplan-card-B_i3R4iG.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-1NoE8x_l.js",
"sha256": "20448d45c7f3cbeed2ca72852535b30c0b7dd5bf70d6b9c354fd71ca693abf84",
"rawBytes": 906,
"gzipBytes": 551,
"path": "houseplan-assets/space-deletion-D2aHGv12.js",
"sha256": "0f0018f508fee171aaa272cd947f303967dfe59f2e283d97ec004fca91f2b0fa",
"rawBytes": 911,
"gzipBytes": 552,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "15b38319f0dc22546a9f747ae4c227b49f10b5f21595cdf2ce758e09a7402be5",
"sha256": "ff4dc28297338f8b56f26ca60856468c9f3b2bd1b813731da91fc0324a12e4b4",
"rawBytes": 1089,
"gzipBytes": 752,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
"houseplan-assets/houseplan-card-B_i3R4iG.js"
],
"dynamicImports": []
}
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-Cjdt1t7f.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},_=this._schema,f=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-B_i3R4iG.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}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(_.slice(0,3))}
${f?c`<div class="default-floor-error" role="alert"
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:f})}
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(_.slice(3))}
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-Cjdt1t7f.js";import{i as b,c as $,a as v,s as y,t as f}from"./space-deletion-1NoE8x_l.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";class C{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)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_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){if(confirm(this.host._t("confirm.delete_plan",{name:t})))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 p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-B_i3R4iG.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-D2aHGv12.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";class C{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)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_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){if(confirm(this.host._t("confirm.delete_plan",{name:t})))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 p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
${s.map(s=>{return p`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
@@ -46,7 +46,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
<label>${this.host._t("space.plan_label")}</label>
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"file"===t.source}
@change=${()=>this.host._spaceDialog=y(t,"file")} />
@change=${()=>this.host._spaceDialog=f(t,"file")} />
<span>${this.host._t("space.source_file")}</span>
</label>
${"file"===t.source?p`<div class="planrow">
@@ -68,7 +68,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
${t.pickSaved?this._renderServerPlans(t):_}`:_}
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"draw"===t.source}
@change=${()=>this.host._spaceDialog=y(t,"draw")} />
@change=${()=>this.host._spaceDialog=f(t,"draw")} />
<span>${this.host._t("space.source_draw")}</span>
</label>
@@ -85,7 +85,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
<label class="dispsection">${this.host._t("space.display_section")}</label>
<label class="srcrow">
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=f(t,"showBorders",s)})}
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=y(t,"showBorders",s)})}
<span>${this.host._t("space.show_borders")}</span>
</label>
<label>${this.host._t("space.zero_wall_style")}</label>
@@ -99,7 +99,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
</select>
<div class="rhint">${this.host._t("space.zero_wall_help")}</div>
<label class="srcrow">
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=f(t,"showNames",s)})}
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=y(t,"showNames",s)})}
<span>${this.host._t("space.show_names")}</span>
</label>
<label class="srcrow">
@@ -1 +1 @@
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";export{a,e as c,o as i,r as s,s as t};
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";export{t as a,e as c,o as i,r as s,s as t};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";try{await import("./houseplan-assets/houseplan-card-Cjdt1t7f.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).":"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__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";try{await import("./houseplan-assets/houseplan-card-B_i3R4iG.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).":"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)}
+62
View File
@@ -677,6 +677,44 @@ export function prepareGoldenFixture(scenario) {
if (missing.length) throw new Error(`golden layoutOverrides reference missing item(s): ${missing.join(', ')}`);
fixture.layout = { ...(fixture.layout || {}), ...structuredClone(scenario.layoutOverrides) };
}
if (scenario.vacuumTrail) {
const deviceId = 'golden-vacuum-trail';
const vacuumEntity = 'vacuum.golden_vacuum_trail';
const sourceEntity = 'camera.golden_vacuum_trail_map';
fixture.devices[deviceId] = {
id: deviceId, name: 'Golden vacuum trail', model: 'GOLDEN-VACUUM-TRAIL',
area_id: 'golden_geo_sw', identifiers: [['houseplan_golden', deviceId]],
config_entries: ['golden_entry'], entry_type: null, via_device_id: null, disabled_by: null,
};
fixture.entities[vacuumEntity] = {
entity_id: vacuumEntity, device_id: deviceId, platform: 'houseplan_golden',
config_entry_id: 'golden_entry', disabled_by: null,
};
fixture.entities[sourceEntity] = {
entity_id: sourceEntity, device_id: deviceId, platform: 'houseplan_golden',
config_entry_id: 'golden_entry', disabled_by: null,
};
fixture.states[vacuumEntity] = {
entity_id: vacuumEntity, state: 'cleaning',
attributes: { friendly_name: 'Golden vacuum trail' },
};
fixture.states[sourceEntity] = {
entity_id: sourceEntity, state: 'idle', attributes: {
friendly_name: 'Golden vacuum trail map', map_name: 'golden-trail',
vacuum_position: { x: 850, y: 760, a: 0 },
path: { path: structuredClone(scenario.vacuumTrail.current) },
},
};
fixture.layout[deviceId] = { s: scenario.space, x: 0.12, y: 0.84 };
fixture.config.markers = [...(fixture.config.markers || []), {
id: deviceId, binding: `device:${deviceId}`, space: scenario.space,
area: 'golden_geo_sw',
vacuum: {
source: sourceEntity, trail_mode: 'always',
calibration: { 'golden-trail': [1, 0, 0, 0, 1, 0] },
},
}];
}
return fixture;
}
@@ -740,6 +778,14 @@ export async function prepareGoldenScenario(page, scenario) {
return { config: structuredClone(fixture.config), rev: 1, can_write: true };
if (message.type === 'houseplan/layout/get')
return { layout: structuredClone(fixture.layout || {}), rev: 1 };
if (message.type === 'houseplan/trail/get') return scenario.vacuumTrail ? {
trails: { 'golden-vacuum-trail': {
previous: {
map_id: 'golden-trail', started: 1, ended: 2,
points: structuredClone(scenario.vacuumTrail.previous),
},
} },
} : { trails: {} };
if (message.type === 'config/device_registry/list') return Object.values(fixture.devices || {});
if (message.type === 'config/entity_registry/list') return Object.values(fixture.entities || {});
if (message.type === 'config_entries/get')
@@ -1574,6 +1620,22 @@ export async function prepareGoldenScenario(page, scenario) {
throw new Error(`golden day-cycle contract did not render: ${scenario.id}`);
}
}
if (scenario.vacuumTrail) {
await until(() => !!card.renderRoot.querySelector('.vactrail > path.case'));
const currentCase = card.renderRoot.querySelector('.vactrail > path.case');
const currentCore = card.renderRoot.querySelector('.vactrail > path.core');
const previousCase = card.renderRoot.querySelector('.vactrail .prev path.case');
const previousCore = card.renderRoot.querySelector('.vactrail .prev path.core');
const currentD = currentCase?.getAttribute('d') || '';
const previousD = previousCase?.getAttribute('d') || '';
if ((currentD.match(/M /g) || []).length !== 2
|| (currentD.match(/Q /g) || []).length < 4
|| (previousD.match(/Q /g) || []).length < 2
|| currentD !== currentCore?.getAttribute('d')
|| previousD !== previousCore?.getAttribute('d')) {
throw new Error(`golden vacuum trail contract is incomplete: ${scenario.id}`);
}
}
await document.fonts?.ready;
await frame();
return {
+10 -1
View File
@@ -1,7 +1,7 @@
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
export const GOLDEN_MATRIX_VERSION = 49;
export const GOLDEN_MATRIX_VERSION = 50;
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
@@ -222,6 +222,15 @@ export const GOLDEN_SCENARIOS = Object.freeze([
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'geometry-view-light-fit', fixture: 'visual', space: 'golden-geometry', mode: 'view',
theme: 'light', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'vacuum-trail-smoothing-dark', fixture: 'visual', space: 'golden-geometry', mode: 'view',
vacuumTrail: {
current: [
[[180, 220], [360, 220], [360, 420], [520, 420]],
[[650, 240], [780, 240], [780, 500], [650, 620], [850, 760]],
],
previous: [[160, 760], [300, 650], [420, 740], [560, 600]],
},
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'room-label-parity-view-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',
roomLabelParity: true, theme: 'dark', viewport: { width: 1000, height: 900 }, ...page },
{ id: 'room-label-parity-plan-dark', fixture: 'visual', space: 'golden-lighting', mode: 'plan',
+3 -1
View File
@@ -68,7 +68,7 @@ const out = await page.evaluate(async () => {
entity_id: 'camera.static_robot_map', state: 'idle',
attributes: {
map_name: 'm1', vacuum_position: { x: 650, y: 700, a: 0 },
path: [[200, 200], [420, 440], [650, 700]],
path: [[200, 200], [420, 200], [420, 440], [650, 700]],
},
},
'sun.sun': {
@@ -110,6 +110,7 @@ const out = await page.evaluate(async () => {
const before = layers();
const beforeSpillParts = [...root(original).querySelectorAll('.glow-pool')]
.map((node) => node.getAttribute('data-lit-parts'));
const isoVacuumTrailD = root(original).querySelector('.vactrail path.case')?.getAttribute('d') || '';
const wallFingerprint = root(original).querySelector('[data-hp="iso-walls"]')?.dataset.fingerprint;
const cachedGeometry = original._isoGeometryCache.get(wallFingerprint)?.geometry;
const light = original.hass.states['light.ceiling'];
@@ -154,6 +155,7 @@ const out = await page.evaluate(async () => {
&& before.verticalOpenings === 5
&& before.devices > 0 && before.hover > 0 && before.vacuumPucks > 0
&& before.vacuumTrails >= 2,
vacuumTrailCurved: (isoVacuumTrailD.match(/Q /g) || []).length === 1,
liveLayersStable: JSON.stringify(after) === JSON.stringify(before),
spillBarrierStable: JSON.stringify(afterSpillParts) === JSON.stringify(beforeSpillParts)
&& beforeSpillParts.length >= 2 && beforeSpillParts.every((parts) => Number(parts) > 0),
+11 -3
View File
@@ -140,6 +140,7 @@ const out = await page.evaluate(async () => {
c.hass = { ...c.hass, states: { ...c.hass.states } }; await c.updateComplete;
const resumedD = sr().querySelector('.vactrail > path.case')?.getAttribute('d') || '';
o.srvResumedCurrentKeepsEarlierPoints = (resumedD.match(/[ML]/g) || []).length === 4;
o.srvResumedCurrentSmooth = (resumedD.match(/Q /g) || []).length === 2;
// ---- server-side runs: current + one previous (owner 2026-07-31) ----
cfg.markers.find((m) => m.id === 'e_vacuum_robo').vacuum.trail_mode = 'always';
@@ -155,11 +156,17 @@ const out = await page.evaluate(async () => {
await c.updateComplete;
c.hass = { ...c.hass, states: { ...c.hass.states } }; await c.updateComplete;
const prevG = sr().querySelector('.vactrail g.prev');
o.srvPrevRunShown = !!prevG && prevG.querySelectorAll('polyline').length === 2
&& prevG.querySelector('.case').getAttribute('points').split(' ').length === 3;
const prevCase = prevG?.querySelector('path.case');
const prevCore = prevG?.querySelector('path.core');
const prevD = prevCase?.getAttribute('d') || '';
o.srvPrevRunShown = !!prevG && prevG.querySelectorAll('path').length === 2
&& prevD === prevCore?.getAttribute('d')
&& (prevD.match(/M /g) || []).length === 1
&& (prevD.match(/Q /g) || []).length === 1;
const cur = sr().querySelector('.vactrail > path.case');
// 4 server points, the live tail trimmed while moving -> 3 rendered
o.srvCurTrimmed = !!cur && (cur.getAttribute('d').match(/[ML]/g) || []).length === 3;
o.srvCurTrimmed = !!cur && (cur.getAttribute('d').match(/[ML]/g) || []).length === 3
&& (cur.getAttribute('d').match(/Q /g) || []).length === 1;
o.srvPrevFaded = !!prevG && getComputedStyle(prevG).opacity === '0.4';
// XCME integration path outranks the simultaneous server run. Its two
// subpaths remain two SVG moveto sections: no synthetic bridge over a gap.
@@ -280,6 +287,7 @@ checkAll(out, {
puckGoneWhenDocked: true, trailHiddenAfterDock: true, hiddenNoPuck: true,
unknownMapNoPuck: true,
srvEndedCurrentHiddenDefault: true, srvResumedCurrentKeepsEarlierPoints: true,
srvResumedCurrentSmooth: true,
srvPrevRunShown: true, srvCurTrimmed: true, srvPrevFaded: true,
multiSubpathNoBridge: true, integrationPathAlwaysAtRest: true,
tipExists: true, tipGluedToPuck: true, neverHidesAll: true,
+48 -48
View File
@@ -1,111 +1,111 @@
{
"schema": 1,
"fingerprint": "20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019",
"fingerprint": "dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
"houseplan-assets/houseplan-card-B_i3R4iG.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 271455,
"initialViewGzipBytes": 271846,
"lazyFiles": [
"houseplan-assets/de-DRFSDdUU.js",
"houseplan-assets/editor-Rs89b855.js",
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/de-W0NvUh84.js",
"houseplan-assets/editor-CyHGDaga.js",
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"lazyGzipBytes": 165917,
"lazyGzipBytes": 165918,
"lazyEditorFiles": [
"houseplan-assets/editor-Rs89b855.js",
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/editor-CyHGDaga.js",
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"lazyEditorGzipBytes": 136808,
"lazyEditorGzipBytes": 136811,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"lazyOnboardingGzipBytes": 7195,
"lazyOnboardingGzipBytes": 7199,
"lazyLocaleFiles": [
"houseplan-assets/de-DRFSDdUU.js"
"houseplan-assets/de-W0NvUh84.js"
],
"lazyLocaleGzipBytes": 22465,
"lazyLocaleGzipBytes": 22460,
"files": [
{
"path": "houseplan-assets/de-DRFSDdUU.js",
"sha256": "93319bdb17d7b9902672342eb6daa15f32ab09658dd6415deb157decb0edf613",
"path": "houseplan-assets/de-W0NvUh84.js",
"sha256": "37aaf04bdd37c2780782d25e6167ce5e61e2022f1c30c8febfa01fb63bf7eb0a",
"rawBytes": 73919,
"gzipBytes": 22465,
"gzipBytes": 22460,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Rs89b855.js",
"sha256": "93bd34541456059f9dd5369dd9a51a5bd8a39f99089927d610bf6fe6cfbf87c5",
"path": "houseplan-assets/editor-CyHGDaga.js",
"sha256": "059234eb256c8ffe07feba0342796637b8b71e49068bc9f5c338a8e3033d96d2",
"rawBytes": 3831,
"gzipBytes": 1582,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
"houseplan-assets/houseplan-card-B_i3R4iG.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Cjdt1t7f.js",
"sha256": "10d36979e2f624b0dc5e98a445ea0c8dedec6469857b12dbd99c84f8dded4ee5",
"rawBytes": 957284,
"gzipBytes": 270703,
"path": "houseplan-assets/houseplan-card-B_i3R4iG.js",
"sha256": "00bfdc2f641863996f10c6d32da364da10d45d5af2c0dc2dc755e929da771d55",
"rawBytes": 958245,
"gzipBytes": 271094,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-DRFSDdUU.js",
"houseplan-assets/editor-Rs89b855.js",
"houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
"houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js"
"houseplan-assets/de-W0NvUh84.js",
"houseplan-assets/editor-CyHGDaga.js",
"houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
"houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BL8faVDv.js",
"sha256": "ea20c56b4b365bd0accc4cd07950322b5b9dda70bc15a17b5d69c14df0ed0a55",
"path": "houseplan-assets/houseplan-editor-runtime-o9_82gfm.js",
"sha256": "c90c6f8d2160755c7931cf80aaedfe988ed5bb5822aa1432eab8944dc700a063",
"rawBytes": 520292,
"gzipBytes": 134675,
"gzipBytes": 134677,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/houseplan-card-B_i3R4iG.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-Ci7QymAY.js",
"sha256": "f18f2ec9af6091f8f731a2c8d969f05cdd8e34dec1c76c35a11857f7ab1e9d3a",
"path": "houseplan-assets/houseplan-onboarding-runtime-BLEW_xwn.js",
"sha256": "a1fafac0f5ffc1839c18f66398c7f397c38feeda8296b6aad2a06d34cf7dedce",
"rawBytes": 26356,
"gzipBytes": 6644,
"gzipBytes": 6647,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js",
"houseplan-assets/space-deletion-1NoE8x_l.js"
"houseplan-assets/houseplan-card-B_i3R4iG.js",
"houseplan-assets/space-deletion-D2aHGv12.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-1NoE8x_l.js",
"sha256": "20448d45c7f3cbeed2ca72852535b30c0b7dd5bf70d6b9c354fd71ca693abf84",
"rawBytes": 906,
"gzipBytes": 551,
"path": "houseplan-assets/space-deletion-D2aHGv12.js",
"sha256": "0f0018f508fee171aaa272cd947f303967dfe59f2e283d97ec004fca91f2b0fa",
"rawBytes": 911,
"gzipBytes": 552,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "15b38319f0dc22546a9f747ae4c227b49f10b5f21595cdf2ce758e09a7402be5",
"sha256": "ff4dc28297338f8b56f26ca60856468c9f3b2bd1b813731da91fc0324a12e4b4",
"rawBytes": 1089,
"gzipBytes": 752,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Cjdt1t7f.js"
"houseplan-assets/houseplan-card-B_i3R4iG.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-Cjdt1t7f.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},_=this._schema,f=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-B_i3R4iG.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}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(_.slice(0,3))}
${f?c`<div class="default-floor-error" role="alert"
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:f})}
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(_.slice(3))}
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-Cjdt1t7f.js";import{i as b,c as $,a as v,s as y,t as f}from"./space-deletion-1NoE8x_l.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";class C{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)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_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){if(confirm(this.host._t("confirm.delete_plan",{name:t})))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 p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-B_i3R4iG.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-D2aHGv12.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";class C{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)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_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){if(confirm(this.host._t("confirm.delete_plan",{name:t})))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 p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
${s.map(s=>{return p`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
@@ -46,7 +46,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
<label>${this.host._t("space.plan_label")}</label>
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"file"===t.source}
@change=${()=>this.host._spaceDialog=y(t,"file")} />
@change=${()=>this.host._spaceDialog=f(t,"file")} />
<span>${this.host._t("space.source_file")}</span>
</label>
${"file"===t.source?p`<div class="planrow">
@@ -68,7 +68,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
${t.pickSaved?this._renderServerPlans(t):_}`:_}
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"draw"===t.source}
@change=${()=>this.host._spaceDialog=y(t,"draw")} />
@change=${()=>this.host._spaceDialog=f(t,"draw")} />
<span>${this.host._t("space.source_draw")}</span>
</label>
@@ -85,7 +85,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
<label class="dispsection">${this.host._t("space.display_section")}</label>
<label class="srcrow">
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=f(t,"showBorders",s)})}
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=y(t,"showBorders",s)})}
<span>${this.host._t("space.show_borders")}</span>
</label>
<label>${this.host._t("space.zero_wall_style")}</label>
@@ -99,7 +99,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b
</select>
<div class="rhint">${this.host._t("space.zero_wall_help")}</div>
<label class="srcrow">
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=f(t,"showNames",s)})}
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=y(t,"showNames",s)})}
<span>${this.host._t("space.show_names")}</span>
</label>
<label class="srcrow">
@@ -1 +1 @@
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";export{a,e as c,o as i,r as s,s as t};
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";export{t as a,e as c,o as i,r as s,s as t};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="20bb943045ad5eaa2ebc207fc30c1f5a570b0157da1a32450c71882cbd939019";try{await import("./houseplan-assets/houseplan-card-Cjdt1t7f.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).":"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__="dcade390c5de1a3b8ba29307c6f93fdfd8bda1809b228f1242b561be04d14fe4";try{await import("./houseplan-assets/houseplan-card-B_i3R4iG.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).":"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)}
+8
View File
@@ -242,6 +242,14 @@ priority decision. `resolveVacSource()` is sticky for saved sources and limits
automatic selection to compatible entities on the same HA device; the card
adds registry status through the shared `resolveHaBindingStatus()` authority.
`smoothVacPath()` is the shared pure presentation step for current and previous
runs. It consumes calibrated flat plan coordinates and returns typed
`move|line|quadratic` commands with a caller-supplied physical radius; the card
then applies flat/isometric scene projection and SVG serialization. Each corner
uses a quadratic inside the adjacent-segment convex hull, bounded by half of
both segment lengths, so the 17.5 cm product limit, exact endpoints and literal
subpath gaps are structural rather than renderer-specific accidents.
Room auto-calibration uses the same shoelace `areaCentroid()` for plan polygons
and robot outlines. Residuals are converted through resolved grid pitch and
cell centimetres; matrices above the 40 cm threshold remain proposals until an
+6
View File
@@ -2,6 +2,12 @@
## Unreleased
- Robot-vacuum trails now follow the same recorded points with softly rounded
corners instead of a visibly segmented polyline. Current and previous runs
use the same bounded curve, preserve telemetry gaps and never deviate by more
than 17.5 cm from the recorded route
([#209](https://github.com/Matysh/houseplan-card/issues/209)).
- A robot vacuum that reports its live position no longer freezes the card
on a freshly opened page: the map-id lookup ran inside the render cycle
and required editor code that a cold tab has not loaded yet, so the very
+6
View File
@@ -8,6 +8,12 @@
## Не выпущено
- След пылесоса теперь повторяет те же записанные точки с мягко скруглёнными
поворотами вместо заметной ломаной. Текущая и предыдущая уборки используют
одну ограниченную кривую, сохраняют разрывы телеметрии и не отклоняются от
записанного маршрута более чем на 17,5 см
([#209](https://github.com/Matysh/houseplan-card/issues/209)).
- Робот-пылесос с живой позицией больше не замораживает карточку на только
что открытой странице: определение id карты выполнялось внутри цикла
отрисовки и требовало код редактора, который холодная вкладка ещё не
+1 -1
View File
@@ -33,7 +33,7 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
| Furniture | #159 replaces the flat ~30-item picker with a two-level category/variant palette and 56 top-view symbols. The reviewed 77-SVG MIT source pack is vendored under `assets/furniture/houseplan-0.3.0`; plan art stays in the initial View graph, front-view category art stays in the lazy editor graph, and existing saved geometry is unchanged. |
| Tests | Four layers: Node unit (`npm test`: frontend pure modules + tooling policy), pure backend (`pytest tests_backend`, runs anywhere), HA-harness backend (same folder, CI only — needs py3.13 + pytest-homeassistant-custom-component), and browser smokes (`demo/smoke_*.mjs`, headless chromium). **Counts are not written down here** — they went stale within two releases while the version line beside them was kept current, which reads as less coverage than exists (review R5-2). Run `npm run inventory` for the current numbers, or read them off the last CI run |
| Input support | Owner's rule since 2026-08-08: View and kiosk are fully supported and release-blocking on touch. All three editors are desktop-first; touch editing is best effort and may be awkward, reduced or absent when parity is expensive. `docs/TOUCH-SUPPORT.md` defines the non-negotiable safety floor and documentation/test rules |
| Vacuums | Live puck, server-side trails and fit calibration are shipped. The local v1.61 Stage 1 contract in docs/VACUUM.md adds explicit Dreame/XCME/Valetudo coverage, registry-less source selection, capability diagnostics, path-gap preservation and source-health warnings; #205's issue branch resumes one ended same-map run through an inclusive 30-minute station/pause grace. Roomba remains Stage 2 |
| Vacuums | Live puck, server-side trails and fit calibration are shipped. The local v1.61 Stage 1 contract in docs/VACUUM.md adds explicit Dreame/XCME/Valetudo coverage, registry-less source selection, capability diagnostics, path-gap preservation and source-health warnings; #205 resumes one ended same-map run through an inclusive 30-minute station/pause grace. #209 renders current and previous trails through the same bounded 17.5 cm rounded-corner curve without changing stored points or gaps. Roomba remains Stage 2 |
| Demo stand | **https://demo.houseplan.tech** — public, login `demo`/`demo`, resets to a pristine synthetic home every hour. **https://dev.houseplan.tech** — closed (basic auth), auto-deploys the `dev` branch every 10 min. Host: `ssh -i ~/.ssh/hp_stand hp@135.106.166.146`; layout, seeds and gotchas in the memory note `houseplan-demo-stand`. Since 2026-07-31 the stand covers most of the manual checklist: a scripted robot vacuum (`demo/stand/demo_robot` — Tasshack-shaped map sensor, serpentine run, pre-solved calibration, seeded server trail), Zigbee-style LQI template sensors, hand/auto-triggered leak+smoke alarms, an hvac_action climate marker and working script/scene/automation targets for tap-run. The stand-specific how-to-check guide is **docs/TESTING-DEMO.md** |
| Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts |
| Product scope | `docs/SCOPE.md` is the feature guard rail; `docs/TOUCH-SUPPORT.md` is the input-support contract — check both before accepting interaction work |
+20
View File
@@ -37,6 +37,26 @@
мутантам с браузерным гвардом; компиляция тестов в worktree стартует с
тёплого `test-build/` основного дерева.
## Vacuum trail smoothing (#209)
- [ ] Pure `smoothVacPath` tests prove exact endpoints, separate subpaths,
degenerate/reversal fallbacks, the 17.5 cm sampled deviation bound and
the 64-subpath/4000-point `≤2N` command budget
[unit: `test/vacuum.test.mjs`].
- [ ] The production bundle renders current and previous runs through paired
case/core `<path>` elements with quadratic commands, unchanged source
authority/modes, live-target trim and puck tip
[auto: `demo/smoke_vacuum.mjs`].
- [ ] Flat and dormant-Iso projections retain the same curved live layer on
touch and across HA updates [auto: `demo/smoke_isometric_live_touch.mjs`].
- [ ] `vacuum-trail-smoothing-dark` contains two current subpaths plus a stored
previous run; its semantic guard requires two `M` sections, quadratic
commands and byte-identical case/core geometry before PNG comparison.
The baseline is accepted only from reviewed Linux CI
[golden: `demo/golden/matrix.mjs`].
- [ ] Replacing the quadratic corner with a straight vertex makes the focused
unit guard red [mutation: `vacuum-trail-smoothing-disabled`].
## Stable wall-segment identity (#282)
- [ ] Wall junction limits (#329): drawing refuses an apex under 15°, a seventh
+3
View File
@@ -1414,6 +1414,9 @@ Map Extractor, dreame-vacuum (Tasshack) и Valetudo-подобные камер
- Позиция старше 60 секунд визуально считается устаревшей.
- Разрывы integration path сохраняются отдельными участками и не соединяются
ложной линией. Хранятся максимум 64 рисуемых участка и 4000 точек.
- Текущий и предыдущий следы автоматически показываются плавными кривыми. Они
сохраняют исходные концы и разрывы маршрута и отклоняются от записанной
ломаной не более чем на 17,5 см независимо от масштаба и вида плана.
- Для текущего пути приоритет таков: путь интеграции → серверный текущий run →
локальный буфер. Пустой или одноточечный участок приоритет не перехватывает.
- Остановка, пауза или визит на базу сразу скрывают текущий след в режиме
+7
View File
@@ -106,6 +106,13 @@ two points are discarded before limits are applied. The newest 64 drawable
subpaths are kept, with at most 4000 total points; both endpoints of every kept
subpath survive deterministic proportional thinning.
Current and previous trails use the same bounded rounded-corner curve. The
curve preserves every subpath endpoint and never leaves the recorded polyline
by more than 17.5 cm in physical plan coordinates. Smoothing happens after
vacuum calibration and before flat/isometric projection, so zoom, viewport,
DPR and projection do not change that limit. The live target is still trimmed
before rendering, and smoothing never bridges an integration data gap.
| Display mode | While moving | After movement stops |
|---|---|---|
| `never` | Hidden | Hidden |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 116 KiB

+22 -22
View File
@@ -2,8 +2,8 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": null,
"sourceFingerprint": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"oxipng": "oxipng 10.2.0",
"sourceFingerprint": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "f8c9541c9d7fde462d0efba63dff70944dc03df76ce988eecaf2b269e2863a96"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,8 +26,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "21c6fe7d79ff5c878ade8e98a64cc90dfd7bcc66338c864fb5a0bd411471313d"
},
"space-create": {
"file": "03-space-create.png",
@@ -37,8 +37,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "f6d190f0b2ac9125cea68aea2ebbcdf9a7081ae2a3565ff253fb1ed0c4a56483"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -48,8 +48,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "ca8bb143d0d6679ff09f8e9c1f5ad68b2ca7cfd96a4740a4a650c251f55dfba5"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -59,8 +59,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "06b67c5108f9ee29a226cc2920484ccffe0c79840aec09721072b25b91ef8f03"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -70,8 +70,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "9e435a6fe89564b9215b82a61fb7eae998266dd05483524ffa84160c35630e0c"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "cc5abe1b3364bf9812bea6ea5cfa84f35120ff9639ed3c4c92958cb646a8caf7"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,8 +92,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "7e308a2b105fa6a9f788449ca30b8183375a32f0d0f8726f4a131e4f35527f57"
},
"room-card": {
"file": "08-room-card.png",
@@ -103,8 +103,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "f1bc10b478f7afc68e70cb4c37148f680b0bbd41d50a7d63a21067fc952ff134"
},
"device-info": {
"file": "09-device-info.png",
@@ -114,8 +114,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "0896c482848c70ed7071b4e2396294fd83072352403a859447b53fc3c10cc6ae",
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
"sourceSha256": "4b183589451e9a238ceeab91d847fcc1806215e85ee64f2c27b4b1cb167f6181",
"imageSha256": "2634cd7ead5720ce46ea4d6fa8f31006b740c520a73ddc97afe9a6d9d3b49778"
}
}
}
+12
View File
@@ -2441,6 +2441,18 @@ const MUTANT_DEFINITIONS = [
replace: ' resumed = False',
}],
},
{
id: 'vacuum-trail-smoothing-disabled',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="smoothVacPath rounds corners" test/vacuum.test.mjs',
because: 'the bounded curve is the user-visible point of #209; restoring a straight vertex '
+ 'must be rejected by the focused geometry test rather than blessed by a matching empty golden',
patches: [{
file: 'src/vacuum.ts',
find: " commands.push({ kind: 'quadratic', control: b, point: after });",
replace: " commands.push({ kind: 'line', point: b });",
}],
},
{
id: 'device-unavailable-hover-restored',
guard: 'node demo/smoke_device_icon_design.mjs',
+25 -14
View File
@@ -104,7 +104,8 @@ import { mdiHomeCityOutline } from '@mdi/js';
import {
Affine, applyAffine, readVacTelemetry,
autoCalibrate, pushTrailPoint, isVacMoving, vacTrailMode, vacMapIdWithFallback,
parseVacSourceCandidate, resolveVacSource, resolveCurrentVacPath, trimVacPathTarget, areaCentroid,
parseVacSourceCandidate, resolveVacSource, resolveCurrentVacPath, trimVacPathTarget, normalizeVacPath,
smoothVacPath, VAC_TRAIL_SMOOTH_RADIUS_CM, areaCentroid,
vacCalibrationResidualCm, vacRoomNameMatchCount, VAC_CALIBRATION_WARN_CM,
VAC_TELEPORT_GAP_MS, VAC_STALE_MS,
FitParams, fitMatrix, fitFromMatrix, initialFit, reanchorFit, VacRoom,
@@ -11815,6 +11816,21 @@ export class HouseplanCard extends LitElement {
});
}
/** One bounded current/previous trail serializer for flat and Iso surfaces. */
private _vacTrailPathD(path: VacPath, matrix: Affine): string {
const calibrated = path.map((segment) => segment.map(([x, y]) => applyAffine(matrix, x, y)));
const commands = smoothVacPath(calibrated, this._cmToUnits(VAC_TRAIL_SMOOTH_RADIUS_CM));
const pointText = (point: readonly [number, number]): string => {
const scene = this._scenePoint(point);
return `${scene[0].toFixed(1)} ${scene[1].toFixed(1)}`;
};
return commands.map((segment) => segment.map((command) => {
if (command.kind === 'move') return `M ${pointText(command.point)}`;
if (command.kind === 'line') return `L ${pointText(command.point)}`;
return `Q ${pointText(command.control)} ${pointText(command.point)}`;
}).join(' ')).join(' ');
}
/** Puck + trail for every live vacuum of the space. */
private _renderVacuums(devs: DevItem[], view: { x: number; y: number; w: number; h: number }): TemplateResult | typeof nothing {
if (this._markup || this._mode === 'decor') return nothing;
@@ -11846,13 +11862,12 @@ export class HouseplanCard extends LitElement {
const srvPrev = srv?.previous?.map_id === mapNow && Array.isArray(srv.previous.points) ? srv.previous : null;
// the PREVIOUS run stays visible even at rest: users compare where the
// robot has been against where it has not (owner call 2026-07-31)
if (tmode === 'always' && srvPrev && srvPrev.points.length > 1) {
const pts = srvPrev.points.map(([x, y]: number[]) => {
const [cx2, cy2] = applyAffine(matrix, x, y);
const point = this._scenePoint([cx2, cy2]);
return point[0].toFixed(1) + ',' + point[1].toFixed(1);
}).join(' ');
trails.push(svg`<g class="prev"><polyline class="case" points="${pts}"></polyline><polyline class="core" points="${pts}"></polyline></g>`);
if (tmode === 'always' && srvPrev) {
const previous = normalizeVacPath(srvPrev.points);
const pathD = this._vacTrailPathD(previous, matrix);
if (pathD) {
trails.push(svg`<g class="prev"><path class="case" d="${pathD}"></path><path class="core" d="${pathD}"></path></g>`);
}
}
// One arbitration authority: integration multi-subpath → server run →
// local runtime. Only drawable segments participate.
@@ -11863,13 +11878,9 @@ export class HouseplanCard extends LitElement {
const raw: VacPath = moving && (current.source === 'integration' || current.source === 'server')
? trimVacPathTarget(current.path) : current.path;
if (raw.length) {
const pathD = raw.map((segment) => segment.map(([x, y], index) => {
const [cx, cy] = applyAffine(matrix, x, y);
const point = this._scenePoint([cx, cy]);
return `${index ? 'L' : 'M'} ${point[0].toFixed(1)} ${point[1].toFixed(1)}`;
}).join(' ')).join(' ');
const pathD = this._vacTrailPathD(raw, matrix);
// A single SVG path with several M commands keeps gaps literal.
trails.push(svg`<path class="case" d="${pathD}"></path><path class="core" d="${pathD}"></path>`);
if (pathD) trails.push(svg`<path class="case" d="${pathD}"></path><path class="core" d="${pathD}"></path>`);
const last = raw[raw.length - 1];
if (moving && last?.length >= 2) {
const anchor = last[last.length - 1];
+61
View File
@@ -11,6 +11,13 @@ export type Affine = [number, number, number, number, number, number];
export type Pt = [number, number];
export type VacPath = Pt[][];
export type VacPathCommand =
| { kind: 'move'; point: Pt }
| { kind: 'line'; point: Pt }
| { kind: 'quadratic'; control: Pt; point: Pt };
export const VAC_TRAIL_SMOOTH_RADIUS_CM = 17.5;
export const VAC_PATH_MAX_SEGMENTS = 64;
export const VAC_PATH_MAX_POINTS = 4000;
@@ -228,6 +235,60 @@ export function trimVacPathTarget(path: VacPath): VacPath {
return out;
}
const finitePoint = (point: Pt): boolean => Number.isFinite(point[0]) && Number.isFinite(point[1]);
const samePoint = (a: Pt, b: Pt): boolean => a[0] === b[0] && a[1] === b[1];
/**
* Turn calibrated plan-space trail points into bounded rounded-corner commands.
*
* Each corner is trimmed by at most half of either adjacent segment and by the
* caller's physical radius. The quadratic lies in the P-B-Q convex hull, so it
* cannot be farther from the source polyline than that radius. This function
* deliberately knows nothing about SVG, Lit, scale conversion or flat/iso
* projection; the renderer serialises and projects the typed commands.
*/
export function smoothVacPath(path: VacPath, maxRadius: number): VacPathCommand[][] {
if (!Number.isFinite(maxRadius) || maxRadius <= 0) return [];
const out: VacPathCommand[][] = [];
for (const rawSegment of path) {
if (!rawSegment.every(finitePoint)) continue;
const segment = rawSegment.filter((point, index) => index === 0 || !samePoint(point, rawSegment[index - 1]));
if (segment.length < 2) continue;
const commands: VacPathCommand[] = [{ kind: 'move', point: segment[0] }];
if (segment.length === 2) {
commands.push({ kind: 'line', point: segment[1] });
out.push(commands);
continue;
}
for (let index = 1; index < segment.length - 1; index++) {
const a = segment[index - 1], b = segment[index], c = segment[index + 1];
const inX = b[0] - a[0], inY = b[1] - a[1];
const outX = c[0] - b[0], outY = c[1] - b[1];
const inLength = Math.hypot(inX, inY), outLength = Math.hypot(outX, outY);
const dot = (inX * outX + inY * outY) / (inLength * outLength);
// An almost exact reversal would fold a quadratic back onto itself. Keep
// that telemetry cusp literal instead of inventing a loop around it.
if (!Number.isFinite(dot) || dot <= -0.999) {
commands.push({ kind: 'line', point: b });
continue;
}
const radius = Math.min(maxRadius, inLength / 2, outLength / 2);
if (!Number.isFinite(radius) || radius <= 0) {
commands.push({ kind: 'line', point: b });
continue;
}
const before: Pt = [b[0] - inX * radius / inLength, b[1] - inY * radius / inLength];
const after: Pt = [b[0] + outX * radius / outLength, b[1] + outY * radius / outLength];
commands.push({ kind: 'line', point: before });
commands.push({ kind: 'quadratic', control: b, point: after });
}
commands.push({ kind: 'line', point: segment[segment.length - 1] });
out.push(commands);
}
return out;
}
/**
* Map-id normalisation contract, shared with the backend recorder
* (custom_components/houseplan/trails.py: resolve_map_id). The FIRST value
+13 -1
View File
@@ -372,7 +372,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
assert.ok(scenario);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(GOLDEN_MATRIX_VERSION, 49);
assert.equal(GOLDEN_MATRIX_VERSION, 50);
assert.equal(space.settings.sun_rays, true);
assert.equal(scenario.northDeg, 90,
'the sign-sensitive golden must keep a non-zero north direction');
@@ -390,6 +390,18 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
assert.equal(scenario.sunRayPixels.minChannelDelta >= 4, true);
});
test('vacuum smoothing golden covers current gaps and the previous run', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'vacuum-trail-smoothing-dark');
assert.ok(scenario);
assert.equal(scenario.mode, 'view');
assert.equal(scenario.vacuumTrail?.current.length, 2);
assert.equal(scenario.vacuumTrail?.current.flat().length >= 8, true);
assert.equal(scenario.vacuumTrail?.previous.length >= 4, true);
const fixture = prepareGoldenFixture(scenario);
assert.equal(fixture.config.markers.some((marker) => marker.id === 'golden-vacuum-trail'), true);
assert.equal(fixture.states['vacuum.golden_vacuum_trail']?.state, 'cleaning');
});
test('issue #277 golden pairs safe/disabled handles and an opening clamp in both themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.safeResizePreview);
assert.deepEqual(scenarios.map((item) => item.theme).sort(), ['dark', 'light']);
+84 -1
View File
@@ -5,7 +5,8 @@ import {
solveAffine, applyAffine, affineResidual, readVacTelemetry, autoCalibrate,
thinPath, pushTrailPoint, isVacMoving, isVacSourceState, vacMapIdFromAttrs,
vacMapIdWithFallback, areaCentroid, normalizeVacPath, resolveCurrentVacPath,
trimVacPathTarget, parseVacSourceCandidate, resolveVacSource,
trimVacPathTarget, smoothVacPath, VAC_TRAIL_SMOOTH_RADIUS_CM,
parseVacSourceCandidate, resolveVacSource,
vacCalibrationResidualCm, vacRoomNameMatchCount, VAC_CALIBRATION_WARN_CM,
} from '../test-build/vacuum.js';
@@ -181,6 +182,88 @@ test('resolveCurrentVacPath arbitrates only drawable sources and preserves gaps'
]);
});
test('smoothVacPath rounds corners within 17.5 cm and preserves exact endpoints', () => {
const gridPitch = 100;
const cellCm = 5;
const radius = (VAC_TRAIL_SMOOTH_RADIUS_CM / cellCm) * gridPitch;
const source = [[[0, 0], [900, 0], [900, 500], [1400, 650]]];
const [commands] = smoothVacPath(source, radius);
assert.deepEqual(commands[0], { kind: 'move', point: source[0][0] });
assert.deepEqual(commands.at(-1), { kind: 'line', point: source[0].at(-1) });
assert.equal(commands.filter((command) => command.kind === 'quadratic').length, 2);
const distanceToSegment = (point, a, b) => {
const dx = b[0] - a[0], dy = b[1] - a[1];
const denominator = dx * dx + dy * dy;
const t = denominator ? Math.max(0, Math.min(1,
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / denominator)) : 0;
return Math.hypot(point[0] - (a[0] + dx * t), point[1] - (a[1] + dy * t));
};
const distanceToSource = (point) => Math.min(...source[0].slice(1).map(
(end, index) => distanceToSegment(point, source[0][index], end),
));
for (let index = 0; index < commands.length; index++) {
const command = commands[index];
if (command.kind !== 'quadratic') continue;
const start = commands[index - 1].point;
for (let sample = 0; sample <= 100; sample++) {
const t = sample / 100, u = 1 - t;
const point = [
u * u * start[0] + 2 * u * t * command.control[0] + t * t * command.point[0],
u * u * start[1] + 2 * u * t * command.control[1] + t * t * command.point[1],
];
const deviationCm = (distanceToSource(point) / gridPitch) * cellCm;
assert.ok(deviationCm <= VAC_TRAIL_SMOOTH_RADIUS_CM + 1e-9,
`curve deviated by ${deviationCm} cm`);
}
}
});
test('smoothVacPath preserves subpath gaps and bounds short uneven corners', () => {
const path = [
[[0, 0], [2, 0], [2, 1000]],
[[5000, 5000], [5010, 5000], [5010, 5001], [6000, 5001]],
];
const result = smoothVacPath(path, 100);
assert.equal(result.length, 2);
assert.deepEqual(result.map((segment) => segment[0]), [
{ kind: 'move', point: [0, 0] },
{ kind: 'move', point: [5000, 5000] },
]);
assert.deepEqual(result.map((segment) => segment.at(-1).point), [[2, 1000], [6000, 5001]]);
const firstCurve = result[0].find((command) => command.kind === 'quadratic');
assert.deepEqual(firstCurve.point, [2, 1]);
});
test('smoothVacPath fails closed for degenerate, non-finite and reversal input', () => {
assert.deepEqual(smoothVacPath([], 10), []);
assert.deepEqual(smoothVacPath([[[0, 0]]], 10), []);
assert.deepEqual(smoothVacPath([[[0, 0], [1, 1]]], 0), []);
assert.deepEqual(smoothVacPath([[[0, 0], [NaN, 1], [2, 2]]], 10), []);
const duplicate = smoothVacPath([[[0, 0], [0, 0], [10, 0]]], 10);
assert.deepEqual(duplicate, [[
{ kind: 'move', point: [0, 0] },
{ kind: 'line', point: [10, 0] },
]]);
const reversal = smoothVacPath([[[0, 0], [10, 0], [0, 0]]], 10)[0];
assert.equal(reversal.some((command) => command.kind === 'quadratic'), false);
assert.equal(JSON.stringify(reversal).includes('null'), false);
});
test('smoothVacPath stays linear and finite at the 64/4000 path budget', () => {
const path = Array.from({ length: 64 }, (_, segment) => {
const length = segment < 32 ? 63 : 62;
return Array.from({ length }, (_, point) => [segment * 1000 + point, point % 2]);
});
const pointCount = path.reduce((sum, segment) => sum + segment.length, 0);
assert.equal(pointCount, 4000);
const result = smoothVacPath(path, 17.5);
const commandCount = result.reduce((sum, segment) => sum + segment.length, 0);
assert.equal(result.length, 64);
assert.ok(commandCount <= pointCount * 2);
assert.equal(JSON.stringify(result).includes('null'), false);
});
test('parseVacSourceCandidate is deterministic and explains XCME/camera capability', () => {
const positioned = parseVacSourceCandidate('camera.map', {
attributes: { friendly_name: 'Map', vacuum_position: { x: 1, y: 2 }, rooms: {} },