mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
feat v1.17.0 (issue #1): place individual entities as their own icons (not just whole devices) — binding picker surfaces entities while searching; entity markers get an auto icon (name/device_class) + temperature. +1 test.
This commit is contained in:
@@ -160,6 +160,9 @@ matches, the entity *device class* decides (thermometer for temperature sensors,
|
|||||||
|
|
||||||
### Step 6. Adding your own devices manually
|
### Step 6. Adding your own devices manually
|
||||||
|
|
||||||
|
You can also place a **single entity** (not just a whole device): start typing in the binding search and individual entities appear next to devices — handy when one device exposes several values (e.g. temperature and humidity) and you want each as its own icon.
|
||||||
|
|
||||||
|
|
||||||
Not everything has to be left to the automation. With the **+** button in the header you can place any device, group or a **virtual point** on the plan (for example, an "Inlet valve" that does not exist as a device). Set a name, icon, model, link, description and, if you wish, attach a **PDF manual**.
|
Not everything has to be left to the automation. With the **+** button in the header you can place any device, group or a **virtual point** on the plan (for example, an "Inlet valve" that does not exist as a device). Set a name, icon, model, link, description and, if you wish, attach a **PDF manual**.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -164,6 +164,9 @@ title: План дома
|
|||||||
|
|
||||||
### Шаг 6. Добавление своих устройств вручную
|
### Шаг 6. Добавление своих устройств вручную
|
||||||
|
|
||||||
|
Можно поставить и **отдельную сущность** (не только устройство целиком): начните печатать в поиске привязки — рядом с устройствами появятся отдельные сущности. Удобно, когда одно устройство отдаёт несколько значений (например, температуру и влажность), а вы хотите каждое своей иконкой.
|
||||||
|
|
||||||
|
|
||||||
Не всё нужно оставлять на автоматику. Кнопкой **+** в шапке можно поставить на план любое устройство, группу или **виртуальную точку** (например, «Вентиль на вводе», которого нет как устройства). Задайте имя, иконку, модель, ссылку, описание и при желании приложите **PDF-инструкцию**.
|
Не всё нужно оставлять на автоматику. Кнопкой **+** в шапке можно поставить на план любое устройство, группу или **виртуальную точку** (например, «Вентиль на вводе», которого нет как устройства). Задайте имя, иконку, модель, ссылку, описание и при желании приложите **PDF-инструкцию**.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
|
|||||||
FILES_URL = "/houseplan_files/files"
|
FILES_URL = "/houseplan_files/files"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.16.2"
|
VERSION = "1.17.0"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"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",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.16.2"
|
"version": "1.17.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.17.0 — 2026-07-11 (place individual entities, not just whole devices — issue #1)
|
||||||
|
- **You can now put a single entity on the plan as its own icon** — e.g. a climate sensor
|
||||||
|
exposes temperature AND humidity; add the device (shows temperature) and separately add the
|
||||||
|
humidity entity as a second icon. In the "add device" dialog, start typing in the binding
|
||||||
|
search and individual entities now appear alongside devices/helpers (surfaced only while
|
||||||
|
searching, so the default list stays clean); the sub-label shows the domain and parent device.
|
||||||
|
- Entity markers now get a **sensible auto icon** (name rules → `device_class` → chip) instead of
|
||||||
|
the generic shape, and a **temperature value** when the entity is a thermometer/air-monitor.
|
||||||
|
The `entity:<eid>` binding already existed (used by helpers/light groups); this exposes it for
|
||||||
|
any entity. (+1 test: 54 → 55.)
|
||||||
|
|
||||||
## v1.16.2 — 2026-07-11 (docs+log: correct card resource URL — fixes "Custom element doesn't exist")
|
## v1.16.2 — 2026-07-11 (docs+log: correct card resource URL — fixes "Custom element doesn't exist")
|
||||||
- **Support issue #2**: users adding a Lovelace resource that points at the on-disk path
|
- **Support issue #2**: users adding a Lovelace resource that points at the on-disk path
|
||||||
`/custom_components/houseplan/frontend/houseplan-card.js` get a `text/plain` MIME error and
|
`/custom_components/houseplan/frontend/houseplan-card.js` get a `text/plain` MIME error and
|
||||||
|
|||||||
+2
-2
@@ -13,14 +13,14 @@
|
|||||||
|
|
||||||
| Item | State |
|
| Item | State |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Version | **v1.16.2** everywhere (manifest, const.py, package.json, CARD_VERSION) |
|
| Version | **v1.17.0** everywhere (manifest, const.py, package.json, CARD_VERSION) |
|
||||||
| GitHub | https://github.com/Matysh/houseplan-card — branch `main`, releases v1.9.3…v1.11.2 |
|
| GitHub | https://github.com/Matysh/houseplan-card — branch `main`, releases v1.9.3…v1.11.2 |
|
||||||
| CI | `.github/workflows/validate.yml` (hacs + hassfest + frontend + backend) — **fully green** since v1.11.1; `release.yml` auto-attaches the card bundle (needs `permissions: contents: write`, fixed) |
|
| CI | `.github/workflows/validate.yml` (hacs + hassfest + frontend + backend) — **fully green** since v1.11.1; `release.yml` auto-attaches the card bundle (needs `permissions: contents: write`, fixed) |
|
||||||
| HACS | Works as custom repository (id 1290210112 on the home instance). **Inclusion PR: https://github.com/hacs/default/pull/9004** (queue ≈2 months as of 2026-07). Lesson: #8995 was auto-closed by hacs-bot — the PR body MUST be their exact template with every checkbox ticked and all 3 links (release, HACS action run, hassfest run); a custom body gets closed without discussion |
|
| HACS | Works as custom repository (id 1290210112 on the home instance). **Inclusion PR: https://github.com/hacs/default/pull/9004** (queue ≈2 months as of 2026-07). Lesson: #8995 was auto-closed by hacs-bot — the PR body MUST be their exact template with every checkbox ticked and all 3 links (release, HACS action run, hassfest run); a custom body gets closed without discussion |
|
||||||
| Brands | Ships **inside the integration**: `custom_components/houseplan/brand/{icon,icon@2x,logo,logo@2x}.png` (HA ≥2026.3 local-brands mechanism). home-assistant/brands PR #10700 was auto-closed — that repo no longer accepts custom integrations |
|
| Brands | Ships **inside the integration**: `custom_components/houseplan/brand/{icon,icon@2x,logo,logo@2x}.png` (HA ≥2026.3 local-brands mechanism). home-assistant/brands PR #10700 was auto-closed — that repo no longer accepts custom integrations |
|
||||||
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed v1.11.2, installed *via HACS* (custom repo) — updates flow through HACS now |
|
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed v1.11.2, installed *via HACS* (custom repo) — updates flow through HACS now |
|
||||||
| Localization | UI en/ru (`src/i18n.ts`), auto by `hass.locale` + `language` card option; codebase and docs are English-first (`README.ru.md` is the Russian copy) |
|
| Localization | UI en/ru (`src/i18n.ts`), auto by `hass.locale` + `language` card option; codebase and docs are English-first (`README.ru.md` is the Russian copy) |
|
||||||
| Tests | 54 frontend (node:test, incl. a 12-test buildDevices suite on a fake hass) + 10 pure backend (anywhere) + 12 HA-harness backend (CI only, py3.13; skipped locally — sandbox has py3.10) |
|
| Tests | 55 frontend (node:test, incl. a 12-test buildDevices suite on a fake hass) + 10 pure backend (anywhere) + 12 HA-harness backend (CI only, py3.13; skipped locally — sandbox has py3.10) |
|
||||||
|
|
||||||
## Recent milestones (details in CHANGELOG.md)
|
## Recent milestones (details in CHANGELOG.md)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.16.2",
|
"version": "1.17.0",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+6
-2
@@ -261,18 +261,22 @@ export function buildDevices(ctx: BuildCtx): DevItem[] {
|
|||||||
const area = m.area || reg?.area_id || (reg?.device_id && h.devices[reg.device_id]?.area_id) || '';
|
const area = m.area || reg?.area_id || (reg?.device_id && h.devices[reg.device_id]?.area_id) || '';
|
||||||
const space = (area && areaToSpace[area]) || m.space || firstSpaceId;
|
const space = (area && areaToSpace[area]) || m.space || firstSpaceId;
|
||||||
const st = h.states[ref];
|
const st = h.states[ref];
|
||||||
|
const nm = reg?.name || st?.attributes?.friendly_name || ref;
|
||||||
|
let icon = resolveIcon(h, nm, '', [ref], iconRules);
|
||||||
|
if (ref.startsWith('lock.')) icon = 'mdi:lock';
|
||||||
const item: DevItem = {
|
const item: DevItem = {
|
||||||
id: m.id,
|
id: m.id,
|
||||||
name: reg?.name || st?.attributes?.friendly_name || ref,
|
name: nm,
|
||||||
model: '',
|
model: '',
|
||||||
area,
|
area,
|
||||||
space,
|
space,
|
||||||
icon: 'mdi:shape-outline',
|
icon,
|
||||||
entities: [ref],
|
entities: [ref],
|
||||||
primary: ref,
|
primary: ref,
|
||||||
bindingKind: 'entity',
|
bindingKind: 'entity',
|
||||||
bindingRef: ref,
|
bindingRef: ref,
|
||||||
};
|
};
|
||||||
|
if (icon === 'mdi:thermometer' || icon === 'mdi:air-filter') item.temp = tempFor(h, [ref]);
|
||||||
applyMarker(item, m);
|
applyMarker(item, m);
|
||||||
rest.push(item);
|
rest.push(item);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+18
-1
@@ -26,7 +26,7 @@ import './space-card';
|
|||||||
import { cardStyles } from './styles';
|
import { cardStyles } from './styles';
|
||||||
import { langOf, t, type I18nKey } from './i18n';
|
import { langOf, t, type I18nKey } from './i18n';
|
||||||
|
|
||||||
const CARD_VERSION = '1.16.2';
|
const CARD_VERSION = '1.17.0';
|
||||||
const LS_KEY = 'houseplan_card_layout_v1';
|
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_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||||
@@ -1262,6 +1262,23 @@ class HouseplanCard extends LitElement {
|
|||||||
sub: eid.split('.')[0] + ' · ' + (reg.platform === 'group' ? this._t('marker.sub_group') : this._t('marker.sub_helper')),
|
sub: eid.split('.')[0] + ' · ' + (reg.platform === 'group' ? this._t('marker.sub_group') : this._t('marker.sub_helper')),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Individual entities — surfaced only while searching (avoids a huge default list); lets you
|
||||||
|
// place a single entity of a multi-entity device (e.g. the humidity of a climate sensor) as
|
||||||
|
// its own icon. Uses the same entity: binding as helpers/groups.
|
||||||
|
const q = (this._markerDialog?.bindingFilter || '').toLowerCase().trim();
|
||||||
|
if (q) {
|
||||||
|
const seen = new Set(list.map((o) => o.value));
|
||||||
|
for (const [eid, reg] of Object.entries<any>(h.entities)) {
|
||||||
|
const v = 'entity:' + eid;
|
||||||
|
if (taken.has(v) || seen.has(v) || reg.hidden) continue;
|
||||||
|
const stt = h.states[eid];
|
||||||
|
const label = reg.name || stt?.attributes?.friendly_name || eid;
|
||||||
|
const dev = reg.device_id ? h.devices[reg.device_id] : null;
|
||||||
|
const devName = dev ? (dev.name_by_user || dev.name || '') : '';
|
||||||
|
if (!(label + ' ' + eid + ' ' + devName).toLowerCase().includes(q)) continue;
|
||||||
|
list.push({ value: v, label, sub: eid.split('.')[0] + ' · ' + this._t('marker.sub_entity') + (devName ? ' · ' + devName : '') });
|
||||||
|
}
|
||||||
|
}
|
||||||
const f = (this._markerDialog?.bindingFilter || '').toLowerCase().trim();
|
const f = (this._markerDialog?.bindingFilter || '').toLowerCase().trim();
|
||||||
const filtered = f
|
const filtered = f
|
||||||
? list.filter((o) => (o.label + ' ' + o.sub + ' ' + o.value).toLowerCase().includes(f))
|
? list.filter((o) => (o.label + ' ' + o.sub + ' ' + o.value).toLowerCase().includes(f))
|
||||||
|
|||||||
+2
-1
@@ -178,5 +178,6 @@
|
|||||||
"editor.show_button": "Show button",
|
"editor.show_button": "Show button",
|
||||||
"editor.button_label": "Button label",
|
"editor.button_label": "Button label",
|
||||||
"editor.button_target": "Target dashboard path",
|
"editor.button_target": "Target dashboard path",
|
||||||
"editor.aspect_ratio": "Aspect ratio (e.g. 16:9 or auto)"
|
"editor.aspect_ratio": "Aspect ratio (e.g. 16:9 or auto)",
|
||||||
|
"marker.sub_entity": "entity"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -178,5 +178,6 @@
|
|||||||
"editor.show_button": "Показывать кнопку",
|
"editor.show_button": "Показывать кнопку",
|
||||||
"editor.button_label": "Текст кнопки",
|
"editor.button_label": "Текст кнопки",
|
||||||
"editor.button_target": "Путь дашборда (куда вести)",
|
"editor.button_target": "Путь дашборда (куда вести)",
|
||||||
"editor.aspect_ratio": "Соотношение сторон (напр. 16:9 или auto)"
|
"editor.aspect_ratio": "Соотношение сторон (напр. 16:9 или auto)",
|
||||||
|
"marker.sub_entity": "сущность"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,3 +224,28 @@ test('isTempEntity: excludes chip temperature and diagnostic-category entities',
|
|||||||
// если в устройстве и чип, и настоящий — берётся настоящий
|
// если в устройстве и чип, и настоящий — берётся настоящий
|
||||||
assert.equal(tempFor(hass, ['sensor.plug_device_temperature', 'sensor.room_temperature']), 21);
|
assert.equal(tempFor(hass, ['sensor.plug_device_temperature', 'sensor.room_temperature']), 21);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildDevices: entity marker gets an auto icon + temperature (split a multi-entity device)', () => {
|
||||||
|
const h = mkHass({
|
||||||
|
entities: {
|
||||||
|
'sensor.clim_temp': { entity_id: 'sensor.clim_temp', device_id: 'clim', platform: 'demo' },
|
||||||
|
'sensor.clim_hum': { entity_id: 'sensor.clim_hum', device_id: 'clim', platform: 'demo' },
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
'sensor.clim_temp': { state: '22.5', attributes: { device_class: 'temperature', friendly_name: 'Climate Temperature' } },
|
||||||
|
'sensor.clim_hum': { state: '55', attributes: { device_class: 'humidity', friendly_name: 'Climate Humidity' } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const ctx = baseCtx(h, { markers: [
|
||||||
|
{ id: 'e1', binding: 'entity:sensor.clim_temp' },
|
||||||
|
{ id: 'e2', binding: 'entity:sensor.clim_hum' },
|
||||||
|
] });
|
||||||
|
const devs = buildDevices(ctx);
|
||||||
|
const t = devs.find((d) => d.id === 'e1');
|
||||||
|
const hum = devs.find((d) => d.id === 'e2');
|
||||||
|
assert.equal(t.icon, 'mdi:thermometer');
|
||||||
|
assert.equal(t.temp, 22.5);
|
||||||
|
// humidity resolves to a real icon (device_class fallback), not the old generic shape
|
||||||
|
assert.ok(hum && hum.icon && hum.icon !== 'mdi:shape-outline');
|
||||||
|
assert.equal(hum.primary, 'sensor.clim_hum'); // more-info target
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user