From 5615afa33b4bde72d946e4771a345e2f2c924865 Mon Sep 17 00:00:00 2001 From: Matysh Date: Wed, 29 Jul 2026 18:00:22 +0300 Subject: [PATCH] v1.51.2: the v1.51.1 review (HP-1511-01, HP-1511-02) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HP-1511-01: defaultPositions ran over different rosters — the full card reserves grid cells for hidden devices, the static card compacted them away, so an undragged marker sat in different spots on the two cards. The static card feeds spaceDevs (hidden included) to the shared grid and renders devs (visible) — exactly the split HP-1510-01 introduced for LQI. - HP-1511-02: a hidden ripple-display marker rendered as an icon-less inactive pulse. A ghost drops the display dressing entirely: ripple presentation off, noicon off, base icon on, whatever marker.display says. smoke_hidden_flag: the weak 'has icon OR noicon' assertion is gone — every ghost must carry a base icon; new autoGridParity vector (vb-coordinate comparison, the cards render in different view systems) and a ripple-ghost vector. The demo stub got a connection.subscribeEvents so the static card's module-level config cache can be invalidated between in-test cards. --- custom_components/houseplan/const.py | 2 +- .../houseplan/frontend/houseplan-card.js | 10 +- custom_components/houseplan/manifest.json | 2 +- demo/smoke_hidden_flag.mjs | 96 ++++++++++++++++++- demo/srv/assets/houseplan-card.js | 10 +- dist/houseplan-card.js | 10 +- docs/CHANGELOG.md | 15 +++ docs/CHANGELOG.ru.md | 15 +++ docs/STATUS.md | 4 +- docs/TESTING.md | 5 + package-lock.json | 4 +- package.json | 2 +- src/houseplan-card.ts | 11 ++- src/space-render.ts | 7 +- 14 files changed, 162 insertions(+), 31 deletions(-) diff --git a/custom_components/houseplan/const.py b/custom_components/houseplan/const.py index cee8190..da356b0 100755 --- a/custom_components/houseplan/const.py +++ b/custom_components/houseplan/const.py @@ -45,7 +45,7 @@ PLAN_ORPHAN_TTL_S = 3600 SCHEDULED_GRACE_S = 30 * 24 * 3600 FILES_DIR = "houseplan/files" CONF_ADMIN_ONLY = "admin_only" -VERSION = "1.51.1" +VERSION = "1.51.2" DEFAULT_CONFIG: dict = { "spaces": [], diff --git a/custom_components/houseplan/frontend/houseplan-card.js b/custom_components/houseplan/frontend/houseplan-card.js index 38b26f0..0109c0b 100755 --- a/custom_components/houseplan/frontend/houseplan-card.js +++ b/custom_components/houseplan/frontend/houseplan-card.js @@ -1583,7 +1583,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow z-index: 120; max-width: 90vw; } - `,Gi=1e3;function Vi(t,e){const i=Number(t),s=Number.isFinite(i)&&i>0?i:1,o=s>=1?e:e*s,n=s>=1?e/s:e;return{x:(e-o)/2,y:(e-n)/2,w:o,h:n}}function Ki(t){if(null==t.x||null==t.y)return{x:t.x,y:t.y,w:t.w,h:t.h};const e=Number(t.w)||0,i=Number(t.h)||0;return{x:e<0?t.x+e:t.x,y:i<0?t.y+i:t.y,w:Math.abs(e),h:Math.abs(i)}}function Zi(t){return t&&Array.isArray(t.spaces)?t.spaces.map(t=>{const e=Gi,i=function(t){return Array.isArray(t)&&4===t.length&&t.every(t=>Number.isFinite(t))&&t[2]>1e-6&&t[3]>1e-6?t:[0,0,1,1]}(t.view_box);return{id:t.id,title:t.title,vb:[i[0]*Gi,i[1]*e,i[2]*Gi,i[3]*e],bg:t.plan_url?{href:bi(t.plan_url),...Vi(t.plan_aspect,Gi)}:null,rooms:(t.rooms||[]).map(t=>{const i={...t,...Ki(t)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Gi:void 0,y:null!=i.y?i.y*e:void 0,w:null!=i.w?i.w*Gi:void 0,h:null!=i.h?i.h*e:void 0,poly:i.poly?i.poly.map(t=>[t[0]*Gi,t[1]*e]):void 0}})}}):[]}function Ji(t){if(t.poly&&t.poly.length){const e=t.poly.map(t=>t[0]),i=t.poly.map(t=>t[1]),s=Math.min(...e),o=Math.min(...i);return{x:s,y:o,w:Math.max(...e)-s,h:Math.max(...i)-o}}return{x:t.x??0,y:t.y??0,w:t.w??0,h:t.h??0}}function Xi(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}function Yi(t){const e=Zi(t.cfg),i=e.find(e=>e.id===t.spaceId);if(!i)return null;const s=i.vb,o=ti(t.cfg.spaces.find(e=>e.id===t.spaceId)),n=t.iconSize??2.5,r=n>8?2.5:n,a={};for(const e of t.cfg.spaces||[])for(const t of e.rooms||[])t.area&&(a[t.area]=e.id);const l=t.cfg.settings?.exclude_integrations?new Set(t.cfg.settings.exclude_integrations):ht,c=pt(t.cfg.settings?.icon_rules?.length?t.cfg.settings.icon_rules:dt),h=Oi({hass:t.hass,areaToSpace:a,markers:t.cfg.markers||[],settings:t.cfg.settings||{},excluded:l,showAll:!!t.cfg.settings?.show_all,firstSpaceId:e[0]?.id||"",loc:e=>Bi(t.lang,e),iconRules:c}),d=h.filter(e=>e.space===t.spaceId),p=d.filter(t=>!t.hidden),u=function(t,e,i){const s={},o=i/100*Gi*1.3;for(const i of e.rooms){if(!i.area)continue;const e=t.filter(t=>t.area===i.area);if(!e.length)continue;const n=Ji(i),r=.1*Math.min(n.w,n.h),a=n.w-2*r,l=n.h-2*r,c=Math.max(1,Math.round(Math.sqrt(e.length*a/Math.max(l,1)))),h=a/c,d=l/Math.max(Math.ceil(e.length/c),1),p=e.map((t,e)=>({x:n.x+r+h*(e%c+.5),y:n.y+r+d*(Math.floor(e/c)+.5)}));je(p,n,o,.5*r),e.forEach((t,e)=>s[t.id]=p[e])}return s}(p,i,r),_=i.rooms.filter(t=>t.area||o.showBorders).map(e=>{let s="room "+(i.bg?"overlay":"yard"),n="";const r=vi(o.fill,e);if(o.showBorders||"none"!==r){s+=" styled";const i=[`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`],a=e.area?ni(r,"lqi"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i||o.virtual)continue;const e=Ti(t,o.entities);null!=e&&s.push(e)}return He(s)}(t.hass,d,e.area):null,"light"===r?Ii(t.hass,d,e.area):"none","temp"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i)continue;if("mdi:thermometer"!==o.icon&&"mdi:air-filter"!==o.icon)continue;const e=zi(t,o.entities);null!=e&&s.push(e)}return s.length?Math.round(s.reduce((t,e)=>t+e,0)/s.length*10)/10:null}(t.hass,d,e.area):null,o.tempMin,o.tempMax,si(t.cfg?.settings)):null;a?(s+=" filled",i.push(`--room-fill:${a.c}`,`--room-fill-op:${a.a.toFixed(3)}`)):i.push("--room-fill:transparent","--room-fill-op:0"),n=i.join(";")}const a=!i.bg&&!o.showNames,l=Xi(e),c=e.poly?j``:j``;return j`${c}${a?j`${e.name}`:G}`}),m=p.map(e=>{const o=function(t,e,i,s,o){const n=e[t.id];if(n&&n.s===t.space)return{x:n.x*Gi,y:n.y*Gi};if(s[t.id])return s[t.id];const r=o.vb;return{x:r[0]+r[2]/2,y:r[1]+r[3]/2}}(e,t.layout,t.cfg,u,i),n=(o.x-s[0])/s[2]*100,r=(o.y-s[1])/s[3]*100;return B`
+ `,Gi=1e3;function Vi(t,e){const i=Number(t),s=Number.isFinite(i)&&i>0?i:1,o=s>=1?e:e*s,n=s>=1?e/s:e;return{x:(e-o)/2,y:(e-n)/2,w:o,h:n}}function Ki(t){if(null==t.x||null==t.y)return{x:t.x,y:t.y,w:t.w,h:t.h};const e=Number(t.w)||0,i=Number(t.h)||0;return{x:e<0?t.x+e:t.x,y:i<0?t.y+i:t.y,w:Math.abs(e),h:Math.abs(i)}}function Zi(t){return t&&Array.isArray(t.spaces)?t.spaces.map(t=>{const e=Gi,i=function(t){return Array.isArray(t)&&4===t.length&&t.every(t=>Number.isFinite(t))&&t[2]>1e-6&&t[3]>1e-6?t:[0,0,1,1]}(t.view_box);return{id:t.id,title:t.title,vb:[i[0]*Gi,i[1]*e,i[2]*Gi,i[3]*e],bg:t.plan_url?{href:bi(t.plan_url),...Vi(t.plan_aspect,Gi)}:null,rooms:(t.rooms||[]).map(t=>{const i={...t,...Ki(t)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Gi:void 0,y:null!=i.y?i.y*e:void 0,w:null!=i.w?i.w*Gi:void 0,h:null!=i.h?i.h*e:void 0,poly:i.poly?i.poly.map(t=>[t[0]*Gi,t[1]*e]):void 0}})}}):[]}function Ji(t){if(t.poly&&t.poly.length){const e=t.poly.map(t=>t[0]),i=t.poly.map(t=>t[1]),s=Math.min(...e),o=Math.min(...i);return{x:s,y:o,w:Math.max(...e)-s,h:Math.max(...i)-o}}return{x:t.x??0,y:t.y??0,w:t.w??0,h:t.h??0}}function Xi(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}function Yi(t){const e=Zi(t.cfg),i=e.find(e=>e.id===t.spaceId);if(!i)return null;const s=i.vb,o=ti(t.cfg.spaces.find(e=>e.id===t.spaceId)),n=t.iconSize??2.5,r=n>8?2.5:n,a={};for(const e of t.cfg.spaces||[])for(const t of e.rooms||[])t.area&&(a[t.area]=e.id);const l=t.cfg.settings?.exclude_integrations?new Set(t.cfg.settings.exclude_integrations):ht,c=pt(t.cfg.settings?.icon_rules?.length?t.cfg.settings.icon_rules:dt),h=Oi({hass:t.hass,areaToSpace:a,markers:t.cfg.markers||[],settings:t.cfg.settings||{},excluded:l,showAll:!!t.cfg.settings?.show_all,firstSpaceId:e[0]?.id||"",loc:e=>Bi(t.lang,e),iconRules:c}),d=h.filter(e=>e.space===t.spaceId),p=d.filter(t=>!t.hidden),u=function(t,e,i){const s={},o=i/100*Gi*1.3;for(const i of e.rooms){if(!i.area)continue;const e=t.filter(t=>t.area===i.area);if(!e.length)continue;const n=Ji(i),r=.1*Math.min(n.w,n.h),a=n.w-2*r,l=n.h-2*r,c=Math.max(1,Math.round(Math.sqrt(e.length*a/Math.max(l,1)))),h=a/c,d=l/Math.max(Math.ceil(e.length/c),1),p=e.map((t,e)=>({x:n.x+r+h*(e%c+.5),y:n.y+r+d*(Math.floor(e/c)+.5)}));je(p,n,o,.5*r),e.forEach((t,e)=>s[t.id]=p[e])}return s}(d,i,r),_=i.rooms.filter(t=>t.area||o.showBorders).map(e=>{let s="room "+(i.bg?"overlay":"yard"),n="";const r=vi(o.fill,e);if(o.showBorders||"none"!==r){s+=" styled";const i=[`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`],a=e.area?ni(r,"lqi"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i||o.virtual)continue;const e=Ti(t,o.entities);null!=e&&s.push(e)}return He(s)}(t.hass,d,e.area):null,"light"===r?Ii(t.hass,d,e.area):"none","temp"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i)continue;if("mdi:thermometer"!==o.icon&&"mdi:air-filter"!==o.icon)continue;const e=zi(t,o.entities);null!=e&&s.push(e)}return s.length?Math.round(s.reduce((t,e)=>t+e,0)/s.length*10)/10:null}(t.hass,d,e.area):null,o.tempMin,o.tempMax,si(t.cfg?.settings)):null;a?(s+=" filled",i.push(`--room-fill:${a.c}`,`--room-fill-op:${a.a.toFixed(3)}`)):i.push("--room-fill:transparent","--room-fill-op:0"),n=i.join(";")}const a=!i.bg&&!o.showNames,l=Xi(e),c=e.poly?j``:j``;return j`${c}${a?j`${e.name}`:G}`}),m=p.map(e=>{const o=function(t,e,i,s,o){const n=e[t.id];if(n&&n.s===t.space)return{x:n.x*Gi,y:n.y*Gi};if(s[t.id])return s[t.id];const r=o.vb;return{x:r[0]+r[2]/2,y:r[1]+r[3]/2}}(e,t.layout,t.cfg,u,i),n=(o.x-s[0])/s[2]*100,r=(o.y-s[1])/s[3]*100;return B`
`}),g=o.showNames?i.rooms.filter(t=>t.name).map(e=>{const n=function(t,e,i){const s=i["rl_"+(t.id||"")];if(s&&s.s===e)return{x:s.x*Gi,y:s.y*Gi};const o=Xi(t);return{x:o[0],y:o[1]}}(e,i.id,t.layout,t.cfg),r=(n.x-s[0])/s[2]*100,a=(n.y-s[1])/s[3]*100,l=Math.min(1,o.opacity+.25);return B`
${e.name}
`}):[],f=i.bg?t.displayUrl?t.displayUrl(i.bg.href):i.bg.href:"";return B`
@@ -2003,8 +2003,8 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow ${this._kioskDialog?this._renderKioskDialog():G} ${this._toast?B`
${this._toast}
`:G} - `}_renderDevice(t,e,i=!0){const s=this._pos(t),o=(s.x-e.x)/e.w*100,n=(s.y-e.y)/e.h*100,r=t.hidden?"":this._stateClass(t),a=t.hidden?null:this._liveTemp(t),l=t.hidden?null:this._liveHum(t),c=!i||t.virtual||t.hidden?null:Ti(this.hass,t.entities),h=t.marker,d=h?.display||"badge",p="ripple"===d||"icon_ripple"===d,u=t.primary?this.hass.states[t.primary]:void 0,_="value"!==d||t.hidden?null:null!=a?a+"°":null!=l?l+"%":u&&!isNaN(parseFloat(u.state))?parseFloat(u.state)+(u.attributes?.unit_of_measurement?" "+u.attributes.unit_of_measurement:""):null,m=t.primary?t.primary.split(".")[0]:null,g=this._config?.live_states&&!t.hidden?ri(t.icon,m,u?.attributes?.device_class,u?.state,!!h?.icon):t.icon,f=(h?.controls||[]).filter(di),b=this._config?.live_states&&!t.hidden?f.length?f.map(t=>ai(this.hass.states[t])).find(t=>t)||null:"light"===m?ai(u):null:null,v=this._config?.live_states&&!t.hidden&&function(t,e,i){return"on"===i&&("siren"===t||"binary_sensor"===t&&!!e&&xi.has(e))}(m,u?.attributes?.device_class,u?.state),y=p&&!t.hidden&&!!t.primary&&$e(this.hass.states[t.primary]?.state),w=Number(h?.size)>0?Number(h.size):1,x=Number(h?.angle)||0,k=Number(h?.ripple_size)>0?Number(h.ripple_size):3,$=[`left:${o}%`,`top:${n}%`];return 1!==w&&$.push(`--dev-scale:${w}`),p&&($.push(`--ripple-scale:${k}`),h?.ripple_color?$.push(`--ripple-color:${h.ripple_color}`):b&&$.push(`--ripple-color:${b}`)),b&&$.push(`--light-color:${b}`),B`
ai(this.hass.states[t])).find(t=>t)||null:"light"===m?ai(u):null:null,v=this._config?.live_states&&!t.hidden&&function(t,e,i){return"on"===i&&("siren"===t||"binary_sensor"===t&&!!e&&xi.has(e))}(m,u?.attributes?.device_class,u?.state),y=p&&!t.hidden&&!!t.primary&&$e(this.hass.states[t.primary]?.state),w=Number(h?.size)>0?Number(h.size):1,x=Number(h?.angle)||0,k=Number(h?.ripple_size)>0?Number(h.ripple_size):3,$=[`left:${o}%`,`top:${n}%`];return 1!==w&&$.push(`--dev-scale:${w}`),p&&($.push(`--ripple-scale:${k}`),h?.ripple_color?$.push(`--ripple-color:${h.ripple_color}`):b&&$.push(`--ripple-color:${b}`)),b&&$.push(`--light-color:${b}`),B`
this._clickDevice(e,t)} @contextmenu=${e=>this._ctxDevice(e,t)} @@ -2017,7 +2017,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow > ${p?B``:G} ${this._newIds.has(t.id)?B``:G} - ${null!=_?B`${_}`:"ripple"!==d?B``:G} + ${null!=_?B`${_}`:"ripple"!==d||t.hidden?B``:G} ${null!=a&&null==_?B`${a}°`:G} ${null!=l&&null==_?B`${l}%`:G} ${null!=c?B`${c}`:G} @@ -2668,4 +2668,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow `}
-
`}}fs.properties={_hdrH:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fs.ZOOM_MAX=8,fs.ZOOM_MIN=.4,fs._touchSeen=!1,fs._noHoverMq="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,fs.styles=Wi,customElements.get("houseplan-card")||customElements.define("houseplan-card",fs),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.51.1 ","background:#3ea6ff;color:#04121f;font-weight:700",""); +
`}}fs.properties={_hdrH:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fs.ZOOM_MAX=8,fs.ZOOM_MIN=.4,fs._touchSeen=!1,fs._noHoverMq="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,fs.styles=Wi,customElements.get("houseplan-card")||customElements.define("houseplan-card",fs),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.51.2 ","background:#3ea6ff;color:#04121f;font-weight:700",""); diff --git a/custom_components/houseplan/manifest.json b/custom_components/houseplan/manifest.json index f17d1ce..075b1a4 100755 --- a/custom_components/houseplan/manifest.json +++ b/custom_components/houseplan/manifest.json @@ -16,5 +16,5 @@ "issue_tracker": "https://github.com/Matysh/houseplan-card/issues", "requirements": [], "single_config_entry": true, - "version": "1.51.1" + "version": "1.51.2" } diff --git a/demo/smoke_hidden_flag.mjs b/demo/smoke_hidden_flag.mjs index a2aae24..faa7957 100644 --- a/demo/smoke_hidden_flag.mjs +++ b/demo/smoke_hidden_flag.mjs @@ -93,11 +93,97 @@ Object.assign(out, await page.evaluate(async () => { o.ghostHidesValue = pm === null; // «42 kW» не отрисован o.ghostNoLiveBadges = ghosts.every((g) => !g.querySelector('.valtext') && !g.querySelector('.tval') && !g.querySelector('.hval') && !g.querySelector('.lqi')); - o.ghostKeepsIcon = ghosts.every((g) => !!g.querySelector('ha-icon') || g.classList.contains('noicon')); + // у КАЖДОГО призрака есть базовая иконка — «noicon» не оправдание + // (HP-1511-02: ripple-призрак был безликим пульсом) + o.ghostKeepsIcon = ghosts.every((g) => !!g.querySelector('ha-icon') && !g.classList.contains('noicon')); c._setMode('view'); c._showHidden = false; return o; })); +// --- HP-1511-02: ripple-призрак с базовой иконкой, без пульса ------------- +Object.assign(out, await page.evaluate(async () => { + const o = {}; + const c = window.__card; + const sr = () => c.shadowRoot || c.renderRoot; + const lamp = c._devices.find((x) => x.space === 'f1' && x.bindingKind === 'device' && !x.hidden); + c._serverCfg.markers = c._serverCfg.markers || []; + c._serverCfg.markers = c._serverCfg.markers.filter((m) => m.id !== lamp.id); + c._serverCfg.markers.push({ id: lamp.id, binding: 'device:' + lamp.bindingRef, + display: 'ripple', hidden: true }); + c._cfgEpoch++; c._regSignature = ''; + c._maybeRebuildDevices(); + c._setMode('devices'); c._showHidden = true; + c.requestUpdate(); await c.updateComplete; + const g = [...sr().querySelectorAll('.dev.ghost')].find((x) => x.querySelector('.ripple') || !x.classList.contains('noicon')); + const ghosts = [...sr().querySelectorAll('.dev.ghost')]; + o.rippleGhostHasIcon = ghosts.length > 0 && ghosts.every((x) => !!x.querySelector('ha-icon')); + o.rippleGhostNoNoicon = ghosts.every((x) => !x.classList.contains('noicon')); + o.rippleGhostNoRipple = ghosts.every((x) => !x.querySelector('.ripple')); + c._serverCfg.markers = c._serverCfg.markers.filter((m) => m.id !== lamp.id); + c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices(); + c._setMode('view'); c._showHidden = false; + return o; +})); + +// --- HP-1511-01: авто-сетка одинакова на обеих карточках ------------------ +Object.assign(out, await page.evaluate(async () => { + const o = {}; + const c = window.__card; + await customElements.whenDefined('houseplan-space-card'); + const cfg = JSON.parse(JSON.stringify(c._serverCfg)); + const f1 = cfg.spaces.find((s) => s.id === 'f1'); + // один видимый, остальные зоны living_room скрыты; layout пуст + const vis = c._devices.filter((x) => x.area === 'living_room' && !x.virtual); + const keep = vis[0]; + for (const d of vis.slice(1)) { + cfg.markers = (cfg.markers || []).filter((m) => m.id !== d.id); + cfg.markers.push({ id: d.id, binding: d.bindingKind + ':' + d.bindingRef, hidden: true }); + } + cfg.markers = cfg.markers.filter((m) => m.id !== keep.id || !m.hidden); + c._serverCfg = cfg; c._cfgEpoch++; c._regSignature = ''; + c._layout = {}; // пустой layout: работают только авто-позиции + c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete; + const full = [...(c.shadowRoot || c.renderRoot).querySelectorAll('.dev')] + .find((el) => el.style.left && !el.classList.contains('ghost')); + // проценты полной карточки считаются от content-fit view — переводим в vb + const v = c._viewOr(c._baseVb()); + const fullVb = full && [ + v.x + (parseFloat(full.style.left) / 100) * v.w, + v.y + (parseFloat(full.style.top) / 100) * v.h, + ]; + + const hass = { ...c.hass, callWS: async (m) => { + // rev уникален: у статичной карточки модульный кэш конфига по rev, + // и одинаковый rev в соседних тестах подсовывает чужой конфиг + if (m.type === 'houseplan/config/get') return { config: cfg, rev: 31 }; + if (m.type === 'houseplan/layout/get') return { layout: {}, rev: 31 }; + return { ok: true }; + }, + // модульный кэш конфига инвалидируется только через это событие — стаб + // сохраняет колбэк, чтобы следующий тест мог сбросить кэш + connection: { subscribeEvents: async (cb) => { window.__hpInvalidate = cb; return () => {}; } } }; + const host = document.createElement('div'); + document.body.appendChild(host); + const card = document.createElement('houseplan-space-card'); + card.setConfig({ type: 'custom:houseplan-space-card', space: 'f1' }); + card.hass = hass; + host.appendChild(card); + const t0 = Date.now(); + while (!card.renderRoot?.querySelector('[style*="left"]') && Date.now() - t0 < 6000) { + await new Promise((r) => setTimeout(r, 60)); + } + await card.updateComplete; + const st = [...card.renderRoot.querySelectorAll('[style*="left"]')] + .find((el) => /%$/.test(el.style.left || '') && el.style.top); + // статичная карточка рендерит от полного квадрата 0..1000 + const stVb = st && [parseFloat(st.style.left) * 10, parseFloat(st.style.top) * 10]; + o.autoGridParity = !!fullVb && !!stVb + && Math.abs(fullVb[0] - stVb[0]) < 6 && Math.abs(fullVb[1] - stVb[1]) < 6; + if (!o.autoGridParity) console.log('full', fullVb, 'static', stVb); + host.remove(); + return o; +})); + // --- HP-1510-01: LQI комнаты одинаков на полной и статичной карточке ------ Object.assign(out, await page.evaluate(async () => { const o = {}; @@ -120,11 +206,13 @@ Object.assign(out, await page.evaluate(async () => { .find((r) => (r.getAttribute('style') || '').includes('--room-fill')); const fullFill = fullRoom ? (fullRoom.getAttribute('style').match(/--room-fill:([^;]+)/) || [])[1] : null; + window.__hpInvalidate?.(); // сбросить модульный кэш от предыдущей карточки const hass = { ...c.hass, callWS: async (m) => { - if (m.type === 'houseplan/config/get') return { config: cfg, rev: 1 }; - if (m.type === 'houseplan/layout/get') return { layout: {}, rev: 1 }; + if (m.type === 'houseplan/config/get') return { config: cfg, rev: 57 }; + if (m.type === 'houseplan/layout/get') return { layout: {}, rev: 57 }; return { ok: true }; - } }; + }, + connection: { subscribeEvents: async () => () => {} } }; const host = document.createElement('div'); document.body.appendChild(host); const card = document.createElement('houseplan-space-card'); diff --git a/demo/srv/assets/houseplan-card.js b/demo/srv/assets/houseplan-card.js index 38b26f0..0109c0b 100755 --- a/demo/srv/assets/houseplan-card.js +++ b/demo/srv/assets/houseplan-card.js @@ -1583,7 +1583,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow z-index: 120; max-width: 90vw; } - `,Gi=1e3;function Vi(t,e){const i=Number(t),s=Number.isFinite(i)&&i>0?i:1,o=s>=1?e:e*s,n=s>=1?e/s:e;return{x:(e-o)/2,y:(e-n)/2,w:o,h:n}}function Ki(t){if(null==t.x||null==t.y)return{x:t.x,y:t.y,w:t.w,h:t.h};const e=Number(t.w)||0,i=Number(t.h)||0;return{x:e<0?t.x+e:t.x,y:i<0?t.y+i:t.y,w:Math.abs(e),h:Math.abs(i)}}function Zi(t){return t&&Array.isArray(t.spaces)?t.spaces.map(t=>{const e=Gi,i=function(t){return Array.isArray(t)&&4===t.length&&t.every(t=>Number.isFinite(t))&&t[2]>1e-6&&t[3]>1e-6?t:[0,0,1,1]}(t.view_box);return{id:t.id,title:t.title,vb:[i[0]*Gi,i[1]*e,i[2]*Gi,i[3]*e],bg:t.plan_url?{href:bi(t.plan_url),...Vi(t.plan_aspect,Gi)}:null,rooms:(t.rooms||[]).map(t=>{const i={...t,...Ki(t)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Gi:void 0,y:null!=i.y?i.y*e:void 0,w:null!=i.w?i.w*Gi:void 0,h:null!=i.h?i.h*e:void 0,poly:i.poly?i.poly.map(t=>[t[0]*Gi,t[1]*e]):void 0}})}}):[]}function Ji(t){if(t.poly&&t.poly.length){const e=t.poly.map(t=>t[0]),i=t.poly.map(t=>t[1]),s=Math.min(...e),o=Math.min(...i);return{x:s,y:o,w:Math.max(...e)-s,h:Math.max(...i)-o}}return{x:t.x??0,y:t.y??0,w:t.w??0,h:t.h??0}}function Xi(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}function Yi(t){const e=Zi(t.cfg),i=e.find(e=>e.id===t.spaceId);if(!i)return null;const s=i.vb,o=ti(t.cfg.spaces.find(e=>e.id===t.spaceId)),n=t.iconSize??2.5,r=n>8?2.5:n,a={};for(const e of t.cfg.spaces||[])for(const t of e.rooms||[])t.area&&(a[t.area]=e.id);const l=t.cfg.settings?.exclude_integrations?new Set(t.cfg.settings.exclude_integrations):ht,c=pt(t.cfg.settings?.icon_rules?.length?t.cfg.settings.icon_rules:dt),h=Oi({hass:t.hass,areaToSpace:a,markers:t.cfg.markers||[],settings:t.cfg.settings||{},excluded:l,showAll:!!t.cfg.settings?.show_all,firstSpaceId:e[0]?.id||"",loc:e=>Bi(t.lang,e),iconRules:c}),d=h.filter(e=>e.space===t.spaceId),p=d.filter(t=>!t.hidden),u=function(t,e,i){const s={},o=i/100*Gi*1.3;for(const i of e.rooms){if(!i.area)continue;const e=t.filter(t=>t.area===i.area);if(!e.length)continue;const n=Ji(i),r=.1*Math.min(n.w,n.h),a=n.w-2*r,l=n.h-2*r,c=Math.max(1,Math.round(Math.sqrt(e.length*a/Math.max(l,1)))),h=a/c,d=l/Math.max(Math.ceil(e.length/c),1),p=e.map((t,e)=>({x:n.x+r+h*(e%c+.5),y:n.y+r+d*(Math.floor(e/c)+.5)}));je(p,n,o,.5*r),e.forEach((t,e)=>s[t.id]=p[e])}return s}(p,i,r),_=i.rooms.filter(t=>t.area||o.showBorders).map(e=>{let s="room "+(i.bg?"overlay":"yard"),n="";const r=vi(o.fill,e);if(o.showBorders||"none"!==r){s+=" styled";const i=[`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`],a=e.area?ni(r,"lqi"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i||o.virtual)continue;const e=Ti(t,o.entities);null!=e&&s.push(e)}return He(s)}(t.hass,d,e.area):null,"light"===r?Ii(t.hass,d,e.area):"none","temp"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i)continue;if("mdi:thermometer"!==o.icon&&"mdi:air-filter"!==o.icon)continue;const e=zi(t,o.entities);null!=e&&s.push(e)}return s.length?Math.round(s.reduce((t,e)=>t+e,0)/s.length*10)/10:null}(t.hass,d,e.area):null,o.tempMin,o.tempMax,si(t.cfg?.settings)):null;a?(s+=" filled",i.push(`--room-fill:${a.c}`,`--room-fill-op:${a.a.toFixed(3)}`)):i.push("--room-fill:transparent","--room-fill-op:0"),n=i.join(";")}const a=!i.bg&&!o.showNames,l=Xi(e),c=e.poly?j``:j``;return j`${c}${a?j`${e.name}`:G}`}),m=p.map(e=>{const o=function(t,e,i,s,o){const n=e[t.id];if(n&&n.s===t.space)return{x:n.x*Gi,y:n.y*Gi};if(s[t.id])return s[t.id];const r=o.vb;return{x:r[0]+r[2]/2,y:r[1]+r[3]/2}}(e,t.layout,t.cfg,u,i),n=(o.x-s[0])/s[2]*100,r=(o.y-s[1])/s[3]*100;return B`
+ `,Gi=1e3;function Vi(t,e){const i=Number(t),s=Number.isFinite(i)&&i>0?i:1,o=s>=1?e:e*s,n=s>=1?e/s:e;return{x:(e-o)/2,y:(e-n)/2,w:o,h:n}}function Ki(t){if(null==t.x||null==t.y)return{x:t.x,y:t.y,w:t.w,h:t.h};const e=Number(t.w)||0,i=Number(t.h)||0;return{x:e<0?t.x+e:t.x,y:i<0?t.y+i:t.y,w:Math.abs(e),h:Math.abs(i)}}function Zi(t){return t&&Array.isArray(t.spaces)?t.spaces.map(t=>{const e=Gi,i=function(t){return Array.isArray(t)&&4===t.length&&t.every(t=>Number.isFinite(t))&&t[2]>1e-6&&t[3]>1e-6?t:[0,0,1,1]}(t.view_box);return{id:t.id,title:t.title,vb:[i[0]*Gi,i[1]*e,i[2]*Gi,i[3]*e],bg:t.plan_url?{href:bi(t.plan_url),...Vi(t.plan_aspect,Gi)}:null,rooms:(t.rooms||[]).map(t=>{const i={...t,...Ki(t)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Gi:void 0,y:null!=i.y?i.y*e:void 0,w:null!=i.w?i.w*Gi:void 0,h:null!=i.h?i.h*e:void 0,poly:i.poly?i.poly.map(t=>[t[0]*Gi,t[1]*e]):void 0}})}}):[]}function Ji(t){if(t.poly&&t.poly.length){const e=t.poly.map(t=>t[0]),i=t.poly.map(t=>t[1]),s=Math.min(...e),o=Math.min(...i);return{x:s,y:o,w:Math.max(...e)-s,h:Math.max(...i)-o}}return{x:t.x??0,y:t.y??0,w:t.w??0,h:t.h??0}}function Xi(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}function Yi(t){const e=Zi(t.cfg),i=e.find(e=>e.id===t.spaceId);if(!i)return null;const s=i.vb,o=ti(t.cfg.spaces.find(e=>e.id===t.spaceId)),n=t.iconSize??2.5,r=n>8?2.5:n,a={};for(const e of t.cfg.spaces||[])for(const t of e.rooms||[])t.area&&(a[t.area]=e.id);const l=t.cfg.settings?.exclude_integrations?new Set(t.cfg.settings.exclude_integrations):ht,c=pt(t.cfg.settings?.icon_rules?.length?t.cfg.settings.icon_rules:dt),h=Oi({hass:t.hass,areaToSpace:a,markers:t.cfg.markers||[],settings:t.cfg.settings||{},excluded:l,showAll:!!t.cfg.settings?.show_all,firstSpaceId:e[0]?.id||"",loc:e=>Bi(t.lang,e),iconRules:c}),d=h.filter(e=>e.space===t.spaceId),p=d.filter(t=>!t.hidden),u=function(t,e,i){const s={},o=i/100*Gi*1.3;for(const i of e.rooms){if(!i.area)continue;const e=t.filter(t=>t.area===i.area);if(!e.length)continue;const n=Ji(i),r=.1*Math.min(n.w,n.h),a=n.w-2*r,l=n.h-2*r,c=Math.max(1,Math.round(Math.sqrt(e.length*a/Math.max(l,1)))),h=a/c,d=l/Math.max(Math.ceil(e.length/c),1),p=e.map((t,e)=>({x:n.x+r+h*(e%c+.5),y:n.y+r+d*(Math.floor(e/c)+.5)}));je(p,n,o,.5*r),e.forEach((t,e)=>s[t.id]=p[e])}return s}(d,i,r),_=i.rooms.filter(t=>t.area||o.showBorders).map(e=>{let s="room "+(i.bg?"overlay":"yard"),n="";const r=vi(o.fill,e);if(o.showBorders||"none"!==r){s+=" styled";const i=[`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`],a=e.area?ni(r,"lqi"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i||o.virtual)continue;const e=Ti(t,o.entities);null!=e&&s.push(e)}return He(s)}(t.hass,d,e.area):null,"light"===r?Ii(t.hass,d,e.area):"none","temp"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i)continue;if("mdi:thermometer"!==o.icon&&"mdi:air-filter"!==o.icon)continue;const e=zi(t,o.entities);null!=e&&s.push(e)}return s.length?Math.round(s.reduce((t,e)=>t+e,0)/s.length*10)/10:null}(t.hass,d,e.area):null,o.tempMin,o.tempMax,si(t.cfg?.settings)):null;a?(s+=" filled",i.push(`--room-fill:${a.c}`,`--room-fill-op:${a.a.toFixed(3)}`)):i.push("--room-fill:transparent","--room-fill-op:0"),n=i.join(";")}const a=!i.bg&&!o.showNames,l=Xi(e),c=e.poly?j``:j``;return j`${c}${a?j`${e.name}`:G}`}),m=p.map(e=>{const o=function(t,e,i,s,o){const n=e[t.id];if(n&&n.s===t.space)return{x:n.x*Gi,y:n.y*Gi};if(s[t.id])return s[t.id];const r=o.vb;return{x:r[0]+r[2]/2,y:r[1]+r[3]/2}}(e,t.layout,t.cfg,u,i),n=(o.x-s[0])/s[2]*100,r=(o.y-s[1])/s[3]*100;return B`
`}),g=o.showNames?i.rooms.filter(t=>t.name).map(e=>{const n=function(t,e,i){const s=i["rl_"+(t.id||"")];if(s&&s.s===e)return{x:s.x*Gi,y:s.y*Gi};const o=Xi(t);return{x:o[0],y:o[1]}}(e,i.id,t.layout,t.cfg),r=(n.x-s[0])/s[2]*100,a=(n.y-s[1])/s[3]*100,l=Math.min(1,o.opacity+.25);return B`
${e.name}
`}):[],f=i.bg?t.displayUrl?t.displayUrl(i.bg.href):i.bg.href:"";return B`
@@ -2003,8 +2003,8 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow ${this._kioskDialog?this._renderKioskDialog():G} ${this._toast?B`
${this._toast}
`:G} - `}_renderDevice(t,e,i=!0){const s=this._pos(t),o=(s.x-e.x)/e.w*100,n=(s.y-e.y)/e.h*100,r=t.hidden?"":this._stateClass(t),a=t.hidden?null:this._liveTemp(t),l=t.hidden?null:this._liveHum(t),c=!i||t.virtual||t.hidden?null:Ti(this.hass,t.entities),h=t.marker,d=h?.display||"badge",p="ripple"===d||"icon_ripple"===d,u=t.primary?this.hass.states[t.primary]:void 0,_="value"!==d||t.hidden?null:null!=a?a+"°":null!=l?l+"%":u&&!isNaN(parseFloat(u.state))?parseFloat(u.state)+(u.attributes?.unit_of_measurement?" "+u.attributes.unit_of_measurement:""):null,m=t.primary?t.primary.split(".")[0]:null,g=this._config?.live_states&&!t.hidden?ri(t.icon,m,u?.attributes?.device_class,u?.state,!!h?.icon):t.icon,f=(h?.controls||[]).filter(di),b=this._config?.live_states&&!t.hidden?f.length?f.map(t=>ai(this.hass.states[t])).find(t=>t)||null:"light"===m?ai(u):null:null,v=this._config?.live_states&&!t.hidden&&function(t,e,i){return"on"===i&&("siren"===t||"binary_sensor"===t&&!!e&&xi.has(e))}(m,u?.attributes?.device_class,u?.state),y=p&&!t.hidden&&!!t.primary&&$e(this.hass.states[t.primary]?.state),w=Number(h?.size)>0?Number(h.size):1,x=Number(h?.angle)||0,k=Number(h?.ripple_size)>0?Number(h.ripple_size):3,$=[`left:${o}%`,`top:${n}%`];return 1!==w&&$.push(`--dev-scale:${w}`),p&&($.push(`--ripple-scale:${k}`),h?.ripple_color?$.push(`--ripple-color:${h.ripple_color}`):b&&$.push(`--ripple-color:${b}`)),b&&$.push(`--light-color:${b}`),B`
ai(this.hass.states[t])).find(t=>t)||null:"light"===m?ai(u):null:null,v=this._config?.live_states&&!t.hidden&&function(t,e,i){return"on"===i&&("siren"===t||"binary_sensor"===t&&!!e&&xi.has(e))}(m,u?.attributes?.device_class,u?.state),y=p&&!t.hidden&&!!t.primary&&$e(this.hass.states[t.primary]?.state),w=Number(h?.size)>0?Number(h.size):1,x=Number(h?.angle)||0,k=Number(h?.ripple_size)>0?Number(h.ripple_size):3,$=[`left:${o}%`,`top:${n}%`];return 1!==w&&$.push(`--dev-scale:${w}`),p&&($.push(`--ripple-scale:${k}`),h?.ripple_color?$.push(`--ripple-color:${h.ripple_color}`):b&&$.push(`--ripple-color:${b}`)),b&&$.push(`--light-color:${b}`),B`
this._clickDevice(e,t)} @contextmenu=${e=>this._ctxDevice(e,t)} @@ -2017,7 +2017,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow > ${p?B``:G} ${this._newIds.has(t.id)?B``:G} - ${null!=_?B`${_}`:"ripple"!==d?B``:G} + ${null!=_?B`${_}`:"ripple"!==d||t.hidden?B``:G} ${null!=a&&null==_?B`${a}°`:G} ${null!=l&&null==_?B`${l}%`:G} ${null!=c?B`${c}`:G} @@ -2668,4 +2668,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow `}
-
`}}fs.properties={_hdrH:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fs.ZOOM_MAX=8,fs.ZOOM_MIN=.4,fs._touchSeen=!1,fs._noHoverMq="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,fs.styles=Wi,customElements.get("houseplan-card")||customElements.define("houseplan-card",fs),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.51.1 ","background:#3ea6ff;color:#04121f;font-weight:700",""); +
`}}fs.properties={_hdrH:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fs.ZOOM_MAX=8,fs.ZOOM_MIN=.4,fs._touchSeen=!1,fs._noHoverMq="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,fs.styles=Wi,customElements.get("houseplan-card")||customElements.define("houseplan-card",fs),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.51.2 ","background:#3ea6ff;color:#04121f;font-weight:700",""); diff --git a/dist/houseplan-card.js b/dist/houseplan-card.js index 38b26f0..0109c0b 100755 --- a/dist/houseplan-card.js +++ b/dist/houseplan-card.js @@ -1583,7 +1583,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow z-index: 120; max-width: 90vw; } - `,Gi=1e3;function Vi(t,e){const i=Number(t),s=Number.isFinite(i)&&i>0?i:1,o=s>=1?e:e*s,n=s>=1?e/s:e;return{x:(e-o)/2,y:(e-n)/2,w:o,h:n}}function Ki(t){if(null==t.x||null==t.y)return{x:t.x,y:t.y,w:t.w,h:t.h};const e=Number(t.w)||0,i=Number(t.h)||0;return{x:e<0?t.x+e:t.x,y:i<0?t.y+i:t.y,w:Math.abs(e),h:Math.abs(i)}}function Zi(t){return t&&Array.isArray(t.spaces)?t.spaces.map(t=>{const e=Gi,i=function(t){return Array.isArray(t)&&4===t.length&&t.every(t=>Number.isFinite(t))&&t[2]>1e-6&&t[3]>1e-6?t:[0,0,1,1]}(t.view_box);return{id:t.id,title:t.title,vb:[i[0]*Gi,i[1]*e,i[2]*Gi,i[3]*e],bg:t.plan_url?{href:bi(t.plan_url),...Vi(t.plan_aspect,Gi)}:null,rooms:(t.rooms||[]).map(t=>{const i={...t,...Ki(t)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Gi:void 0,y:null!=i.y?i.y*e:void 0,w:null!=i.w?i.w*Gi:void 0,h:null!=i.h?i.h*e:void 0,poly:i.poly?i.poly.map(t=>[t[0]*Gi,t[1]*e]):void 0}})}}):[]}function Ji(t){if(t.poly&&t.poly.length){const e=t.poly.map(t=>t[0]),i=t.poly.map(t=>t[1]),s=Math.min(...e),o=Math.min(...i);return{x:s,y:o,w:Math.max(...e)-s,h:Math.max(...i)-o}}return{x:t.x??0,y:t.y??0,w:t.w??0,h:t.h??0}}function Xi(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}function Yi(t){const e=Zi(t.cfg),i=e.find(e=>e.id===t.spaceId);if(!i)return null;const s=i.vb,o=ti(t.cfg.spaces.find(e=>e.id===t.spaceId)),n=t.iconSize??2.5,r=n>8?2.5:n,a={};for(const e of t.cfg.spaces||[])for(const t of e.rooms||[])t.area&&(a[t.area]=e.id);const l=t.cfg.settings?.exclude_integrations?new Set(t.cfg.settings.exclude_integrations):ht,c=pt(t.cfg.settings?.icon_rules?.length?t.cfg.settings.icon_rules:dt),h=Oi({hass:t.hass,areaToSpace:a,markers:t.cfg.markers||[],settings:t.cfg.settings||{},excluded:l,showAll:!!t.cfg.settings?.show_all,firstSpaceId:e[0]?.id||"",loc:e=>Bi(t.lang,e),iconRules:c}),d=h.filter(e=>e.space===t.spaceId),p=d.filter(t=>!t.hidden),u=function(t,e,i){const s={},o=i/100*Gi*1.3;for(const i of e.rooms){if(!i.area)continue;const e=t.filter(t=>t.area===i.area);if(!e.length)continue;const n=Ji(i),r=.1*Math.min(n.w,n.h),a=n.w-2*r,l=n.h-2*r,c=Math.max(1,Math.round(Math.sqrt(e.length*a/Math.max(l,1)))),h=a/c,d=l/Math.max(Math.ceil(e.length/c),1),p=e.map((t,e)=>({x:n.x+r+h*(e%c+.5),y:n.y+r+d*(Math.floor(e/c)+.5)}));je(p,n,o,.5*r),e.forEach((t,e)=>s[t.id]=p[e])}return s}(p,i,r),_=i.rooms.filter(t=>t.area||o.showBorders).map(e=>{let s="room "+(i.bg?"overlay":"yard"),n="";const r=vi(o.fill,e);if(o.showBorders||"none"!==r){s+=" styled";const i=[`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`],a=e.area?ni(r,"lqi"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i||o.virtual)continue;const e=Ti(t,o.entities);null!=e&&s.push(e)}return He(s)}(t.hass,d,e.area):null,"light"===r?Ii(t.hass,d,e.area):"none","temp"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i)continue;if("mdi:thermometer"!==o.icon&&"mdi:air-filter"!==o.icon)continue;const e=zi(t,o.entities);null!=e&&s.push(e)}return s.length?Math.round(s.reduce((t,e)=>t+e,0)/s.length*10)/10:null}(t.hass,d,e.area):null,o.tempMin,o.tempMax,si(t.cfg?.settings)):null;a?(s+=" filled",i.push(`--room-fill:${a.c}`,`--room-fill-op:${a.a.toFixed(3)}`)):i.push("--room-fill:transparent","--room-fill-op:0"),n=i.join(";")}const a=!i.bg&&!o.showNames,l=Xi(e),c=e.poly?j``:j``;return j`${c}${a?j`${e.name}`:G}`}),m=p.map(e=>{const o=function(t,e,i,s,o){const n=e[t.id];if(n&&n.s===t.space)return{x:n.x*Gi,y:n.y*Gi};if(s[t.id])return s[t.id];const r=o.vb;return{x:r[0]+r[2]/2,y:r[1]+r[3]/2}}(e,t.layout,t.cfg,u,i),n=(o.x-s[0])/s[2]*100,r=(o.y-s[1])/s[3]*100;return B`
+ `,Gi=1e3;function Vi(t,e){const i=Number(t),s=Number.isFinite(i)&&i>0?i:1,o=s>=1?e:e*s,n=s>=1?e/s:e;return{x:(e-o)/2,y:(e-n)/2,w:o,h:n}}function Ki(t){if(null==t.x||null==t.y)return{x:t.x,y:t.y,w:t.w,h:t.h};const e=Number(t.w)||0,i=Number(t.h)||0;return{x:e<0?t.x+e:t.x,y:i<0?t.y+i:t.y,w:Math.abs(e),h:Math.abs(i)}}function Zi(t){return t&&Array.isArray(t.spaces)?t.spaces.map(t=>{const e=Gi,i=function(t){return Array.isArray(t)&&4===t.length&&t.every(t=>Number.isFinite(t))&&t[2]>1e-6&&t[3]>1e-6?t:[0,0,1,1]}(t.view_box);return{id:t.id,title:t.title,vb:[i[0]*Gi,i[1]*e,i[2]*Gi,i[3]*e],bg:t.plan_url?{href:bi(t.plan_url),...Vi(t.plan_aspect,Gi)}:null,rooms:(t.rooms||[]).map(t=>{const i={...t,...Ki(t)};return{id:i.id,name:i.name,area:i.area??null,open_to:i.open_to||void 0,settings:i.settings||void 0,x:null!=i.x?i.x*Gi:void 0,y:null!=i.y?i.y*e:void 0,w:null!=i.w?i.w*Gi:void 0,h:null!=i.h?i.h*e:void 0,poly:i.poly?i.poly.map(t=>[t[0]*Gi,t[1]*e]):void 0}})}}):[]}function Ji(t){if(t.poly&&t.poly.length){const e=t.poly.map(t=>t[0]),i=t.poly.map(t=>t[1]),s=Math.min(...e),o=Math.min(...i);return{x:s,y:o,w:Math.max(...e)-s,h:Math.max(...i)-o}}return{x:t.x??0,y:t.y??0,w:t.w??0,h:t.h??0}}function Xi(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}function Yi(t){const e=Zi(t.cfg),i=e.find(e=>e.id===t.spaceId);if(!i)return null;const s=i.vb,o=ti(t.cfg.spaces.find(e=>e.id===t.spaceId)),n=t.iconSize??2.5,r=n>8?2.5:n,a={};for(const e of t.cfg.spaces||[])for(const t of e.rooms||[])t.area&&(a[t.area]=e.id);const l=t.cfg.settings?.exclude_integrations?new Set(t.cfg.settings.exclude_integrations):ht,c=pt(t.cfg.settings?.icon_rules?.length?t.cfg.settings.icon_rules:dt),h=Oi({hass:t.hass,areaToSpace:a,markers:t.cfg.markers||[],settings:t.cfg.settings||{},excluded:l,showAll:!!t.cfg.settings?.show_all,firstSpaceId:e[0]?.id||"",loc:e=>Bi(t.lang,e),iconRules:c}),d=h.filter(e=>e.space===t.spaceId),p=d.filter(t=>!t.hidden),u=function(t,e,i){const s={},o=i/100*Gi*1.3;for(const i of e.rooms){if(!i.area)continue;const e=t.filter(t=>t.area===i.area);if(!e.length)continue;const n=Ji(i),r=.1*Math.min(n.w,n.h),a=n.w-2*r,l=n.h-2*r,c=Math.max(1,Math.round(Math.sqrt(e.length*a/Math.max(l,1)))),h=a/c,d=l/Math.max(Math.ceil(e.length/c),1),p=e.map((t,e)=>({x:n.x+r+h*(e%c+.5),y:n.y+r+d*(Math.floor(e/c)+.5)}));je(p,n,o,.5*r),e.forEach((t,e)=>s[t.id]=p[e])}return s}(d,i,r),_=i.rooms.filter(t=>t.area||o.showBorders).map(e=>{let s="room "+(i.bg?"overlay":"yard"),n="";const r=vi(o.fill,e);if(o.showBorders||"none"!==r){s+=" styled";const i=[`--room-stroke:${o.color}`,`--room-stroke-op:${o.showBorders?o.opacity:0}`],a=e.area?ni(r,"lqi"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i||o.virtual)continue;const e=Ti(t,o.entities);null!=e&&s.push(e)}return He(s)}(t.hass,d,e.area):null,"light"===r?Ii(t.hass,d,e.area):"none","temp"===r?function(t,e,i){const s=[];for(const o of e){if(o.area!==i)continue;if("mdi:thermometer"!==o.icon&&"mdi:air-filter"!==o.icon)continue;const e=zi(t,o.entities);null!=e&&s.push(e)}return s.length?Math.round(s.reduce((t,e)=>t+e,0)/s.length*10)/10:null}(t.hass,d,e.area):null,o.tempMin,o.tempMax,si(t.cfg?.settings)):null;a?(s+=" filled",i.push(`--room-fill:${a.c}`,`--room-fill-op:${a.a.toFixed(3)}`)):i.push("--room-fill:transparent","--room-fill-op:0"),n=i.join(";")}const a=!i.bg&&!o.showNames,l=Xi(e),c=e.poly?j``:j``;return j`${c}${a?j`${e.name}`:G}`}),m=p.map(e=>{const o=function(t,e,i,s,o){const n=e[t.id];if(n&&n.s===t.space)return{x:n.x*Gi,y:n.y*Gi};if(s[t.id])return s[t.id];const r=o.vb;return{x:r[0]+r[2]/2,y:r[1]+r[3]/2}}(e,t.layout,t.cfg,u,i),n=(o.x-s[0])/s[2]*100,r=(o.y-s[1])/s[3]*100;return B`
`}),g=o.showNames?i.rooms.filter(t=>t.name).map(e=>{const n=function(t,e,i){const s=i["rl_"+(t.id||"")];if(s&&s.s===e)return{x:s.x*Gi,y:s.y*Gi};const o=Xi(t);return{x:o[0],y:o[1]}}(e,i.id,t.layout,t.cfg),r=(n.x-s[0])/s[2]*100,a=(n.y-s[1])/s[3]*100,l=Math.min(1,o.opacity+.25);return B`
${e.name}
`}):[],f=i.bg?t.displayUrl?t.displayUrl(i.bg.href):i.bg.href:"";return B`
@@ -2003,8 +2003,8 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow ${this._kioskDialog?this._renderKioskDialog():G} ${this._toast?B`
${this._toast}
`:G} - `}_renderDevice(t,e,i=!0){const s=this._pos(t),o=(s.x-e.x)/e.w*100,n=(s.y-e.y)/e.h*100,r=t.hidden?"":this._stateClass(t),a=t.hidden?null:this._liveTemp(t),l=t.hidden?null:this._liveHum(t),c=!i||t.virtual||t.hidden?null:Ti(this.hass,t.entities),h=t.marker,d=h?.display||"badge",p="ripple"===d||"icon_ripple"===d,u=t.primary?this.hass.states[t.primary]:void 0,_="value"!==d||t.hidden?null:null!=a?a+"°":null!=l?l+"%":u&&!isNaN(parseFloat(u.state))?parseFloat(u.state)+(u.attributes?.unit_of_measurement?" "+u.attributes.unit_of_measurement:""):null,m=t.primary?t.primary.split(".")[0]:null,g=this._config?.live_states&&!t.hidden?ri(t.icon,m,u?.attributes?.device_class,u?.state,!!h?.icon):t.icon,f=(h?.controls||[]).filter(di),b=this._config?.live_states&&!t.hidden?f.length?f.map(t=>ai(this.hass.states[t])).find(t=>t)||null:"light"===m?ai(u):null:null,v=this._config?.live_states&&!t.hidden&&function(t,e,i){return"on"===i&&("siren"===t||"binary_sensor"===t&&!!e&&xi.has(e))}(m,u?.attributes?.device_class,u?.state),y=p&&!t.hidden&&!!t.primary&&$e(this.hass.states[t.primary]?.state),w=Number(h?.size)>0?Number(h.size):1,x=Number(h?.angle)||0,k=Number(h?.ripple_size)>0?Number(h.ripple_size):3,$=[`left:${o}%`,`top:${n}%`];return 1!==w&&$.push(`--dev-scale:${w}`),p&&($.push(`--ripple-scale:${k}`),h?.ripple_color?$.push(`--ripple-color:${h.ripple_color}`):b&&$.push(`--ripple-color:${b}`)),b&&$.push(`--light-color:${b}`),B`
ai(this.hass.states[t])).find(t=>t)||null:"light"===m?ai(u):null:null,v=this._config?.live_states&&!t.hidden&&function(t,e,i){return"on"===i&&("siren"===t||"binary_sensor"===t&&!!e&&xi.has(e))}(m,u?.attributes?.device_class,u?.state),y=p&&!t.hidden&&!!t.primary&&$e(this.hass.states[t.primary]?.state),w=Number(h?.size)>0?Number(h.size):1,x=Number(h?.angle)||0,k=Number(h?.ripple_size)>0?Number(h.ripple_size):3,$=[`left:${o}%`,`top:${n}%`];return 1!==w&&$.push(`--dev-scale:${w}`),p&&($.push(`--ripple-scale:${k}`),h?.ripple_color?$.push(`--ripple-color:${h.ripple_color}`):b&&$.push(`--ripple-color:${b}`)),b&&$.push(`--light-color:${b}`),B`
this._clickDevice(e,t)} @contextmenu=${e=>this._ctxDevice(e,t)} @@ -2017,7 +2017,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow > ${p?B``:G} ${this._newIds.has(t.id)?B``:G} - ${null!=_?B`${_}`:"ripple"!==d?B``:G} + ${null!=_?B`${_}`:"ripple"!==d||t.hidden?B``:G} ${null!=a&&null==_?B`${a}°`:G} ${null!=l&&null==_?B`${l}%`:G} ${null!=c?B`${c}`:G} @@ -2668,4 +2668,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow `}
-
`}}fs.properties={_hdrH:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fs.ZOOM_MAX=8,fs.ZOOM_MIN=.4,fs._touchSeen=!1,fs._noHoverMq="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,fs.styles=Wi,customElements.get("houseplan-card")||customElements.define("houseplan-card",fs),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.51.1 ","background:#3ea6ff;color:#04121f;font-weight:700",""); +
`}}fs.properties={_hdrH:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fs.ZOOM_MAX=8,fs.ZOOM_MIN=.4,fs._touchSeen=!1,fs._noHoverMq="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,fs.styles=Wi,customElements.get("houseplan-card")||customElements.define("houseplan-card",fs),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.51.2 ","background:#3ea6ff;color:#04121f;font-weight:700",""); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e3f504e..0c03d06 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## v1.51.2 — 2026-07-29 + +**From the v1.51.1 review** + +- **The auto grid is the same on both cards (HP-1511-01).** The full card + reserves grid cells for hidden devices (their ghosts keep a place in the + device editor); the static card compacted the grid over visible ones only, + so a freshly discovered marker with no saved position landed in different + spots on the two cards. The static card now feeds the full roster to the + same grid and still draws only the visible. +- **A ripple-display ghost keeps its base icon (HP-1511-02).** Hidden markers + with the "ripple" presentation rendered as an icon-less inactive pulse — + unrecognisable in the editor. A ghost now drops the display dressing + entirely: base icon and name, whatever the display mode. + ## v1.51.1 — 2026-07-29 **From the v1.51.0 review** diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index 1c79236..83f4816 100755 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -6,6 +6,21 @@ > **Правило проекта:** оба файла пополняются в одном коммите с самим > изменением — как и остальная документация (см. docs/STATUS.md). +## v1.51.2 — 2026-07-29 + +**По ревью v1.51.1** + +- **Авто-сетка одинакова на обеих карточках (HP-1511-01).** Полная карточка + резервирует клетки сетки за скрытыми устройствами (их призраки держат + место в редакторе); статичная уплотняла сетку по одним видимым — свежее + устройство без сохранённой позиции вставало на разных местах двух + карточек. Теперь статичная передаёт сетке полный список и по-прежнему + рисует только видимых. +- **Ripple-призрак сохраняет базовую иконку (HP-1511-02).** Скрытый маркер с + отображением «пульсация» рендерился безликим неподвижным пульсом — в + редакторе его нельзя было узнать. Призрак теперь полностью снимает + «одежду» отображения: базовая иконка и имя, каким бы ни был режим. + ## v1.51.1 — 2026-07-29 **По ревью v1.51.0** diff --git a/docs/STATUS.md b/docs/STATUS.md index 987f8ce..a86fe80 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -15,12 +15,12 @@ | Item | State | |---|---| -| Version | **v1.51.1** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance | +| Version | **v1.51.2** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance | | Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command (merge dev→main, one tag, one release with a summary changelog, CI checked on dev beforehand) | | GitHub | https://github.com/Matysh/houseplan-card — **`main` carries every published release, the latest tag is the current version above**; `dev` is where work lands and is merged into `main` at release time (so `dev` is normally equal to or ahead of `main`, never behind). Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) | | CI | validate.yml (hacs + hassfest + frontend + backend) green; release.yml attaches the bundle on release publish | | HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled; ~864 older open PRs but merge rate ≈180/mo; realistic ETA 1–3 months (checked 2026-07-24) | -| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), deployed **v1.51.1** via direct copy (HACS custom repo also installed) | +| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), deployed **v1.51.2** via direct copy (HACS custom repo also installed) | | Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover | | Tests | Four layers: frontend unit (`npm test`, node:test over `test-build/`), pure backend (`pytest tests_backend`, runs anywhere), HA-harness backend (same folder, CI only — needs py3.13 + pytest-homeassistant-custom-component), and browser smokes (`demo/smoke_*.mjs`, headless chromium). **Counts are not written down here** — they went stale within two releases while the version line beside them was kept current, which reads as less coverage than exists (review R5-2). Run `npm run inventory` for the current numbers, or read them off the last CI run | | Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts | diff --git a/docs/TESTING.md b/docs/TESTING.md index 9c22fd8..74e75a7 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -244,6 +244,11 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi at 70% of a device icon and zooming WITH the plan; the small metric rows under the room name now show in the plan editor too [auto: smoke_room_cards gearDetached/plainInPlan] +- [ ] Auto-grid parity (v1.51.2, HP-1511-01): with an empty layout, a visible + device among hidden ones sits at the same spot on both cards + [auto: smoke_hidden_flag autoGridParity] +- [ ] Ripple ghost (v1.51.2, HP-1511-02): a hidden ripple-display marker shows + its base icon, no pulse [auto: smoke_hidden_flag rippleGhost*] - [ ] Hidden LQI parity (v1.51.1, HP-1510-01): a room whose only Zigbee devices are hidden paints the same lqi fill on the full and the static card [auto: smoke_hidden_flag lqiParity] diff --git a/package-lock.json b/package-lock.json index 94f16f8..452f171 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "houseplan-card", - "version": "1.51.1", + "version": "1.51.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "houseplan-card", - "version": "1.51.1", + "version": "1.51.2", "license": "MIT", "dependencies": { "lit": "^3.1.3", diff --git a/package.json b/package.json index 5fa76be..21e132f 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "houseplan-card", - "version": "1.51.1", + "version": "1.51.2", "description": "Interactive house plan Lovelace card for Home Assistant", "license": "MIT", "type": "module", diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index 13de12e..9cafd78 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -36,7 +36,7 @@ import { cardStyles } from './styles'; import { fitInSquare, contentBounds, spaceModels } from './space-geometry'; import { langOf, t, type I18nKey } from './i18n'; -const CARD_VERSION = '1.51.1'; +const CARD_VERSION = '1.51.2'; const LS_KEY = 'houseplan_card_layout_v1'; const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering const LS_ZOOM = 'houseplan_card_zoom_v1'; @@ -4216,7 +4216,10 @@ class HouseplanCard extends LitElement { const lqi = showLqi && !d.virtual && !d.hidden ? lqiFor(this.hass, d.entities) : null; const m = d.marker; const disp = m?.display || 'badge'; - const ripple = disp === 'ripple' || disp === 'icon_ripple'; + // a ghost drops the ripple presentation entirely (HP-1511-02): an + // icon-less pulse is unrecognisable, and the release contract says a + // ghost keeps the base icon and name — display modes are status dressing + const ripple = (disp === 'ripple' || disp === 'icon_ripple') && !d.hidden; // value-only display: the measurement IS the marker const primarySt = d.primary ? this.hass.states[d.primary] : undefined; const valText = disp === 'value' && !d.hidden @@ -4255,7 +4258,7 @@ class HouseplanCard extends LitElement { } if (lightC) st.push(`--light-color:${lightC}`); return html`
this._clickDevice(e, d)} @contextmenu=${(e: MouseEvent) => this._ctxDevice(e, d)} @@ -4274,7 +4277,7 @@ class HouseplanCard extends LitElement { ${this._newIds.has(d.id) ? html`` : nothing} ${valText != null ? html`${valText}` - : disp !== 'ripple' + : disp !== 'ripple' || d.hidden ? html`` : nothing} ${temp != null && valText == null ? html`${temp}°` : nothing} diff --git a/src/space-render.ts b/src/space-render.ts index d5fbd9e..d0ae71c 100644 --- a/src/space-render.ts +++ b/src/space-render.ts @@ -72,7 +72,12 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null { // the same room show different Zigbee health on the two cards. const spaceDevs = all.filter((d) => d.space === o.spaceId); const devs = spaceDevs.filter((d) => !d.hidden); - const defPos = defaultPositions(devs, space, iconPct); + // The auto grid is computed over the FULL roster, hidden included — the + // full card reserves grid cells for hidden devices (their ghosts keep a + // place in the device editor), so the static card must too, or the same + // visible marker with no saved position lands in different spots on the + // two cards (HP-1511-01). Rendering still draws `devs` only. + const defPos = defaultPositions(spaceDevs, space, iconPct); const roomShapes = space.rooms .filter((r) => r.area || disp.showBorders)