mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
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:
@@ -13,7 +13,7 @@ FILES_URL = "/houseplan_files/files"
|
||||
CONTENT_URL = "/api/houseplan/content"
|
||||
FILES_DIR = "houseplan/files"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.44.4"
|
||||
VERSION = "1.44.5"
|
||||
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"spaces": [],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,5 +16,5 @@
|
||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.44.4"
|
||||
"version": "1.44.5"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+5
-5
File diff suppressed because one or more lines are too long
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## v1.44.5 — 2026-07-27
|
||||
- **Room climate now counts every sensor in the area**, including devices that
|
||||
are not placed on the plan (hidden by curation or by you). Previously the
|
||||
average was taken over the visible icons only, so hiding a thermometer
|
||||
silently removed it from the room card, the tooltip and the temperature fill.
|
||||
Curation still applies (fridges, TRVs and chip-temperature plugs stay out),
|
||||
and an explicit per-room source still wins.
|
||||
- The room tooltip no longer says "open the area" — clicking a room stopped
|
||||
navigating in v1.40.1; the link icon on the room card does that.
|
||||
|
||||
|
||||
## v1.44.4 — 2026-07-27 (audit follow-up: B2, B5, L4)
|
||||
- **One authorization policy (B2).** The HTTP upload view still failed **open**
|
||||
when the config entry was unavailable while the WebSocket path failed closed —
|
||||
|
||||
+11
-7
@@ -6,6 +6,17 @@
|
||||
> **Правило проекта:** оба файла пополняются в одном коммите с самим
|
||||
> изменением — как и остальная документация (см. docs/STATUS.md).
|
||||
|
||||
## v1.44.5 — 2026-07-27
|
||||
- **Климат комнаты считается по всем датчикам зоны**, включая устройства,
|
||||
которых нет на плане (скрыты курированием или вами). Раньше среднее бралось
|
||||
только по видимым значкам, поэтому скрытый термометр молча выпадал из
|
||||
карточки комнаты, подсказки и температурной заливки. Курирование сохранено
|
||||
(холодильники, термоголовки и розетки с температурой чипа не считаются), а
|
||||
явно выбранный источник в настройках комнаты по-прежнему главнее.
|
||||
- Из подсказки к комнате убрана фраза «открыть зону» — клик по комнате перестал
|
||||
никуда вести ещё в v1.40.1, для перехода есть значок-ссылка у названия.
|
||||
|
||||
|
||||
## v1.44.4 — 2026-07-27 (доработка по аудиту: B2, B5, L4)
|
||||
- **Единая политика авторизации (B2).** HTTP-загрузка по-прежнему **разрешала**
|
||||
запись, когда запись о конфигурации недоступна, тогда как WebSocket-путь уже
|
||||
@@ -37,13 +48,6 @@
|
||||
планшеты продолжали работать. Тест бэкенда скачивает подписанный адрес **без**
|
||||
заголовка авторизации и проверяет 200, а без подписи — 401.
|
||||
|
||||
|
||||
> Русская версия [docs/CHANGELOG.md](CHANGELOG.md). Переведены записи начиная
|
||||
> с v1.42.0 (2026-07-26); более ранние доступны только в английском файле.
|
||||
>
|
||||
> **Правило проекта:** оба файла пополняются в одном коммите с самим
|
||||
> изменением — как и остальная документация (см. docs/STATUS.md).
|
||||
|
||||
## v1.44.2 — 2026-07-27 (внешнее код-ревью: CR-1…CR-3)
|
||||
|
||||
Второе, состязательное ревью (версии v1.44.0) дало три находки — все закрыты.
|
||||
|
||||
@@ -65,6 +65,14 @@
|
||||
pointer through the tolerant helper; decor shapes cannot be dragged more
|
||||
than a quarter of the plan outside the viewBox [auto: smoke_decor]
|
||||
|
||||
- [ ] Room climate counts hidden sensors (v1.44.5): a thermometer that is NOT
|
||||
placed on the plan (hidden by curation 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 tooltip wording (v1.44.5): hovering a room shows its name (plus
|
||||
temperature/signal when available) and no longer claims "open the area" —
|
||||
room clicks were removed in v1.40.1 [manual]
|
||||
|
||||
## Environments matrix
|
||||
|
||||
Run the *core flows* (marked ★ below) in each environment at least once per minor release:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.44.4",
|
||||
"version": "1.44.5",
|
||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+47
-1
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Модель",
|
||||
|
||||
+42
-1
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildDevices, lightGroups, primaryEntity, lqiFor, tempFor, humFor, areaLights, areaTemp, areaHum, areaLightStats, sourceValue } from '../test-build/devices.js';
|
||||
import { buildDevices, lightGroups, primaryEntity, lqiFor, tempFor, humFor, areaLights, areaTemp, areaHum, areaLightStats, sourceValue , areaClimate } from '../test-build/devices.js';
|
||||
import { compileIconRules, iconFor } from '../test-build/rules.js';
|
||||
|
||||
/** Minimal fake hass around the pieces buildDevices reads. */
|
||||
@@ -359,3 +359,44 @@ test('sourceValue: explicit entity and device sources (tier 3)', () => {
|
||||
assert.equal(sourceValue(hass, '', 'temp'), null);
|
||||
assert.equal(sourceValue(hass, 'garbage', 'temp'), null);
|
||||
});
|
||||
|
||||
test('areaClimate: counts sensors that are NOT on the plan (field report)', () => {
|
||||
const hass = {
|
||||
devices: {
|
||||
d1: { id: 'd1', name: 'Датчик температуры спальня', area_id: 'bed' },
|
||||
d2: { id: 'd2', name: 'Холодильник', model: 'LG', area_id: 'bed' },
|
||||
d3: { id: 'd3', name: 'Qingping air monitor', area_id: 'bed' },
|
||||
d4: { id: 'd4', name: 'Датчик температуры кухня', area_id: 'kitchen' },
|
||||
},
|
||||
entities: {
|
||||
'sensor.bed_t': { device_id: 'd1' },
|
||||
'sensor.bed_h': { device_id: 'd1' },
|
||||
'sensor.fridge_t': { device_id: 'd2' },
|
||||
'sensor.air_t': { device_id: 'd3' },
|
||||
'sensor.air_h': { device_id: 'd3' },
|
||||
'sensor.kitchen_t': { device_id: 'd4' },
|
||||
},
|
||||
states: {
|
||||
'sensor.bed_t': { state: '21.0', attributes: { device_class: 'temperature', unit_of_measurement: '°C' } },
|
||||
'sensor.bed_h': { state: '40', attributes: { device_class: 'humidity', unit_of_measurement: '%' } },
|
||||
'sensor.fridge_t': { state: '4', attributes: { device_class: 'temperature', unit_of_measurement: '°C' } },
|
||||
'sensor.air_t': { state: '23.0', attributes: { device_class: 'temperature', unit_of_measurement: '°C' } },
|
||||
'sensor.air_h': { state: '50', attributes: { device_class: 'humidity', unit_of_measurement: '%' } },
|
||||
'sensor.kitchen_t': { state: '30.0', attributes: { device_class: 'temperature', unit_of_measurement: '°C' } },
|
||||
},
|
||||
};
|
||||
// среднее по двум термометрам зоны; ни одно устройство не «размещено» на плане
|
||||
assert.equal(areaClimate(hass, 'bed', 'temp'), 22);
|
||||
assert.equal(areaClimate(hass, 'bed', 'hum'), 45);
|
||||
// холодильник по-прежнему не считается климатом комнаты
|
||||
assert.notEqual(areaClimate(hass, 'bed', 'temp'), (21 + 4 + 23) / 3);
|
||||
// чужая зона не подмешивается
|
||||
assert.equal(areaClimate(hass, 'kitchen', 'temp'), 30);
|
||||
assert.equal(areaClimate(hass, 'nowhere', 'temp'), null);
|
||||
// сущность со своей area_id учитывается, даже если устройство в другой зоне
|
||||
const hass2 = {
|
||||
...hass,
|
||||
entities: { ...hass.entities, 'sensor.kitchen_t': { device_id: 'd4', area_id: 'bed' } },
|
||||
};
|
||||
assert.equal(areaClimate(hass2, 'kitchen', 'temp'), null);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user