fix v1.44.5: room climate counts hidden sensors; drop the stale room tooltip

- areaClimate() walks the HA registry for the area instead of the list
  of VISIBLE icons: a thermometer hidden by curation or by the user was
  silently dropped from the room card, tooltip and temperature fill
  (field report). Curation still filters fridges/TRVs; the auto icon is
  used on purpose so a custom marker icon cannot change what a device
  measures; an explicit per-room source still wins
- room tooltip no longer says 'open the area' — room clicks were removed
  in v1.40.1 (the link icon does it)
- +1 unit test (120); both changelogs updated
This commit is contained in:
Matysh
2026-07-27 14:21:50 +03:00
parent 09b0ba41a5
commit 715a93ec61
14 changed files with 143 additions and 34 deletions
+47 -1
View File
@@ -162,7 +162,7 @@ export function lightGroups(hass: any, enabled: boolean): { eid: string; name: s
}
/** Icon with the full fallback chain: name rules → entity device_class → chip. */
function resolveIcon(hass: any, name: string, model: string | undefined, entIds: string[], rules?: CompiledIconRule[]): string {
export function resolveIcon(hass: any, name: string, model: string | undefined, entIds: string[], rules?: CompiledIconRule[]): string {
const byRules = iconFor(name, model, rules);
if (byRules !== FALLBACK_ICON) return byRules;
const classes: string[] = [];
@@ -400,6 +400,52 @@ export function areaHum(
return Math.round(vals.reduce((a, b) => a + b, 0) / vals.length);
}
/**
* Room climate from EVERY sensor of the area — including devices that are not
* placed on the plan (hidden by curation or by the user). The old helpers read
* the visible-icon list, so hiding a thermometer silently removed it from the
* room card (field report, 2026-07-27).
*
* Curation is kept: only devices the card itself recognises as thermometers /
* air monitors count, so fridges, TRVs and chip-temperature plugs stay out.
* The AUTO icon is used on purpose — a custom marker icon must not change what
* a device measures.
*/
export function areaClimate(
hass: any, area: string, kind: 'temp' | 'hum', rules?: CompiledIconRule[],
): number | null {
if (!area || !hass?.entities) return null;
const groups = new Map<string, { name: string; model?: string; ents: string[] }>();
for (const [eid, reg] of Object.entries<any>(hass.entities)) {
const dev = reg.device_id ? hass.devices?.[reg.device_id] : null;
const entArea = reg.area_id || dev?.area_id || null;
if (entArea !== area) continue;
const key = reg.device_id || eid;
if (!groups.has(key)) {
const st = hass.states?.[eid];
groups.set(key, {
name: (dev ? dev.name_by_user || dev.name : reg.name || st?.attributes?.friendly_name || eid) || eid,
model: dev?.model,
ents: [],
});
}
groups.get(key)!.ents.push(eid);
}
const vals: number[] = [];
for (const g of groups.values()) {
const icon = resolveIcon(hass, g.name, g.model, g.ents, rules);
const ok = kind === 'temp'
? icon === 'mdi:thermometer' || icon === 'mdi:air-filter'
: icon === 'mdi:thermometer' || icon === 'mdi:air-filter' || icon === 'mdi:water-percent';
if (!ok) continue;
const v = kind === 'temp' ? tempFor(hass, g.ents) : humFor(hass, g.ents);
if (v != null) vals.push(v);
}
if (!vals.length) return null;
const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
return kind === 'temp' ? Math.round(avg * 10) / 10 : Math.round(avg);
}
/** How many of the area's lights are on: {on, total}, or null without lights. */
export function areaLightStats(
hass: any,
+6 -5
View File
@@ -22,7 +22,7 @@ import {
isActiveState, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY,
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
} from './logic';
import { buildDevices, lqiFor, tempFor, humFor, isHumEntity, areaLights, areaTemp, areaHum, areaLightStats, sourceValue } from './devices';
import { buildDevices, lqiFor, tempFor, humFor, isHumEntity, areaLights, areaTemp, areaHum, areaLightStats, sourceValue, areaClimate } from './devices';
import type {
OpeningCfg,
RoomCfg, SpaceModel, PdfRef, Marker, ServerConfig, DevItem, CardConfig,
@@ -32,7 +32,7 @@ import './space-card';
import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.44.4';
const CARD_VERSION = '1.44.5';
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';
@@ -3654,7 +3654,7 @@ class HouseplanCard extends LitElement {
style = st.join(';');
}
const tip = (e: MouseEvent) =>
this._showTip(e, r.name, this._t('tip.room'),
this._showTip(e, r.name, '',
showLqi ? this._roomLqi(r.area) : null,
this._roomTemp(r));
const label = !space.bg && !disp.showNames && !this._markup;
@@ -3834,14 +3834,15 @@ class HouseplanCard extends LitElement {
private _roomTemp(r: RoomCfg): number | null {
const src = r.settings?.temp_source;
if (src) return sourceValue(this.hass, src, 'temp');
return r.area ? areaTemp(this.hass, this._devices, r.area) : null;
// every sensor of the area, placed on the plan or not (field report)
return r.area ? areaClimate(this.hass, r.area, 'temp', this._iconRules) : 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.hass, src, 'hum');
return r.area ? areaHum(this.hass, this._devices, r.area) : null;
return r.area ? areaClimate(this.hass, r.area, 'hum', this._iconRules) : null;
}
private _resetRoomDialogFields(): void {
-1
View File
@@ -59,7 +59,6 @@
"markup.delete": "Delete",
"markup.hint_points": "points: {n} · Esc/Ctrl+Z — undo a dot · close the outline by clicking the first one",
"markup.hint_start": "click a grid dot to start the outline",
"tip.room": "room — open the area",
"tip.lqi": "average zigbee signal:",
"info.device_header": "Device on the plan",
"info.model": "Model",
-1
View File
@@ -59,7 +59,6 @@
"markup.delete": "Удалить",
"markup.hint_points": "точек: {n} · Esc/Ctrl+Z — убрать точку · замкните контур кликом по первой",
"markup.hint_start": "кликните точку сетки, чтобы начать контур",
"tip.room": "комната — открыть зону",
"tip.lqi": "средний сигнал zigbee:",
"info.device_header": "Устройство на плане",
"info.model": "Модель",