mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 21:28:59 +00:00
fix: follow marker placement for room climate
Issue: #317 User-Visible: yes
This commit is contained in:
@@ -1,39 +1,39 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f",
|
||||
"fingerprint": "a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js",
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 256091,
|
||||
"initialViewGzipBytes": 256127,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-SftP4Auy.js",
|
||||
"houseplan-assets/editor-DuOmG3Hn.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/de-AOEY0-Iu.js",
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"lazyGzipBytes": 160351,
|
||||
"lazyGzipBytes": 160345,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-DuOmG3Hn.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 131833,
|
||||
"lazyEditorGzipBytes": 131827,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7198,
|
||||
"lazyOnboardingGzipBytes": 7197,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-SftP4Auy.js"
|
||||
"houseplan-assets/de-AOEY0-Iu.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 21873,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-SftP4Auy.js",
|
||||
"sha256": "3bee20ff5a52b6c6726cf857545bc3e1061a1249a515df5660491fd662218c94",
|
||||
"path": "houseplan-assets/de-AOEY0-Iu.js",
|
||||
"sha256": "af2926bc08c1d4d617b844f53b7aa8ef772b9751c15570b78b2fb24ab1109c20",
|
||||
"rawBytes": 71477,
|
||||
"gzipBytes": 21873,
|
||||
"isEntry": false,
|
||||
@@ -41,71 +41,71 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-DuOmG3Hn.js",
|
||||
"sha256": "d90a38e511769dc68d69081375ffb5067b196d429ab59fc4de06068baeafcac8",
|
||||
"path": "houseplan-assets/editor-2znZ5Si4.js",
|
||||
"sha256": "f701b93ee31570464f43993fb38d74f142f7e2ff1033e38f6ba79a8c17b6e858",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1584,
|
||||
"gzipBytes": 1582,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js"
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-BU843YpJ.js",
|
||||
"sha256": "1937126237e02356adad8b8c34b4adb29892bc183d6bcaa17e2542404907c811",
|
||||
"rawBytes": 910424,
|
||||
"gzipBytes": 255907,
|
||||
"path": "houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"sha256": "6d133886e636a0cd8a38a321f4bec4eac300bd7191631494e895c299b2d9bb6e",
|
||||
"rawBytes": 910936,
|
||||
"gzipBytes": 255944,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-SftP4Auy.js",
|
||||
"houseplan-assets/editor-DuOmG3Hn.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js"
|
||||
"houseplan-assets/de-AOEY0-Iu.js",
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
|
||||
"sha256": "51fdd8f9766569a3b1af3b04d91dea81e6b555d9784bf5be53c6f82a42fbe51e",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"sha256": "a644ece8d0cb490b6c41949e25fa80e64580be2d0d4a0e3fc4e1669b33777f9e",
|
||||
"rawBytes": 504959,
|
||||
"gzipBytes": 129696,
|
||||
"gzipBytes": 129693,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
|
||||
"sha256": "19299e14098c6e413ca410d7b2c3715d57c945fdb690991f92fa2dc769d9c84d",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"sha256": "ad84c9b77ef8b3920a69ba80700c97d558c2f6229d2cb589e9548451ef3328c9",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6645,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-DIa5HjfJ.js",
|
||||
"sha256": "93f68c225dc00086ea86ac8a97ddfdf49b99ff17a8221f3cc3796f92d5e9f3cb",
|
||||
"path": "houseplan-assets/space-deletion-B9Tp5xY6.js",
|
||||
"sha256": "00bb44aff178ffb82ec71bcd59b67d1a12137ecb4379ec0d606bd613b4447c1b",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 553,
|
||||
"gzipBytes": 552,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "823dc598d07959ff800a942ca1b705bd54b94dd4def67d66668f2a5552cc271f",
|
||||
"sha256": "6c720bd98beee922433c17737fe95dc4c98c69ab182ddcddd05c42a82d50f968",
|
||||
"rawBytes": 190,
|
||||
"gzipBytes": 184,
|
||||
"gzipBytes": 183,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js"
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-BU843YpJ.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-BsbMwnTm.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);
|
||||
+82
-82
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+6
-6
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";export{t as a,e as c,o as i,r as s,s as t};
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";export{t as a,e as c,o as i,r as s,s as t};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-BU843YpJ.js";
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-BsbMwnTm.js";
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Issue #317: a real sensor explicitly placed into a House Plan room owns the
|
||||
// automatic room-climate vote, including when that room has no HA Area. The
|
||||
// same frame feeds label, tooltip, temperature fill and hosted Static.
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch();
|
||||
const out = await page.evaluate(async () => {
|
||||
const result = {};
|
||||
const card = window.__card;
|
||||
const root = () => card.shadowRoot || card.renderRoot;
|
||||
const baseHass = window.__mkHass();
|
||||
const localRoom = {
|
||||
id: 'local-climate', name: 'Local climate', area: null,
|
||||
poly: [[0.08, 0.62], [0.38, 0.62], [0.38, 0.82], [0.08, 0.82]],
|
||||
};
|
||||
const config = JSON.parse(JSON.stringify(card._serverCfg));
|
||||
const floor = config.spaces.find((space) => space.id === 'f1');
|
||||
floor.rooms.push(localRoom);
|
||||
floor.settings = {
|
||||
...(floor.settings || {}), show_names: true, label_temp: true,
|
||||
label_hum: true, fill_mode: 'temp',
|
||||
};
|
||||
config.markers = [
|
||||
...(config.markers || []).filter((marker) => marker.binding !== 'device:d_temp'),
|
||||
{
|
||||
id: 'd_temp', binding: 'device:d_temp', space: 'f1', area: null,
|
||||
room_id: localRoom.id, hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
card._serverCfg = config;
|
||||
card._cfgEpoch++;
|
||||
card._regSignature = '';
|
||||
card._maybeRebuildDevices();
|
||||
card.hass = { ...baseHass };
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
|
||||
const localKey = '@room/f1/local-climate';
|
||||
result.localAutomatic = card._climate().get(localKey)?.temp === 22.4;
|
||||
result.registryRoomReleased = card._climate().get('living_room')?.temp == null;
|
||||
const localLabel = root().querySelector('[data-hp="room-label"][data-id="local-climate"]');
|
||||
result.labelShowsAutomatic = localLabel?.textContent.includes('22.4°') === true;
|
||||
result.oldLabelDropsMovedValue = root()
|
||||
.querySelector('[data-hp="room-label"][data-id="r1"]')?.textContent.includes('22.4°') === false;
|
||||
const localShape = root().querySelector('[data-hp="room"][data-id="local-climate"]');
|
||||
const oldShape = root().querySelector('[data-hp="room"][data-id="r1"]');
|
||||
result.localTempFill = localShape?.classList.contains('filled') === true;
|
||||
result.oldTempFillRemoved = oldShape?.classList.contains('filled') === false;
|
||||
|
||||
localShape?.dispatchEvent(new PointerEvent('pointermove', {
|
||||
bubbles: true, pointerType: 'mouse', clientX: 120, clientY: 180,
|
||||
}));
|
||||
await card.updateComplete;
|
||||
result.tooltipSharesAutomatic = card._tip?.title === 'Local climate'
|
||||
&& card._tip?.temp === 22.4;
|
||||
|
||||
// Hosted Static receives the same config and active HA snapshot. It has no
|
||||
// tooltip/metric row by design, but its temperature fill must use the same
|
||||
// room target rather than the old visible-device/HA-Area helper.
|
||||
await customElements.whenDefined('houseplan-space-card');
|
||||
const staticHass = {
|
||||
...baseHass,
|
||||
callWS: async (message) => {
|
||||
if (message.type === 'houseplan/config/get') return { config, rev: 317, can_write: true };
|
||||
if (message.type === 'houseplan/layout/get') return { layout: {}, rev: 317 };
|
||||
return baseHass.callWS(message);
|
||||
},
|
||||
};
|
||||
const staticCard = document.createElement('houseplan-space-card');
|
||||
staticCard.setConfig({ type: 'custom:houseplan-space-card', space: 'f1', show_button: false });
|
||||
staticCard.hass = staticHass;
|
||||
document.body.appendChild(staticCard);
|
||||
const deadline = Date.now() + 6000;
|
||||
while (!staticCard.renderRoot?.querySelector('[data-hp="room"][data-id="local-climate"]')
|
||||
&& Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 60));
|
||||
await staticCard.updateComplete;
|
||||
const staticRoot = staticCard.shadowRoot || staticCard.renderRoot;
|
||||
result.staticLocalTempFill = staticRoot
|
||||
?.querySelector('[data-hp="room"][data-id="local-climate"]')
|
||||
?.classList.contains('filled') === true;
|
||||
result.staticOldFillRemoved = staticRoot
|
||||
?.querySelector('[data-hp="room"][data-id="r1"]')
|
||||
?.classList.contains('filled') === false;
|
||||
staticCard.remove();
|
||||
|
||||
// A configured source remains an override, not another vote or a fallback.
|
||||
const configuredRoom = floor.rooms.find((room) => room.id === localRoom.id);
|
||||
configuredRoom.settings = { temp_source: 'entity:sensor.override_temperature' };
|
||||
card._serverCfg = { ...config, spaces: [...config.spaces] };
|
||||
card._cfgEpoch++;
|
||||
card.hass = {
|
||||
...baseHass,
|
||||
states: {
|
||||
...baseHass.states,
|
||||
'sensor.override_temperature': {
|
||||
entity_id: 'sensor.override_temperature', state: '17',
|
||||
attributes: { device_class: 'temperature', unit_of_measurement: '°C' },
|
||||
},
|
||||
},
|
||||
};
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
result.explicitSourceWins = card._roomTemp(card._spaceModel().rooms
|
||||
.find((room) => room.id === localRoom.id)) === 17;
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
checkAll(out);
|
||||
await finish(browser, out);
|
||||
Vendored
+46
-46
@@ -1,39 +1,39 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f",
|
||||
"fingerprint": "a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js",
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 256091,
|
||||
"initialViewGzipBytes": 256127,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-SftP4Auy.js",
|
||||
"houseplan-assets/editor-DuOmG3Hn.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/de-AOEY0-Iu.js",
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"lazyGzipBytes": 160351,
|
||||
"lazyGzipBytes": 160345,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-DuOmG3Hn.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 131833,
|
||||
"lazyEditorGzipBytes": 131827,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7198,
|
||||
"lazyOnboardingGzipBytes": 7197,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-SftP4Auy.js"
|
||||
"houseplan-assets/de-AOEY0-Iu.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 21873,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-SftP4Auy.js",
|
||||
"sha256": "3bee20ff5a52b6c6726cf857545bc3e1061a1249a515df5660491fd662218c94",
|
||||
"path": "houseplan-assets/de-AOEY0-Iu.js",
|
||||
"sha256": "af2926bc08c1d4d617b844f53b7aa8ef772b9751c15570b78b2fb24ab1109c20",
|
||||
"rawBytes": 71477,
|
||||
"gzipBytes": 21873,
|
||||
"isEntry": false,
|
||||
@@ -41,71 +41,71 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-DuOmG3Hn.js",
|
||||
"sha256": "d90a38e511769dc68d69081375ffb5067b196d429ab59fc4de06068baeafcac8",
|
||||
"path": "houseplan-assets/editor-2znZ5Si4.js",
|
||||
"sha256": "f701b93ee31570464f43993fb38d74f142f7e2ff1033e38f6ba79a8c17b6e858",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1584,
|
||||
"gzipBytes": 1582,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js"
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-BU843YpJ.js",
|
||||
"sha256": "1937126237e02356adad8b8c34b4adb29892bc183d6bcaa17e2542404907c811",
|
||||
"rawBytes": 910424,
|
||||
"gzipBytes": 255907,
|
||||
"path": "houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"sha256": "6d133886e636a0cd8a38a321f4bec4eac300bd7191631494e895c299b2d9bb6e",
|
||||
"rawBytes": 910936,
|
||||
"gzipBytes": 255944,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-SftP4Auy.js",
|
||||
"houseplan-assets/editor-DuOmG3Hn.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js"
|
||||
"houseplan-assets/de-AOEY0-Iu.js",
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-pSbu_R5t.js",
|
||||
"sha256": "51fdd8f9766569a3b1af3b04d91dea81e6b555d9784bf5be53c6f82a42fbe51e",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"sha256": "a644ece8d0cb490b6c41949e25fa80e64580be2d0d4a0e3fc4e1669b33777f9e",
|
||||
"rawBytes": 504959,
|
||||
"gzipBytes": 129696,
|
||||
"gzipBytes": 129693,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DXJhYLas.js",
|
||||
"sha256": "19299e14098c6e413ca410d7b2c3715d57c945fdb690991f92fa2dc769d9c84d",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"sha256": "ad84c9b77ef8b3920a69ba80700c97d558c2f6229d2cb589e9548451ef3328c9",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6645,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js",
|
||||
"houseplan-assets/space-deletion-DIa5HjfJ.js"
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-DIa5HjfJ.js",
|
||||
"sha256": "93f68c225dc00086ea86ac8a97ddfdf49b99ff17a8221f3cc3796f92d5e9f3cb",
|
||||
"path": "houseplan-assets/space-deletion-B9Tp5xY6.js",
|
||||
"sha256": "00bb44aff178ffb82ec71bcd59b67d1a12137ecb4379ec0d606bd613b4447c1b",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 553,
|
||||
"gzipBytes": 552,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "823dc598d07959ff800a942ca1b705bd54b94dd4def67d66668f2a5552cc271f",
|
||||
"sha256": "6c720bd98beee922433c17737fe95dc4c98c69ab182ddcddd05c42a82d50f968",
|
||||
"rawBytes": 190,
|
||||
"gzipBytes": 184,
|
||||
"gzipBytes": 183,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BU843YpJ.js"
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-BU843YpJ.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-BsbMwnTm.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);
|
||||
+82
-82
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";export{t as a,e as c,o as i,r as s,s as t};
|
||||
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";export{t as a,e as c,o as i,r as s,s as t};
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0c61742c98d55aec077478fcee7b8acf52e2ac1b70e2ed23532483b845ca884f";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-BU843YpJ.js";
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-BsbMwnTm.js";
|
||||
|
||||
+16
-6
@@ -1145,12 +1145,22 @@ preserved instead of repeatedly snapping back to that hash. The legacy field
|
||||
initializer is never a cold-start choice by itself. A plan with no spaces keeps
|
||||
`null` authority and does not invent an id.
|
||||
|
||||
**Room climate is one pass per hass snapshot** (review R2-3). `areaClimateMap()`
|
||||
classifies the whole registry once and returns `Map<area, {temp, hum}>`; the
|
||||
card memoizes it on `hass` identity, which Home Assistant replaces on every
|
||||
state change. Per-room lookups are O(1). `areaClimate()` survives as a
|
||||
single-area wrapper for tests — using it in a render reintroduces the
|
||||
O(rooms × entities) cost it was extracted from.
|
||||
**Room climate is one pass per hass snapshot** (review R2-3, issue #317).
|
||||
`roomClimateMap()` classifies the whole active registry once and returns one
|
||||
`{temp, hum}` aggregate per effective room target. HA-area rooms keep their
|
||||
area key. An explicitly placed marker overrides registry placement; an
|
||||
area-less room uses a collision-safe `space + room_id` key. Exact `entity:`
|
||||
placement wins over its parent `device:` placement for that entity, so a
|
||||
reading cannot remain in the old Area and vote twice. The card memoizes the map
|
||||
on `hass`, rules and markers; per-room lookups are O(1). `areaClimateMap()` and
|
||||
`areaClimate()` survive as compatibility wrappers — using the single-area
|
||||
wrapper in a render reintroduces the O(rooms × entities) cost the map removed.
|
||||
|
||||
Explicit room `temp_source`/`hum_source` remains above the automatic aggregate.
|
||||
Hidden live markers still contribute; removed and HA-disabled bindings do not.
|
||||
Full View and hosted Static use the same resolver and bounded active render
|
||||
snapshot, so a state tick cannot update a temperature fill through a different
|
||||
membership rule.
|
||||
|
||||
**File uploads go over HTTP** (not WS, which has a message-size limit): `POST /api/houseplan/upload`
|
||||
(multipart: marker_id + file), HomeAssistantView, requires_auth. Served from `/houseplan_files/files/`.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Room temperature and humidity now follow where a real sensor is placed in
|
||||
House Plan instead of remaining in its Home Assistant Area. This also gives
|
||||
automatic climate values to rooms without an HA Area; labels, tooltips and
|
||||
temperature fills stay in sync ([#317](https://github.com/Matysh/houseplan-card/issues/317)).
|
||||
|
||||
- Active physical wall controllers that Home Assistant exposes without any own
|
||||
entities no longer look permanently unavailable: they now stay opaque and
|
||||
follow their controlled target as yellow when working or neutral otherwise
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Температура и влажность комнаты теперь следуют фактическому размещению
|
||||
реального датчика в House Plan, а не остаются в его HA-зоне. Автоматические
|
||||
климатические показатели работают и в комнатах без HA-зоны; подпись,
|
||||
подсказка и температурная заливка согласованы
|
||||
([#317](https://github.com/Matysh/houseplan-card/issues/317)).
|
||||
|
||||
- Активные физические настенные выключатели, для которых Home Assistant не
|
||||
предоставляет собственных сущностей, больше не выглядят постоянно
|
||||
недоступными: теперь они остаются непрозрачными и становятся жёлтыми при
|
||||
|
||||
+11
-1
@@ -704,6 +704,14 @@ missing destructive confirmation or an editor exception that breaks View.
|
||||
placed on the plan (hidden by filtering or by the user) still feeds the
|
||||
room card, the tooltip and the temperature fill; fridges/TRVs still do
|
||||
not; an explicit per-room source still wins [auto: unit devices.test]
|
||||
- [ ] Room climate follows explicit House Plan placement (#317): a real
|
||||
temperature/humidity sensor moved away from registry Area A votes exactly
|
||||
once in its marker target, including an area-less `space + room_id` room;
|
||||
hidden markers still vote, while removed/HA-disabled ones do not. Exact
|
||||
entity placement wins over its parent device only for that entity, and
|
||||
explicit room sources remain authoritative [auto:
|
||||
smoke_room_climate_placement; units: test/devices.test.mjs; mutation:
|
||||
room-climate-ignores-marker-placement]
|
||||
- [ ] Room hover + tooltip: in View, hovering any room visibly highlights it
|
||||
(filled, transparent and area-less alike) and shows its name plus clean-
|
||||
floor area; temperature/signal follow when available. Thick walls reduce
|
||||
@@ -1441,7 +1449,9 @@ separately promised workflows:
|
||||
- [ ] Climate cost does not grow with rooms (v1.45.0, review R2-3): on a plan
|
||||
with dozens of rooms an unrelated HA state update triggers ONE registry
|
||||
pass, repeated renders on the same snapshot trigger none, and a changed
|
||||
sensor value is still visible immediately [auto: smoke_climate_once]
|
||||
sensor value is still visible immediately. Explicit marker targets and
|
||||
area-less rooms are indexed before this same pass; they do not add a pass
|
||||
per room [auto: smoke_climate_once + smoke_room_climate_placement]
|
||||
- [ ] Plan upload survives a concurrent config revision (v1.44.8): with a second
|
||||
tab open on the same plan, attach a background image in space settings —
|
||||
the plan shows immediately, `plan_url` is in `.storage/houseplan.config`,
|
||||
|
||||
+6
-2
@@ -428,8 +428,12 @@ per gesture, names the rule the next step would break.
|
||||
### HA area binding
|
||||
|
||||
One HA area may be bound to one room. The binding drives automatic device
|
||||
placement, room LQI, light and aggregate climate. A room without an area may
|
||||
still contain manually assigned devices and explicit measurement sources.
|
||||
placement, room LQI, light and aggregate climate. A real temperature or
|
||||
humidity sensor manually assigned to another House Plan room follows that
|
||||
placement for the automatic room average instead of remaining in its registry
|
||||
HA area. This also works for a room without an area; its manually assigned real
|
||||
sensors provide the automatic average, while an explicit room measurement
|
||||
source still takes priority.
|
||||
|
||||
## 9. Doors, windows, gates and locks
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ Undo оптимизации.
|
||||
|
||||
### Связь с HA-зоной
|
||||
|
||||
Одна HA-зона может быть назначена только одной комнате. Связь нужна для автоматического размещения устройств, LQI, света и средних климатических показателей. Комната без HA-зоны всё равно может иметь вручную назначенные устройства и отдельные источники температуры/влажности.
|
||||
Одна HA-зона может быть назначена только одной комнате. Связь нужна для автоматического размещения устройств, LQI, света и средних климатических показателей. Если реальный датчик температуры или влажности вручную назначен другой комнате House Plan, в автоматическом среднем он следует этому размещению и больше не учитывается в исходной HA-зоне. Это работает и для комнаты без HA-зоны: вручную назначенные реальные датчики автоматически дают ей среднее, а отдельно выбранный источник температуры/влажности по-прежнему имеет высший приоритет.
|
||||
|
||||
### Инструменты плана
|
||||
|
||||
@@ -798,8 +798,8 @@ binding tombstone не вырезает её из живого устройст
|
||||
| Является источником света | **Авто** использует роль устройства, **Всегда** создаёт собственный источник даже без entity HA, **Никогда** исключает только собственный источник. Внешние `controls` продолжают учитываться в свете комнаты |
|
||||
| Ведущая сущность | Появляется для роли **Всегда**, если у устройства несколько `light.*`/`switch.*`. Выбранная сущность определяет состояние и service call; исчезнувший выбор не стирается и временно заменяется безопасным fallback |
|
||||
| Цвет и яркость свечения | **Из источника** использует живые данные HA; **Задать цвет** сохраняет живую яркость; **Задать цвет и яркость** фиксирует оба параметра (1–100%) |
|
||||
| Использовать температуру climate | Добавляет `current_temperature` термостата/кондиционера в маркер и среднее комнаты |
|
||||
| Скрыть / Показать | Кнопка в левом нижнем углу диалога. После сохранения убирает или возвращает значок; LQI скрытого устройства всё ещё может участвовать в среднем комнаты |
|
||||
| Использовать температуру climate | Добавляет `current_temperature` термостата/кондиционера в маркер и среднее назначенной ему комнаты |
|
||||
| Скрыть / Показать | Кнопка в левом нижнем углу диалога. После сохранения убирает или возвращает значок; LQI и климатические данные скрытого устройства всё ещё могут участвовать в среднем назначенной комнаты |
|
||||
| Удалить | Кнопка рядом со «Скрыть». После подтверждения сразу полностью убирает устройство с плана и позволяет позднее добавить его заново |
|
||||
| Радиус свечения | Локально переопределяет общий радиус, 0.1–100 м в хранилище |
|
||||
| Иконка | Ручная `mdi:*`; пусто — автоматический подбор. Автовариант показывается в поле как значок с именем `mdi:*`, но не сохраняется как ручная замена |
|
||||
@@ -861,7 +861,7 @@ binding tombstone не вырезает её из живого устройст
|
||||
- в редакторе виден только при включённом в каталоге **Показывать скрытые на плане**;
|
||||
- не показывает цвет состояния, число, температуру, влажность и LQI;
|
||||
- не создаёт световое пятно и не считается источником для заливки «Свет»;
|
||||
- может продолжать участвовать в среднем LQI комнаты, потому что оно считается по реестру устройств зоны.
|
||||
- может продолжать участвовать в среднем LQI и климатических показателях назначенной комнаты; скрытие значка не меняет принадлежность данных.
|
||||
|
||||
### Деактивированные в Home Assistant
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": null,
|
||||
"sourceFingerprint": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceFingerprint": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -103,7 +103,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "19a7251bbfeac721a2d2e08aa441e00c2bd751924b56cd5fa3d223b4b023b745",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,19 @@ function relocateEditorPatch(patch, cardSource, editorSource) {
|
||||
// `find` обязан встречаться в файле ровно один раз: патч, который ложится «куда
|
||||
// попало», проверяет не то, что объявлен проверять. Это контролирует --check.
|
||||
const MUTANT_DEFINITIONS = [
|
||||
{
|
||||
id: 'room-climate-ignores-marker-placement',
|
||||
guard: 'node demo/smoke_room_climate_placement.mjs',
|
||||
because: 'a real sensor manually placed into a House Plan room must leave its registry HA '
|
||||
+ 'Area and feed the local room label, tooltip and temperature fill exactly once (#317)',
|
||||
patches: [{
|
||||
file: 'src/devices.ts',
|
||||
find: ' const target = entityTargets.get(eid)\n'
|
||||
+ ' || (reg.device_id ? deviceTargets.get(reg.device_id) : null)\n'
|
||||
+ ' || reg.area_id || dev?.area_id || null;',
|
||||
replace: ' const target = reg.area_id || dev?.area_id || null;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'editor-runtime-fingerprint-handshake',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
|
||||
/** @type {SmokeLink[]} */
|
||||
export const SMOKE_LINKS = [
|
||||
{
|
||||
symbols: ['roomClimateKey', 'roomClimateMap'],
|
||||
smokes: ['smoke_room_climate_placement.mjs'],
|
||||
because: 'the #317 production-bundle scenario proves one manually placed sensor through '
|
||||
+ 'room label, tooltip, temperature fill and hosted Static; those DOM surfaces cannot call '
|
||||
+ 'the pure room-target helpers directly',
|
||||
},
|
||||
{
|
||||
symbols: [
|
||||
'LanguageRuntime', 'LanguageRuntimeContract', 'languageRenderGate',
|
||||
|
||||
+66
-12
@@ -1383,6 +1383,32 @@ const NON_AIR_RE = new RegExp(
|
||||
*/
|
||||
export interface AreaClimate { temp: number | null; hum: number | null }
|
||||
|
||||
/**
|
||||
* Stable lookup key for one room's automatic climate aggregate.
|
||||
*
|
||||
* HA-area rooms deliberately keep their historical raw area id. A local room
|
||||
* has no HA identity, so its key is namespaced by both space and room id; room
|
||||
* ids alone are not globally unique across imported spaces.
|
||||
*/
|
||||
export function roomClimateKey(
|
||||
spaceId: string | null | undefined,
|
||||
room: { id?: string | null; area?: string | null },
|
||||
): string | null {
|
||||
if (room.area) return room.area;
|
||||
if (!spaceId || !room.id) return null;
|
||||
return `@room/${encodeURIComponent(spaceId)}/${encodeURIComponent(room.id)}`;
|
||||
}
|
||||
|
||||
function markerClimateTarget(marker: Marker): string | null {
|
||||
// Persisted local-room placement uses an explicit null Area. Do not treat a
|
||||
// stale room_id without its space as a valid target or let it collide with a
|
||||
// room imported into another space.
|
||||
if (marker.area === null && marker.space && marker.room_id) {
|
||||
return roomClimateKey(marker.space, { id: marker.room_id, area: null });
|
||||
}
|
||||
return marker.area || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Climate for EVERY area in one registry pass (review R2-3).
|
||||
*
|
||||
@@ -1391,8 +1417,8 @@ export interface AreaClimate { temp: number | null; hum: number | null }
|
||||
* render — an entire frame spent re-reading metadata that did not change. The
|
||||
* caller computes this map once per `hass` snapshot and looks rooms up in O(1).
|
||||
*/
|
||||
export function areaClimateMap(
|
||||
hass: any, rules?: CompiledIconRule[], markers?: Marker[] | null,
|
||||
export function roomClimateMap(
|
||||
hass: BuildCtx['hass'], rules?: CompiledIconRule[], markers?: Marker[] | null,
|
||||
): Map<string, AreaClimate> {
|
||||
const out = new Map<string, AreaClimate>();
|
||||
if (!hass?.entities) return out;
|
||||
@@ -1403,13 +1429,27 @@ export function areaClimateMap(
|
||||
// markers the pass below is byte-for-byte the old one.
|
||||
const removed = removedPlanBindings(markers);
|
||||
const climOpt = new Set<string>();
|
||||
const entityTargets = new Map<string, string>();
|
||||
const deviceTargets = new Map<string, string>();
|
||||
for (const m of markers || []) {
|
||||
if (m?.removed || m?.use_climate_temp !== true) continue;
|
||||
if (m?.removed) continue;
|
||||
const i = (m.binding || '').indexOf(':');
|
||||
if (i > 0) climOpt.add(m.binding.slice(i + 1));
|
||||
if (i <= 0) continue;
|
||||
const kind = m.binding.slice(0, i);
|
||||
const ref = m.binding.slice(i + 1);
|
||||
if (!ref) continue;
|
||||
if (m.use_climate_temp === true) climOpt.add(ref);
|
||||
const target = markerClimateTarget(m);
|
||||
if (!target) continue;
|
||||
// Match buildDevices(): malformed duplicate live bindings resolve to the
|
||||
// first saved marker, never to two rooms and never to two votes.
|
||||
if (kind === 'entity' && !entityTargets.has(ref)) entityTargets.set(ref, target);
|
||||
else if (kind === 'device' && !deviceTargets.has(ref)) deviceTargets.set(ref, target);
|
||||
}
|
||||
// area -> device (or lone entity) -> the entities that belong to it
|
||||
const byArea = new Map<string, Map<string, { name: string; model?: string; ents: string[] }>>();
|
||||
// effective room target -> physical device (or lone entity) -> entities.
|
||||
// Choosing the target before grouping guarantees that a manually moved
|
||||
// reading cannot remain in its registry Area at the same time (#317).
|
||||
const byTarget = new Map<string, Map<string, { name: string; model?: string; ents: string[] }>>();
|
||||
for (const [eid, reg] of Object.entries<any>(hass.entities)) {
|
||||
// A device tombstone suppresses all of its data. An entity tombstone only
|
||||
// suppresses a standalone entity; inside its live parent device it remains
|
||||
@@ -1417,8 +1457,11 @@ export function areaClimateMap(
|
||||
if ((reg.device_id && removed.devices.has(reg.device_id) && !removed.liveEntities.has(eid))
|
||||
|| (!reg.device_id && removed.entities.has(eid) && !removed.liveEntities.has(eid))) continue;
|
||||
const dev = reg.device_id ? hass.devices?.[reg.device_id] : null;
|
||||
const area = reg.area_id || dev?.area_id || null;
|
||||
if (!area) continue;
|
||||
if (!isRegistryEntryEnabled(reg) || (dev && !isRegistryEntryEnabled(dev))) continue;
|
||||
const target = entityTargets.get(eid)
|
||||
|| (reg.device_id ? deviceTargets.get(reg.device_id) : null)
|
||||
|| reg.area_id || dev?.area_id || null;
|
||||
if (!target) continue;
|
||||
// Not every "temperature" is room air. Real finds on a live install: the
|
||||
// NAS processor temperature, the water in a smart kettle, a sauna heater at
|
||||
// 90 C and a virtual better_thermostat duplicating the real sensor (field
|
||||
@@ -1433,8 +1476,8 @@ export function areaClimateMap(
|
||||
if (EXCLUDED_DOMAINS.has(reg.platform)) continue; // filtered-out integrations
|
||||
if (NON_AIR_RE.test(eid)) continue; // water/chip/flow/target/...
|
||||
}
|
||||
let groups = byArea.get(area);
|
||||
if (!groups) { groups = new Map(); byArea.set(area, groups); }
|
||||
let groups = byTarget.get(target);
|
||||
if (!groups) { groups = new Map(); byTarget.set(target, groups); }
|
||||
const key = reg.device_id || eid;
|
||||
let g = groups.get(key);
|
||||
if (!g) {
|
||||
@@ -1448,7 +1491,7 @@ export function areaClimateMap(
|
||||
}
|
||||
g.ents.push(eid);
|
||||
}
|
||||
for (const [area, groups] of byArea) {
|
||||
for (const [target, groups] of byTarget) {
|
||||
const temps: number[] = [];
|
||||
const hums: number[] = [];
|
||||
for (const [key, g] of groups) {
|
||||
@@ -1469,7 +1512,7 @@ export function areaClimateMap(
|
||||
}
|
||||
}
|
||||
if (!temps.length && !hums.length) continue;
|
||||
out.set(area, {
|
||||
out.set(target, {
|
||||
temp: temps.length ? Math.round((temps.reduce((a, b) => a + b, 0) / temps.length) * 10) / 10 : null,
|
||||
hum: hums.length ? Math.round(hums.reduce((a, b) => a + b, 0) / hums.length) : null,
|
||||
});
|
||||
@@ -1477,6 +1520,17 @@ export function areaClimateMap(
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Historical area-only name retained for callers and integrations. The map
|
||||
* still uses raw HA area ids; when explicit local-room markers are supplied it
|
||||
* may additionally contain namespaced room keys returned by roomClimateKey().
|
||||
*/
|
||||
export function areaClimateMap(
|
||||
hass: BuildCtx['hass'], rules?: CompiledIconRule[], markers?: Marker[] | null,
|
||||
): Map<string, AreaClimate> {
|
||||
return roomClimateMap(hass, rules, markers);
|
||||
}
|
||||
|
||||
/** One area's reading. Convenience wrapper — prefer the map for many areas. */
|
||||
export function areaClimate(
|
||||
hass: any, area: string, kind: 'temp' | 'hum', rules?: CompiledIconRule[],
|
||||
|
||||
+11
-9
@@ -112,7 +112,7 @@ import {
|
||||
} from './vacuum';
|
||||
import {
|
||||
buildDevices, deviceFromMarkerDraft, seedHiddenBindings, lqiFor, tempFor, humFor, climateTempFor, isHumEntity,
|
||||
areaTemp, areaHum, sourceValue, areaClimateMap,
|
||||
areaTemp, areaHum, sourceValue, roomClimateKey, roomClimateMap,
|
||||
resolvedLightSources, resolvedLightState, resolvedLightStats,
|
||||
hasOwnSpatialSource, hasOwnStatefulLightSource, ownControllableEntities,
|
||||
forcedLightEntityOf,
|
||||
@@ -11956,21 +11956,23 @@ export class HouseplanCard extends LitElement {
|
||||
private _roomTemp(r: RoomCfg): number | null {
|
||||
const src = r.settings?.temp_source;
|
||||
if (src) return sourceValue(this._renderPlanHass, src, 'temp', this._markers);
|
||||
// every sensor of the area, placed on the plan or not (field report)
|
||||
return r.area ? this._climate().get(r.area)?.temp ?? null : null;
|
||||
const key = roomClimateKey(this._spaceModel()?.id, r);
|
||||
return key ? this._climate().get(key)?.temp ?? null : null;
|
||||
}
|
||||
|
||||
/** Room humidity honouring the tier-3 source override. */
|
||||
private _roomHum(r: RoomCfg): number | null {
|
||||
const src = r.settings?.hum_source;
|
||||
if (src) return sourceValue(this._renderPlanHass, src, 'hum', this._markers);
|
||||
return r.area ? this._climate().get(r.area)?.hum ?? null : null;
|
||||
const key = roomClimateKey(this._spaceModel()?.id, r);
|
||||
return key ? this._climate().get(key)?.hum ?? null : null;
|
||||
}
|
||||
|
||||
private _climateCache: { h: any; r: any; mk: any; m: Map<string, AreaClimate> } | null = null;
|
||||
|
||||
/**
|
||||
* Climate for every area, computed ONCE per hass snapshot (review R2-3).
|
||||
* Climate for every HA Area and explicitly placed local room, computed ONCE
|
||||
* per hass snapshot (review R2-3, issue #317).
|
||||
* Home Assistant hands out a new `hass` object on every state change, so
|
||||
* identity is exactly the right cache key: fresh states always recompute,
|
||||
* and the 60 rooms of one render share a single registry pass instead of
|
||||
@@ -11983,7 +11985,7 @@ export class HouseplanCard extends LitElement {
|
||||
const planHass = this._renderPlanHass;
|
||||
const c = this._climateCache;
|
||||
if (c && c.h === planHass && c.r === this._iconRules && c.mk === mk) return c.m;
|
||||
const m = areaClimateMap(planHass, this._iconRules, mk);
|
||||
const m = roomClimateMap(planHass, this._iconRules, mk);
|
||||
this._climateCache = { h: planHass, r: this._iconRules, mk, m };
|
||||
return m;
|
||||
}
|
||||
@@ -12087,10 +12089,10 @@ export class HouseplanCard extends LitElement {
|
||||
const top = ((point[1] - view.y) / view.h) * 100;
|
||||
const op = Math.min(1, disp.opacity + 0.25);
|
||||
const k = this._labelScale(r);
|
||||
// Optional metrics row. Light sources may use an explicit room_id even
|
||||
// when this room has no HA area; the other aggregate metrics still need it.
|
||||
// Optional metrics row. Temperature/humidity and light sources may use an
|
||||
// explicit room_id even when this room has no HA Area. LQI remains area-only.
|
||||
const rows: TemplateResult[] = [];
|
||||
if (r.area || r.settings?.temp_source || r.settings?.hum_source || disp.labelLight) {
|
||||
if (disp.labelTemp || disp.labelHum || (disp.labelLqi && r.area) || disp.labelLight) {
|
||||
if (disp.labelTemp) {
|
||||
const t = this._roomTemp(r);
|
||||
if (t != null) rows.push(html`<span class="rlm"><ha-icon icon="mdi:thermometer"></ha-icon>${t}°</span>`);
|
||||
|
||||
+1
-1
@@ -2011,7 +2011,7 @@ export function isAlarmState(
|
||||
* Parse a marker's room reference. Two shapes:
|
||||
* - `space#area` — a room bound to an HA area (historical form)
|
||||
* - `space#@roomId` — a room WITHOUT an area (sub-area rooms, issue #3):
|
||||
* devices are placed into it manually, by room id.
|
||||
* devices and their room-aware data are assigned manually, by room id.
|
||||
*/
|
||||
export function parseRoomRef(
|
||||
v: string | null | undefined,
|
||||
|
||||
+19
-2
@@ -7,7 +7,10 @@
|
||||
* Geometry/model math lives in space-geometry.ts (pure, unit-tested).
|
||||
*/
|
||||
import { html, svg, nothing, type TemplateResult } from 'lit';
|
||||
import { buildDevices, areaLqi, areaTemp, resolvedLightSources, resolvedLightState } from './devices';
|
||||
import {
|
||||
buildDevices, areaLqi, roomClimateKey, roomClimateMap, sourceValue,
|
||||
resolvedLightSources, resolvedLightState,
|
||||
} from './devices';
|
||||
import {
|
||||
spaceDisplayOf, fillColorsOf, roomFillModeOf, roomGlowOf,
|
||||
roomCustomFillOf, resolveEffectiveRoomFill, stageBgOf, paperRoomShapes,
|
||||
@@ -197,6 +200,20 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
// two cards (HP-1511-01). Rendering still draws `devs` only.
|
||||
const defPos = defaultPositions(spaceDevs, space, iconPct);
|
||||
|
||||
// Hosted Static uses the same room-aware automatic aggregate as the full
|
||||
// card. Build it once for the whole render: room labels/fills must never
|
||||
// rescan the registry or fall back to visible markers only (#317).
|
||||
const iconRules = compileIconRules(
|
||||
o.cfg.settings?.icon_rules?.length ? o.cfg.settings.icon_rules : DEFAULT_ICON_RULES,
|
||||
);
|
||||
const roomClimate = roomClimateMap(planHass, iconRules, o.cfg.markers || []);
|
||||
const roomTemperature = (room: typeof space.rooms[number]): number | null => {
|
||||
const source = room.settings?.temp_source;
|
||||
if (source) return sourceValue(planHass, source, 'temp', o.cfg.markers || []);
|
||||
const key = roomClimateKey(space.id, room);
|
||||
return key ? roomClimate.get(key)?.temp ?? null : null;
|
||||
};
|
||||
|
||||
// docs/CANVAS.md §4: the static card frames the CONTENT, exactly like the
|
||||
// full one — `space.vb` is only the stored hint now, and on a plan drawn
|
||||
// past the old unit square it framed empty canvas with the house off-screen.
|
||||
@@ -268,7 +285,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
fill === 'light'
|
||||
? resolvedLightState(resolvedLightSources(planHass, spaceDevs, room, o.virtualLights))
|
||||
: 'none',
|
||||
fill === 'temp' && room.area ? areaTemp(planHass, spaceDevs, room.area) : null,
|
||||
fill === 'temp' ? roomTemperature(room) : null,
|
||||
disp.tempMin,
|
||||
disp.tempMax,
|
||||
colors,
|
||||
|
||||
+4
-1
@@ -142,7 +142,10 @@ export interface Marker {
|
||||
calibration?: Record<string, number[]>;
|
||||
segment_map?: Record<string, string>;
|
||||
} | null;
|
||||
room_id?: string | null; // manual placement into a room WITHOUT an HA area (sub-area rooms)
|
||||
/** Manual placement into a House Plan room without an HA Area. Room-aware
|
||||
* consumers resolve it together with `space`; visual placement is not its
|
||||
* only use. */
|
||||
room_id?: string | null;
|
||||
/** `ripple` is legacy read compatibility; UI maps it to icon_ripple. */
|
||||
display?: 'badge' | 'ripple' | 'icon_ripple' | 'value' | 'static_icon' | null; // how the device is drawn
|
||||
ripple_color?: string | null;
|
||||
|
||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
buildDevices, lightGroups, primaryEntity, lqiFor, tempFor, humFor, climateTempFor,
|
||||
areaLights, areaTemp, areaHum, areaLightStats, sourceValue, areaClimate, areaClimateMap,
|
||||
roomClimateKey, roomClimateMap,
|
||||
litLightEntity, resolvedDeviceStateEntities, resolvedLightSources, resolvedLightState,
|
||||
resolvedLightStats, hasOwnSpatialSource, selectSpatialGlowSource, seedHiddenBindings,
|
||||
deletePlanMarkerRecords, effectiveMarkerControls, persistedExternalControls,
|
||||
@@ -1300,6 +1301,133 @@ test('areaClimateMap: температура и влажность живут в
|
||||
assert.deepEqual(areaClimateMap(hass).get('hall'), { temp: 21.4, hum: 48 });
|
||||
});
|
||||
|
||||
test('issue 317: explicit device placement moves one climate vote between HA areas', () => {
|
||||
const hass = {
|
||||
devices: {
|
||||
moved: { id: 'moved', name: 'Moved thermometer', area_id: 'old' },
|
||||
stayed: { id: 'stayed', name: 'Stayed thermometer', area_id: 'old' },
|
||||
},
|
||||
entities: {
|
||||
'sensor.moved_temperature': { device_id: 'moved', platform: 'demo' },
|
||||
'sensor.stayed_temperature': { device_id: 'stayed', platform: 'demo' },
|
||||
},
|
||||
states: {
|
||||
'sensor.moved_temperature': { state: '20', attributes: { device_class: 'temperature' } },
|
||||
'sensor.stayed_temperature': { state: '30', attributes: { device_class: 'temperature' } },
|
||||
},
|
||||
};
|
||||
const moved = [{
|
||||
id: 'moved', binding: 'device:moved', space: 'f1', area: 'new', hidden: true,
|
||||
}];
|
||||
const map = roomClimateMap(hass, undefined, moved);
|
||||
assert.deepEqual(map.get('old'), { temp: 30, hum: null });
|
||||
assert.deepEqual(map.get('new'), { temp: 20, hum: null });
|
||||
|
||||
// Explicit and registry placement agree: the physical device still votes once.
|
||||
const same = [{ id: 'moved', binding: 'device:moved', space: 'f1', area: 'old' }];
|
||||
assert.deepEqual(roomClimateMap(hass, undefined, same).get('old'), { temp: 25, hum: null });
|
||||
});
|
||||
|
||||
test('issue 317: a hidden real sensor supplies an area-less House Plan room', () => {
|
||||
const hass = {
|
||||
devices: { q: { id: 'q', name: 'Qingping Air Monitor', area_id: 'registry-room' } },
|
||||
entities: {
|
||||
'sensor.q_temperature': { device_id: 'q', platform: 'xiaomi' },
|
||||
'sensor.q_humidity': { device_id: 'q', platform: 'xiaomi' },
|
||||
},
|
||||
states: {
|
||||
'sensor.q_temperature': { state: '21.4', attributes: { device_class: 'temperature' } },
|
||||
'sensor.q_humidity': { state: '48', attributes: { device_class: 'humidity' } },
|
||||
},
|
||||
};
|
||||
const marker = {
|
||||
id: 'q', binding: 'device:q', space: 'garden', area: null, room_id: 'shed', hidden: true,
|
||||
};
|
||||
const key = roomClimateKey('garden', { id: 'shed', area: null });
|
||||
assert.equal(key, '@room/garden/shed');
|
||||
const map = roomClimateMap(hass, undefined, [marker]);
|
||||
assert.deepEqual(map.get(key), { temp: 21.4, hum: 48 });
|
||||
assert.equal(map.get('registry-room'), undefined, 'manual placement owns the readings');
|
||||
assert.equal(roomClimateKey('other', { id: 'shed', area: null }), '@room/other/shed');
|
||||
assert.equal(roomClimateKey('', { id: 'shed', area: null }), null);
|
||||
});
|
||||
|
||||
test('issue 317: exact entity placement wins over its parent marker only for that entity', () => {
|
||||
const hass = {
|
||||
devices: { hub: { id: 'hub', name: 'Climate hub', area_id: 'registry-room' } },
|
||||
entities: {
|
||||
'sensor.hub_a_temperature': { device_id: 'hub', platform: 'demo' },
|
||||
'sensor.hub_b_temperature': { device_id: 'hub', platform: 'demo' },
|
||||
},
|
||||
states: {
|
||||
'sensor.hub_a_temperature': { state: '20', attributes: { device_class: 'temperature' } },
|
||||
'sensor.hub_b_temperature': { state: '40', attributes: { device_class: 'temperature' } },
|
||||
},
|
||||
};
|
||||
const markers = [
|
||||
{ id: 'hub', binding: 'device:hub', space: 'f1', area: 'parent-room' },
|
||||
{
|
||||
id: 'hub-a', binding: 'entity:sensor.hub_a_temperature',
|
||||
space: 'f1', area: null, room_id: 'local-room',
|
||||
},
|
||||
];
|
||||
const map = roomClimateMap(hass, undefined, markers);
|
||||
assert.deepEqual(map.get('parent-room'), { temp: 40, hum: null });
|
||||
assert.deepEqual(map.get(roomClimateKey('f1', { id: 'local-room', area: null })), {
|
||||
temp: 20, hum: null,
|
||||
});
|
||||
assert.equal(map.get('registry-room'), undefined);
|
||||
});
|
||||
|
||||
test('issue 317: removed and HA-disabled bindings cannot feed a local room', () => {
|
||||
const hass = {
|
||||
devices: {
|
||||
hub: { id: 'hub', name: 'Climate hub', area_id: 'old' },
|
||||
disabled: { id: 'disabled', name: 'Disabled thermometer', area_id: 'old', disabled_by: 'user' },
|
||||
},
|
||||
entities: {
|
||||
'sensor.kept_temperature': { device_id: 'hub', platform: 'demo' },
|
||||
'sensor.removed_temperature': { device_id: 'hub', platform: 'demo' },
|
||||
'sensor.disabled_temperature': { device_id: 'disabled', platform: 'demo' },
|
||||
},
|
||||
states: {
|
||||
'sensor.kept_temperature': { state: '20', attributes: { device_class: 'temperature' } },
|
||||
'sensor.removed_temperature': { state: '40', attributes: { device_class: 'temperature' } },
|
||||
'sensor.disabled_temperature': { state: '90', attributes: { device_class: 'temperature' } },
|
||||
},
|
||||
};
|
||||
const markers = [
|
||||
{ id: 'hub', binding: 'device:hub', removed: true },
|
||||
{
|
||||
id: 'kept', binding: 'entity:sensor.kept_temperature',
|
||||
space: 'f1', area: null, room_id: 'local-room',
|
||||
},
|
||||
{
|
||||
id: 'disabled', binding: 'device:disabled',
|
||||
space: 'f1', area: null, room_id: 'local-room',
|
||||
},
|
||||
];
|
||||
assert.deepEqual(
|
||||
roomClimateMap(hass, undefined, markers).get('@room/f1/local-room'),
|
||||
{ temp: 20, hum: null },
|
||||
);
|
||||
});
|
||||
|
||||
test('issue 317: opted climate current_temperature follows manual placement', () => {
|
||||
const hass = {
|
||||
devices: { ac: { id: 'ac', name: 'Air conditioner', area_id: 'old' } },
|
||||
entities: { 'climate.ac': { device_id: 'ac', platform: 'demo' } },
|
||||
states: { 'climate.ac': { state: 'cool', attributes: { current_temperature: 23.5 } } },
|
||||
};
|
||||
const marker = {
|
||||
id: 'ac', binding: 'device:ac', space: 'f2', area: null, room_id: 'bed',
|
||||
use_climate_temp: true,
|
||||
};
|
||||
const map = roomClimateMap(hass, undefined, [marker]);
|
||||
assert.deepEqual(map.get('@room/f2/bed'), { temp: 23.5, hum: null });
|
||||
assert.equal(map.get('old'), undefined);
|
||||
});
|
||||
|
||||
|
||||
test('primaryEntity: a service switch never beats the visible main function (TRV)', () => {
|
||||
// the owner's radiator heads, verified live: switch outranks climate in the
|
||||
|
||||
Reference in New Issue
Block a user