fix: show entityless active controllers

Issue: #318
User-Visible: yes
This commit is contained in:
Matysh
2026-08-28 13:07:58 +00:00
committed by claude[bot]
parent 4b81dc2ad9
commit 72913fee84
41 changed files with 460 additions and 167 deletions
@@ -1,111 +1,111 @@
{
"schema": 1,
"fingerprint": "13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf",
"fingerprint": "0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DqEPY1YF.js",
"houseplan-assets/houseplan-card-BU843YpJ.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 256046,
"initialViewGzipBytes": 256091,
"lazyFiles": [
"houseplan-assets/de-C7PgDVgU.js",
"houseplan-assets/editor-CPiO_Ss0.js",
"houseplan-assets/houseplan-editor-runtime-BgHN32PH.js",
"houseplan-assets/houseplan-onboarding-runtime-yqs9kwpI.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/de-SftP4Auy.js",
"houseplan-assets/editor-DuOmG3Hn.js",
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"lazyGzipBytes": 160338,
"lazyGzipBytes": 160351,
"lazyEditorFiles": [
"houseplan-assets/editor-CPiO_Ss0.js",
"houseplan-assets/houseplan-editor-runtime-BgHN32PH.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/editor-DuOmG3Hn.js",
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"lazyEditorGzipBytes": 131830,
"lazyEditorGzipBytes": 131833,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-yqs9kwpI.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"lazyOnboardingGzipBytes": 7192,
"lazyOnboardingGzipBytes": 7198,
"lazyLocaleFiles": [
"houseplan-assets/de-C7PgDVgU.js"
"houseplan-assets/de-SftP4Auy.js"
],
"lazyLocaleGzipBytes": 21866,
"lazyLocaleGzipBytes": 21873,
"files": [
{
"path": "houseplan-assets/de-C7PgDVgU.js",
"sha256": "0a7d0b50dde016600e4a01b38063b13eb08179de87091fc30af08597bd09d49e",
"path": "houseplan-assets/de-SftP4Auy.js",
"sha256": "3bee20ff5a52b6c6726cf857545bc3e1061a1249a515df5660491fd662218c94",
"rawBytes": 71477,
"gzipBytes": 21866,
"gzipBytes": 21873,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CPiO_Ss0.js",
"sha256": "00cfc5201c071a576ab5c2db63d1d8467bff3f702e81ca49c297700c6ba1712b",
"path": "houseplan-assets/editor-DuOmG3Hn.js",
"sha256": "d90a38e511769dc68d69081375ffb5067b196d429ab59fc4de06068baeafcac8",
"rawBytes": 3831,
"gzipBytes": 1582,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DqEPY1YF.js"
"houseplan-assets/houseplan-card-BU843YpJ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DqEPY1YF.js",
"sha256": "8ed3aca1f9b05e41e123c54584bc498cfbabe35d232b708768487185d282f7eb",
"rawBytes": 910277,
"gzipBytes": 255864,
"path": "houseplan-assets/houseplan-card-BU843YpJ.js",
"sha256": "1937126237e02356adad8b8c34b4adb29892bc183d6bcaa17e2542404907c811",
"rawBytes": 910424,
"gzipBytes": 255907,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-C7PgDVgU.js",
"houseplan-assets/editor-CPiO_Ss0.js",
"houseplan-assets/houseplan-editor-runtime-BgHN32PH.js",
"houseplan-assets/houseplan-onboarding-runtime-yqs9kwpI.js"
"houseplan-assets/de-SftP4Auy.js",
"houseplan-assets/editor-DuOmG3Hn.js",
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BgHN32PH.js",
"sha256": "da7c8f5cb805e66604343cf899d8eadf33c6a349ba7d865067ff5cf7fb639bb7",
"path": "houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
"sha256": "51fdd8f9766569a3b1af3b04d91dea81e6b555d9784bf5be53c6f82a42fbe51e",
"rawBytes": 504959,
"gzipBytes": 129698,
"gzipBytes": 129696,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DqEPY1YF.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/houseplan-card-BU843YpJ.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-yqs9kwpI.js",
"sha256": "3047ab81aef88f76caa129e43ba4746cc560de6036405759dbd00d1ad98983d6",
"path": "houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
"sha256": "19299e14098c6e413ca410d7b2c3715d57c945fdb690991f92fa2dc769d9c84d",
"rawBytes": 26356,
"gzipBytes": 6642,
"gzipBytes": 6645,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DqEPY1YF.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/houseplan-card-BU843YpJ.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-niSjMtPX.js",
"sha256": "2935c48da7310444e002c3426b3b50ec693c7b1484c8b067173621692a4d19a5",
"path": "houseplan-assets/space-deletion-DIa5HjfJ.js",
"sha256": "93f68c225dc00086ea86ac8a97ddfdf49b99ff17a8221f3cc3796f92d5e9f3cb",
"rawBytes": 911,
"gzipBytes": 550,
"gzipBytes": 553,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "147f3e320c09da67cbe342bcb38567c0df6e6b685d0e52bab4277ee69b896361",
"sha256": "823dc598d07959ff800a942ca1b705bd54b94dd4def67d66668f2a5552cc271f",
"rawBytes": 190,
"gzipBytes": 182,
"gzipBytes": 184,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DqEPY1YF.js"
"houseplan-assets/houseplan-card-BU843YpJ.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-DqEPY1YF.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__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-BU843YpJ.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}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";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 c,b$ as n,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-DqEPY1YF.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-niSjMtPX.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";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),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;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),c=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:c,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__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";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 c,b$ as n,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-BU843YpJ.js";import{i as b,c as $,a as v,s as y,t as f}from"./space-deletion-DIa5HjfJ.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";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),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;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),c=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:c,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__="13fcebb68eb62b100bccb7b77eb09b8fce2f
<label>${this.host._t("space.plan_label")}</label>
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"file"===t.source}
@change=${()=>this.host._spaceDialog=f(t,"file")} />
@change=${()=>this.host._spaceDialog=y(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__="13fcebb68eb62b100bccb7b77eb09b8fce2f
${t.pickSaved?this._renderServerPlans(t):_}`:_}
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"draw"===t.source}
@change=${()=>this.host._spaceDialog=f(t,"draw")} />
@change=${()=>this.host._spaceDialog=y(t,"draw")} />
<span>${this.host._t("space.source_draw")}</span>
</label>
@@ -85,7 +85,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="13fcebb68eb62b100bccb7b77eb09b8fce2f
<label class="dispsection">${this.host._t("space.display_section")}</label>
<label class="srcrow">
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=y(t,"showBorders",s)})}
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=f(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__="13fcebb68eb62b100bccb7b77eb09b8fce2f
</select>
<div class="rhint">${this.host._t("space.zero_wall_help")}</div>
<label class="srcrow">
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=y(t,"showNames",s)})}
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=f(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 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__="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";export{t as 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__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";export{t as a,e as c,o as i,r as s,s as t};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-DqEPY1YF.js";
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-BU843YpJ.js";
+132
View File
@@ -155,6 +155,138 @@ const out = await page.evaluate(async () => {
result.activeTargetWorksOnBothSurfaces = activePresentation.visual.status === 'working'
&& activePreview?.presentation?.visual?.status === 'working';
// #318: an active physical HA device may legitimately expose no entity
// registry rows. Empty telemetry is not evidence that this controller is
// offline; plan and preview must follow only the external target status.
const entitylessDeviceId = 'issue-318-entityless-device';
const entitylessMarkerId = 'issue-318-entityless-marker';
const entitylessTarget = 'light.issue_318_target';
const entitylessTargetDeviceId = 'issue-318-lights';
const entitylessMarker = {
id: entitylessMarkerId,
binding: `device:${entitylessDeviceId}`,
name: 'Issue 318 entityless switch',
icon: 'mdi:light-switch',
tap_action: 'toggle',
controls: [entitylessTarget],
space: c._space,
area: room.area,
};
c.hass = {
...c.hass,
devices: {
...c.hass.devices,
[entitylessDeviceId]: {
id: entitylessDeviceId,
name: 'Issue 318 entityless switch',
model: 'Entityless wireless switch',
area_id: room.area,
identifiers: [['demo', entitylessDeviceId]],
entry_type: null,
via_device_id: null,
},
[entitylessTargetDeviceId]: {
id: entitylessTargetDeviceId,
name: 'Issue 318 lights',
model: 'Demo light group',
area_id: room.area,
identifiers: [['demo', entitylessTargetDeviceId]],
entry_type: null,
via_device_id: null,
},
},
entities: {
...c.hass.entities,
[entitylessTarget]: {
entity_id: entitylessTarget,
device_id: entitylessTargetDeviceId,
platform: 'demo',
},
},
states: {
...c.hass.states,
[entitylessTarget]: { entity_id: entitylessTarget, state: 'on', attributes: {} },
},
};
c._serverCfg = {
...c._serverCfg,
markers: [
...c._serverCfg.markers.filter((item) => item.id !== entitylessMarkerId),
entitylessMarker,
],
};
c._layout = {
...c._layout,
[entitylessMarkerId]: { s: c._space, x: 0.58, y: 0.5 },
};
c._regSignature = '';
c._maybeRebuildDevices();
c._setMode('view');
c.requestUpdate();
await c.updateComplete;
const entitylessController = () => c._devices.find((item) => item.id === entitylessMarkerId);
const onPresentation = c._devicePresentation(entitylessController(), true);
const onElement = sr().querySelector(`.dev[data-id="${entitylessMarkerId}"]`);
result.entitylessDeviceHasEmptyRoster = entitylessController()?.entities?.length === 0;
result.entitylessOnIsAvailableWorking = onPresentation.visual.availability === 'available'
&& onPresentation.visual.status === 'working'
&& onElement?.getAttribute('data-state') === 'working'
&& !onElement?.classList.contains('unavail');
c._setMode('devices');
await c.updateComplete;
c._openMarkerDialog(entitylessController());
await c.updateComplete;
const entitylessPreview = sr().querySelector('hp-device-preview');
await entitylessPreview?.updateComplete;
result.entitylessPreviewMatchesOnPlan = entitylessPreview?.presentation?.visual?.availability
=== onPresentation.visual.availability
&& entitylessPreview?.presentation?.visual?.status === onPresentation.visual.status
&& entitylessPreview?.presentation?.sourceKind === onPresentation.sourceKind
&& JSON.stringify(entitylessPreview?.presentation?.classes)
=== JSON.stringify(onPresentation.classes);
c.hass = {
...c.hass,
states: {
...c.hass.states,
[entitylessTarget]: { ...c.hass.states[entitylessTarget], state: 'off' },
},
};
c.requestUpdate();
await c.updateComplete;
await entitylessPreview?.updateComplete;
const offPresentation = c._devicePresentation(entitylessController(), true, true);
result.entitylessOffIsAvailableNeutralEverywhere = offPresentation.visual.availability
=== 'available'
&& offPresentation.visual.status === 'neutral'
&& entitylessPreview?.presentation?.visual?.availability === 'available'
&& entitylessPreview?.presentation?.visual?.status === 'neutral'
&& entitylessPreview?.presentation?.sourceKind === offPresentation.sourceKind
&& JSON.stringify(entitylessPreview?.presentation?.classes)
=== JSON.stringify(offPresentation.classes);
c.hass = {
...c.hass,
states: {
...c.hass.states,
[entitylessTarget]: { ...c.hass.states[entitylessTarget], state: 'unavailable' },
},
};
c.requestUpdate();
await c.updateComplete;
await entitylessPreview?.updateComplete;
const unavailableTargetPresentation = c._devicePresentation(entitylessController(), true, true);
result.entitylessUnavailableTargetIsAvailableNeutralEverywhere
= unavailableTargetPresentation.visual.availability === 'available'
&& unavailableTargetPresentation.visual.status === 'neutral'
&& entitylessPreview?.presentation?.visual?.availability === 'available'
&& entitylessPreview?.presentation?.visual?.status === 'neutral'
&& entitylessPreview?.presentation?.sourceKind === unavailableTargetPresentation.sourceKind
&& JSON.stringify(entitylessPreview?.presentation?.classes)
=== JSON.stringify(unavailableTargetPresentation.classes);
return result;
});
+49 -49
View File
@@ -1,111 +1,111 @@
{
"schema": 1,
"fingerprint": "13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf",
"fingerprint": "0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DqEPY1YF.js",
"houseplan-assets/houseplan-card-BU843YpJ.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 256046,
"initialViewGzipBytes": 256091,
"lazyFiles": [
"houseplan-assets/de-C7PgDVgU.js",
"houseplan-assets/editor-CPiO_Ss0.js",
"houseplan-assets/houseplan-editor-runtime-BgHN32PH.js",
"houseplan-assets/houseplan-onboarding-runtime-yqs9kwpI.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/de-SftP4Auy.js",
"houseplan-assets/editor-DuOmG3Hn.js",
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"lazyGzipBytes": 160338,
"lazyGzipBytes": 160351,
"lazyEditorFiles": [
"houseplan-assets/editor-CPiO_Ss0.js",
"houseplan-assets/houseplan-editor-runtime-BgHN32PH.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/editor-DuOmG3Hn.js",
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"lazyEditorGzipBytes": 131830,
"lazyEditorGzipBytes": 131833,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-yqs9kwpI.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"lazyOnboardingGzipBytes": 7192,
"lazyOnboardingGzipBytes": 7198,
"lazyLocaleFiles": [
"houseplan-assets/de-C7PgDVgU.js"
"houseplan-assets/de-SftP4Auy.js"
],
"lazyLocaleGzipBytes": 21866,
"lazyLocaleGzipBytes": 21873,
"files": [
{
"path": "houseplan-assets/de-C7PgDVgU.js",
"sha256": "0a7d0b50dde016600e4a01b38063b13eb08179de87091fc30af08597bd09d49e",
"path": "houseplan-assets/de-SftP4Auy.js",
"sha256": "3bee20ff5a52b6c6726cf857545bc3e1061a1249a515df5660491fd662218c94",
"rawBytes": 71477,
"gzipBytes": 21866,
"gzipBytes": 21873,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CPiO_Ss0.js",
"sha256": "00cfc5201c071a576ab5c2db63d1d8467bff3f702e81ca49c297700c6ba1712b",
"path": "houseplan-assets/editor-DuOmG3Hn.js",
"sha256": "d90a38e511769dc68d69081375ffb5067b196d429ab59fc4de06068baeafcac8",
"rawBytes": 3831,
"gzipBytes": 1582,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DqEPY1YF.js"
"houseplan-assets/houseplan-card-BU843YpJ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DqEPY1YF.js",
"sha256": "8ed3aca1f9b05e41e123c54584bc498cfbabe35d232b708768487185d282f7eb",
"rawBytes": 910277,
"gzipBytes": 255864,
"path": "houseplan-assets/houseplan-card-BU843YpJ.js",
"sha256": "1937126237e02356adad8b8c34b4adb29892bc183d6bcaa17e2542404907c811",
"rawBytes": 910424,
"gzipBytes": 255907,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-C7PgDVgU.js",
"houseplan-assets/editor-CPiO_Ss0.js",
"houseplan-assets/houseplan-editor-runtime-BgHN32PH.js",
"houseplan-assets/houseplan-onboarding-runtime-yqs9kwpI.js"
"houseplan-assets/de-SftP4Auy.js",
"houseplan-assets/editor-DuOmG3Hn.js",
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BgHN32PH.js",
"sha256": "da7c8f5cb805e66604343cf899d8eadf33c6a349ba7d865067ff5cf7fb639bb7",
"path": "houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
"sha256": "51fdd8f9766569a3b1af3b04d91dea81e6b555d9784bf5be53c6f82a42fbe51e",
"rawBytes": 504959,
"gzipBytes": 129698,
"gzipBytes": 129696,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DqEPY1YF.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/houseplan-card-BU843YpJ.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-yqs9kwpI.js",
"sha256": "3047ab81aef88f76caa129e43ba4746cc560de6036405759dbd00d1ad98983d6",
"path": "houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
"sha256": "19299e14098c6e413ca410d7b2c3715d57c945fdb690991f92fa2dc769d9c84d",
"rawBytes": 26356,
"gzipBytes": 6642,
"gzipBytes": 6645,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DqEPY1YF.js",
"houseplan-assets/space-deletion-niSjMtPX.js"
"houseplan-assets/houseplan-card-BU843YpJ.js",
"houseplan-assets/space-deletion-DIa5HjfJ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-niSjMtPX.js",
"sha256": "2935c48da7310444e002c3426b3b50ec693c7b1484c8b067173621692a4d19a5",
"path": "houseplan-assets/space-deletion-DIa5HjfJ.js",
"sha256": "93f68c225dc00086ea86ac8a97ddfdf49b99ff17a8221f3cc3796f92d5e9f3cb",
"rawBytes": 911,
"gzipBytes": 550,
"gzipBytes": 553,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "147f3e320c09da67cbe342bcb38567c0df6e6b685d0e52bab4277ee69b896361",
"sha256": "823dc598d07959ff800a942ca1b705bd54b94dd4def67d66668f2a5552cc271f",
"rawBytes": 190,
"gzipBytes": 182,
"gzipBytes": 184,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DqEPY1YF.js"
"houseplan-assets/houseplan-card-BU843YpJ.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-DqEPY1YF.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__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-BU843YpJ.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}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";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 c,b$ as n,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-DqEPY1YF.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-niSjMtPX.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";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),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;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),c=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:c,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__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";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 c,b$ as n,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-BU843YpJ.js";import{i as b,c as $,a as v,s as y,t as f}from"./space-deletion-DIa5HjfJ.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";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),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;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),c=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:c,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__="13fcebb68eb62b100bccb7b77eb09b8fce2f
<label>${this.host._t("space.plan_label")}</label>
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"file"===t.source}
@change=${()=>this.host._spaceDialog=f(t,"file")} />
@change=${()=>this.host._spaceDialog=y(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__="13fcebb68eb62b100bccb7b77eb09b8fce2f
${t.pickSaved?this._renderServerPlans(t):_}`:_}
<label class="srcrow">
<input type="radio" name="plansrc" .checked=${"draw"===t.source}
@change=${()=>this.host._spaceDialog=f(t,"draw")} />
@change=${()=>this.host._spaceDialog=y(t,"draw")} />
<span>${this.host._t("space.source_draw")}</span>
</label>
@@ -85,7 +85,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="13fcebb68eb62b100bccb7b77eb09b8fce2f
<label class="dispsection">${this.host._t("space.display_section")}</label>
<label class="srcrow">
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=y(t,"showBorders",s)})}
${this._boolInput(t.showBorders,s=>{this.host._spaceDialog=f(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__="13fcebb68eb62b100bccb7b77eb09b8fce2f
</select>
<div class="rhint">${this.host._t("space.zero_wall_help")}</div>
<label class="srcrow">
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=y(t,"showNames",s)})}
${this._boolInput(t.showNames,s=>{this.host._spaceDialog=f(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 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__="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";export{t as 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__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";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__="13fcebb68eb62b100bccb7b77eb09b8fce2f41e6c77bcbe8870e6dc4d97eedbf";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-DqEPY1YF.js";
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-BU843YpJ.js";
+7 -2
View File
@@ -1315,8 +1315,13 @@ its configured space.
icon working state mirrors the effective light graph. Controller availability
is deliberately separate (#251): at least one live own active entity
(including battery/LQI/update diagnostics) keeps a physical controller
available, while an all-unavailable target graph is neutral. A virtual
controller is available by definition. An explicit Toggle whose configured
available, while an all-unavailable target graph is neutral. An active
physical `device:` binding with an empty own entity roster is also available:
absence of telemetry is not proof that the device is offline, and its target
graph still decides working versus neutral (#318). Once that own roster is
non-empty, all-missing/`unknown`/`unavailable` states remain positive offline
evidence and fade the controller. A virtual controller is available by
definition. An explicit Toggle whose configured
group has no executable unavailable/missing/HA-disabled target produces the
card's standard local explanatory toast and no service/press feedback;
partial groups keep executing their available subset.
+5
View File
@@ -2,6 +2,11 @@
## Unreleased
- Active physical wall controllers that Home Assistant exposes without any own
entities no longer look permanently unavailable: they now stay opaque and
follow their controlled target as yellow when working or neutral otherwise
([#318](https://github.com/Matysh/houseplan-card/issues/318)).
- An outdated tab or third-party client can no longer silently replace a newer
saved plan by omitting its configuration revision. House Plan rejects that
write as a conflict and keeps the server copy intact; revision-less bootstrap
+6
View File
@@ -8,6 +8,12 @@
## Не выпущено
- Активные физические настенные выключатели, для которых Home Assistant не
предоставляет собственных сущностей, больше не выглядят постоянно
недоступными: теперь они остаются непрозрачными и становятся жёлтыми при
работающей управляемой цели или нейтральными в остальных случаях
([#318](https://github.com/Matysh/houseplan-card/issues/318)).
- Устаревшая вкладка или сторонний клиент больше не может молча заменить новый
сохранённый план, не передав ревизию конфигурации. House Plan отклоняет такую
запись как конфликт и сохраняет серверную копию; инициализация без ревизии
+2 -1
View File
@@ -39,7 +39,7 @@ mutation evidence в одном pull request.
| S02 | cover — побочная capability при light/role | `source.cover_capability_bypassed` | cover не перехватывает выбранный face | normal | `presentation-source-decision-trace`; `presentation-row-contract` |
| S03 | есть active external controls | `source.controls` + `availability.controller_available` | status берётся от целей, availability — от контроллера | normal | `controller-availability-follows-target`; `presentation-row-contract` |
| S04 | цели unavailable, controller имеет live diagnostic | `availability.controller_available` | доступная нейтральная подложка, не `unavail` | normal | `controller-diagnostics-do-not-prove-online`; `presentation-row-contract` |
| S05 | цель работает, controller не имеет live entity | `availability.controller_unavailable` | faded controller; нет yellow/pulse | normal action сохраняется | `controller-availability-follows-target`; `presentation-row-contract` |
| S05 | у controller есть собственные entities, но ни одной live entity; цель работает | `availability.controller_unavailable` | faded controller; нет yellow/pulse | normal action сохраняется | `controller-availability-follows-target`; `presentation-row-contract` |
| S06 | virtual controller + controls | `source.virtual_controller` | controller доступен; status следует цели | normal | `presentation-source-decision-trace`; `presentation-row-contract` |
| S07 | все saved controls отфильтрованы tombstone | `source.filtered_saved_controls` | controller-role сохранён, availability дают свои diagnostics | normal | `wireless-controller-loses-filtered-target-role`; `wireless-controller-preview-drops-sibling-markers` |
| S08 | manual virtual light с outgoing controls | `source.manual_virtual_light` | собственный manual source владеет face | normal | `presentation-source-decision-trace`; `presentation-row-contract` |
@@ -49,6 +49,7 @@ mutation evidence в одном pull request.
| S12 | пригодного source нет | `source.none` | neutral base-icon fallback | normal | `presentation-source-decision-trace`; `presentation-row-contract` |
| S13 | critical alarm sibling вне обычного source | `source.critical_sibling` + `status.alarm` | alarm добавлен в aggregate и побеждает normal status | normal | `device-presentation-policy-alarm`; `presentation-row-contract` |
| S14 | static plan fast path без source details | `source.skipped_static_fast_path` | source graph намеренно не вычисляется; static face остаётся neutral | normal; без source-derived данных | `presentation-static-source-fast-path`; `presentation-row-contract` |
| S15 | active physical `device:` controller с пустым собственным roster + controls | `availability.controller_available` | status следует цели: working = yellow, off/unavailable/missing = neutral; controller не faded | normal | `entityless-active-controller-stays-available`; `presentation-row-contract` |
## Финальное лицо, контент и диагностика
+8 -1
View File
@@ -1608,7 +1608,13 @@ separately promised workflows:
controller is encountered first. Controller availability is independent
(#251): live battery/LQI/update keeps it neutral and opaque when every
target is unavailable, all unavailable own entities fade it even if a
target is on, and a virtual controller remains available. A separately
target is on, and a virtual controller remains available. An active
physical `device:` controller with an empty own roster remains available
(#318): target `on` is yellow, while target `off`, unavailable, missing or
runtime-filtered is neutral; the plan and device-dialog preview must agree.
This exception must not apply to a non-empty all-unavailable roster,
`entity:` bindings, HA-disabled/orphaned bindings or virtual lifecycle.
A separately
deleted target marker also cannot turn a live wireless controller into
`unavailable` or make dialog preview disagree with the saved plan (#274):
both projections consume the complete marker roster, preserve the live
@@ -1620,6 +1626,7 @@ separately promised workflows:
device-presentation.test.mjs, device-toggle.test.mjs; golden:
device-icon-state-table light/dark; mutation: controller-availability-follows-target,
controller-diagnostics-do-not-prove-online,
entityless-active-controller-stays-available,
wireless-controller-loses-filtered-target-role,
wireless-controller-preview-drops-sibling-markers,
unavailable-toggle-stays-silent, partial-group-shows-noop-toast; #274:
+7 -2
View File
@@ -601,8 +601,13 @@ For a controller with `controls`, target work and controller availability are
independent. The controlled lights still decide whether the marker is yellow,
but only the controller's own active entities decide whether it fades. A live
battery, Zigbee LQI or update entity therefore keeps a wireless switch neutral
and opaque when all of its lamps are unavailable. A controller with no live own
entity fades even if a target is on; a virtual controller is always available.
and opaque when all of its lamps are unavailable. If an active physical HA
device exposes no entities at all, House Plan also keeps its controller opaque:
missing telemetry alone is not evidence that the device is offline, so its
controlled target makes it yellow when working and neutral otherwise. If the
device does expose own entities but all of them are missing, `unknown` or
`unavailable`, the controller fades even if a target is on. A virtual controller
is always available.
This remains true when the same target was separately removed from the plan:
the removed marker is not restored, but it cannot make the controller look
offline or make its editor preview disagree with the plan.
+7 -3
View File
@@ -976,9 +976,13 @@ hover → состояние → нейтрально**. Если все сущ
активные сущности контроллера. Живой `battery`, `linkquality` или `update`
удерживает беспроводной выключатель непрозрачным, даже если все лампы
недоступны; в таком случае выключатель нейтрален, а не жёлтый. Если ни одна
собственная сущность не имеет живого состояния, контроллер бледнеет даже при
включённой цели. Виртуальный контроллер не имеет физического HA-устройства и
считается доступным.
собственная сущность из непустого списка не имеет живого состояния, контроллер
бледнеет даже при включённой цели. Но если активное физическое устройство HA
вообще не предоставляет собственных сущностей, House Plan не считает отсутствие
телеметрии доказательством недоступности: такой контроллер остаётся непрозрачным,
становится жёлтым при работающей цели и нейтральным во всех остальных состояниях
цели. Виртуальный контроллер не имеет физического HA-устройства и считается
доступным.
То же правило действует, если маркер этой цели отдельно удалён с плана:
удалённый маркер не возвращается, но он не может сделать контроллер визуально
недоступным или вызвать расхождение плана и preview в редакторе устройства.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 134 KiB

+21 -21
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": null,
"sourceFingerprint": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"sourceFingerprint": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "9190bd86a07b0cb0502019d76d3286c0542181e1b4a4f862077d9cefa120613c"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,8 +26,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "f234e55b02d2558c58935b3bc6340615fb343d3cf92b7d3a98303b1a2f4f346d"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
},
"space-create": {
"file": "03-space-create.png",
@@ -37,8 +37,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "a86d4211af24923d5048e9094d0d35f9b28129394d00cd72582a8f8c2dd7ff75"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -48,8 +48,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "c7effd4ccd19bcaff9138458583368aaab488dcb6fc7ea5ab766383cb5377ac4"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -59,8 +59,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "af1194f073f00b03af02b949a072cffb21bfd29995aa65a067eaaaec74ef8490"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -70,8 +70,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "f7fb7495c82c89d49acfca61738b63d9a97da057e37c0e6549c4866fcf677d89"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "7151c96dc048381f7a7070ecb85a82a890b2f4c703628f14fa472eb8dacbf3dc"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,8 +92,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "9f08f3f3711b100d68fb1a05af269e43e47d8f8d12c0cbe4f9ab557138722989"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
},
"room-card": {
"file": "08-room-card.png",
@@ -103,8 +103,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "66d8b0a509909822ff6b891a483bad9d2c72b20f08e287355883aa5205c6cee0"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
"file": "09-device-info.png",
@@ -114,8 +114,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "4293c7df2738c5efb43d8278a9f3cfe2a6406b50fd90980bc9729c1eedfe9e76",
"imageSha256": "f4d3bda00a2fce54f408b1f20e03b5c590ba4b168fd214a28aafb1692b1f3124"
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
}
}
}
+4 -4
View File
@@ -231,10 +231,10 @@ npm run bundle:budget
node scripts/check-docs.mjs
node scripts/smoke-select.mjs --base origin/dev --head HEAD
node demo/smoke_wireless_controller_parity.mjs
node scripts/mutation-gate.mjs --only <new-empty-roster-mutant>
node scripts/mutation-gate.mjs --only controller-availability-follows-target
node scripts/mutation-gate.mjs --only wireless-controller-loses-filtered-target-role
node scripts/mutation-gate.mjs --only wireless-controller-preview-drops-sibling-markers
node scripts/mutation-gate.mjs --id=<new-empty-roster-mutant>
node scripts/mutation-gate.mjs --id=controller-availability-follows-target
node scripts/mutation-gate.mjs --id=wireless-controller-loses-filtered-target-role
node scripts/mutation-gate.mjs --id=wireless-controller-preview-drops-sibling-markers
```
Если `smoke-select` выберет дополнительные smokes, они также обязательны.
+15 -2
View File
@@ -2316,11 +2316,24 @@ const MUTANT_DEFINITIONS = [
+ 'controllers; excluding those siblings restores the field defect (#251)',
patches: [{
file: 'src/device-presentation.ts',
find: ' const live = (d.entities || []).some((eid) => {',
replace: " const live = (d.entities || []).filter((eid) => !eid.startsWith('sensor.') "
find: ' const live = ownEntities.some((eid) => {',
replace: " const live = ownEntities.filter((eid) => !eid.startsWith('sensor.') "
+ "&& !eid.startsWith('update.')).some((eid) => {",
}],
},
{
id: 'entityless-active-controller-stays-available',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="issue 318 keeps an active entityless physical controller available" '
+ 'test/device-presentation.test.mjs',
because: 'an active physical controller whose HA device exposes no own entities has no offline '
+ 'evidence; its face must remain available while controls decide working versus neutral (#318)',
patches: [{
file: 'src/device-presentation.ts',
find: " if (activeEntitylessDevice) return 'available';",
replace: " if (activeEntitylessDevice) return 'unavailable';",
}],
},
{
id: 'wireless-controller-loses-filtered-target-role',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+10 -3
View File
@@ -202,14 +202,21 @@ function sourceStateText(hass: any, eid: string): string {
*
* HA exposes entity states rather than one device state. Controls are not
* controller evidence: a live battery/LQI/update sibling is, while an
* event-only `unknown` entity is not. A virtual controller has no physical HA
* binding which can go offline, so it is available by definition.
* event-only `unknown` entity is not. An active physical device with no entity
* roster has no evidence that it is offline, so it remains available while
* its target status is resolved independently. A virtual controller has no
* physical HA binding which can go offline, so it is available by definition.
*/
export function controllerAvailability(hass: any, d: DevItem): DeviceAvailability {
if (d.virtual || d.bindingKind === 'virtual' || d.marker?.binding === 'virtual') {
return 'available';
}
const live = (d.entities || []).some((eid) => {
const ownEntities = d.entities || [];
const activeEntitylessDevice = ownEntities.length === 0
&& (d.bindingKind === 'device' || d.marker?.binding?.startsWith('device:'))
&& d.bindingStatus?.kind === 'active';
if (activeEntitylessDevice) return 'available';
const live = ownEntities.some((eid) => {
const state = String(hass?.states?.[eid]?.state ?? '').trim().toLowerCase();
return state !== '' && state !== 'unknown' && state !== 'unavailable';
});
+8
View File
@@ -208,6 +208,14 @@ const rowRunners = {
assert.equal(r.sourceKind, 'none');
assert.deepEqual(r.visual, neutral);
}),
S15: presentationRow({
entities: [], primary: null,
bindingStatus: { kind: 'active', enabledEntityIds: [], allEntityIds: [] },
marker: { id: 'd1', binding: 'device:d1', controls: ['light.main'] },
}, {}, (r) => {
assert.equal(r.sourceKind, 'controls');
assert.deepEqual([r.visual.availability, r.visual.status], ['available', 'working']);
}),
F01: policyRow({
sourceVisual: { availability: 'available', status: 'alarm', activity: 'none' },
}, (r) => assert.equal(r.visual.status, 'alarm')),
+99
View File
@@ -352,6 +352,105 @@ test('issue 251 separates controller availability from controlled target status'
assert.ok(alarm.classes.includes('alarm'));
});
test('issue 318 keeps an active entityless physical controller available', () => {
const h = hass({
'light.wall_group': state('light.wall_group', 'on', { friendly_name: 'Wall lights' }),
}, {
'light.wall_group': {
entity_id: 'light.wall_group', device_id: 'lights', platform: 'demo',
},
});
const controller = device({
id: 'entityless-wall', name: 'Entityless wall switch', entities: [], primary: null,
bindingRef: 'entityless-wall',
bindingStatus: { kind: 'active', enabledEntityIds: [], allEntityIds: [] },
marker: {
id: 'entityless-wall', binding: 'device:entityless-wall', tap_action: 'toggle',
controls: ['light.wall_group'],
},
});
let result = resolveDevicePresentation(h, controller, options);
assert.equal(result.sourceKind, 'controls');
assert.deepEqual(result.visual, {
availability: 'available', status: 'working', activity: 'running',
});
assert.ok(result.classes.includes('on'));
assert.ok(!result.classes.includes('unavail'));
assert.equal(deviceA11yState(result), 'working');
h.states['light.wall_group'].state = 'off';
result = resolveDevicePresentation(h, controller, options);
assert.deepEqual(result.visual, {
availability: 'available', status: 'neutral', activity: 'none',
});
assert.ok(!result.classes.includes('on'));
assert.ok(!result.classes.includes('unavail'));
assert.equal(deviceA11yState(result), 'neutral');
h.states['light.wall_group'].state = 'unavailable';
result = resolveDevicePresentation(h, controller, options);
assert.deepEqual(result.visual, {
availability: 'available', status: 'neutral', activity: 'none',
});
delete h.states['light.wall_group'];
result = resolveDevicePresentation(h, controller, options);
assert.equal(result.visual.availability, 'available');
assert.equal(result.visual.status, 'neutral');
const runtimeFiltered = resolveDevicePresentation(h, {
...controller,
controls: [],
}, options);
assert.equal(runtimeFiltered.sourceKind, 'none');
assert.deepEqual(runtimeFiltered.visual, {
availability: 'available', status: 'neutral', activity: 'none',
});
h.states['light.wall_group'] = state('light.wall_group', 'on');
const provenOffline = resolveDevicePresentation(h, {
...controller,
entities: ['event.entityless_wall_action'],
primary: 'event.entityless_wall_action',
bindingStatus: {
kind: 'active',
enabledEntityIds: ['event.entityless_wall_action'],
allEntityIds: ['event.entityless_wall_action'],
},
}, options);
assert.equal(provenOffline.visual.availability, 'unavailable');
assert.equal(provenOffline.visual.status, 'working');
assert.ok(provenOffline.classes.includes('unavail'));
assert.ok(!provenOffline.classes.includes('on'));
assert.equal(deviceA11yState(provenOffline), 'unavailable');
const entityBinding = resolveDevicePresentation(h, {
...controller,
bindingKind: 'entity', bindingRef: 'switch.entityless_wall',
marker: {
...controller.marker,
binding: 'entity:switch.entityless_wall',
},
}, options);
assert.equal(entityBinding.visual.availability, 'unavailable');
const orphaned = resolveDevicePresentation(h, {
...controller,
bindingStatus: { kind: 'orphaned', reason: 'device_missing' },
}, options);
assert.equal(orphaned.visual.availability, 'unavailable');
const virtual = resolveDevicePresentation(h, {
...controller,
virtual: true,
bindingKind: 'virtual', bindingRef: '',
marker: { ...controller.marker, binding: 'virtual' },
}, options);
assert.equal(virtual.visual.availability, 'available');
assert.equal(virtual.visual.status, 'working');
});
test('issue 274 keeps a wireless controller available and gives its draft the full marker roster', () => {
const own = [
'event.wall_action', 'sensor.wall_battery', 'sensor.wall_linkquality', 'update.wall',
+1
View File
@@ -25,6 +25,7 @@ const ROWS = [
['S12', 'source.none', 'presentation-source-decision-trace'],
['S13', 'source.critical_sibling', 'device-presentation-policy-alarm'],
['S14', 'source.skipped_static_fast_path', 'presentation-static-source-fast-path'],
['S15', 'availability.controller_available', 'entityless-active-controller-stays-available'],
['F01', 'status.alarm', 'device-presentation-policy-alarm'],
['F02', 'status.unavailable', 'device-presentation-policy-unavailable'],
['F03', 'source.device_role', 'presentation-source-decision-trace'],