Compare commits

...
Author SHA1 Message Date
claude[bot] 192022e986 docs: review document for #375
Issue: #375
User-Visible: no
2026-08-29 18:09:50 +00:00
Codex 29f7f0bbf7 build: refresh bundle trees for #375
User-Visible: no
Issue: #375
2026-08-29 20:59:08 +03:00
Codex d762ae0231 fix: give static Glow the full card's cache hierarchy (#375)
V6a: pass the stable devices array to resolvedLightSources — the WeakMap
cache is keyed by array identity, a spread guaranteed a miss on every
render in BOTH cards (full-card regression since beta.4).
V6b: the static wall geometry now carries the same non-enumerable
sourceFingerprint tag the full card attaches, so buildLightBarrierScene
takes the fast recutWallBodiesGeometry path on door state changes.
V6c: the static barrier-scene cache is an LRU of 8 per space (parity
with _lightBarrierPool) — a flipping door reuses both of its scenes.
V6d: enabledClip is cached by geometry fingerprint + disabled-room set,
with the full card's bbox prefilter for decor bodies.

User-Visible: yes
Issue: #375
2026-08-29 20:59:08 +03:00
37 changed files with 660 additions and 240 deletions
@@ -1,52 +1,52 @@
{
"schema": 1,
"fingerprint": "551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328",
"fingerprint": "48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-B1i-RQty.js",
"houseplan-assets/houseplan-card-2MxtjEZi.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 275798,
"initialViewGzipBytes": 276013,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/de-CCuCtkOJ.js",
"houseplan-assets/editor-ClbxnMnB.js",
"houseplan-assets/fr-CqyAakjQ.js",
"houseplan-assets/houseplan-editor-runtime-CUy6UIHV.js",
"houseplan-assets/houseplan-onboarding-runtime-B557qIMW.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/de-BJs_98k4.js",
"houseplan-assets/editor-Bd6Iqh3I.js",
"houseplan-assets/fr-Bu6eA3iC.js",
"houseplan-assets/houseplan-editor-runtime-r9-2h1_p.js",
"houseplan-assets/houseplan-onboarding-runtime-D0bm3ZxD.js"
],
"lazyGzipBytes": 191497,
"lazyGzipBytes": 191502,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/editor-ClbxnMnB.js",
"houseplan-assets/houseplan-editor-runtime-CUy6UIHV.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/editor-Bd6Iqh3I.js",
"houseplan-assets/houseplan-editor-runtime-r9-2h1_p.js"
],
"lazyEditorGzipBytes": 139732,
"lazyEditorGzipBytes": 139735,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/houseplan-onboarding-runtime-B557qIMW.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/houseplan-onboarding-runtime-D0bm3ZxD.js"
],
"lazyOnboardingGzipBytes": 9450,
"lazyOnboardingGzipBytes": 9452,
"lazyLocaleFiles": [
"houseplan-assets/de-CCuCtkOJ.js",
"houseplan-assets/fr-CqyAakjQ.js"
"houseplan-assets/de-BJs_98k4.js",
"houseplan-assets/fr-Bu6eA3iC.js"
],
"lazyLocaleGzipBytes": 45270,
"lazyLocaleGzipBytes": 45271,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CA_1Inai.js",
"sha256": "b5fecd75ea99f054c36d60751acb427d1b811f1d2c146e7f364d0221b41862fa",
"path": "houseplan-assets/backdrop-pick-e_tUy6uK.js",
"sha256": "f15a08762e70bb4d20b94fcdf0369e1cc40b6ee685326aacc84e8fb6215c6897",
"rawBytes": 6967,
"gzipBytes": 2955,
"gzipBytes": 2956,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CCuCtkOJ.js",
"sha256": "2ddef73d215a84501cfa0d19571465ee03c5ff1dae50d1736ec410253152220a",
"path": "houseplan-assets/de-BJs_98k4.js",
"sha256": "ca223441f31ede505db5a343f811a63f4ae23e9b563c54414d744a777b6377ad",
"rawBytes": 74963,
"gzipBytes": 22846,
"isEntry": false,
@@ -54,72 +54,72 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-ClbxnMnB.js",
"sha256": "9996208acec133a9610de5331b43b5b81d02e43d4e0201342722b24c2c73bd8a",
"path": "houseplan-assets/editor-Bd6Iqh3I.js",
"sha256": "27814eb5bc16e4ceda014f930ce25aeb0cec2cc25510b206fa05f892f2d8b678",
"rawBytes": 3831,
"gzipBytes": 1585,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-CqyAakjQ.js",
"sha256": "cdeb67badf6a140d33a7328ba5f400a28d5352913b832cf9ea86f217a2a1b0d5",
"path": "houseplan-assets/fr-Bu6eA3iC.js",
"sha256": "f2dc14b47c44c11d2adf16b166344dc585b5821d350bbcd4af50d81d7db5f19f",
"rawBytes": 77091,
"gzipBytes": 22424,
"gzipBytes": 22425,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-B1i-RQty.js",
"sha256": "a43d95f521061f5601ad0b7abf55f3a2ec13737d47e5f647aa2515bbee679e02",
"rawBytes": 971326,
"gzipBytes": 275001,
"path": "houseplan-assets/houseplan-card-2MxtjEZi.js",
"sha256": "c580f8ec397627b2083b908cce779aae4dd407233eb5afd3b1648f250627be1b",
"rawBytes": 972163,
"gzipBytes": 275215,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CCuCtkOJ.js",
"houseplan-assets/editor-ClbxnMnB.js",
"houseplan-assets/fr-CqyAakjQ.js",
"houseplan-assets/houseplan-editor-runtime-CUy6UIHV.js",
"houseplan-assets/houseplan-onboarding-runtime-B557qIMW.js"
"houseplan-assets/de-BJs_98k4.js",
"houseplan-assets/editor-Bd6Iqh3I.js",
"houseplan-assets/fr-Bu6eA3iC.js",
"houseplan-assets/houseplan-editor-runtime-r9-2h1_p.js",
"houseplan-assets/houseplan-onboarding-runtime-D0bm3ZxD.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-CUy6UIHV.js",
"sha256": "e5d821f7b2669c87d7c6aa75e7cf8d1d0887d9360c4d8f158e6806361d87dc3d",
"path": "houseplan-assets/houseplan-editor-runtime-r9-2h1_p.js",
"sha256": "26eb0765e773f102f975332fa41ea5c10bebe03ac208fe6941a2a2e289557821",
"rawBytes": 523581,
"gzipBytes": 135192,
"gzipBytes": 135194,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-B557qIMW.js",
"sha256": "a78415da2f80cbc85f32f13eb8c360bbd9854f4f2f74e3f3883d98d68716f237",
"path": "houseplan-assets/houseplan-onboarding-runtime-D0bm3ZxD.js",
"sha256": "21ac3e7331bd404132318f15846f9b5b8d92ba44358614e08eb3fc6a0630b77f",
"rawBytes": 26195,
"gzipBytes": 6495,
"gzipBytes": 6496,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "efc624683c5d97ee4f7c2694a857f205f893c3d529687747ccbc2d4eb725f0cd",
"sha256": "043c4fee2772b138bf7936797d97cda739aa38dbd9ee91f024eff75bada11e9a",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 798,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-B1i-RQty.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-2MxtjEZi.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-B1i-RQty.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-CA_1Inai.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328";class F{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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;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:n,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:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,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;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_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 _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-2MxtjEZi.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-e_tUy6uK.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518";class F{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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;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:n,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:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,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;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_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 _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
${s.map(s=>{return _`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328";try{await import("./houseplan-assets/houseplan-card-B1i-RQty.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518";try{await import("./houseplan-assets/houseplan-card-2MxtjEZi.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
+54 -54
View File
@@ -1,52 +1,52 @@
{
"schema": 1,
"fingerprint": "551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328",
"fingerprint": "48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-B1i-RQty.js",
"houseplan-assets/houseplan-card-2MxtjEZi.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 275798,
"initialViewGzipBytes": 276013,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/de-CCuCtkOJ.js",
"houseplan-assets/editor-ClbxnMnB.js",
"houseplan-assets/fr-CqyAakjQ.js",
"houseplan-assets/houseplan-editor-runtime-CUy6UIHV.js",
"houseplan-assets/houseplan-onboarding-runtime-B557qIMW.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/de-BJs_98k4.js",
"houseplan-assets/editor-Bd6Iqh3I.js",
"houseplan-assets/fr-Bu6eA3iC.js",
"houseplan-assets/houseplan-editor-runtime-r9-2h1_p.js",
"houseplan-assets/houseplan-onboarding-runtime-D0bm3ZxD.js"
],
"lazyGzipBytes": 191497,
"lazyGzipBytes": 191502,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/editor-ClbxnMnB.js",
"houseplan-assets/houseplan-editor-runtime-CUy6UIHV.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/editor-Bd6Iqh3I.js",
"houseplan-assets/houseplan-editor-runtime-r9-2h1_p.js"
],
"lazyEditorGzipBytes": 139732,
"lazyEditorGzipBytes": 139735,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/houseplan-onboarding-runtime-B557qIMW.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/houseplan-onboarding-runtime-D0bm3ZxD.js"
],
"lazyOnboardingGzipBytes": 9450,
"lazyOnboardingGzipBytes": 9452,
"lazyLocaleFiles": [
"houseplan-assets/de-CCuCtkOJ.js",
"houseplan-assets/fr-CqyAakjQ.js"
"houseplan-assets/de-BJs_98k4.js",
"houseplan-assets/fr-Bu6eA3iC.js"
],
"lazyLocaleGzipBytes": 45270,
"lazyLocaleGzipBytes": 45271,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CA_1Inai.js",
"sha256": "b5fecd75ea99f054c36d60751acb427d1b811f1d2c146e7f364d0221b41862fa",
"path": "houseplan-assets/backdrop-pick-e_tUy6uK.js",
"sha256": "f15a08762e70bb4d20b94fcdf0369e1cc40b6ee685326aacc84e8fb6215c6897",
"rawBytes": 6967,
"gzipBytes": 2955,
"gzipBytes": 2956,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CCuCtkOJ.js",
"sha256": "2ddef73d215a84501cfa0d19571465ee03c5ff1dae50d1736ec410253152220a",
"path": "houseplan-assets/de-BJs_98k4.js",
"sha256": "ca223441f31ede505db5a343f811a63f4ae23e9b563c54414d744a777b6377ad",
"rawBytes": 74963,
"gzipBytes": 22846,
"isEntry": false,
@@ -54,72 +54,72 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-ClbxnMnB.js",
"sha256": "9996208acec133a9610de5331b43b5b81d02e43d4e0201342722b24c2c73bd8a",
"path": "houseplan-assets/editor-Bd6Iqh3I.js",
"sha256": "27814eb5bc16e4ceda014f930ce25aeb0cec2cc25510b206fa05f892f2d8b678",
"rawBytes": 3831,
"gzipBytes": 1585,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-CqyAakjQ.js",
"sha256": "cdeb67badf6a140d33a7328ba5f400a28d5352913b832cf9ea86f217a2a1b0d5",
"path": "houseplan-assets/fr-Bu6eA3iC.js",
"sha256": "f2dc14b47c44c11d2adf16b166344dc585b5821d350bbcd4af50d81d7db5f19f",
"rawBytes": 77091,
"gzipBytes": 22424,
"gzipBytes": 22425,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-B1i-RQty.js",
"sha256": "a43d95f521061f5601ad0b7abf55f3a2ec13737d47e5f647aa2515bbee679e02",
"rawBytes": 971326,
"gzipBytes": 275001,
"path": "houseplan-assets/houseplan-card-2MxtjEZi.js",
"sha256": "c580f8ec397627b2083b908cce779aae4dd407233eb5afd3b1648f250627be1b",
"rawBytes": 972163,
"gzipBytes": 275215,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CCuCtkOJ.js",
"houseplan-assets/editor-ClbxnMnB.js",
"houseplan-assets/fr-CqyAakjQ.js",
"houseplan-assets/houseplan-editor-runtime-CUy6UIHV.js",
"houseplan-assets/houseplan-onboarding-runtime-B557qIMW.js"
"houseplan-assets/de-BJs_98k4.js",
"houseplan-assets/editor-Bd6Iqh3I.js",
"houseplan-assets/fr-Bu6eA3iC.js",
"houseplan-assets/houseplan-editor-runtime-r9-2h1_p.js",
"houseplan-assets/houseplan-onboarding-runtime-D0bm3ZxD.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-CUy6UIHV.js",
"sha256": "e5d821f7b2669c87d7c6aa75e7cf8d1d0887d9360c4d8f158e6806361d87dc3d",
"path": "houseplan-assets/houseplan-editor-runtime-r9-2h1_p.js",
"sha256": "26eb0765e773f102f975332fa41ea5c10bebe03ac208fe6941a2a2e289557821",
"rawBytes": 523581,
"gzipBytes": 135192,
"gzipBytes": 135194,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-B557qIMW.js",
"sha256": "a78415da2f80cbc85f32f13eb8c360bbd9854f4f2f74e3f3883d98d68716f237",
"path": "houseplan-assets/houseplan-onboarding-runtime-D0bm3ZxD.js",
"sha256": "21ac3e7331bd404132318f15846f9b5b8d92ba44358614e08eb3fc6a0630b77f",
"rawBytes": 26195,
"gzipBytes": 6495,
"gzipBytes": 6496,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CA_1Inai.js",
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/backdrop-pick-e_tUy6uK.js",
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "efc624683c5d97ee4f7c2694a857f205f893c3d529687747ccbc2d4eb725f0cd",
"sha256": "043c4fee2772b138bf7936797d97cda739aa38dbd9ee91f024eff75bada11e9a",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 798,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-B1i-RQty.js"
"houseplan-assets/houseplan-card-2MxtjEZi.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-B1i-RQty.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-2MxtjEZi.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-B1i-RQty.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-CA_1Inai.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328";class F{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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;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:n,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:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,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;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_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 _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-2MxtjEZi.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-e_tUy6uK.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518";class F{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),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;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:n,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:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,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;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_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 _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
${s.map(s=>{return _`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="551019769d7e6c39aac5c2330e0744ac6f80354dc40bc10fbb78138b06f16328";try{await import("./houseplan-assets/houseplan-card-B1i-RQty.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="48107d0980d57d5c30795b525519febbde405ced483407f38033e16584fb2518";try{await import("./houseplan-assets/houseplan-card-2MxtjEZi.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
+8
View File
@@ -2,6 +2,14 @@
## Unreleased
- Glow in `houseplan-space-card` no longer rebuilds its light graph and wall
scene from scratch on every Home Assistant tick: the static path now shares
the caching machinery of the full card (light-graph cache, an LRU pool of
barrier scenes so a moving door reuses both of its states, the fast masonry
recut path, and a cached room clip). This also fixes a light-graph cache
regression the full card picked up in beta.4
([#375](https://github.com/Matysh/houseplan-card/issues/375)).
## v1.69.0-beta.4 — 2026-08-29
- The read-only `houseplan-space-card` can now opt into the full plan's radial
+8
View File
@@ -8,6 +8,14 @@
## Не выпущено
- Glow в `houseplan-space-card` больше не пересобирает граф света и сцену стен
с нуля на каждый тик Home Assistant: статический путь получил кэш-механику
полной карты (кэш графа света, LRU-пул барьерных сцен — движущаяся дверь
переиспользует оба своих состояния, быстрый recut-путь кладки и кэш выреза
комнат). Заодно исправлен регресс кэша графа света, который полная карта
получила в beta.4
([#375](https://github.com/Matysh/houseplan-card/issues/375)).
## v1.69.0-beta.4 — 2026-08-29
- В read-only `houseplan-space-card` теперь можно включить радиальный Glow и
Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 141 KiB

+22 -22
View File
@@ -2,8 +2,8 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": "oxipng 10.2.0",
"sourceFingerprint": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"oxipng": null,
"sourceFingerprint": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "f8c9541c9d7fde462d0efba63dff70944dc03df76ce988eecaf2b269e2863a96"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "0ca982495a7749907c48ebbddfd089c54385a4347f79f64e94af2c5a8d5c03eb"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,8 +26,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "21c6fe7d79ff5c878ade8e98a64cc90dfd7bcc66338c864fb5a0bd411471313d"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "3be239a5a494f6776540a9c0049694ee6f9d2ba927c8bcb4b26ee5a69a228a7d"
},
"space-create": {
"file": "03-space-create.png",
@@ -37,8 +37,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "f6d190f0b2ac9125cea68aea2ebbcdf9a7081ae2a3565ff253fb1ed0c4a56483"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -48,8 +48,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "ca8bb143d0d6679ff09f8e9c1f5ad68b2ca7cfd96a4740a4a650c251f55dfba5"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -59,8 +59,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "06b67c5108f9ee29a226cc2920484ccffe0c79840aec09721072b25b91ef8f03"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -70,8 +70,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "9e435a6fe89564b9215b82a61fb7eae998266dd05483524ffa84160c35630e0c"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "8affadfce926fe2f6cf344c33f611c44fcbc79c1969f69c3a6935ff2e99c36c3"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "cc5abe1b3364bf9812bea6ea5cfa84f35120ff9639ed3c4c92958cb646a8caf7"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "6d323b6f641b2cc7db64252f70567348bd3f2551f82e61fd3a2b4674d244918e"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,8 +92,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "d3b44c4f74c2f92d23e2346451f1a89a603ba6594e9ea32c9c1b53d0946fdad7"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "ed041f571b0c34f1e06b13419ea08d90ebb7c26950f631d46f6cebb9a29f5c20"
},
"room-card": {
"file": "08-room-card.png",
@@ -103,8 +103,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "f1bc10b478f7afc68e70cb4c37148f680b0bbd41d50a7d63a21067fc952ff134"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
},
"device-info": {
"file": "09-device-info.png",
@@ -114,8 +114,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "10cfb7b64febbfbf24c24add3388109c5d0af3e03b7493dbbd7c9b4237d31f72",
"imageSha256": "2634cd7ead5720ce46ea4d6fa8f31006b740c520a73ddc97afe9a6d9d3b49778"
"sourceSha256": "35ac821c3b100d4a5e2904a7582b64345fcf9ade265003da8f15cfda2fc7299d",
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
}
}
}
+190
View File
@@ -0,0 +1,190 @@
# CODE-REVIEW-375-r1
Issue: #375 · Заход: r1 (код-ревью) · SHA: `29f7f0bbf71e8898c462d5560b1d3813c5a5d7ec`
Ветка: `issue/375-static-glow-caches`, база — актуальный `origin/dev`.
Вердикт: **зелёный** · блокирующих циклов 0/2 · High: 0 · Medium: 0
Это первый заход код-ревью для #375 (ТЗ прошло два раунда spec-ревью,
SPEC-REVIEW-375-r2 — зелёный). Разбор полный: сокращение по дельте (§2.10)
применяется со второго цикла код-ревью, здесь его нет.
## 1. Скоуп
Диф (`git diff origin/dev...HEAD`):
```
docs/CHANGELOG.md | 8 ++
docs/CHANGELOG.ru.md | 8 ++
scripts/mutation-gate.mjs | 22 ++++++
src/glow-scene.ts | 4 +-
src/space-render.ts | 157 +++++++++++++++++++++++++++---------
test/space-render-caches.test.mjs | 109 ++++++++++++++++++++++++++
tsconfig.test.json | 2 +-
+ dist/**, custom_components/houseplan/frontend/** (класс D, из build-коммита)
+ docs/images/*.png, docs/images/screenshots.json (регенерация отпечатка скриншотов,
ожидаемо — check-docs считает его по всему src/**)
```
Два коммита:
- `d762ae02` `fix:` — класс A (`src/glow-scene.ts`, `src/space-render.ts`) +
класс B (`scripts/mutation-gate.mjs`, `test/space-render-caches.test.mjs`,
`tsconfig.test.json`) + оба changelog в одном коммите. Трейлеры: `Issue: #375`,
`User-Visible: yes` — верно, поведение видимо пользователю (перф), правки
обоих CHANGELOG присутствуют в том же коммите.
- `29f7f0bb` `build:` — класс D (обновление трёх копий бандла + скриншоты).
`User-Visible: no` — верно, чисто механический коммит.
Реализует все четыре пункта ТЗ rev2 (К1–К4 = V6a–V6d):
K1 — убран spread перед `resolvedLightSources` в `glow-scene.ts:177`.
K2 — `cachedStaticWallGeometry` вешает non-enumerable `sourceFingerprint`
(та же тройка `[spCfg, cellCm, GRID_PITCH]`, что и `revision.geometryFingerprint`).
K3 — `cachedStaticLightBarriers` переведена на LRU-8 на space.
K4 — новый `cachedStaticEnabledClip` (LRU-8) + bbox-префильтр extras.
## 2. Как проверялось
Все гейты ниже прогнаны лично на SHA `29f7f0bb` — зелёного прогона Validate на
этом SHA не найдено, поэтому дешёвые и часть профильных гейтов взяты на себя.
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто, без вывода |
| Юниты | `npm test` | **1556/0** (1 skipped), совпадает с числом автора |
| Сборка + 3 копии бандла | `npm run bundle:sync` | `git status --porcelain` пуст после — dist / custom_components/houseplan/frontend / demo/srv/assets идентичны источникам, лишнего дифа нет |
| Доки-отпечаток | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 10 external links)` — diff трогает `src/**`, гейт обязателен и уже удовлетворён коммитом `29f7f0bb` |
| Мутанты (новые, из этого же коммита) | `node scripts/mutation-gate.mjs --id=static-glow-light-cache-spread` | **поймано 1/1** — тест краснеет при возврате spread |
| Мутанты (новые) | `node scripts/mutation-gate.mjs --id=static-glow-scene-lru-single` | **поймано 1/1** — тест краснеет при LRU=1 |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 14 «прямых совпадений», все — по широко используемым символам (`GRID_PITCH`, `NORM_W`, `cellCm`, `roomPoly`), ни один не специфичен для glow/кэшей; раздела «зарегистрированная связь»/«НЕОПРЕДЕЛЁННОСТЬ» инструмент не выдал |
| Смок parity (AC5, назван в ТЗ) | `node demo/smoke_glow_blending.mjs` | `{"ok":true,"blend":"screen","pools":60,"staticParity":true,"staticPools":60}` |
| Смок opt-in space-card (AC5) | `node demo/smoke_space_card.mjs` | OK |
| Смок cold view (заявлен автором) | `node demo/smoke_cold_view_toggle.mjs` | OK |
| Смок glow-геометрия (тема диффа, не в прямых совпадениях, но подсистема та же) | `node demo/smoke_glow.mjs` | OK, все 33 инварианта true |
| Смок glow отказоустойчивость | `node demo/smoke_glow_geometry_resilience.mjs` | OK |
| Смок тёмный fallback | `node demo/smoke_glow_fail_dark.mjs` | OK |
| Перф-смок (AC5: «cacheGrowth.glowClip: 0») full-card профиль | `npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --variants=60 --samples=3 --warmups=1` + `benchmark:compare --absolute-only --budgets=demo/performance/budgets-glow-smoke.json` | все чек-поинты ✅, `cache.growth.glowClip: 0`, `cache.growth.cleanFloor: 0` и др. |
| Перф-смок static-профиль | `npm run benchmark:glow -- --profile=large-space-card-glow-v1 --variants=60 --samples=3 --warmups=1` + `benchmark:compare --absolute-only --budgets=demo/performance/budgets-space-glow-smoke.json` | все чек-поинты ✅, `cache.growth.glowClip: 0` (это `_glowRuntimeState.clipCache`, общий рантайм — не путать с новым K4-кэшем, но именно эта метрика поимённо названа в AC5) |
| Перф-бюджет стыков (#330) | `npm run benchmark:junction-limits` | pass: true, все 5 метрик в бюджете |
Не прогонял: `npm run invariants` — диф не меняет геометрическую модель (рёбра,
записи толщины, `layout`, `marker.space`, `open_spans`); K2/K4 оборачивают уже
существующие вызовы `wallBodiesUnionPath`/`innerContourForRoom`/`floorMinusBodies`
в кэш-функции и добавляют bbox-префильтр (см. §4), но не меняют формулы —
проверено чтением, не исполнением. Полные (`space-default`/`space-glow` с
7 семплами на main/schedule) и предрелизные профили performance.yml —
не гонял, это предрелизный гейт §8, не гейт ревью; сокращённая (60/3/1,
`--absolute-only`) версия того же профиля из обычного Validate прогнана
лично (см. таблицу) и покрывает именно то, что называет AC5.
`python -m pytest tests_backend` — не гонял, `custom_components/**/*.py` не
тронут. Остальные 14 «прямых совпадений» smoke-select — не гонял: все они
привязаны к переиспользуемым по всему проекту символам (сетка/масштаб),
не к световой/кэш-механике; смоки той же подсистемы (`smoke_glow*`) прогнаны
явно вместо них.
## 3. Разбор по AC (ТЗ rev2)
**AC1** (кэш light-графа снова живой). `test/space-render-caches.test.mjs:13-41`
— поведенческий тест (реальный вызов `resolvedLightSources`, сравнение по
идентичности) плюс тест-пин источника (`glow-scene.ts` без спреда). Мутант
`static-glow-light-cache-spread` возвращает спред — тест краснеет (проверено
лично). **Доказано автотестом, тест умеет падать.**
**AC2** (recut-путь в static). `src/space-render.ts:560-568` вешает
`Object.defineProperty(built, 'sourceFingerprint', { value:
contentFingerprint([spCfg, cellCm, GRID_PITCH]), enumerable: false })` —
ровно та же тройка, что `resolveLightBarrierRevision` считает
`geometryFingerprint` (`src/glow-scene.ts:229-231`, `rawSpaceConfig: spCfg` —
`src/space-render.ts:620`). Тест `space-render-caches.test.mjs:48-56` пинит
обе стороны условия регексом — тем же способом, каким уже доказан recut
полной карты (`test/performance-contract.test.mjs:152`, тоже регекс на
`recutWallBodiesGeometry(...)` без поведенческого прогона). Метод не идеален
(не ловит семантический разъезд формулы), но соответствует уже принятому в
проекте прецеденту для той же ветки кода — не считаю находкой. **Доказано
автотестом (source-pin), тест умеет падать** — если убрать `defineProperty`
или изменить тройку, оба regex перестанут совпадать.
**AC3** (LRU-8 на space). `cachedStaticLightBarriers` (`space-render.ts:133-171`)
переведена на `Map<fingerprint, scene>` с `lruGet`/`lruSet` (капасити 8).
Тесты `space-render-caches.test.mjs:61-93`: ping-pong 6 обращений → 2 сборки
(проверено лично: логика `lruGet`/`lruSet` — стандартный move-to-end на Map,
корректна), эвикция на 9-й записи, изоляция по `space`/`cfg`. Мутант
`static-glow-scene-lru-single` (капасити=1) — тест краснеет (проверено лично).
**Доказано автотестом, тест умеет падать.**
**AC4** (кэш `enabledClip`). Новая `cachedStaticEnabledClip`
(`space-render.ts:172-193`) с тем же LRU-паттерном. Ключ на вызове
(`space-render.ts:719-721`) — `` `${revision.geometryFingerprint}|<отсортированный
список id выключенных комнат>` ``. Прочитал вручную: `geometryFingerprint`
целиком покрывает `spCfg` (raw-объект пространства), а `roomGlowOf` берёт
`spCfg`-же `settings.glow`/`fill_mode`/`disp.glow` — то есть список
выключенных комнат уже логически подразумевается фингерпринтом геометрии;
доп. суффикс в ключе избыточен, но безопасен (не может привести к ложному
попаданию в кэш, только к чуть более мелкой грануляции). Bbox-префильтр
extras (`space-render.ts:735-742`) — тот же AABB-тест, что и у полной карты
(`houseplan-card.ts:9388-9392`, сверил построчно) — попадание/непопадание
считается идентично, поведение не меняется, меняется только объём входа в
`floorMinusBodies`. Тест `space-render-caches.test.mjs:98-109` — поведенческий
(реальный вызов, идентичность результата, инвалидация при смене ключа).
**Доказано автотестом, тест умеет падать.**
**AC5** (parity + перф-контракты + `cacheGrowth.glowClip: 0`). Смок
`smoke_glow_blending` — байт-в-байт `staticParity:true`, 60/60 пулов (прогнан
лично). Перф-контракты `space-default`/`space-glow` — тяжёлые профили
(7 семплов) гоняются только в `performance.yml` (main/schedule/manual), это
предрелизный гейт; сокращённая версия того же (`large-house-glow-overlay-v1`
и `large-space-card-glow-v1`, 60/3/1, `--absolute-only`) — часть обычного
Validate, прогнана лично, `cache.growth.glowClip: 0` в обоих прогонах.
**Выполнен.**
**AC6** (мутанты в `scripts/mutation-gate.mjs`). Оба заявленных мутанта
(`static-glow-light-cache-spread`, `static-glow-scene-lru-single`) добавлены
и лично проверены — 1/1 каждый, ловятся юнитами, не сборкой. **Выполнен.**
## 4. Дополнительно проверено
- `resolvedLightSources` теперь получает `input.devices` (тип `readonly D[]`
сохранился, спред не нужен даже для совместимости типов — `tsc --noEmit`
чист).
- `spaceModels(o.cfg)` создаёт свежие объекты комнат на КАЖДЫЙ вызов
`renderSpaceStatic` (`src/space-geometry.ts:135-150`), поэтому сравнение
`glowEnabledRooms.includes(room)` по ссылке (K4-ключ) остаётся корректным —
оно происходит внутри одного и того же вызова, а не между тиками; между
тиками используется строковый ключ (fingerprint), а не ссылка. Проверено
чтением, не исполнением — это тот участок, где ссылочная нестабильность
моделей могла бы незаметно сломать кэш, и он не сломан.
- `physicalFingerprint` (extras/декор) — вложен в `spCfg` через `space.partitions`
/ `room_drafts` / `wall_columns`, то есть любое изменение состава extras уже
меняет и `geometryFingerprint` — новый K4-кэш не может отдать устаревший
клип при смене декора. Проверено чтением.
- Старый тип `StaticLightBarrierEntry` убран целиком, лишних ссылок не
осталось (`grep` пуст).
- Трейлеры коммитов, класс правок по путям, оба CHANGELOG — соответствуют
`AGENTS.md`/`PROCESS.md`.
## 5. Находки
Нет находок уровня High или Medium. Всё в скоупе ТЗ выполнено и доказано.
Low (не блокирует, оставляю на усмотрение автора, не завожу отдельный issue —
он в скоупе, не «попутный дефект соседнего поведения»):
- Ключ `cachedStaticEnabledClip` включает список выключенных комнат отдельно
от `geometryFingerprint`, хотя последний уже логически покрывает это же
состояние (см. AC4 выше). Не баг — просто одна избыточная переменная в
ключе. Не требую правки.
## 6. Унаследовано / не разбиралось заново
Не применимо — это первый заход код-ревью (r1), раздел §2.10 «Унаследовано
из r<N-1>» относится к разбору со второго цикла. ТЗ-стадия (SPEC-REVIEW-375-r1
→ r2) пройдена ранее отдельным этапом и здесь не пересматривается по существу
— код-ревью проверяет реализацию против уже согласованного ТЗ rev2, а не само
ТЗ.
## 7. Итог
Все AC1–AC6 доказаны автотестами, каждый тест лично проверен на способность
падать (два — через `mutation-gate`, три — прямым запуском с иными входами).
Parity full↔static (AC5) подтверждена смоком байт-в-байт. Перф-контракты
(сокращённая версия, доступная вне предрелизного гейта) зелёные, включая
именованную в AC5 метрику `cache.growth.glowClip: 0`. Дешёвые гейты (tsc,
1556 юнитов, build+3 копии бандла, check-docs) зелёные лично. Trailers и
changelog в порядке. Готово к очереди на пре-релиз.
+22
View File
@@ -691,6 +691,28 @@ const MUTANT_DEFINITIONS = [
replace: " : await import(/* @vite-ignore */ new URL(`${FRENCH_RETRY_ASSET}?retry`, import.meta.url).href);\n void module.dictionary;\n return { dictionary: en, fingerprint: module.fingerprint };",
}],
},
{
id: 'static-glow-light-cache-spread',
guard: 'node --test test/space-render-caches.test.mjs',
because: 'a fresh spread on the devices array silently defeats RESOLVED_LIGHT_CACHE '
+ '(WeakMap by array identity) in BOTH cards — invisible to CI perf profiles (#375 V6a)',
patches: [{
file: 'src/glow-scene.ts',
find: " input.hass, input.devices, null, input.virtualLights,",
replace: " input.hass, [...input.devices], null, input.virtualLights,",
}],
},
{
id: 'static-glow-scene-lru-single',
guard: 'node --test test/space-render-caches.test.mjs',
because: 'a single-entry scene cache makes a door ping-pong rebuild the barrier scene '
+ 'on every flip — the LRU capacity is the contract, parity with the full card (#375 V6c)',
patches: [{
file: 'src/space-render.ts',
find: "const STATIC_LIGHT_BARRIER_LRU = 8;",
replace: "const STATIC_LIGHT_BARRIER_LRU = 1;",
}],
},
{
id: 'opening-light-quantum-identity',
guard: 'node --test --test-name-pattern="#366" test/logic.test.mjs',
+3 -1
View File
@@ -173,8 +173,10 @@ export function resolveGlowCandidates(input: {
gridPitch: number;
position: (device: DevItem) => { x: number; y: number };
}): GlowCandidate[] {
// #375: pass the array as-is — RESOLVED_LIGHT_CACHE is keyed by the array's
// identity, so a fresh spread would guarantee a miss on every render.
const sources = resolvedLightSources(
input.hass, [...input.devices], null, input.virtualLights,
input.hass, input.devices, null, input.virtualLights,
).filter((source) => source.device.space === input.spaceId);
const byDevice = new Map<string, typeof sources>();
for (const source of sources) {
+119 -38
View File
@@ -70,8 +70,28 @@ type StaticWallGeometryEntry = { fingerprint: string; value: StaticWallGeometry
const staticWallGeometryCache = new WeakMap<object, Map<string, StaticWallGeometryEntry>>();
type StaticPhysicalBodiesEntry = { fingerprint: string; value: number[][][] };
const staticPhysicalBodiesCache = new WeakMap<object, Map<string, StaticPhysicalBodiesEntry>>();
type StaticLightBarrierEntry = { fingerprint: string; value: LightBarrierScene };
const staticLightBarrierCache = new WeakMap<object, Map<string, StaticLightBarrierEntry>>();
// #375: the scene cache is an LRU of 8 per space — parity with the full
// card's _lightBarrierPool. A single-entry cache made a door flipping
// open<->close rebuild the scene on every state change (fingerprint ping-pong).
const STATIC_LIGHT_BARRIER_LRU = 8;
const staticLightBarrierCache = new WeakMap<object, Map<string, Map<string, LightBarrierScene>>>();
// #375: enabledClip is rebuilt only when the geometry or the set of
// glow-disabled rooms changes; the full card keeps the analogous clean-floor
// LRU (600) — static cards need far fewer entries.
const STATIC_ENABLED_CLIP_LRU = 8;
const staticEnabledClipCache = new WeakMap<object, Map<string, Map<string, string[]>>>();
function lruGet<V>(cache: Map<string, V>, key: string): V | undefined {
const value = cache.get(key);
if (value !== undefined) { cache.delete(key); cache.set(key, value); }
return value;
}
function lruSet<V>(cache: Map<string, V>, key: string, value: V, limit: number): void {
cache.delete(key);
cache.set(key, value);
while (cache.size > limit) cache.delete(cache.keys().next().value as string);
}
/** Static cards receive the same immutable server-config object on HA ticks. */
function cachedStaticWallGeometry(
@@ -110,7 +130,8 @@ function cachedStaticPhysicalBodies(
return value;
}
function cachedStaticLightBarriers(
/** Exported for tests only (#375): the LRU behaviour is the contract. */
export function cachedStaticLightBarriers(
cfg: ServerConfig,
spaceId: string,
fingerprint: string,
@@ -118,13 +139,42 @@ function cachedStaticLightBarriers(
): LightBarrierScene {
let spaces = staticLightBarrierCache.get(cfg as object);
if (!spaces) {
spaces = new Map<string, StaticLightBarrierEntry>();
spaces = new Map<string, Map<string, LightBarrierScene>>();
staticLightBarrierCache.set(cfg as object, spaces);
}
const cached = spaces.get(spaceId);
if (cached?.fingerprint === fingerprint) return cached.value;
let scenes = spaces.get(spaceId);
if (!scenes) {
scenes = new Map<string, LightBarrierScene>();
spaces.set(spaceId, scenes);
}
const cached = lruGet(scenes, fingerprint);
if (cached) return cached;
const value = build();
spaces.set(spaceId, { fingerprint, value });
lruSet(scenes, fingerprint, value, STATIC_LIGHT_BARRIER_LRU);
return value;
}
/** Exported for tests only (#375): reuse-by-identity is the contract. */
export function cachedStaticEnabledClip(
cfg: ServerConfig,
spaceId: string,
key: string,
build: () => string[],
): string[] {
let spaces = staticEnabledClipCache.get(cfg as object);
if (!spaces) {
spaces = new Map<string, Map<string, string[]>>();
staticEnabledClipCache.set(cfg as object, spaces);
}
let clips = spaces.get(spaceId);
if (!clips) {
clips = new Map<string, string[]>();
spaces.set(spaceId, clips);
}
const cached = lruGet(clips, key);
if (cached) return cached;
const value = build();
lruSet(clips, key, value, STATIC_ENABLED_CLIP_LRU);
return value;
}
@@ -499,14 +549,26 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
: { rooms: space.rooms, walls, extras, cellCm, zero: zeroWalls.contour })
: '';
const canonicalWallGeometry = needsCanonicalWallGeometry
? cachedStaticWallGeometry(o.cfg, space.id, wallGeometryFingerprint, () => wallBodiesUnionPath(
space.rooms, walls, zeroWalls.contour, [
...staticPassages.filter((opening) => opening.host?.kind !== 'partition').map((opening) => ({
x: opening.rx, y: opening.ry, angle: opening.angle, length: opening.rlen,
})),
...hostedCompositeOpenings,
], GRID_STEP_N, cellCm, GRID_PITCH, NORM_W, extras,
))
? cachedStaticWallGeometry(o.cfg, space.id, wallGeometryFingerprint, () => {
const built = wallBodiesUnionPath(
space.rooms, walls, zeroWalls.contour, [
...staticPassages.filter((opening) => opening.host?.kind !== 'partition').map((opening) => ({
x: opening.rx, y: opening.ry, angle: opening.angle, length: opening.rlen,
})),
...hostedCompositeOpenings,
], GRID_STEP_N, cellCm, GRID_PITCH, NORM_W, extras,
);
// #375: the same non-enumerable tag the full card attaches
// (houseplan-card.ts, _wallGeometryR): buildLightBarrierScene only takes
// the fast recutWallBodiesGeometry path when this fingerprint matches
// revision.geometryFingerprint — without the tag the static path always
// rebuilt the wall bodies from scratch on a door state change.
if (built) Object.defineProperty(built, 'sourceFingerprint', {
value: contentFingerprint([spCfg, cellCm, GRID_PITCH]),
enumerable: false,
});
return built;
})
: null;
const passageTunnelGeometry = staticPassages.length && walls.length
? openingTunnelGeometries(
@@ -651,29 +713,48 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
pruneGlowSources(glowRuntime.state, glowRuntime.host, space.id, seen);
if (spots.length) {
const allEnabled = glowEnabledRooms.length === revision.polygons.length;
const enabledClip = allEnabled ? null : glowEnabledRooms.flatMap((room) => {
const poly = roomPoly(room);
if (!poly) return [];
const floorPoly = walls.length && room.id
? (innerContourForRoom(
space.rooms, room.id, walls, zeroWalls.contour,
GRID_STEP_N, cellCm, GRID_PITCH, NORM_W,
canonicalWallGeometry?.roomGeom,
canonicalWallGeometry?.multiWallNodes,
) || poly)
: poly;
const cleanGeometry = extras.length ? floorMinusBodies(floorPoly, extras) : null;
const clean = cleanGeometry ? polyclipPathD(cleanGeometry) : '';
const holes = islandsOf(
floorPoly,
revision.polygons
.filter(({ room: other }) => other !== room)
.map(({ poly: other }) => other),
);
const path = (points: number[][]) =>
`M ${points.map((point) => `${point[0]} ${point[1]}`).join(' L ')} Z`;
return [[clean || path(floorPoly), ...holes.map(path)].join(' ')];
});
// #375: the clip depends only on the geometry (revision.geometryFingerprint
// covers spCfg) and on WHICH rooms have glow disabled — not on entity
// states. Cache it instead of recomputing boolean geometry every hass tick.
const enabledClipKey = `${revision.geometryFingerprint}|${
space.rooms.filter((room) => !glowEnabledRooms.includes(room))
.map((room) => room.id || `#${space.rooms.indexOf(room)}`).sort().join(',')}`;
const enabledClip = allEnabled ? null : cachedStaticEnabledClip(
o.cfg, space.id, enabledClipKey, () => glowEnabledRooms.flatMap((room) => {
const poly = roomPoly(room);
if (!poly) return [];
const floorPoly = walls.length && room.id
? (innerContourForRoom(
space.rooms, room.id, walls, zeroWalls.contour,
GRID_STEP_N, cellCm, GRID_PITCH, NORM_W,
canonicalWallGeometry?.roomGeom,
canonicalWallGeometry?.multiWallNodes,
) || poly)
: poly;
// #375: bbox prefilter, same as the full card's clean-floor cache —
// bodies entirely outside the room's bounding box cannot cut it.
const xs = floorPoly.map((point: number[]) => point[0]);
const ys = floorPoly.map((point: number[]) => point[1]);
const box = [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
const bodies = extras.filter((body) => {
const bx = body.map((point: number[]) => point[0]);
const by = body.map((point: number[]) => point[1]);
return Math.max(...bx) >= box[0] && Math.min(...bx) <= box[2]
&& Math.max(...by) >= box[1] && Math.min(...by) <= box[3];
});
const cleanGeometry = bodies.length ? floorMinusBodies(floorPoly, bodies) : null;
const clean = cleanGeometry ? polyclipPathD(cleanGeometry) : '';
const holes = islandsOf(
floorPoly,
revision.polygons
.filter(({ room: other }) => other !== room)
.map(({ poly: other }) => other),
);
const path = (points: number[][]) =>
`M ${points.map((point) => `${point[0]} ${point[1]}`).join(' L ')} Z`;
return [[clean || path(floorPoly), ...holes.map(path)].join(' ')];
}),
);
const feather = resolveGlowFeather(
glowRuntime.state,
o.stageWidth && vb[2] ? o.stageWidth / vb[2] : 1,
+109
View File
@@ -0,0 +1,109 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolvedLightSources } from '../test-build/devices.js';
import {
cachedStaticLightBarriers, cachedStaticEnabledClip,
} from '../test-build/space-render.js';
// #375 AC1: the light-graph cache is keyed by the ARRAY IDENTITY. The glow
// scene must therefore pass the caller's array as-is; a fresh spread would
// guarantee a miss on every render — in the space card AND the full card.
test('#375 AC1: resolvedLightSources reuses the cached graph for a stable array', () => {
const hass = {
states: { 'light.l': { state: 'on', attributes: {} } },
entities: {},
devices: {},
};
const lamp = {
id: 'lamp', name: 'Lamp', model: '', area: 'room', space: 'floor',
icon: 'mdi:lightbulb', is_light: true, binding: 'entity:light.l',
entity: 'light.l', entities: ['light.l'], x: 0.5, y: 0.5,
};
const devices = [lamp];
const manual = { rev: 1, configRev: 1, off: new Set() };
const first = resolvedLightSources(hass, devices, null, manual);
const second = resolvedLightSources(hass, devices, null, manual);
assert.equal(first, second,
'same array + same state must return the SAME cached sources object');
const spreadMiss = resolvedLightSources(hass, [...devices], null, manual);
assert.notEqual(first, spreadMiss,
'a spread copy is a different WeakMap key — full rebuild (the bug of #375)');
});
test('#375 AC1: the glow scene passes input.devices without a spread', () => {
const source = readFileSync(new URL('../src/glow-scene.ts', import.meta.url), 'utf8');
assert.match(source, /resolvedLightSources\(\s*input\.hass, input\.devices,/,
'resolveGlowCandidates must hand the stable array to the cache');
assert.ok(!source.includes('[...input.devices]'),
'no spread on the cache key (#375 V6a)');
});
// #375 AC2: the static path attaches the same sourceFingerprint tag the full
// card attaches, from the same triple buildLightBarrierRevision fingerprints —
// proven the same way the full card's recut wiring is proven
// (performance-contract.test.mjs): by pinning the source.
test('#375 AC2: static wall geometry carries the recut fingerprint tag', () => {
const render = readFileSync(new URL('../src/space-render.ts', import.meta.url), 'utf8');
assert.match(render,
/Object\.defineProperty\(built, 'sourceFingerprint', \{\s*\n\s*value: contentFingerprint\(\[spCfg, cellCm, GRID_PITCH\]\),/,
'the tag must be the exact revision triple (rawSpaceConfig=spCfg, cellCm, gridPitch=GRID_PITCH)');
const scene = readFileSync(new URL('../src/glow-scene.ts', import.meta.url), 'utf8');
assert.match(scene, /sharedFingerprint === revision\.geometryFingerprint/,
'glow-scene honours the tag by comparing it with the revision fingerprint');
});
// #375 AC3: a door flipping open<->close alternates two scene fingerprints;
// the static cache must hold both (LRU 8, parity with the full card's pool).
test('#375 AC3: scene cache survives a fingerprint ping-pong', () => {
const cfg = { marker: 'cfg-a' };
let builds = 0;
const build = () => ({ built: ++builds });
const sequence = ['open', 'closed', 'open', 'closed', 'open', 'closed'];
const seen = sequence.map((fp) => cachedStaticLightBarriers(cfg, 'space-1', fp, build));
assert.equal(builds, 2,
'six alternating lookups must build exactly twice — one per fingerprint');
assert.equal(seen[0], seen[2], 'the "open" scene is reused by identity');
assert.equal(seen[1], seen[3], 'the "closed" scene is reused by identity');
});
test('#375 AC3: scene cache evicts least-recently-used beyond 8 entries', () => {
const cfg = { marker: 'cfg-b' };
let builds = 0;
const build = () => ({ built: ++builds });
for (let i = 0; i < 9; i++) cachedStaticLightBarriers(cfg, 's', `fp-${i}`, build);
assert.equal(builds, 9);
cachedStaticLightBarriers(cfg, 's', 'fp-8', build);
assert.equal(builds, 9, 'the newest entry is still cached');
cachedStaticLightBarriers(cfg, 's', 'fp-0', build);
assert.equal(builds, 10, 'the oldest entry was evicted at capacity 8');
});
test('#375 AC3: scene cache is namespaced by space and by config object', () => {
let builds = 0;
const build = () => ({ built: ++builds });
const cfg = { marker: 'cfg-c' };
cachedStaticLightBarriers(cfg, 'space-1', 'fp', build);
cachedStaticLightBarriers(cfg, 'space-2', 'fp', build);
cachedStaticLightBarriers({ marker: 'cfg-d' }, 'space-1', 'fp', build);
assert.equal(builds, 3, 'no cross-talk between spaces or config revisions');
});
// #375 AC4: the enabled-rooms clip is pure in the geometry fingerprint and the
// disabled-room set — a repeated hass tick must not redo boolean geometry.
test('#375 AC4: enabledClip is reused by identity for an unchanged key', () => {
const cfg = { marker: 'cfg-e' };
let builds = 0;
const build = () => { builds++; return ['M 0 0 L 1 0 L 1 1 Z']; };
const key = 'geom-fp|kitchen';
const first = cachedStaticEnabledClip(cfg, 'space-1', key, build);
const second = cachedStaticEnabledClip(cfg, 'space-1', key, build);
assert.equal(builds, 1, 'the second tick must be a cache hit');
assert.equal(first, second, 'reuse is by identity — no fresh arrays per tick');
cachedStaticEnabledClip(cfg, 'space-1', 'geom-fp|kitchen,porch', build);
assert.equal(builds, 2, 'a different disabled set is a different clip');
});
+1 -1
View File
@@ -15,7 +15,7 @@
"src/ha-binding-status.ts",
"src/integration-provider.ts", "src/vacuum.ts",
"src/sun.ts",
"src/light-visibility.ts", "src/glow-scene.ts",
"src/light-visibility.ts", "src/glow-scene.ts", "src/space-render.ts",
"src/resize.ts", "src/resize-labels.ts", "src/resize-controller.ts", "src/wall-record-preservation.ts",
"src/rules.ts",
"src/devices.ts", "src/device-inbox.ts",