mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c",
|
||||
"fingerprint": "3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js",
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 277319,
|
||||
"initialViewGzipBytes": 277948,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/de-FazIOsg8.js",
|
||||
"houseplan-assets/editor-CcxmUFV-.js",
|
||||
"houseplan-assets/fr-CFpxbPTS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CisKVaG7.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-eqCB-Ax0.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/de-B775TcPH.js",
|
||||
"houseplan-assets/editor-CIkons5x.js",
|
||||
"houseplan-assets/fr-Cml_YfSz.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CM78ZHZU.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BP9ODa6H.js"
|
||||
],
|
||||
"lazyGzipBytes": 191754,
|
||||
"lazyGzipBytes": 192277,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/editor-CcxmUFV-.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CisKVaG7.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/editor-CIkons5x.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CM78ZHZU.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 139917,
|
||||
"lazyEditorGzipBytes": 140230,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-eqCB-Ax0.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BP9ODa6H.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 9451,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-FazIOsg8.js",
|
||||
"houseplan-assets/fr-CFpxbPTS.js"
|
||||
"houseplan-assets/de-B775TcPH.js",
|
||||
"houseplan-assets/fr-Cml_YfSz.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 45342,
|
||||
"lazyLocaleGzipBytes": 45551,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"sha256": "f7bdc0ee64684220a751eadda1397edb922b9c785993f5bce1d6b8273c619f21",
|
||||
"path": "houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"sha256": "2527114d8c2e58532848ac449cb7ea6927b86f5883bad84c649dc740c3872eea",
|
||||
"rawBytes": 6967,
|
||||
"gzipBytes": 2956,
|
||||
"gzipBytes": 2955,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-FazIOsg8.js",
|
||||
"sha256": "50ca05e9607b7f597cd20b1a1fce1f92fd9bb07a4cb0db4e86c3c921053c9e89",
|
||||
"rawBytes": 75090,
|
||||
"gzipBytes": 22886,
|
||||
"path": "houseplan-assets/de-B775TcPH.js",
|
||||
"sha256": "287e8f70aad3d82cfe292f204e5fe58eef3738631ee3be7078de6fd88c5fde1f",
|
||||
"rawBytes": 75572,
|
||||
"gzipBytes": 22996,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-CcxmUFV-.js",
|
||||
"sha256": "27077f8f6ecd61eea9e8405099b91974f423b8f7a53eef9ae8cf066803c475ef",
|
||||
"path": "houseplan-assets/editor-CIkons5x.js",
|
||||
"sha256": "ecaeb979d94de7e54f08b090355bf757b64a34932cddc9442f023804565ff56c",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1583,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-CFpxbPTS.js",
|
||||
"sha256": "ce2f711435309dac25e14c0024eea6da2c443949728a0fad1b5062851fcc26d6",
|
||||
"rawBytes": 77225,
|
||||
"gzipBytes": 22456,
|
||||
"path": "houseplan-assets/fr-Cml_YfSz.js",
|
||||
"sha256": "eeaa5abe3d199602537bab6f7f40075ceab398a25515685dd5e465d3ea9c9b51",
|
||||
"rawBytes": 77710,
|
||||
"gzipBytes": 22555,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-B5mDyjuC.js",
|
||||
"sha256": "d2f3c9df28d6acc99e4c7960c6f3cf1d5b516b42c3b10102429ba263532f569a",
|
||||
"rawBytes": 976343,
|
||||
"gzipBytes": 276523,
|
||||
"path": "houseplan-assets/houseplan-card-wtxKVHkp.js",
|
||||
"sha256": "796b979c90d4791cbdadb991c407dbd7d6768880b5a856cc1b7ec19f1e317c7a",
|
||||
"rawBytes": 979430,
|
||||
"gzipBytes": 277152,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-FazIOsg8.js",
|
||||
"houseplan-assets/editor-CcxmUFV-.js",
|
||||
"houseplan-assets/fr-CFpxbPTS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CisKVaG7.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-eqCB-Ax0.js"
|
||||
"houseplan-assets/de-B775TcPH.js",
|
||||
"houseplan-assets/editor-CIkons5x.js",
|
||||
"houseplan-assets/fr-Cml_YfSz.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CM78ZHZU.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BP9ODa6H.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CisKVaG7.js",
|
||||
"sha256": "d99951a97b78d46d05501d0e2e3f5a9cd73a1088a5e9a382d8899903b040bb97",
|
||||
"rawBytes": 524211,
|
||||
"gzipBytes": 135378,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CM78ZHZU.js",
|
||||
"sha256": "e439a389b8556a98b6ee0641cbda9a47978b2a10e4279d6663848ec97005771e",
|
||||
"rawBytes": 526486,
|
||||
"gzipBytes": 135692,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-eqCB-Ax0.js",
|
||||
"sha256": "aba9dab2a60c386b0638636d2ebcc683bd44a56fea5ce594fda9456ee171371d",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BP9ODa6H.js",
|
||||
"sha256": "d49d4e5b08de22682d8c548ffa90bd73175fc1d069eca75f98905b9a804cafb0",
|
||||
"rawBytes": 26195,
|
||||
"gzipBytes": 6495,
|
||||
"gzipBytes": 6496,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "376b5951ff76686ebc299f63f2a21bf56f331e1027c52eabd487664fc86cd45e",
|
||||
"sha256": "785c3ec627b80c3cda8831faaa08f31c7021ad47b0be372d3326ce5456d7d087",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 796,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
+1
-1
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
+5
-5
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-B5mDyjuC.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__="3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f";import{c as e,l as o,t,c$ as s,A as a,d0 as i,d1 as l,d2 as n,E as r,b as c}from"./houseplan-card-wtxKVHkp.js";class f 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,f={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")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${d(h.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
${d(h.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+43
-43
File diff suppressed because one or more lines are too long
+153
-126
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c";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 _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-B5mDyjuC.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-DFFLwJ_d.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c";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__="3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f";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 _,cT as d,ap as u,ar as g,cU as m}from"./houseplan-card-wtxKVHkp.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-TMl_2iBT.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f";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__="b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c";try{await import("./houseplan-assets/houseplan-card-B5mDyjuC.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__="3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f";try{await import("./houseplan-assets/houseplan-card-wtxKVHkp.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)}
|
||||
|
||||
@@ -510,6 +510,13 @@ def create_export(
|
||||
badge["enabled"] = False
|
||||
badge["source"] = None
|
||||
dropped_marker_links += 1
|
||||
value_source = marker.get("value_source")
|
||||
ref = value_source.get("ref") if isinstance(value_source, dict) \
|
||||
and value_source.get("kind") == "derived_marker_state" else None
|
||||
if isinstance(ref, str) and ref.startswith("marker:") \
|
||||
and ref[len("marker:"):] not in selected_ids:
|
||||
marker.pop("value_source", None)
|
||||
dropped_marker_links += 1
|
||||
config = {
|
||||
"spaces": [_json_copy(space)],
|
||||
"markers": _json_copy(selected_markers),
|
||||
@@ -699,7 +706,7 @@ def _transfer_dropped_marker_links(document: dict[str, Any]) -> int:
|
||||
if not isinstance(transfer, dict):
|
||||
raise ImportFailure("invalid_format", "Transfer metadata must be an object")
|
||||
value = transfer.get("dropped_marker_links", 0)
|
||||
maximum = MAX_MARKERS * (MAX_CONTROLS + 1)
|
||||
maximum = MAX_MARKERS * (MAX_CONTROLS + 2)
|
||||
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= maximum:
|
||||
raise ImportFailure("invalid_format", "Invalid dropped marker link count")
|
||||
return value
|
||||
@@ -803,6 +810,16 @@ def _drop_invalid_import_marker_links(
|
||||
badge["enabled"] = False
|
||||
badge["source"] = None
|
||||
dropped += 1
|
||||
source = marker.get("value_source")
|
||||
if isinstance(source, dict) and source.get("kind") == "derived_marker_state":
|
||||
ref = source.get("ref")
|
||||
target_id = ref[len("marker:"):] \
|
||||
if isinstance(ref, str) and ref.startswith("marker:") else ""
|
||||
target = by_id.get(target_id)
|
||||
if not target_id or target is None or target.get("removed") is True \
|
||||
or target.get("is_light") is not True:
|
||||
marker.pop("value_source", None)
|
||||
dropped += 1
|
||||
return dropped
|
||||
|
||||
|
||||
@@ -1110,6 +1127,20 @@ def _repair_target_space_refs(
|
||||
preserve_related(
|
||||
marker_id, "marker.value_badge", old_target, resolve_marker,
|
||||
)
|
||||
source = marker.get("value_source")
|
||||
if isinstance(source, dict) and source.get("kind") == "derived_marker_state":
|
||||
ref = source.get("ref")
|
||||
old_target = ref[len("marker:"):] \
|
||||
if isinstance(ref, str) and ref.startswith("marker:") else None
|
||||
mapped = replace(
|
||||
marker_id, "marker.value_source", old_target, resolve_marker_link,
|
||||
)
|
||||
if mapped is not None:
|
||||
source["ref"] = "marker:" + mapped
|
||||
else:
|
||||
preserve_related(
|
||||
marker_id, "marker.value_source", old_target, resolve_marker,
|
||||
)
|
||||
|
||||
# Destination wins collisions. Rekey only proven dead plan-owned keys;
|
||||
# opaque HA owners remain literal.
|
||||
@@ -1345,13 +1376,17 @@ def build_space_merge(
|
||||
source = badge.get("source") if isinstance(badge, dict) else None
|
||||
if isinstance(source, dict) and source.get("kind") == "derived_marker_state":
|
||||
dropped_marker_links += 1
|
||||
value_source = marker.get("value_source")
|
||||
if isinstance(value_source, dict) \
|
||||
and value_source.get("kind") == "derived_marker_state":
|
||||
dropped_marker_links += 1
|
||||
marker["binding"] = "virtual"
|
||||
marker["display"] = "static_icon"
|
||||
for field in (
|
||||
"area", "controls", "tap_action", "tap_target", "tap_confirm",
|
||||
"vacuum", "is_light", "use_climate_temp", "glow_color",
|
||||
"glow_radius_cm", "light_entity", "toggle_entity", "hidden", "removed",
|
||||
"value_badge",
|
||||
"value_badge", "value_source",
|
||||
):
|
||||
marker.pop(field, None)
|
||||
output_markers.append(marker)
|
||||
@@ -1395,6 +1430,21 @@ def build_space_merge(
|
||||
badge["enabled"] = False
|
||||
badge["source"] = None
|
||||
dropped_marker_links += 1
|
||||
source = marker.get("value_source")
|
||||
if isinstance(source, dict) and source.get("kind") == "derived_marker_state":
|
||||
ref = source.get("ref")
|
||||
old_target = ref[len("marker:"):] \
|
||||
if isinstance(ref, str) and ref.startswith("marker:") else ""
|
||||
target = None if old_target in virtualized_targets else marker_map.get(old_target)
|
||||
if target:
|
||||
source["ref"] = "marker:" + target
|
||||
_report_remap(
|
||||
reference_report, "incoming", "marker.value_source",
|
||||
str(marker.get("id", "?")), old_target,
|
||||
)
|
||||
else:
|
||||
marker.pop("value_source", None)
|
||||
dropped_marker_links += 1
|
||||
|
||||
output_markers_by_id = {
|
||||
str(marker.get("id")): marker for marker in output_markers
|
||||
|
||||
@@ -805,7 +805,7 @@ def _matching_previous_marker(
|
||||
def validate_marker_value_badges(
|
||||
config: dict, previous: dict | None = None, *, validate_all: bool = False
|
||||
) -> None:
|
||||
"""Validate only new/changed badge data; dormant future/legacy data round-trips."""
|
||||
"""Validate changed badge/face sources; dormant future data round-trips."""
|
||||
markers = config.get("markers") or []
|
||||
by_id = {str(marker.get("id")): marker for marker in markers}
|
||||
old_markers = (previous or {}).get("markers") or []
|
||||
@@ -814,30 +814,14 @@ def validate_marker_value_badges(
|
||||
consumed_old_ids: set[str] = set()
|
||||
known_source_fields = {"kind", "entity_id", "attribute", "ref"}
|
||||
|
||||
for marker_id, marker in by_id.items():
|
||||
old_marker = _matching_previous_marker(
|
||||
marker, marker_id, old_by_id, old_markers, new_ids,
|
||||
consumed_old_ids, validate_all,
|
||||
)
|
||||
badge = marker.get("value_badge")
|
||||
old_badge = None if validate_all else (old_marker or {}).get("value_badge")
|
||||
if not validate_all and badge == old_badge:
|
||||
continue
|
||||
if badge is None:
|
||||
continue
|
||||
if not isinstance(badge, dict):
|
||||
raise MarkerControlError("invalid_value_badge", "Value badge must be an object")
|
||||
if not isinstance(badge.get("enabled"), bool):
|
||||
raise MarkerControlError("invalid_value_badge", "Value badge enabled must be boolean")
|
||||
if badge.get("position") not in VALUE_BADGE_POSITIONS:
|
||||
raise MarkerControlError("invalid_value_badge_position", "Invalid value badge position")
|
||||
source = badge.get("source")
|
||||
if badge["enabled"] and not isinstance(source, dict):
|
||||
raise MarkerControlError("value_badge_source_required", "Enabled value badge needs a source")
|
||||
if source is None:
|
||||
continue
|
||||
def validate_source(source: object, field: str) -> None:
|
||||
prefix = "value_badge" if field == "value_badge" else "value_source"
|
||||
source_error = "invalid_value_badge_source" if field == "value_badge" \
|
||||
else "invalid_value_source"
|
||||
attribute_error = "invalid_value_badge_attribute" if field == "value_badge" \
|
||||
else "invalid_value_source_attribute"
|
||||
if not isinstance(source, dict) or source.get("kind") not in VALUE_BADGE_SOURCE_KINDS:
|
||||
raise MarkerControlError("invalid_value_badge_source", "Invalid value badge source")
|
||||
raise MarkerControlError(source_error, f"Invalid {field} source")
|
||||
kind = source["kind"]
|
||||
allowed_fields = {
|
||||
"entity_state": {"kind", "entity_id"},
|
||||
@@ -846,23 +830,51 @@ def validate_marker_value_badges(
|
||||
"derived_marker_state": {"kind", "ref"},
|
||||
}[kind]
|
||||
if (known_source_fields & set(source)) - allowed_fields:
|
||||
raise MarkerControlError("invalid_value_badge_source", "Inconsistent value badge source")
|
||||
raise MarkerControlError(source_error, f"Inconsistent {field} source")
|
||||
if kind in {"entity_state", "entity_attribute"}:
|
||||
entity_id = source.get("entity_id")
|
||||
if not isinstance(entity_id, str) or not _CONTROL_ENTITY_ID_RE.fullmatch(entity_id):
|
||||
raise MarkerControlError("invalid_value_badge_source", "Invalid value badge entity id")
|
||||
if kind == "entity_attribute":
|
||||
if source.get("attribute") not in VALUE_BADGE_ATTRIBUTES:
|
||||
raise MarkerControlError("invalid_value_badge_attribute", "Invalid value badge attribute")
|
||||
raise MarkerControlError(source_error, f"Invalid {field} entity id")
|
||||
if kind == "entity_attribute" and source.get("attribute") not in VALUE_BADGE_ATTRIBUTES:
|
||||
raise MarkerControlError(attribute_error, f"Invalid {field} attribute")
|
||||
if kind == "derived_marker_state":
|
||||
ref = source.get("ref")
|
||||
if not isinstance(ref, str) or not ref.startswith(MARKER_CONTROL_PREFIX) or not ref[len(MARKER_CONTROL_PREFIX):]:
|
||||
raise MarkerControlError("invalid_value_badge_source", "Invalid marker value badge target")
|
||||
if not isinstance(ref, str) or not ref.startswith(MARKER_CONTROL_PREFIX) \
|
||||
or not ref[len(MARKER_CONTROL_PREFIX):]:
|
||||
raise MarkerControlError(source_error, f"Invalid {field} target")
|
||||
target = by_id.get(ref[len(MARKER_CONTROL_PREFIX):])
|
||||
if target is None or target.get("removed") is True:
|
||||
raise MarkerControlError("value_badge_marker_missing", "Marker value badge target does not exist")
|
||||
raise MarkerControlError(f"{prefix}_marker_missing", f"{field} target does not exist")
|
||||
if target.get("is_light") is not True:
|
||||
raise MarkerControlError("value_badge_marker_not_light", "Marker value badge target is not a forced light")
|
||||
raise MarkerControlError(f"{prefix}_marker_not_light", f"{field} target is not a forced light")
|
||||
|
||||
for marker_id, marker in by_id.items():
|
||||
old_marker = _matching_previous_marker(
|
||||
marker, marker_id, old_by_id, old_markers, new_ids,
|
||||
consumed_old_ids, validate_all,
|
||||
)
|
||||
badge = marker.get("value_badge")
|
||||
old_badge = None if validate_all else (old_marker or {}).get("value_badge")
|
||||
if validate_all or badge != old_badge:
|
||||
if badge is not None:
|
||||
if not isinstance(badge, dict):
|
||||
raise MarkerControlError("invalid_value_badge", "Value badge must be an object")
|
||||
if not isinstance(badge.get("enabled"), bool):
|
||||
raise MarkerControlError("invalid_value_badge", "Value badge enabled must be boolean")
|
||||
if badge.get("position") not in VALUE_BADGE_POSITIONS:
|
||||
raise MarkerControlError("invalid_value_badge_position", "Invalid value badge position")
|
||||
source = badge.get("source")
|
||||
if badge["enabled"] and not isinstance(source, dict):
|
||||
raise MarkerControlError(
|
||||
"value_badge_source_required", "Enabled value badge needs a source"
|
||||
)
|
||||
if source is not None:
|
||||
validate_source(source, "value_badge")
|
||||
|
||||
source = marker.get("value_source")
|
||||
old_source = None if validate_all else (old_marker or {}).get("value_source")
|
||||
if (validate_all or source != old_source) and source is not None:
|
||||
validate_source(source, "value_source")
|
||||
|
||||
|
||||
def validate_marker_light_entities(
|
||||
@@ -1743,6 +1755,12 @@ MARKER_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
),
|
||||
# Explicit source for display:value. Semantic delta validation shares
|
||||
# the badge source contract while keeping untouched future literals.
|
||||
vol.Optional("value_source"): vol.Any(
|
||||
None,
|
||||
vol.Schema({}, extra=vol.ALLOW_EXTRA),
|
||||
),
|
||||
# climate current_temperature: badge + room-average vote (off unless True)
|
||||
vol.Optional("use_climate_temp"): vol.Any(bool, None),
|
||||
vol.Optional("room_id"): vol.Any(str, None),
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// Issue #378: an explicit Value + state source is selected in the real dialog,
|
||||
// previewed, saved, reopened and projected by both renderers without changing
|
||||
// the device action. Missing live data must keep the selection and render a dash.
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 1100, height: 900 }, 1);
|
||||
const res = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const entityId = 'cover.value_face_awning';
|
||||
const deviceId = 'd_value_face_awning';
|
||||
const source = { kind: 'entity_attribute', entity_id: entityId, attribute: 'current_position' };
|
||||
const sourceKey = `attr:${entityId}:current_position`;
|
||||
const calls = [];
|
||||
const setState = async (state, position) => {
|
||||
c.hass = {
|
||||
...c.hass,
|
||||
devices: {
|
||||
...c.hass.devices,
|
||||
[deviceId]: {
|
||||
id: deviceId, name: 'Value face awning', model: 'Test cover', area_id: 'bedroom',
|
||||
identifiers: [['demo', deviceId]], entry_type: null, via_device_id: null,
|
||||
},
|
||||
},
|
||||
entities: {
|
||||
...c.hass.entities,
|
||||
[entityId]: { entity_id: entityId, device_id: deviceId, platform: 'demo' },
|
||||
},
|
||||
states: {
|
||||
...c.hass.states,
|
||||
[entityId]: {
|
||||
entity_id: entityId, state,
|
||||
attributes: {
|
||||
friendly_name: 'Value face awning', device_class: 'awning',
|
||||
supported_features: 15, current_position: position,
|
||||
},
|
||||
},
|
||||
},
|
||||
callService: async (domain, service, data) => {
|
||||
calls.push([domain, service, data]);
|
||||
return {};
|
||||
},
|
||||
};
|
||||
c._regSignature = '';
|
||||
c._maybeRebuildDevices();
|
||||
const item = c._devices.find((candidate) => candidate.bindingRef === deviceId);
|
||||
if (item) c._layout = { ...c._layout, [item.id]: { s: item.space, x: 0.12, y: 0.88 } };
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
};
|
||||
const item = () => c._devices.find((candidate) => candidate.bindingRef === deviceId);
|
||||
const faceValue = (root, selector) => root.querySelector(selector)
|
||||
?.querySelector('.valtext')?.textContent?.trim() || '';
|
||||
|
||||
await setState('open', 42);
|
||||
c._setMode('devices');
|
||||
c._openMarkerDialog(item());
|
||||
await c.updateComplete;
|
||||
c._markerDialog = {
|
||||
...c._markerDialog, display: 'value', tapAction: 'toggle', tapActionTouched: true,
|
||||
};
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
|
||||
const select = sr().querySelector('#marker-value-source');
|
||||
const candidateValues = [...(select?.options || [])].map((option) => option.value);
|
||||
select.value = sourceKey;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
await c.updateComplete;
|
||||
const preview = sr().querySelector('hp-device-preview');
|
||||
await preview?.updateComplete;
|
||||
const preview42 = faceValue(preview?.renderRoot, '.dev') === '42 %';
|
||||
const draftSourceExact = JSON.stringify(c._markerDialog?.valueSource) === JSON.stringify(source);
|
||||
|
||||
await c._saveMarker();
|
||||
await c.updateComplete;
|
||||
let saved = (c._serverCfg.markers || []).find((marker) => marker.binding === `device:${deviceId}`);
|
||||
const savedExact = JSON.stringify(saved?.value_source) === JSON.stringify(source);
|
||||
const actionSaved = saved?.tap_action === 'toggle';
|
||||
|
||||
c._openMarkerDialog(item());
|
||||
await c.updateComplete;
|
||||
const reopenedSelect = sr().querySelector('#marker-value-source');
|
||||
const reopenedExact = reopenedSelect?.value === sourceKey
|
||||
&& reopenedSelect?.selectedOptions?.[0]?.value === sourceKey;
|
||||
|
||||
// Cancel an unsaved switch to auto: the persisted source must survive.
|
||||
reopenedSelect.value = '';
|
||||
reopenedSelect.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
||||
await c.updateComplete;
|
||||
c._closeMarkerDialog();
|
||||
await c.updateComplete;
|
||||
saved = (c._serverCfg.markers || []).find((marker) => marker.binding === `device:${deviceId}`);
|
||||
const cancelKeptSource = JSON.stringify(saved?.value_source) === JSON.stringify(source);
|
||||
|
||||
c._setMode('view');
|
||||
c._regSignature = '';
|
||||
c._maybeRebuildDevices();
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
const plan42 = faceValue(sr(), `.dev[data-id="${item().id}"]`) === '42 %';
|
||||
calls.length = 0;
|
||||
c._clickDevice({ stopPropagation() {} }, item());
|
||||
await wait(20);
|
||||
const actionUnchanged = JSON.stringify(calls.at(-1)) === JSON.stringify([
|
||||
'cover', 'close_cover', { entity_id: entityId },
|
||||
]);
|
||||
|
||||
await customElements.whenDefined('houseplan-space-card');
|
||||
const staticCard = document.createElement('houseplan-space-card');
|
||||
staticCard.setConfig({ type: 'custom:houseplan-space-card', space: item().space });
|
||||
staticCard._snap = {
|
||||
config: structuredClone(c._serverCfg), rev: c._cfgRev,
|
||||
configFingerprint: `fixture-${c._cfgRev}`,
|
||||
layout: structuredClone(c._layout), layoutRev: c._layoutRev,
|
||||
layoutFingerprint: `fixture-${c._layoutRev}`,
|
||||
};
|
||||
staticCard._loadedOnce = true;
|
||||
staticCard.hass = c.hass;
|
||||
document.body.appendChild(staticCard);
|
||||
const started = Date.now();
|
||||
while (!staticCard.renderRoot?.querySelector('.hp-static-stage') && Date.now() - started < 6000) {
|
||||
await wait(60);
|
||||
}
|
||||
await staticCard.updateComplete;
|
||||
const static42 = faceValue(staticCard.renderRoot, `.dev[data-id="${item().id}"]`) === '42 %';
|
||||
|
||||
await setState('unavailable', 42);
|
||||
c._setMode('view');
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
await wait(120);
|
||||
await c.updateComplete;
|
||||
const unavailableDash = faceValue(sr(), `.dev[data-id="${item().id}"]`) === '—';
|
||||
const unavailableStillValue = sr().querySelector(`.dev[data-id="${item().id}"]`)?.classList
|
||||
.contains('valonly') === true;
|
||||
|
||||
await setState('open', 55);
|
||||
c._setMode('view');
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
const recovered55 = faceValue(sr(), `.dev[data-id="${item().id}"]`) === '55 %';
|
||||
|
||||
// A real binding choice resets the old source to automatic before Save.
|
||||
c._setMode('devices');
|
||||
c._openMarkerDialog(item());
|
||||
await c.updateComplete;
|
||||
const virtualBinding = [...sr().querySelectorAll('input[name="bmode"]')]
|
||||
.find((node) => node.parentElement?.textContent?.includes(c._t('marker.virtual_option')));
|
||||
virtualBinding?.click();
|
||||
await c.updateComplete;
|
||||
const bindingResetToAuto = !!virtualBinding && c._markerDialog?.binding === 'virtual'
|
||||
&& c._markerDialog?.valueSource === null
|
||||
&& c._markerDialog?.valueSourceTouched === true;
|
||||
c._closeMarkerDialog();
|
||||
|
||||
return {
|
||||
candidatePresent: candidateValues.includes(sourceKey),
|
||||
preview42, draftSourceExact, savedExact, actionSaved, reopenedExact,
|
||||
cancelKeptSource, plan42, static42, actionUnchanged,
|
||||
unavailableDash, unavailableStillValue, recovered55, bindingResetToAuto,
|
||||
};
|
||||
});
|
||||
|
||||
checkAll(res);
|
||||
await finish(browser, res);
|
||||
Vendored
+56
-56
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c",
|
||||
"fingerprint": "3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js",
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 277319,
|
||||
"initialViewGzipBytes": 277948,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/de-FazIOsg8.js",
|
||||
"houseplan-assets/editor-CcxmUFV-.js",
|
||||
"houseplan-assets/fr-CFpxbPTS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CisKVaG7.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-eqCB-Ax0.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/de-B775TcPH.js",
|
||||
"houseplan-assets/editor-CIkons5x.js",
|
||||
"houseplan-assets/fr-Cml_YfSz.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CM78ZHZU.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BP9ODa6H.js"
|
||||
],
|
||||
"lazyGzipBytes": 191754,
|
||||
"lazyGzipBytes": 192277,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/editor-CcxmUFV-.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CisKVaG7.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/editor-CIkons5x.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CM78ZHZU.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 139917,
|
||||
"lazyEditorGzipBytes": 140230,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-eqCB-Ax0.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BP9ODa6H.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 9451,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-FazIOsg8.js",
|
||||
"houseplan-assets/fr-CFpxbPTS.js"
|
||||
"houseplan-assets/de-B775TcPH.js",
|
||||
"houseplan-assets/fr-Cml_YfSz.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 45342,
|
||||
"lazyLocaleGzipBytes": 45551,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"sha256": "f7bdc0ee64684220a751eadda1397edb922b9c785993f5bce1d6b8273c619f21",
|
||||
"path": "houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"sha256": "2527114d8c2e58532848ac449cb7ea6927b86f5883bad84c649dc740c3872eea",
|
||||
"rawBytes": 6967,
|
||||
"gzipBytes": 2956,
|
||||
"gzipBytes": 2955,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-FazIOsg8.js",
|
||||
"sha256": "50ca05e9607b7f597cd20b1a1fce1f92fd9bb07a4cb0db4e86c3c921053c9e89",
|
||||
"rawBytes": 75090,
|
||||
"gzipBytes": 22886,
|
||||
"path": "houseplan-assets/de-B775TcPH.js",
|
||||
"sha256": "287e8f70aad3d82cfe292f204e5fe58eef3738631ee3be7078de6fd88c5fde1f",
|
||||
"rawBytes": 75572,
|
||||
"gzipBytes": 22996,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-CcxmUFV-.js",
|
||||
"sha256": "27077f8f6ecd61eea9e8405099b91974f423b8f7a53eef9ae8cf066803c475ef",
|
||||
"path": "houseplan-assets/editor-CIkons5x.js",
|
||||
"sha256": "ecaeb979d94de7e54f08b090355bf757b64a34932cddc9442f023804565ff56c",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1583,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-CFpxbPTS.js",
|
||||
"sha256": "ce2f711435309dac25e14c0024eea6da2c443949728a0fad1b5062851fcc26d6",
|
||||
"rawBytes": 77225,
|
||||
"gzipBytes": 22456,
|
||||
"path": "houseplan-assets/fr-Cml_YfSz.js",
|
||||
"sha256": "eeaa5abe3d199602537bab6f7f40075ceab398a25515685dd5e465d3ea9c9b51",
|
||||
"rawBytes": 77710,
|
||||
"gzipBytes": 22555,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-B5mDyjuC.js",
|
||||
"sha256": "d2f3c9df28d6acc99e4c7960c6f3cf1d5b516b42c3b10102429ba263532f569a",
|
||||
"rawBytes": 976343,
|
||||
"gzipBytes": 276523,
|
||||
"path": "houseplan-assets/houseplan-card-wtxKVHkp.js",
|
||||
"sha256": "796b979c90d4791cbdadb991c407dbd7d6768880b5a856cc1b7ec19f1e317c7a",
|
||||
"rawBytes": 979430,
|
||||
"gzipBytes": 277152,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-FazIOsg8.js",
|
||||
"houseplan-assets/editor-CcxmUFV-.js",
|
||||
"houseplan-assets/fr-CFpxbPTS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CisKVaG7.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-eqCB-Ax0.js"
|
||||
"houseplan-assets/de-B775TcPH.js",
|
||||
"houseplan-assets/editor-CIkons5x.js",
|
||||
"houseplan-assets/fr-Cml_YfSz.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CM78ZHZU.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BP9ODa6H.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CisKVaG7.js",
|
||||
"sha256": "d99951a97b78d46d05501d0e2e3f5a9cd73a1088a5e9a382d8899903b040bb97",
|
||||
"rawBytes": 524211,
|
||||
"gzipBytes": 135378,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CM78ZHZU.js",
|
||||
"sha256": "e439a389b8556a98b6ee0641cbda9a47978b2a10e4279d6663848ec97005771e",
|
||||
"rawBytes": 526486,
|
||||
"gzipBytes": 135692,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-eqCB-Ax0.js",
|
||||
"sha256": "aba9dab2a60c386b0638636d2ebcc683bd44a56fea5ce594fda9456ee171371d",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BP9ODa6H.js",
|
||||
"sha256": "d49d4e5b08de22682d8c548ffa90bd73175fc1d069eca75f98905b9a804cafb0",
|
||||
"rawBytes": 26195,
|
||||
"gzipBytes": 6495,
|
||||
"gzipBytes": 6496,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-DFFLwJ_d.js",
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/backdrop-pick-TMl_2iBT.js",
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "376b5951ff76686ebc299f63f2a21bf56f331e1027c52eabd487664fc86cd45e",
|
||||
"sha256": "785c3ec627b80c3cda8831faaa08f31c7021ad47b0be372d3326ce5456d7d087",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 796,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-B5mDyjuC.js"
|
||||
"houseplan-assets/houseplan-card-wtxKVHkp.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c";import{c as e,l as o,t,c_ as s,A as a,c$ as i,d0 as l,d1 as n,E as r,b as c}from"./houseplan-card-B5mDyjuC.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__="3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f";import{c as e,l as o,t,c$ as s,A as a,d0 as i,d1 as l,d2 as n,E as r,b as c}from"./houseplan-card-wtxKVHkp.js";class f 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,f={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")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${d(h.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
${d(h.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+43
-43
File diff suppressed because one or more lines are too long
+153
-126
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c";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 _,cS as d,ap as u,ar as g,cT as m}from"./houseplan-card-B5mDyjuC.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-DFFLwJ_d.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c";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__="3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f";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 _,cT as d,ap as u,ar as g,cU as m}from"./houseplan-card-wtxKVHkp.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-TMl_2iBT.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f";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" />
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b40916c4f480b150ad894b2e5bea3a62b54827233bab2fc96ed6866eedf04f7c";try{await import("./houseplan-assets/houseplan-card-B5mDyjuC.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__="3d6cccf5714d17cd4ec8f516229a0621f28ee00b65b7d84fb542ddc353bc7f5f";try{await import("./houseplan-assets/houseplan-card-wtxKVHkp.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)}
|
||||
|
||||
+12
-5
@@ -214,11 +214,13 @@ Built from the registries (`_buildDevices`), rules carried over 1-to-1 from the
|
||||
|
||||
## Live data
|
||||
|
||||
- Value satellite: `src/device-value-badge.ts` resolves one explicit
|
||||
`marker.value_badge` source/position, or projects the legacy automatic
|
||||
temperature/humidity label when that field is absent. The resolver owns HA
|
||||
formatting, units, unavailable state, candidate discovery and deterministic
|
||||
recommendation; renderers consume only `ResolvedDevicePresentation.valueBadge`.
|
||||
- Value sources: `src/device-value-badge.ts` owns candidate discovery, source
|
||||
keys, HA formatting, units and unavailable handling for both an explicit
|
||||
`marker.value_source` inside the **Value + state** face and an explicit
|
||||
`marker.value_badge` satellite. Absence of `value_source` keeps the legacy
|
||||
automatic face resolver; absence of `value_badge` projects the legacy
|
||||
automatic temperature/humidity satellite. Renderers consume only the
|
||||
corresponding fields of `ResolvedDevicePresentation`.
|
||||
- LQI (zigbee): the average over `*_linkquality` entities → label under the icon; color via
|
||||
`lqiColor()`: ≤40 red → ≥180 green (hsl gradient). The room average is shown in the room tooltip.
|
||||
The same tooltip includes the formatted clean-floor area (inner contour for
|
||||
@@ -303,6 +305,11 @@ Explicit settings override the global legacy temperature gate; explicit off
|
||||
suppresses legacy output. Bottom badges stack above system LQI, and a derived
|
||||
LQI badge de-duplicates that system row. `hp-device-preview` fits and centres
|
||||
the complete face bounding box rather than allowing satellites to clip.
|
||||
An optional `marker.value_source` selects the same source kinds for the inner
|
||||
face when `display: value`. Missing explicit data renders a dash without
|
||||
falling back to another source or the icon; absence/`null` preserves the old
|
||||
automatic face selection. Derived marker references share the same rewrite and
|
||||
space-transfer seam as controls and value badges.
|
||||
`normalizeDeviceDisplay()` is the mandatory compatibility gate for every consumer and maps
|
||||
legacy `ripple` to `icon_ripple`. `markerLqiBand()` remains marker-only semantic
|
||||
metadata for accessibility, while `markerLqiColor()` delegates to the shared
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **Value + state** can now show the exact reading the user needs instead of
|
||||
relying only on the automatically chosen entity state. The Device editor
|
||||
offers the same states, useful attributes, LQI and linked-light values as
|
||||
the adjacent value badge; for example, a cover can show `42 %` position
|
||||
instead of `Open`. Existing markers keep their previous automatic behaviour,
|
||||
and tapping the value still runs the same action
|
||||
([#378](https://github.com/Matysh/houseplan-card/issues/378)).
|
||||
- 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
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- В режиме **«Значение + состояние»** теперь можно выбрать нужный показатель,
|
||||
а не полагаться только на автоматически выбранное состояние сущности.
|
||||
Редактор устройства предлагает те же состояния, полезные атрибуты, LQI и
|
||||
значения связанных источников света, что внешний бейдж: например, положение
|
||||
cover отображается как `42 %` вместо `Открыто`. Старые маркеры сохраняют
|
||||
прежнее автоматическое поведение, а нажатие по значению выполняет то же
|
||||
действие ([#378](https://github.com/Matysh/houseplan-card/issues/378)).
|
||||
- Glow в `houseplan-space-card` больше не пересобирает граф света и сцену стен
|
||||
с нуля на каждый тик Home Assistant: статический путь получил кэш-механику
|
||||
полной карты (кэш графа света, LRU-пул барьерных сцен — движущаяся дверь
|
||||
|
||||
@@ -419,6 +419,17 @@ space transfer remaps an internal target and disables/counts a link whose
|
||||
target is outside the transfer. Older clients ignore the field and may erase
|
||||
it if they reconstruct the same marker after a downgrade.
|
||||
|
||||
`marker.value_source` is an optional explicit source for the inner face of a
|
||||
`display: value` marker. Absence or `null` preserves the historical automatic
|
||||
entity-state choice; an object uses exactly the same discriminated source
|
||||
contract and formatter as `marker.value_badge.source`. A missing explicit
|
||||
source stays selected and renders `—` rather than silently falling back. The
|
||||
top-level schema remains lossless: unchanged future literals round-trip, while
|
||||
new or changed values receive strict delta validation. Marker-id rewrites and
|
||||
full/space transfer preserve, remap or report/drop `derived_marker_state.ref`
|
||||
through the same reference seam as controls and value badges. Older clients
|
||||
ignore the field and may erase it if they reconstruct the marker.
|
||||
|
||||
## Persistent manual virtual-light state
|
||||
|
||||
The exact `virtual` + `is_light:true` + `tap_action:toggle` combination has a
|
||||
|
||||
@@ -64,7 +64,7 @@ mutation evidence в одном pull request.
|
||||
| F07 | `live_states:false`, не alarm | `face.live_states_disabled` | neutral/base icon; ordinary activity off | normal | `device-presentation-policy-live-gate`; `presentation-row-contract` |
|
||||
| F08 | `static_icon` | `face.static` | neutral/base icon; state/RGB/value/metrics/pulse/vacuum off | normal | `device-presentation-policy-static`; `presentation-row-contract` |
|
||||
| F09 | value + один scalar source | `content.value` | HA-formatted full Text face | normal | `device-presentation-policy-value`; `device-long-value-ellipsis-restored` |
|
||||
| F10 | value + missing/unavailable/non-scalar | `content.value_fallback_icon` + `content.value_no_state/non_scalar` | icon fallback и точная причина | normal | `device-presentation-policy-value`; `presentation-row-contract` |
|
||||
| F10 | value auto + missing/unavailable/non-scalar | `content.value_fallback_icon` + `content.value_no_state/non_scalar` | icon fallback и точная причина | normal | `device-presentation-policy-value`; `presentation-row-contract` |
|
||||
| F11 | value + несколько равноправных sources | `content.value_ambiguous_sources` | icon fallback `value_ambiguous_sources` | normal | `device-presentation-policy-value`; `presentation-row-contract` |
|
||||
| F12 | value + virtual marker | `content.value_virtual` | icon fallback `value_virtual` | normal | `device-presentation-policy-value`; `presentation-row-contract` |
|
||||
| F13 | dynamic icon + известный morph | `diagnostics.dynamic_icon` | state icon; действующее cover override сохранено | normal | `device-presentation-policy-diagnostics`; `presentation-row-contract` |
|
||||
@@ -72,6 +72,7 @@ mutation evidence в одном pull request.
|
||||
| F15 | legacy automatic metric | `diagnostics.metrics_enabled` | прежняя temperature/humidity эвристика без записи config | normal | `device-presentation-policy-diagnostics`; `presentation-row-contract` |
|
||||
| F16 | LQI 0/40, 41/179, 180+ | `diagnostics.lqi_low/mid/high` | low/mid/high и continuous canonical colour | normal | `device-marker-lqi-low-boundary-shifted`; `presentation-row-contract` |
|
||||
| F17 | vacuum dynamic/static | `diagnostics.vacuum_live/vacuum_static` | live overlay только у видимого dynamic face | normal | `device-presentation-policy-diagnostics`; `presentation-row-contract` |
|
||||
| F18 | value explicit + missing/unavailable/non-scalar | `content.value` + `content.value_no_state/non_scalar` | выбранный source сохраняется, Text face показывает `—` без auto/icon fallback | normal | `device-presentation-policy-value`; `presentation-row-contract` |
|
||||
|
||||
## Activity и pulse
|
||||
|
||||
|
||||
@@ -652,6 +652,12 @@ alarm remains clear.
|
||||
The four display choices are icon + state; icon + state + activity; value +
|
||||
state; and always-static icon. A separate value badge can show an entity state,
|
||||
useful attribute, average LQI or linked light state on any side of the marker.
|
||||
For **value + state**, the Device editor also offers **Value source**. Keep
|
||||
**Automatic (as before)** for the legacy choice, or select one of those same
|
||||
readings—for example, cover position—to replace the icon with `42 %` rather
|
||||
than `Open`. A temporarily unavailable saved source stays selected and shows
|
||||
`—` until it recovers; it is not silently replaced. Changing this source never
|
||||
changes what a click or tap does.
|
||||
Text and adjacent values are sections of the same shell. They shrink to a
|
||||
readable floor and then expand the shell; they are never ellipsized.
|
||||
The complete visible value capsule is one hover and action target: clicking or
|
||||
|
||||
@@ -1050,12 +1050,17 @@ Hover включается только после события от наст
|
||||
|---|---:|---:|---:|---:|
|
||||
| Значок + состояние | Да | Да | Только тревога | Отдельные компактные °/% и LQI возможны |
|
||||
| Значок + состояние и активность | Да | Да | Да | Отдельные компактные °/% и LQI возможны |
|
||||
| Значение + состояние | Если значение получить нельзя или источник неоднозначен | Да | Только тревога | Температура/влажность либо числовое или текстовое состояние, отформатированное HA |
|
||||
| Значение + состояние | При автоматическом выборе — если значение получить нельзя или источник неоднозначен; при явном — только для virtual marker | Да | Только тревога | Автоматическое либо явно выбранное состояние, атрибут, LQI или состояние связанного источника света |
|
||||
| Всегда статичный значок | Да | Нейтральная в цветах текущей темы | Нет | Нет |
|
||||
|
||||
В режиме «Значение вместо иконки» поддерживаются и числа, и локализованные HA
|
||||
текстовые состояния. Если источник отсутствует, недоступен или неоднозначен,
|
||||
карточка возвращается к иконке; предпросмотр объясняет причину. Длинный текст
|
||||
текстовые состояния. Поле **«Источник значения»** позволяет оставить
|
||||
**«Автоматически (как раньше)»** либо выбрать тот же показатель, который
|
||||
доступен внешнему бейджу: например, положение cover даст `42 %` вместо
|
||||
`Открыто`. В автоматическом режиме отсутствующий, недоступный или неоднозначный
|
||||
источник по-прежнему возвращает иконку. Явно выбранный временно недоступный
|
||||
источник остаётся выбранным и показывает `—` до восстановления, не подменяясь
|
||||
другим. Выбор не меняет действие по клику или тапу. Длинный текст
|
||||
уменьшается до читаемого минимума, затем общий shell расширяется: многоточие и
|
||||
скрытие хвоста не используются. LQI остаётся отдельной строкой под shell.
|
||||
|
||||
|
||||
@@ -178,6 +178,17 @@ export function checkReferences({ config, layout = {} } = {}, { notes = [] } = {
|
||||
: 'активного маркера-источника не существует');
|
||||
}
|
||||
}
|
||||
const valueSource = marker?.value_source;
|
||||
if (valueSource?.kind === 'derived_marker_state') {
|
||||
const ref = String(valueSource.ref ?? '');
|
||||
const target = ref.startsWith('marker:') ? ref.slice('marker:'.length) : '';
|
||||
if (!target || !activeLightMarkerIds.has(target)) {
|
||||
add('marker_value_source', markerId, ref || '?',
|
||||
target && activeMarkerIds.has(target)
|
||||
? 'маркер-цель не является источником света'
|
||||
: 'активного маркера-цели не существует');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, position] of Object.entries(layout || {})) {
|
||||
|
||||
@@ -27,6 +27,17 @@
|
||||
|
||||
/** @type {SmokeLink[]} */
|
||||
export const SMOKE_LINKS = [
|
||||
{
|
||||
symbols: [
|
||||
'resolveValueSource', 'valueSourceWriteFields', 'valueBadgeCandidates',
|
||||
'valueBadgeSourceKey', 'resolveDevicePresentation',
|
||||
],
|
||||
smokes: ['smoke_value_face_source.mjs'],
|
||||
because: 'the #378 production-bundle scenario observes the shared source resolver through '
|
||||
+ 'the real Device dialog, draft preview, persistence, both plan renderers, unavailable '
|
||||
+ 'recovery, action dispatch and binding reset; those DOM effects do not call the pure '
|
||||
+ 'source helpers by name',
|
||||
},
|
||||
{
|
||||
symbols: ['roomClimateKey', 'roomClimateMap'],
|
||||
smokes: ['smoke_room_climate_placement.mjs'],
|
||||
|
||||
+84
-21
@@ -22,7 +22,10 @@ import {
|
||||
import { resolveToggleIntent, toggleCoverEntity } from './device-toggle';
|
||||
import type { DevItem } from './types';
|
||||
import { safeStoredColor } from './color';
|
||||
import { resolveDeviceValueBadge, type ResolvedValueBadge } from './device-value-badge';
|
||||
import {
|
||||
resolveDeviceValueBadge, resolveValueSource, valueBadgeSourceKey,
|
||||
type ResolvedValueBadge,
|
||||
} from './device-value-badge';
|
||||
import {
|
||||
DEVICE_PULSE_DEFAULT_SCALE, resolveDevicePulse, type ResolvedDevicePulse,
|
||||
} from './device-pulse';
|
||||
@@ -80,10 +83,11 @@ export interface ResolvedPresentationSource {
|
||||
}
|
||||
|
||||
export interface ResolvedValueSource {
|
||||
kind: 'temperature' | 'humidity' | 'entity';
|
||||
kind: 'temperature' | 'humidity' | 'entity' | 'derived_lqi' | 'derived_marker_state';
|
||||
eid: string;
|
||||
attribute?: string;
|
||||
text: string;
|
||||
sourceKey: string;
|
||||
}
|
||||
|
||||
export interface PresentationExplanation {
|
||||
@@ -470,24 +474,72 @@ function resolveValue(
|
||||
d: DevItem,
|
||||
sources: ResolvedPresentationSources,
|
||||
showTemperature: boolean,
|
||||
): { source: ResolvedValueSource | null; text: string | null; fallback: ValueFallbackReason | null } {
|
||||
if (d.virtual) return { source: null, text: null, fallback: 'value_virtual' };
|
||||
markerStates: readonly { ref: string; on: boolean; name: string }[] = [],
|
||||
): {
|
||||
source: ResolvedValueSource | null;
|
||||
text: string | null;
|
||||
fullText: string | null;
|
||||
fallback: ValueFallbackReason | null;
|
||||
explicit: boolean;
|
||||
} {
|
||||
if (d.virtual) {
|
||||
return { source: null, text: null, fullText: null, fallback: 'value_virtual', explicit: false };
|
||||
}
|
||||
const explicitSource = d.marker?.value_source;
|
||||
if (explicitSource) {
|
||||
const resolved = resolveValueSource(hass, d, explicitSource, markerStates);
|
||||
const kind: ResolvedValueSource['kind'] = explicitSource.kind === 'derived_lqi'
|
||||
? 'derived_lqi'
|
||||
: explicitSource.kind === 'derived_marker_state'
|
||||
? 'derived_marker_state'
|
||||
: explicitSource.kind === 'entity_attribute' && explicitSource.attribute.includes('temperature')
|
||||
? 'temperature'
|
||||
: explicitSource.kind === 'entity_attribute' && explicitSource.attribute.includes('humidity')
|
||||
? 'humidity' : 'entity';
|
||||
const eid = explicitSource.kind === 'entity_state' || explicitSource.kind === 'entity_attribute'
|
||||
? explicitSource.entity_id
|
||||
: explicitSource.kind === 'derived_marker_state' ? explicitSource.ref : 'derived:lqi';
|
||||
const attribute = explicitSource.kind === 'entity_attribute'
|
||||
? explicitSource.attribute : undefined;
|
||||
return {
|
||||
source: {
|
||||
kind, eid, attribute, text: resolved.text,
|
||||
sourceKey: valueBadgeSourceKey(explicitSource),
|
||||
},
|
||||
text: resolved.text,
|
||||
fullText: resolved.fullText,
|
||||
fallback: resolved.failure === 'non_scalar' ? 'value_non_scalar'
|
||||
: resolved.failure === 'missing' ? 'value_no_state' : null,
|
||||
explicit: true,
|
||||
};
|
||||
}
|
||||
if (showTemperature) {
|
||||
const climate = firstClimateTemperature(hass, d);
|
||||
if (climate) {
|
||||
return {
|
||||
source: { kind: 'temperature', eid: climate.eid, attribute: 'current_temperature', text: climate.text },
|
||||
source: {
|
||||
kind: 'temperature', eid: climate.eid, attribute: 'current_temperature', text: climate.text,
|
||||
sourceKey: `attr:${climate.eid}:current_temperature`,
|
||||
},
|
||||
text: climate.text,
|
||||
fullText: climate.text,
|
||||
fallback: null,
|
||||
explicit: false,
|
||||
};
|
||||
}
|
||||
const temp = firstTemperature(hass, d);
|
||||
if (temp) {
|
||||
return { source: { kind: 'temperature', eid: temp.eid, text: temp.text }, text: temp.text, fallback: null };
|
||||
return {
|
||||
source: { kind: 'temperature', eid: temp.eid, text: temp.text, sourceKey: `state:${temp.eid}` },
|
||||
text: temp.text, fullText: temp.text, fallback: null, explicit: false,
|
||||
};
|
||||
}
|
||||
const hum = firstHumidity(hass, d);
|
||||
if (hum) {
|
||||
return { source: { kind: 'humidity', eid: hum.eid, text: hum.text }, text: hum.text, fallback: null };
|
||||
return {
|
||||
source: { kind: 'humidity', eid: hum.eid, text: hum.text, sourceKey: `state:${hum.eid}` },
|
||||
text: hum.text, fullText: hum.text, fallback: null, explicit: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Passive marker sources deliberately use a `marker:*` identity. They are
|
||||
@@ -510,13 +562,20 @@ function resolveValue(
|
||||
return {
|
||||
source: null,
|
||||
text: null,
|
||||
fullText: null,
|
||||
fallback: valueEids.length ? 'value_ambiguous_sources' : 'value_no_state',
|
||||
explicit: false,
|
||||
};
|
||||
}
|
||||
const eid = valueEids[0];
|
||||
const value = validStateValue(hass, eid);
|
||||
if (value.fallback) return { source: null, text: null, fallback: value.fallback };
|
||||
return { source: { kind: 'entity', eid, text: value.text }, text: value.text, fallback: null };
|
||||
if (value.fallback) {
|
||||
return { source: null, text: null, fullText: null, fallback: value.fallback, explicit: false };
|
||||
}
|
||||
return {
|
||||
source: { kind: 'entity', eid, text: value.text, sourceKey: `state:${eid}` },
|
||||
text: value.text, fullText: value.text, fallback: null, explicit: false,
|
||||
};
|
||||
}
|
||||
|
||||
function signatureOf(
|
||||
@@ -530,7 +589,8 @@ function signatureOf(
|
||||
...sources.visualSources.map((source) => `${source.role}:${source.eid}`),
|
||||
...sources.criticalSources.map((source) => `critical:${source.eid}`),
|
||||
].sort();
|
||||
const value = valueSource ? `${valueSource.kind}:${valueSource.eid}:${valueSource.attribute || ''}` : 'none';
|
||||
const value = valueSource
|
||||
? `${valueSource.kind}:${valueSource.sourceKey}:${valueSource.eid}:${valueSource.attribute || ''}` : 'none';
|
||||
return [binding, sources.sourceKind, ...rows, `value:${value}`].join('\n');
|
||||
}
|
||||
|
||||
@@ -595,6 +655,13 @@ export function resolveDevicePresentation(
|
||||
hass, d, options.lightDevices || [d], options.lightSources,
|
||||
options.registryHass || hass,
|
||||
);
|
||||
const needsMarkerState = d.marker?.value_source?.kind === 'derived_marker_state'
|
||||
|| d.marker?.value_badge?.source?.kind === 'derived_marker_state';
|
||||
const markerStateGraph = options.lightSources
|
||||
|| (needsMarkerState ? resolvedLightSources(hass, options.lightDevices || [d]) : []);
|
||||
const markerStates = markerStateGraph
|
||||
.filter((source) => source.key.startsWith('marker:'))
|
||||
.map((source) => ({ ref: source.key, on: source.on, name: source.device.name }));
|
||||
const status = d.bindingStatus;
|
||||
const haDisabled = status?.kind === 'ha_disabled';
|
||||
const orphaned = status?.kind === 'orphaned';
|
||||
@@ -623,8 +690,8 @@ export function resolveDevicePresentation(
|
||||
const controllerFace = sources.sourceKind === 'controls'
|
||||
|| (configuredController && sources.sourceKind !== 'light' && sources.sourceKind !== 'cover');
|
||||
const value = staticIcon && options.sourceDetails === false
|
||||
? { source: null, text: null, fallback: null }
|
||||
: resolveValue(hass, d, sources, options.showTemperature);
|
||||
? { source: null, text: null, fullText: null, fallback: null, explicit: false }
|
||||
: resolveValue(hass, d, sources, options.showTemperature, markerStates);
|
||||
const sourceSignature = signatureOf(d, sources, value.source);
|
||||
const activitySignature = activitySourceSignature(hass, d, sources);
|
||||
const rt = options.activityRuntime;
|
||||
@@ -664,17 +731,12 @@ export function resolveDevicePresentation(
|
||||
&& !!d.primary && isHumEntity(hass, d.primary)
|
||||
? humFor(hass, d.entities) : null;
|
||||
const lqi = policy.metrics && options.showSignal && !d.virtual ? lqiFor(hass, d.entities) : null;
|
||||
const markerStateGraph = options.lightSources || (d.marker?.value_badge?.source?.kind === 'derived_marker_state'
|
||||
? resolvedLightSources(hass, options.lightDevices || [d])
|
||||
: []);
|
||||
const valueBadge = resolveDeviceValueBadge(hass, d, {
|
||||
showTemperature: options.showTemperature,
|
||||
showSignal: options.showSignal,
|
||||
display,
|
||||
effectiveHidden,
|
||||
markerStates: markerStateGraph
|
||||
.filter((source) => source.key.startsWith('marker:'))
|
||||
.map((source) => ({ ref: source.key, on: source.on, name: source.device.name })),
|
||||
markerStates,
|
||||
});
|
||||
const realVisualSource = sources.visualSources.find((source) => !source.eid.startsWith('marker:'));
|
||||
const actEid = sources.sourceKind === 'cover'
|
||||
@@ -757,8 +819,8 @@ export function resolveDevicePresentation(
|
||||
display,
|
||||
icon,
|
||||
valueText,
|
||||
valueFullText: valueText,
|
||||
fallbackReason: display === 'value' && !valueText ? value.fallback : null,
|
||||
valueFullText: policy.face === 'value' ? value.fullText : null,
|
||||
fallbackReason: display === 'value' ? value.fallback : null,
|
||||
activity,
|
||||
activityGeneration: rt?.gen || 1,
|
||||
pulse,
|
||||
@@ -766,7 +828,8 @@ export function resolveDevicePresentation(
|
||||
tempText: temp == null ? null : String(temp),
|
||||
humText: hum == null ? null : String(hum),
|
||||
valueBadge,
|
||||
lqiText: lqi == null || valueBadge?.isLqi ? null : String(lqi),
|
||||
lqiText: lqi == null || valueBadge?.isLqi || value.source?.kind === 'derived_lqi'
|
||||
? null : String(lqi),
|
||||
lqiColor: lqi == null ? null : markerLqiColor(lqi),
|
||||
lqiBand: lqi == null ? null : markerLqiBand(lqi),
|
||||
lightColor,
|
||||
|
||||
+71
-13
@@ -31,6 +31,8 @@ export interface ResolvedValueBadge {
|
||||
availability: 'available' | 'unavailable' | 'missing';
|
||||
isLqi: boolean;
|
||||
tone: 'temperature' | 'humidity' | 'lqi' | 'default';
|
||||
/** Why the configured source could not produce a scalar value. */
|
||||
failure: 'missing' | 'non_scalar' | null;
|
||||
}
|
||||
|
||||
export interface ValueBadgeCandidate {
|
||||
@@ -90,7 +92,8 @@ export function valueBadgeSourceKey(source: ValueBadgeSource | null | undefined)
|
||||
if (source.kind === 'entity_state') return `state:${source.entity_id}`;
|
||||
if (source.kind === 'entity_attribute') return `attr:${source.entity_id}:${source.attribute}`;
|
||||
if (source.kind === 'derived_marker_state') return `marker:${source.ref}`;
|
||||
return 'derived:lqi';
|
||||
if (source.kind === 'derived_lqi') return 'derived:lqi';
|
||||
return '';
|
||||
}
|
||||
|
||||
export function valueBadgeSourceFromKey(key: string): ValueBadgeSource | null {
|
||||
@@ -153,42 +156,81 @@ function sourceTone(source: ValueBadgeSource): ResolvedValueBadge['tone'] {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function resolveSource(
|
||||
export function resolveValueSource(
|
||||
hass: any, d: DevItem, source: ValueBadgeSource,
|
||||
markerStates: readonly { ref: string; on: boolean; name: string }[] = [],
|
||||
): Omit<ResolvedValueBadge, 'configured' | 'enabled' | 'position'> {
|
||||
let text: string | null = null;
|
||||
let label = '';
|
||||
let availability: ResolvedValueBadge['availability'] = 'available';
|
||||
let failure: ResolvedValueBadge['failure'] = null;
|
||||
if (source.kind === 'entity_state') {
|
||||
label = entityName(hass, source.entity_id);
|
||||
const st = hass?.states?.[source.entity_id];
|
||||
if (!st) availability = 'missing';
|
||||
else if (!sourceRegistryEnabled(hass, source.entity_id)) availability = 'unavailable';
|
||||
else if (['unknown', 'unavailable'].includes(String(st.state).toLowerCase())) availability = 'unavailable';
|
||||
if (!st) {
|
||||
availability = 'missing';
|
||||
failure = 'missing';
|
||||
} else if (!sourceRegistryEnabled(hass, source.entity_id)) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
} else if (!['string', 'number', 'boolean'].includes(typeof st.state)) {
|
||||
availability = 'unavailable';
|
||||
failure = 'non_scalar';
|
||||
} else if (['unknown', 'unavailable'].includes(String(st.state).toLowerCase())) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
}
|
||||
else {
|
||||
const value = hassValue(hass, source.entity_id);
|
||||
text = value ? valueWithUnit(value, String(st.attributes?.unit_of_measurement || '')) : null;
|
||||
if (!text) availability = 'unavailable';
|
||||
if (!text) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
}
|
||||
}
|
||||
} else if (source.kind === 'entity_attribute') {
|
||||
label = `${attrLabel(source.attribute)} · ${entityName(hass, source.entity_id)}`;
|
||||
const st = hass?.states?.[source.entity_id];
|
||||
if (!st) availability = 'missing';
|
||||
else if (!sourceRegistryEnabled(hass, source.entity_id)) availability = 'unavailable';
|
||||
const hasAttribute = !!st && source.attribute in (st.attributes || {});
|
||||
const raw = hasAttribute ? st.attributes?.[source.attribute] : undefined;
|
||||
if (!st) {
|
||||
availability = 'missing';
|
||||
failure = 'missing';
|
||||
} else if (!sourceRegistryEnabled(hass, source.entity_id)) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
} else if (['unknown', 'unavailable'].includes(String(st.state).toLowerCase())) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
} else if (!hasAttribute) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
} else if (!['string', 'number', 'boolean'].includes(typeof raw)) {
|
||||
availability = 'unavailable';
|
||||
failure = 'non_scalar';
|
||||
}
|
||||
else {
|
||||
text = formatAttribute(hass, source.entity_id, source.attribute);
|
||||
if (text == null) availability = 'unavailable';
|
||||
if (text == null) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
}
|
||||
}
|
||||
} else if (source.kind === 'derived_lqi') {
|
||||
label = 'LQI';
|
||||
const lqi = lqiFor(hass, d.entities);
|
||||
if (lqi == null) availability = 'unavailable';
|
||||
if (lqi == null) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
}
|
||||
else text = String(lqi);
|
||||
} else {
|
||||
label = 'Light state';
|
||||
const match = markerStates.find((item) => item.ref === source.ref);
|
||||
if (!match) availability = 'missing';
|
||||
if (!match) {
|
||||
availability = 'missing';
|
||||
failure = 'missing';
|
||||
}
|
||||
else {
|
||||
const state = match.on ? 'on' : 'off';
|
||||
const loc = hass?.localize?.(`state.default.${state}`);
|
||||
@@ -205,6 +247,7 @@ function resolveSource(
|
||||
availability,
|
||||
isLqi: source.kind === 'derived_lqi',
|
||||
tone: sourceTone(source),
|
||||
failure,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,10 +295,11 @@ export function resolveDeviceValueBadge(
|
||||
availability: 'missing',
|
||||
isLqi: false,
|
||||
tone: 'default',
|
||||
failure: 'missing',
|
||||
};
|
||||
return {
|
||||
configured: true, enabled: true, position: stored.position || 'right',
|
||||
...resolveSource(hass, d, stored.source, options.markerStates),
|
||||
...resolveValueSource(hass, d, stored.source, options.markerStates),
|
||||
};
|
||||
}
|
||||
if (!options.showTemperature || options.display === 'value') return null;
|
||||
@@ -283,6 +327,7 @@ export function resolveDeviceValueBadge(
|
||||
? `${attrLabel(source.attribute)} · ${entityName(hass, source.entity_id)}`
|
||||
: source.kind === 'entity_state' ? entityName(hass, source.entity_id) : '',
|
||||
text: legacyText, fullText: legacyText, availability: 'available', isLqi: false, tone,
|
||||
failure: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -317,7 +362,7 @@ export function valueBadgeCandidates(
|
||||
const add = (source: ValueBadgeSource): void => {
|
||||
const key = valueBadgeSourceKey(source);
|
||||
if (candidates.some((item) => item.key === key)) return;
|
||||
const resolved = resolveSource(hass, d, source, markerStates);
|
||||
const resolved = resolveValueSource(hass, d, source, markerStates);
|
||||
const technical = source.kind === 'entity_attribute'
|
||||
? `${source.entity_id} · ${source.attribute}`
|
||||
: source.kind === 'entity_state' ? source.entity_id
|
||||
@@ -395,3 +440,16 @@ export function valueBadgeWriteFields(options: {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Pure persistence gate for the value face; auto is represented by absence. */
|
||||
export function valueSourceWriteFields(options: {
|
||||
touched: boolean;
|
||||
originalHas: boolean;
|
||||
original: ValueBadgeSource | null | undefined;
|
||||
source: ValueBadgeSource | null;
|
||||
}): { value_source?: ValueBadgeSource | null } {
|
||||
if (!options.touched) {
|
||||
return options.originalHas ? { value_source: options.original } : {};
|
||||
}
|
||||
return options.source ? { value_source: options.source } : {};
|
||||
}
|
||||
|
||||
+6
-1
@@ -883,8 +883,13 @@ export function rewriteMarkerControlReferences(
|
||||
source: { ...marker.value_badge.source, ref: to as `marker:${string}` },
|
||||
}
|
||||
: marker.value_badge;
|
||||
const valueSource = marker.value_source?.kind === 'derived_marker_state'
|
||||
&& marker.value_source.ref === from
|
||||
? { ...marker.value_source, ref: to as `marker:${string}` }
|
||||
: marker.value_source;
|
||||
return controls === marker.controls && valueBadge === marker.value_badge
|
||||
? marker : { ...marker, controls, value_badge: valueBadge };
|
||||
&& valueSource === marker.value_source
|
||||
? marker : { ...marker, controls, value_badge: valueBadge, value_source: valueSource };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2107,6 +2107,10 @@ export class HouseplanCard extends LitElement {
|
||||
valueBadgeTouched: boolean;
|
||||
originalHasValueBadge: boolean;
|
||||
originalValueBadge: MarkerValueBadge | null | undefined;
|
||||
valueSource: ValueBadgeSource | null;
|
||||
valueSourceTouched: boolean;
|
||||
originalHasValueSource: boolean;
|
||||
originalValueSource: ValueBadgeSource | null | undefined;
|
||||
useClimateTemp: boolean; // badge + room-average vote from climate current_temperature
|
||||
model: string;
|
||||
link: string;
|
||||
|
||||
@@ -267,7 +267,8 @@ import {
|
||||
} from './activity-runtime';
|
||||
import {
|
||||
recommendedValueBadgeSource, valueBadgeCandidates, valueBadgeSourceFromKey,
|
||||
valueBadgeSourceKey, valueBadgeTitle, valueBadgeWriteFields, type ValueBadgeCandidate,
|
||||
valueBadgeSourceKey, valueBadgeTitle, valueBadgeWriteFields, valueSourceWriteFields,
|
||||
type ValueBadgeCandidate,
|
||||
} from './device-value-badge';
|
||||
import {
|
||||
createRenderDeviceSnapshot, presentationSnapshotKey, renderDeviceSnapshotPositions,
|
||||
@@ -958,7 +959,7 @@ export interface HouseplanEditorHostPort {
|
||||
_layout: Record<string, { x: number; y: number; s?: string; k?: number; }>;
|
||||
_layoutRev: number;
|
||||
_logicalViewCenter: (projection: "flat" | "iso") => { x: number; y: number; } | null;
|
||||
_markerDialog: { devId?: string; uploadId?: string; name: string; binding: string; bindingMode: "virtual" | "ha"; bindingOpen: boolean; showEntities: boolean; bindingFilter: string; icon: string; autoIcon: string; display: DeviceDisplayMode; rippleColor: string; rippleSize: number; size: number; angle: number; tapAction: string; tapActionTouched: boolean; originalHasTapAction: boolean; originalTapAction: string | null | undefined; tapHintAnnouncement: string; toggleEntity: string; toggleEntityTouched: boolean; originalHasToggleEntity: boolean; originalToggleEntity: string | null | undefined; tapTarget: string; tapConfirm: boolean; runFilter: string; controls: string[]; controlsFilter: string; glowRadius: string; lightRole: "auto" | "always" | "never"; lightRoleTouched: boolean; originalHasIsLight: boolean; originalIsLight: boolean | null | undefined; lightEntity: string; lightEntityTouched: boolean; originalHasLightEntity: boolean; originalLightEntity: string | null | undefined; glowMode: "auto" | "color" | "fixed"; glowColor: string; glowBrightness: number; glowColorDrafted: boolean; glowBrightnessDrafted: boolean; glowTouched: boolean; originalHasGlowColor: boolean; originalGlowColor: { c: string; bri?: number | null; } | null | undefined; valueBadgeEnabled: boolean; valueBadgeSource: ValueBadgeSource | null; valueBadgePosition: ValueBadgePosition; valueBadgeTouched: boolean; originalHasValueBadge: boolean; originalValueBadge: MarkerValueBadge | null | undefined; useClimateTemp: boolean; model: string; link: string; description: string; pdfs: PdfRef[]; room: string; hideFromPlan: boolean; busy: boolean; } | null;
|
||||
_markerDialog: { devId?: string; uploadId?: string; name: string; binding: string; bindingMode: "virtual" | "ha"; bindingOpen: boolean; showEntities: boolean; bindingFilter: string; icon: string; autoIcon: string; display: DeviceDisplayMode; rippleColor: string; rippleSize: number; size: number; angle: number; tapAction: string; tapActionTouched: boolean; originalHasTapAction: boolean; originalTapAction: string | null | undefined; tapHintAnnouncement: string; toggleEntity: string; toggleEntityTouched: boolean; originalHasToggleEntity: boolean; originalToggleEntity: string | null | undefined; tapTarget: string; tapConfirm: boolean; runFilter: string; controls: string[]; controlsFilter: string; glowRadius: string; lightRole: "auto" | "always" | "never"; lightRoleTouched: boolean; originalHasIsLight: boolean; originalIsLight: boolean | null | undefined; lightEntity: string; lightEntityTouched: boolean; originalHasLightEntity: boolean; originalLightEntity: string | null | undefined; glowMode: "auto" | "color" | "fixed"; glowColor: string; glowBrightness: number; glowColorDrafted: boolean; glowBrightnessDrafted: boolean; glowTouched: boolean; originalHasGlowColor: boolean; originalGlowColor: { c: string; bri?: number | null; } | null | undefined; valueBadgeEnabled: boolean; valueBadgeSource: ValueBadgeSource | null; valueBadgePosition: ValueBadgePosition; valueBadgeTouched: boolean; originalHasValueBadge: boolean; originalValueBadge: MarkerValueBadge | null | undefined; valueSource: ValueBadgeSource | null; valueSourceTouched: boolean; originalHasValueSource: boolean; originalValueSource: ValueBadgeSource | null | undefined; useClimateTemp: boolean; model: string; link: string; description: string; pdfs: PdfRef[]; room: string; hideFromPlan: boolean; busy: boolean; } | null;
|
||||
_markerPreviewDevicesMemo: { base: readonly DevItem[]; preview: DevItem; devices: readonly DevItem[]; } | null;
|
||||
_markerPreviewMemo: { key: string; device: DevItem | null; } | null;
|
||||
_markers: Marker[];
|
||||
@@ -7642,6 +7643,7 @@ public _openMarkerDialog(d?: DevItem): void {
|
||||
const hasLightEntity = Object.prototype.hasOwnProperty.call(marker || {}, 'light_entity');
|
||||
const hasGlowColor = Object.prototype.hasOwnProperty.call(marker || {}, 'glow_color');
|
||||
const hasValueBadge = Object.prototype.hasOwnProperty.call(marker || {}, 'value_badge');
|
||||
const hasValueSource = Object.prototype.hasOwnProperty.call(marker || {}, 'value_source');
|
||||
const hasTapAction = Object.prototype.hasOwnProperty.call(marker || {}, 'tap_action');
|
||||
const hasToggleEntity = Object.prototype.hasOwnProperty.call(marker || {}, 'toggle_entity');
|
||||
const glowOverride = normalizeGlowColorOverride(marker?.glow_color);
|
||||
@@ -7703,6 +7705,10 @@ public _openMarkerDialog(d?: DevItem): void {
|
||||
valueBadgeTouched: false,
|
||||
originalHasValueBadge: hasValueBadge,
|
||||
originalValueBadge: marker?.value_badge,
|
||||
valueSource: marker?.value_source || null,
|
||||
valueSourceTouched: false,
|
||||
originalHasValueSource: hasValueSource,
|
||||
originalValueSource: marker?.value_source,
|
||||
useClimateTemp: d.marker?.use_climate_temp === true,
|
||||
glowRadius: Number(d.marker?.glow_radius_cm) > 0
|
||||
? String(this.host._imperial
|
||||
@@ -7739,6 +7745,8 @@ public _openMarkerDialog(d?: DevItem): void {
|
||||
originalHasGlowColor: false, originalGlowColor: undefined,
|
||||
valueBadgeEnabled: false, valueBadgeSource: null, valueBadgePosition: 'right',
|
||||
valueBadgeTouched: false, originalHasValueBadge: false, originalValueBadge: undefined,
|
||||
valueSource: null, valueSourceTouched: false,
|
||||
originalHasValueSource: false, originalValueSource: undefined,
|
||||
useClimateTemp: false, glowRadius: '', model: '',
|
||||
link: '', description: '', pdfs: [], room: '', hideFromPlan: false, busy: false,
|
||||
uploadId: 'up_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
||||
@@ -8029,6 +8037,7 @@ public async _saveMarker(): Promise<void> {
|
||||
// pdfs may be rewritten below when rebinding changes the marker id
|
||||
...this._markerLightFields(dlg),
|
||||
...this._markerValueBadgeFields(dlg),
|
||||
...this._markerValueSourceFields(dlg),
|
||||
use_climate_temp: dlg.useClimateTemp ? true : null,
|
||||
glow_radius_cm: (() => {
|
||||
const v = strictNumber(dlg.glowRadius);
|
||||
@@ -8083,6 +8092,10 @@ public async _saveMarker(): Promise<void> {
|
||||
&& marker.value_badge.source.ref === `marker:${oldId}`) {
|
||||
marker.value_badge.source = { kind: 'derived_marker_state', ref: `marker:${id}` };
|
||||
}
|
||||
if (oldId && oldId !== id && marker.value_source?.kind === 'derived_marker_state'
|
||||
&& marker.value_source.ref === `marker:${oldId}`) {
|
||||
marker.value_source = { kind: 'derived_marker_state', ref: `marker:${id}` };
|
||||
}
|
||||
// remove the previous marker (by the old id and by the new id)
|
||||
cfg.markers = cfg.markers.filter(
|
||||
(m) => m.id !== id && m.id !== oldId
|
||||
@@ -11747,6 +11760,17 @@ public _markerValueBadgeFields(
|
||||
});
|
||||
}
|
||||
|
||||
public _markerValueSourceFields(
|
||||
d: NonNullable<HouseplanEditorHostPort['_markerDialog']>,
|
||||
): Pick<Marker, 'value_source'> | Record<string, never> {
|
||||
return valueSourceWriteFields({
|
||||
touched: d.valueSourceTouched,
|
||||
originalHas: d.originalHasValueSource,
|
||||
original: d.originalValueSource,
|
||||
source: d.valueSource,
|
||||
});
|
||||
}
|
||||
|
||||
public _markerDraft(d: NonNullable<HouseplanEditorHostPort['_markerDialog']>): Marker | null {
|
||||
if (d.bindingMode === 'ha' && (!d.binding || d.binding === 'virtual')) return null;
|
||||
const roomRef = parseRoomRef(d.room);
|
||||
@@ -11773,6 +11797,7 @@ public _markerDraft(d: NonNullable<HouseplanEditorHostPort['_markerDialog']>): M
|
||||
controls: controls.length ? controls : null,
|
||||
...this._markerLightFields(d),
|
||||
...this._markerValueBadgeFields(d),
|
||||
...this._markerValueSourceFields(d),
|
||||
use_climate_temp: d.useClimateTemp ? true : null,
|
||||
glow_radius_cm: (() => {
|
||||
const value = strictNumber(d.glowRadius);
|
||||
@@ -12150,11 +12175,11 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
const badgeSourceMissing = !!effectiveBadgeSource
|
||||
&& !badgeCandidates.some((item) => item.key === badgeSourceKey);
|
||||
const selectedBadgeCandidate = badgeCandidates.find((item) => item.key === badgeSourceKey);
|
||||
const innerValueSourceKey = previewPresentation?.valueSource
|
||||
? previewPresentation.valueSource.attribute
|
||||
? `attr:${previewPresentation.valueSource.eid}:${previewPresentation.valueSource.attribute}`
|
||||
: `state:${previewPresentation.valueSource.eid}`
|
||||
: '';
|
||||
const valueSourceKey = valueBadgeSourceKey(d.valueSource);
|
||||
const valueSourceMissing = !!d.valueSource && (!valueSourceKey
|
||||
|| !badgeCandidates.some((item) => item.key === valueSourceKey));
|
||||
const selectedValueSourceCandidate = badgeCandidates.find((item) => item.key === valueSourceKey);
|
||||
const innerValueSourceKey = previewPresentation?.valueSource?.sourceKey || '';
|
||||
const autoHasSpatialSource = this._markerAutoHasSpatialSource(d);
|
||||
const statefulSource = !!previewDevice
|
||||
&& hasOwnStatefulLightSource(this.host._planHass, { ...previewDevice, hidden: false });
|
||||
@@ -12233,6 +12258,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
};
|
||||
this.host._markerDialog = this._announceToggleDraft({
|
||||
...next, ...this._valueBadgeForBinding(next, 'virtual'),
|
||||
valueSource: null, valueSourceTouched: true,
|
||||
});
|
||||
}} />
|
||||
<span>${this.host._t('marker.virtual_option')}</span>
|
||||
@@ -12280,6 +12306,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
};
|
||||
this.host._markerDialog = this._announceToggleDraft({
|
||||
...next, ...this._valueBadgeForBinding(next, c.value),
|
||||
valueSource: null, valueSourceTouched: true,
|
||||
});
|
||||
}}>
|
||||
<span class="cl">${c.label}</span><span class="cs">${c.sub}</span>
|
||||
@@ -12583,6 +12610,39 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
</option>`)}
|
||||
</select>
|
||||
<p class="muted">${this.host._t(DISPLAY_HINT_KEYS[d.display])}</p>
|
||||
${d.display === 'value' ? html`<div class="markerhelpfield markervaluesource">
|
||||
<div class="markerhelplabel">
|
||||
<label for="marker-value-source">${this.host._t('marker.value_source')}</label>
|
||||
${this._help('marker.value_source.help')}
|
||||
</div>
|
||||
<select id="marker-value-source" class="areasel" ?disabled=${isVirtual}
|
||||
@change=${(e: Event) => (this.host._markerDialog = {
|
||||
...d,
|
||||
valueSource: valueBadgeSourceFromKey((e.target as HTMLSelectElement).value),
|
||||
valueSourceTouched: true,
|
||||
})}>
|
||||
<option value="" ?selected=${!d.valueSource}>
|
||||
${this.host._t('marker.value_source_auto')}
|
||||
</option>
|
||||
${valueSourceMissing ? html`<option value=${valueSourceKey || '__missing__'} selected>
|
||||
${this.host._t('marker.value_badge_missing')}
|
||||
</option>` : nothing}
|
||||
${badgeCandidates.map((candidate) => html`<option value=${candidate.key}
|
||||
?selected=${candidate.key === valueSourceKey}
|
||||
title=${candidate.technical}>
|
||||
${this._valueBadgeCandidateLabel(candidate)} · ${candidate.value}
|
||||
</option>`)}
|
||||
</select>
|
||||
${selectedValueSourceCandidate
|
||||
? html`<p class="muted markerbadgetechnical"><code>${selectedValueSourceCandidate.technical}</code></p>`
|
||||
: nothing}
|
||||
${isVirtual ? html`<p class="muted markerlightdisabled" role="note">
|
||||
<ha-icon icon="mdi:information-outline"></ha-icon>
|
||||
${this.host._t('marker.preview.reason.value_virtual')}
|
||||
</p>` : valueSourceMissing ? html`<p class="muted markerlightwarning" role="status">
|
||||
<ha-icon icon="mdi:alert-outline"></ha-icon>${this.host._t('marker.value_source_missing_hint')}
|
||||
</p>` : nothing}
|
||||
</div>` : nothing}
|
||||
${d.display === 'static_icon' && this.host._bindingHasAlarm(d.binding)
|
||||
? html`<div class="habindingbanner" role="note">
|
||||
<ha-icon icon="mdi:alert-outline"></ha-icon>
|
||||
|
||||
+6
-1
@@ -190,7 +190,7 @@
|
||||
"marker.display_hint_badge": "Das Symbol und die dynamische Platte zeigen den Gerätezustand ohne den üblichen Aktivitätspuls. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_icon": "Das Symbol und die dynamische Platte zeigen den Gerätezustand ohne den üblichen Aktivitätspuls. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_icon_ripple": "Das Symbol, die dynamische Platte und der Puls zeigen einen kurzen Puls für Ereignisse und einen kontinuierlichen Puls für Arbeit, Bewegung oder Anwesenheit. Rote Alarme haben eine separate Priorität.",
|
||||
"marker.display_hint_value": "Ein eindeutiger Home Assistant-Wert ersetzt das Symbol, während die Platte weiterhin den Zustand anzeigt. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_value": "Ein ausgewählter oder automatisch ermittelter Home Assistant-Wert ersetzt das Symbol, während die Platte weiterhin den Zustand anzeigt. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_static_icon": "Die themenabhängige Hülle und das Symbol bleiben immer gleich. Zustand, Aktivität, Nichtverfügbarkeit und Alarme verändern nicht das Erscheinungsbild.",
|
||||
"marker.static_alarm_warning": "Dieses Gerät kann Alarme melden. Die statische Anzeige verbirgt die visuelle Alarmanzeige des Markers.",
|
||||
"marker.preview.title": "Anzeigevorschau",
|
||||
@@ -665,6 +665,11 @@
|
||||
"marker.value_badge_attr_volume_level": "Lautstärke · {name}",
|
||||
"marker.value_badge_attr_battery_level": "Batterie · {name}",
|
||||
"marker.value_badge_attr_fan_speed": "Lüftergeschwindigkeit · {name}",
|
||||
"marker.value_source": "Wertquelle",
|
||||
"marker.value_source.help": "Wähle den Wert, der das Symbol ersetzt. Die Tipp-Aktion bleibt unverändert.",
|
||||
"marker.value_source.help.aria": "Hilfe: Wertquelle in der Gerätefläche",
|
||||
"marker.value_source_auto": "Automatisch (wie bisher)",
|
||||
"marker.value_source_missing_hint": "Die gespeicherte Quelle ist derzeit nicht verfügbar. Bis sie wieder verfügbar ist oder ein anderer Wert gewählt wird, zeigt die Fläche —.",
|
||||
"marker.light_role_label": "Ist dieses Gerät eine Lichtquelle?",
|
||||
"marker.light_role.help": "Steuert, ob dieser Marker selbst eine räumliche Lichtquelle ist. Verknüpfte Lichter oben bleiben unabhängige Raumlichtsteuerungen.",
|
||||
"marker.light_role.help.aria": "Hilfe: ob dieses Gerät eine Lichtquelle ist",
|
||||
|
||||
+6
-1
@@ -190,7 +190,7 @@
|
||||
"marker.display_hint_badge": "The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.",
|
||||
"marker.display_hint_icon": "The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.",
|
||||
"marker.display_hint_icon_ripple": "The icon, dynamic plate and pulse show a short pulse for events and a continuous pulse for work, motion or presence. Red alarms have separate priority.",
|
||||
"marker.display_hint_value": "One unambiguous Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.",
|
||||
"marker.display_hint_value": "A selected or automatically resolved Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.",
|
||||
"marker.display_hint_static_icon": "The theme-aware shell and icon always stay the same. State, activity, unavailability and alarms do not change the face.",
|
||||
"marker.static_alarm_warning": "This device can report alarms. Static display hides the marker's visual alarm indication.",
|
||||
"marker.preview.title": "Display preview",
|
||||
@@ -665,6 +665,11 @@
|
||||
"marker.value_badge_attr_volume_level": "Volume · {name}",
|
||||
"marker.value_badge_attr_battery_level": "Battery · {name}",
|
||||
"marker.value_badge_attr_fan_speed": "Fan speed · {name}",
|
||||
"marker.value_source": "Value source",
|
||||
"marker.value_source.help": "Choose the value that replaces the icon. The tap action does not change.",
|
||||
"marker.value_source.help.aria": "Help: value face source",
|
||||
"marker.value_source_auto": "Automatic (as before)",
|
||||
"marker.value_source_missing_hint": "The saved source is currently unavailable. The face shows — until it recovers or you choose another value.",
|
||||
"marker.light_role_label": "Is this device a light source?",
|
||||
"marker.light_role.help": "Controls whether this marker itself is a spatial light source. Linked lights above remain independent room-light controls.",
|
||||
"marker.light_role.help.aria": "Help: whether this device is a light source",
|
||||
|
||||
+6
-1
@@ -190,7 +190,7 @@
|
||||
"marker.display_hint_badge": "L’icône et le fond dynamique affichent l’état de l’appareil sans la pulsation d’activité habituelle. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_icon": "L’icône et le fond dynamique affichent l’état de l’appareil sans la pulsation d’activité habituelle. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_icon_ripple": "L’icône, le fond dynamique et la pulsation affichent une brève animation pour les événements et une animation continue pour le fonctionnement, le mouvement ou la présence. Les alarmes rouges restent prioritaires.",
|
||||
"marker.display_hint_value": "Une valeur Home Assistant non ambiguë remplace l’icône tandis que le fond continue d’indiquer l’état. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_value": "Une valeur Home Assistant sélectionnée ou résolue automatiquement remplace l’icône tandis que le fond continue d’indiquer l’état. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_static_icon": "Le contour adapté au thème et l’icône restent toujours identiques. L’état, l’activité, l’indisponibilité et les alarmes ne modifient pas l’apparence.",
|
||||
"marker.static_alarm_warning": "Cet appareil peut signaler des alarmes. L’affichage statique masque l’indication visuelle d’alarme du marqueur.",
|
||||
"marker.preview.title": "Aperçu de l’affichage",
|
||||
@@ -665,6 +665,11 @@
|
||||
"marker.value_badge_attr_volume_level": "Volume · {name}",
|
||||
"marker.value_badge_attr_battery_level": "Batterie · {name}",
|
||||
"marker.value_badge_attr_fan_speed": "Vitesse du ventilateur · {name}",
|
||||
"marker.value_source": "Source de la valeur",
|
||||
"marker.value_source.help": "Choisissez la valeur qui remplace l’icône. L’action au toucher ne change pas.",
|
||||
"marker.value_source.help.aria": "Aide : source de la valeur dans le marqueur",
|
||||
"marker.value_source_auto": "Automatique (comme avant)",
|
||||
"marker.value_source_missing_hint": "La source enregistrée est indisponible. Le marqueur affiche — jusqu’à son retour ou jusqu’au choix d’une autre valeur.",
|
||||
"marker.light_role_label": "Cet appareil est-il une source lumineuse ?",
|
||||
"marker.light_role.help": "Détermine si ce marqueur est lui-même une source lumineuse spatiale. Les lumières liées ci-dessus restent des contrôles de lumière de pièce indépendants.",
|
||||
"marker.light_role.help.aria": "Aide : cet appareil est-il une source lumineuse",
|
||||
|
||||
+6
-1
@@ -190,7 +190,7 @@
|
||||
"marker.display_hint_badge": "Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.",
|
||||
"marker.display_hint_icon": "Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.",
|
||||
"marker.display_hint_icon_ripple": "Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.",
|
||||
"marker.display_hint_value": "Значок заменяется одним однозначным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.",
|
||||
"marker.display_hint_value": "Значок заменяется выбранным или автоматически найденным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.",
|
||||
"marker.display_hint_static_icon": "Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.",
|
||||
"marker.static_alarm_warning": "Это устройство может сообщать о тревогах. Статичный режим скрывает визуальную тревогу маркера.",
|
||||
"marker.preview.title": "Предпросмотр отображения",
|
||||
@@ -665,6 +665,11 @@
|
||||
"marker.value_badge_attr_volume_level": "Громкость · {name}",
|
||||
"marker.value_badge_attr_battery_level": "Заряд батареи · {name}",
|
||||
"marker.value_badge_attr_fan_speed": "Скорость вентилятора · {name}",
|
||||
"marker.value_source": "Источник значения",
|
||||
"marker.value_source.help": "Выберите, что заменит значок. Действие по нажатию не изменится.",
|
||||
"marker.value_source.help.aria": "Подсказка: источник значения внутри значка",
|
||||
"marker.value_source_auto": "Автоматически (как раньше)",
|
||||
"marker.value_source_missing_hint": "Сохранённый источник сейчас недоступен. До его восстановления или выбора другого значения внутри будет показано —.",
|
||||
"marker.light_role_label": "Является источником света",
|
||||
"marker.light_role.help": "Определяет, является ли сам маркер пространственным источником света. Привязанные выше лампы остаются независимыми источниками для комнаты.",
|
||||
"marker.light_role.help.aria": "Подсказка: является ли устройство источником света",
|
||||
|
||||
@@ -176,6 +176,9 @@ export interface Marker {
|
||||
/** Optional, user-controlled value satellite around the device face.
|
||||
* Missing keeps the legacy temperature/humidity compatibility heuristic. */
|
||||
value_badge?: MarkerValueBadge | null;
|
||||
/** Explicit source for the `value` display face. Missing/null keeps the
|
||||
* historical automatic entity-state resolver. */
|
||||
value_source?: ValueBadgeSource | null;
|
||||
/**
|
||||
* Climate devices (AC, thermostat) know the room temperature
|
||||
* (attributes.current_temperature). Opt-in per marker: show it as a badge
|
||||
|
||||
@@ -305,6 +305,16 @@ const rowRunners = {
|
||||
id: 'vacuum', icon: 'mdi:robot-vacuum', entities: ['vacuum.robot'], primary: 'vacuum.robot',
|
||||
marker: { id: 'vacuum', binding: 'device:vacuum', vacuum: { live: true } },
|
||||
}, {}, (r) => assert.equal(r.vacuumLive, true)),
|
||||
F18: presentationRow({
|
||||
entities: ['switch.offline'], primary: 'switch.offline',
|
||||
marker: {
|
||||
id: 'd1', binding: 'device:d1', display: 'value',
|
||||
value_source: { kind: 'entity_state', entity_id: 'switch.offline' },
|
||||
},
|
||||
}, {}, (r) => {
|
||||
assert.equal(r.valueText, '—');
|
||||
assert.equal(r.fallbackReason, 'value_no_state');
|
||||
}),
|
||||
A01: presentationRow({
|
||||
icon: 'mdi:smoke-detector', entities: ['binary_sensor.smoke'], primary: 'binary_sensor.smoke',
|
||||
}, { liveStates: false }, (r) => assert.deepEqual([r.pulse.kind, r.pulse.reason], ['alarm', 'alarm'])),
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
resolveBindingProviders,
|
||||
resolveEntityProvider,
|
||||
} from '../test-build/integration-provider.js';
|
||||
import { valueBadgeTitle, valueBadgeWriteFields } from '../test-build/device-value-badge.js';
|
||||
import {
|
||||
valueBadgeCandidates, valueBadgeTitle, valueBadgeWriteFields, valueSourceWriteFields,
|
||||
} from '../test-build/device-value-badge.js';
|
||||
import {
|
||||
buildDevices, deviceFromMarkerDraft, resolvedLightSources,
|
||||
} from '../test-build/devices.js';
|
||||
@@ -833,6 +835,137 @@ test('value mode refuses to choose the first of multiple equal control sources',
|
||||
assert.equal(result.visualSources.length, 2);
|
||||
});
|
||||
|
||||
test('issue 378 explicit cover position uses the exact value-badge formatter', () => {
|
||||
const h = hass({
|
||||
'cover.awning': state('cover.awning', 'open', { current_position: 42, supported_features: 15 }),
|
||||
}, {
|
||||
'cover.awning': { entity_id: 'cover.awning', device_id: 'd1', platform: 'demo' },
|
||||
});
|
||||
const source = { kind: 'entity_attribute', entity_id: 'cover.awning', attribute: 'current_position' };
|
||||
const awning = device({
|
||||
name: 'Awning', icon: 'mdi:awning', entities: ['cover.awning'], primary: 'cover.awning',
|
||||
marker: {
|
||||
id: 'd1', binding: 'device:d1', display: 'value', value_source: source,
|
||||
value_badge: { enabled: true, source, position: 'right' },
|
||||
},
|
||||
});
|
||||
const result = resolveDevicePresentation(h, awning, options);
|
||||
assert.equal(result.valueText, '42 %');
|
||||
assert.equal(result.valueFullText, '42 %');
|
||||
assert.equal(result.valueBadge?.text, result.valueText);
|
||||
assert.equal(result.valueSource?.sourceKey, 'attr:cover.awning:current_position');
|
||||
assert.equal(result.sourceKind, 'cover');
|
||||
assert.deepEqual(
|
||||
valueBadgeCandidates(h, awning).find((candidate) => candidate.key === result.valueSource?.sourceKey)?.source,
|
||||
source,
|
||||
);
|
||||
});
|
||||
|
||||
test('issue 378 explicit unavailable source shows a dash and restores without auto fallback', () => {
|
||||
const h = hass({}, {
|
||||
'cover.awning': { entity_id: 'cover.awning', device_id: 'd1', platform: 'demo' },
|
||||
});
|
||||
const awning = device({
|
||||
name: 'Awning', icon: 'mdi:awning', entities: ['cover.awning'], primary: 'cover.awning',
|
||||
marker: {
|
||||
id: 'd1', binding: 'device:d1', display: 'value',
|
||||
value_source: { kind: 'entity_attribute', entity_id: 'cover.awning', attribute: 'current_position' },
|
||||
},
|
||||
});
|
||||
const missing = resolveDevicePresentation(h, awning, options);
|
||||
assert.equal(missing.valueText, '—');
|
||||
assert.equal(missing.valueFullText, 'Unavailable');
|
||||
assert.equal(missing.fallbackReason, 'value_no_state');
|
||||
assert.equal(missing.valueSource?.sourceKey, 'attr:cover.awning:current_position');
|
||||
assert.ok(missing.decisionIds.includes('content.value'));
|
||||
|
||||
h.states['cover.awning'] = state('cover.awning', 'open', {
|
||||
current_position: 55, supported_features: 15,
|
||||
});
|
||||
const restored = resolveDevicePresentation(h, awning, options);
|
||||
assert.equal(restored.valueText, '55 %');
|
||||
assert.equal(restored.fallbackReason, null);
|
||||
|
||||
h.states['cover.awning'] = state('cover.awning', 'unavailable', {
|
||||
current_position: 55, supported_features: 15,
|
||||
});
|
||||
const staleAttribute = resolveDevicePresentation(h, awning, options);
|
||||
assert.equal(staleAttribute.valueText, '—');
|
||||
assert.equal(staleAttribute.fallbackReason, 'value_no_state');
|
||||
|
||||
const legacy = resolveDevicePresentation(hass({}, h.entities), {
|
||||
...awning, marker: { id: 'd1', binding: 'device:d1', display: 'value' },
|
||||
}, options);
|
||||
assert.equal(legacy.valueText, null);
|
||||
assert.equal(legacy.fallbackReason, 'value_no_state');
|
||||
});
|
||||
|
||||
test('issue 378 explicit non-scalar value stays selected and reports the stable reason', () => {
|
||||
const h = hass({
|
||||
'cover.awning': state('cover.awning', 'open', { current_position: { invalid: true } }),
|
||||
}, {
|
||||
'cover.awning': { entity_id: 'cover.awning', device_id: 'd1', platform: 'demo' },
|
||||
});
|
||||
const result = resolveDevicePresentation(h, device({
|
||||
entities: ['cover.awning'], primary: 'cover.awning',
|
||||
marker: {
|
||||
id: 'd1', binding: 'device:d1', display: 'value',
|
||||
value_source: { kind: 'entity_attribute', entity_id: 'cover.awning', attribute: 'current_position' },
|
||||
},
|
||||
}), options);
|
||||
assert.equal(result.valueText, '—');
|
||||
assert.equal(result.fallbackReason, 'value_non_scalar');
|
||||
});
|
||||
|
||||
test('issue 378 derived sources share the plan graph and suppress duplicate LQI', () => {
|
||||
const h = hass({
|
||||
'switch.driver': state('switch.driver', 'on'),
|
||||
'sensor.linkquality': state('sensor.linkquality', '150', { unit_of_measurement: 'lqi' }),
|
||||
}, {
|
||||
'switch.driver': { entity_id: 'switch.driver', device_id: 'lamp', platform: 'demo' },
|
||||
'sensor.linkquality': { entity_id: 'sensor.linkquality', device_id: 'd1', platform: 'demo' },
|
||||
});
|
||||
const lamp = device({
|
||||
id: 'lamp', name: 'Lamp', entities: ['switch.driver'], primary: 'switch.driver',
|
||||
bindingRef: 'lamp', marker: { id: 'lamp', binding: 'device:lamp', is_light: true },
|
||||
});
|
||||
const controller = device({
|
||||
controls: ['marker:lamp'],
|
||||
marker: {
|
||||
id: 'd1', binding: 'device:d1', display: 'value', controls: ['marker:lamp'],
|
||||
value_source: { kind: 'derived_marker_state', ref: 'marker:lamp' },
|
||||
},
|
||||
});
|
||||
const stateResult = resolveDevicePresentation(h, controller, { ...options, lightDevices: [controller, lamp] });
|
||||
assert.equal(stateResult.valueText, 'On');
|
||||
assert.equal(stateResult.valueSource?.kind, 'derived_marker_state');
|
||||
|
||||
const lqiResult = resolveDevicePresentation(h, device({
|
||||
entities: ['sensor.linkquality'], primary: 'sensor.linkquality',
|
||||
marker: { id: 'd1', binding: 'device:d1', display: 'value', value_source: { kind: 'derived_lqi' } },
|
||||
}), options);
|
||||
assert.equal(lqiResult.valueText, '150');
|
||||
assert.equal(lqiResult.lqiText, null);
|
||||
});
|
||||
|
||||
test('issue 378 persistence keeps untouched data and represents auto by absence', () => {
|
||||
assert.deepEqual(valueSourceWriteFields({
|
||||
touched: false, originalHas: false, original: undefined,
|
||||
source: { kind: 'entity_state', entity_id: 'sensor.projected' },
|
||||
}), {});
|
||||
const future = { kind: 'future_source', payload: { preserved: true } };
|
||||
assert.equal(valueSourceWriteFields({
|
||||
touched: false, originalHas: true, original: future, source: null,
|
||||
}).value_source, future);
|
||||
assert.deepEqual(valueSourceWriteFields({
|
||||
touched: true, originalHas: true, original: future, source: null,
|
||||
}), {});
|
||||
assert.deepEqual(valueSourceWriteFields({
|
||||
touched: true, originalHas: false, original: undefined,
|
||||
source: { kind: 'entity_state', entity_id: 'sensor.selected' },
|
||||
}), { value_source: { kind: 'entity_state', entity_id: 'sensor.selected' } });
|
||||
});
|
||||
|
||||
test('hidden design preview is visible without mutating the hidden contract', () => {
|
||||
const h = hass(
|
||||
{ 'switch.relay': state('switch.relay', 'on') },
|
||||
|
||||
@@ -1910,6 +1910,13 @@ test('issue 84 lifecycle: delete/rebind updates marker links and cycles are reje
|
||||
rewriteMarkerControlReferences(withBadge, 'b', 'renamed')[0].value_badge.source.ref,
|
||||
'marker:renamed',
|
||||
);
|
||||
const withValueSource = [{
|
||||
id: 'a', value_source: { kind: 'derived_marker_state', ref: 'marker:b' },
|
||||
}];
|
||||
assert.equal(
|
||||
rewriteMarkerControlReferences(withValueSource, 'b', 'renamed')[0].value_source.ref,
|
||||
'marker:renamed',
|
||||
);
|
||||
assert.equal(markerControlWouldCycle(markers, 'c', 'a'), true);
|
||||
assert.equal(markerControlWouldCycle(markers, 'a', 'c'), false);
|
||||
assert.equal(markerControlWouldCycle(markers, 'a', 'a'), true);
|
||||
|
||||
@@ -43,6 +43,7 @@ const ROWS = [
|
||||
['F15', 'diagnostics.metrics_enabled', 'device-presentation-policy-diagnostics'],
|
||||
['F16', 'diagnostics.lqi_low', 'device-marker-lqi-low-boundary-shifted'],
|
||||
['F17', 'diagnostics.vacuum_live', 'device-presentation-policy-diagnostics'],
|
||||
['F18', 'content.value', 'device-presentation-policy-value'],
|
||||
['A01', 'pulse.alarm_alarm', 'device-presentation-policy-alarm'],
|
||||
['A02', 'activity.pulse_suppressed', 'device-presentation-policy-pulse-gate'],
|
||||
['A03', 'pulse.short_event', 'device-presentation-policy-pulse-gate'],
|
||||
|
||||
@@ -38,7 +38,7 @@ test('persisted dynamic marker selects project their selected option explicitly'
|
||||
]));
|
||||
for (const id of [
|
||||
'marker-room', 'marker-tap-action', 'marker-toggle-entity', 'marker-light-entity', 'marker-display',
|
||||
'marker-value-badge-source', 'marker-value-badge-position',
|
||||
'marker-value-source', 'marker-value-badge-source', 'marker-value-badge-position',
|
||||
]) {
|
||||
const block = byId.get(id);
|
||||
assert.ok(block, id);
|
||||
|
||||
@@ -1511,6 +1511,27 @@ def test_issue_90_space_merge_remaps_internal_badge_marker_link(tmp_path: Path)
|
||||
assert details["dropped_marker_links"] == 0
|
||||
|
||||
|
||||
def test_issue_378_space_merge_remaps_internal_value_face_marker_link(tmp_path: Path) -> None:
|
||||
document = _document(tmp_path, "space")
|
||||
markers = document["payload"]["config"]["markers"]
|
||||
markers[0]["name"] = "Controller"
|
||||
markers[0]["value_source"] = {
|
||||
"kind": "derived_marker_state", "ref": "marker:dumb",
|
||||
}
|
||||
markers.append({
|
||||
"id": "dumb", "binding": "virtual", "space": "ground",
|
||||
"room_id": "living", "name": "Dumb lamp", "is_light": True,
|
||||
})
|
||||
merged, _layout, details = build_space_merge(
|
||||
document, {"spaces": [], "markers": [], "settings": {}}, {}, "skip",
|
||||
)
|
||||
imported = [m for m in merged["markers"] if m.get("space") == details["space_id"]]
|
||||
by_name = {m.get("name"): m for m in imported}
|
||||
assert by_name["Controller"]["value_source"]["ref"] == \
|
||||
"marker:" + by_name["Dumb lamp"]["id"]
|
||||
assert details["dropped_marker_links"] == 0
|
||||
|
||||
|
||||
def test_space_export_drops_marker_links_outside_selection(tmp_path: Path) -> None:
|
||||
config = _config()
|
||||
config["markers"][0]["controls"] = ["marker:outside", "light.keep"]
|
||||
@@ -1549,6 +1570,47 @@ def test_issue_90_space_export_disables_badge_link_outside_selection(tmp_path: P
|
||||
assert document["transfer"]["dropped_marker_links"] == 1
|
||||
|
||||
|
||||
def test_issue_378_space_export_drops_value_face_link_outside_selection(tmp_path: Path) -> None:
|
||||
config = _config()
|
||||
config["markers"][0]["value_source"] = {
|
||||
"kind": "derived_marker_state", "ref": "marker:outside",
|
||||
}
|
||||
config["markers"].append({
|
||||
"id": "outside", "binding": "virtual", "space": "other",
|
||||
"name": "Other lamp", "is_light": True,
|
||||
})
|
||||
document, _ = create_export(
|
||||
SimpleNamespace(instance_id="instance-a"), {"config": config},
|
||||
{"layout": {"lamp": {"s": "ground", "x": 0.5, "y": 0.5}}},
|
||||
kind="space", space_id="ground", card_version="1.61.0", config_root=tmp_path,
|
||||
)
|
||||
marker = document["payload"]["config"]["markers"][0]
|
||||
assert "value_source" not in marker
|
||||
assert document["transfer"]["dropped_marker_links"] == 1
|
||||
|
||||
|
||||
def test_issue_378_virtualized_duplicate_drops_value_face_link(tmp_path: Path) -> None:
|
||||
document = _document(tmp_path, "space")
|
||||
controller = document["payload"]["config"]["markers"][0]
|
||||
controller["name"] = "Controller"
|
||||
controller["value_source"] = {
|
||||
"kind": "derived_marker_state", "ref": "marker:dumb",
|
||||
}
|
||||
document["payload"]["config"]["markers"].append({
|
||||
"id": "dumb", "binding": "virtual", "space": "ground", "is_light": True,
|
||||
})
|
||||
current = {
|
||||
"spaces": [], "markers": [{"id": "existing", "binding": controller["binding"]}],
|
||||
}
|
||||
merged, _layout, details = build_space_merge(document, current, {}, "virtual")
|
||||
imported_controller = next(
|
||||
marker for marker in merged["markers"]
|
||||
if marker.get("name") == controller.get("name") and marker.get("binding") == "virtual"
|
||||
)
|
||||
assert "value_source" not in imported_controller
|
||||
assert details["dropped_marker_links"] == 1
|
||||
|
||||
|
||||
def test_space_merge_drops_link_when_target_is_virtualized_duplicate(tmp_path: Path) -> None:
|
||||
document = _document(tmp_path, "space")
|
||||
controller = document["payload"]["config"]["markers"][0]
|
||||
|
||||
@@ -2400,3 +2400,75 @@ def test_issue_90_marker_value_badge_reference_uses_canonical_ref():
|
||||
with pytest.raises(v.MarkerControlError) as invalid:
|
||||
v.validate_marker_value_badges(config, {"markers": []})
|
||||
assert invalid.value.code == "invalid_value_badge_source"
|
||||
|
||||
|
||||
def test_issue_378_value_face_source_is_lossless_and_strict_only_when_changed():
|
||||
source = {
|
||||
"kind": "entity_attribute", "entity_id": "cover.awning",
|
||||
"attribute": "current_position", "future_source": {"kept": True},
|
||||
}
|
||||
config = {"markers": [{
|
||||
"id": "awning", "binding": "entity:cover.awning", "display": "value",
|
||||
"value_source": source,
|
||||
}]}
|
||||
v.validate_marker_value_badges(config, {"markers": []})
|
||||
stored = v.MARKER_SCHEMA(config["markers"][0])
|
||||
assert stored["value_source"]["future_source"] == {"kept": True}
|
||||
|
||||
future = {"markers": [{
|
||||
"id": "future", "binding": "entity:sensor.future", "display": "value",
|
||||
"value_source": {"kind": "future_source", "payload": {"kept": True}},
|
||||
}]}
|
||||
v.validate_marker_value_badges(future, future)
|
||||
with pytest.raises(v.MarkerControlError) as changed:
|
||||
v.validate_marker_value_badges(future, {"markers": []})
|
||||
assert changed.value.code == "invalid_value_source"
|
||||
with pytest.raises(v.MarkerControlError) as imported:
|
||||
v.validate_marker_value_badges(future, validate_all=True)
|
||||
assert imported.value.code == "invalid_value_source"
|
||||
|
||||
invalid_attr = {"markers": [{
|
||||
"id": "bad", "binding": "entity:sensor.x", "display": "value",
|
||||
"value_source": {
|
||||
"kind": "entity_attribute", "entity_id": "sensor.x",
|
||||
"attribute": "entity_picture",
|
||||
},
|
||||
}]}
|
||||
with pytest.raises(v.MarkerControlError) as attribute:
|
||||
v.validate_marker_value_badges(invalid_attr, {"markers": []})
|
||||
assert attribute.value.code == "invalid_value_source_attribute"
|
||||
|
||||
|
||||
def test_issue_378_value_face_marker_reference_and_id_rename_are_delta_safe():
|
||||
config = {"markers": [
|
||||
{"id": "lamp", "binding": "virtual", "is_light": True},
|
||||
{"id": "controller", "binding": "device:controller", "display": "value",
|
||||
"value_source": {"kind": "derived_marker_state", "ref": "marker:lamp"}},
|
||||
]}
|
||||
v.validate_marker_value_badges(config, {"markers": []})
|
||||
|
||||
missing = {"markers": [
|
||||
config["markers"][0],
|
||||
{**config["markers"][1], "value_source": {
|
||||
"kind": "derived_marker_state", "ref": "marker:gone",
|
||||
}},
|
||||
]}
|
||||
with pytest.raises(v.MarkerControlError) as absent:
|
||||
v.validate_marker_value_badges(missing, config)
|
||||
assert absent.value.code == "value_source_marker_missing"
|
||||
|
||||
not_light = {"markers": [
|
||||
{**config["markers"][0], "is_light": False}, config["markers"][1],
|
||||
]}
|
||||
with pytest.raises(v.MarkerControlError) as target:
|
||||
v.validate_marker_value_badges(not_light, {"markers": []})
|
||||
assert target.value.code == "value_source_marker_not_light"
|
||||
|
||||
# Stable binding identifies a renamed marker, so dormant unchanged source data
|
||||
# remains old data instead of becoming a newly-invalid write.
|
||||
future_old = {"markers": [{
|
||||
"id": "old", "binding": "device:controller", "display": "value",
|
||||
"value_source": {"kind": "future_source", "payload": 1},
|
||||
}]}
|
||||
future_renamed = {"markers": [{**future_old["markers"][0], "id": "new"}]}
|
||||
v.validate_marker_value_badges(future_renamed, future_old)
|
||||
|
||||
Reference in New Issue
Block a user