- All card UI strings moved to src/i18n.ts (en/ru); language follows the HA profile automatically, new 'language: en|ru' card option forces it; GUI editor localized and got the language dropdown; generated device names localized via BuildCtx.loc. - English-only codebase: comments, docstrings, test names, backend error messages and logs. Russian remains only in the ru dictionary, ru.json, iconFor regexes matching Russian device names (+their fixtures) and README.ru.md. - Docs English-first: README (EN) + README.ru.md, ARCHITECTURE/DEVELOPMENT/ ROADMAP/CHANGELOG fully translated; translations/en.json had Russian - fixed. - Removed obsolete RELEASE_NOTES_v1.9.3.md and scripts_publish.sh.
7.9 KiB
House Plan architecture
Updated: 2026-07-04 (v1.2.2). The repository = a HACS integration (category Integration)
that contains both the backend (custom_components/houseplan) and the Lovelace card (src/ → dist/).
Layout
houseplan-card/
├─ src/ # card sources (TypeScript + Lit 3)
│ ├─ houseplan-card.ts # the card: rendering, states, drag, tooltip, sticky header
│ ├─ editor.ts # GUI config editor (ha-form + selectors)
│ ├─ rules.ts # icon rules (iconFor), curation, groups, domain priority
│ └─ data/
│ ├─ house.ts # geometry: ROOMS (rooms→area), FLOOR_VB (viewBox), names
│ └─ backgrounds.ts # VECTOR plans (SVG base64) + FLOOR_BG_RECT (positioning)
├─ dist/houseplan-card.js # build (rollup+terser), ~290 KB, plans embedded
├─ custom_components/houseplan/ # the HA integration
│ ├─ __init__.py # setup: Store, WS commands, JS serving (add_extra_js_url)
│ ├─ websocket_api.py # houseplan/layout/get|set|update
│ ├─ config_flow.py # single entry; admin_only option (editing restricted to admins)
│ ├─ const.py # DOMAIN, STORAGE_KEY, VERSION, FRONTEND_URL
│ └─ frontend/houseplan-card.js # copy of dist, served as /houseplan_files/houseplan-card.js
├─ assets/ # plan sources: f1_plan.svg, f2_plan.svg (REMPLANNER), *_bg.png (old raster versions)
├─ hacs.json # HACS manifest
└─ docs/ # this documentation
Key decisions
- One repository — integration + card. The integration serves the JS
(
async_register_static_paths) and registers it as a Lovelace resource (module) — the frontend waits for resources before rendering, so the card also works on a cold start of the mobile app (unlikeadd_extra_js_url, which remains a fallback for YAML mode). The user does not need to add the resource manually. - Icon layout lives on the server.
helpers.storage.Store(1, "houseplan.layout")→.storage/houseplan.layout. The card reads/writes viahass.callWS(houseplan/layout/get|set|update). Fallback — localStorage (when the integration is absent). - No token. Everything comes from the frontend
hassobject:hass.states(reactive),hass.devices/entities/areas(registries). No direct WS connections. - Reactivity. Every state change in HA leads to set hass → re-render. Temperatures/LQI/on-off are live by definition (verified by substituting state).
Coordinate system
- Base space: 1489×1053 ("pixels" of the old PNG render, 1 unit = 1 px). All rooms, icon positions and floor viewBoxes live in it. DO NOT change without a layout migration.
- Vector plans are inserted as
<image href=svg>into theFLOOR_BG_RECTrectangle:- f1: scale 0.647, offset (490, 27) → rect [490, 27, 774.2, 949.3]
- f2: scale 0.896, offset (351, 21) → rect [351, 21, 1048.4, 961.4]
- computed via raster correlation (cv2.matchTemplate on binarized darkness maps) of the SVG render against the reference PNG; accuracy ~1 px. The scripts are reproducible (docs/DEVELOPMENT.md).
- Rooms (
ROOMS) are snapped to the inner faces of walls (semi-automatic: search for the nearest "dark line" along the profile + manual fine-tuning against overlay renders).
Card data model (runtime)
DevItem: id (device_id), name, model, area, floor, icon, entities[], primary (the entity used for
more-info by domain priority), temp, members[] (light group), link/linkPrimary (Z2M group).
Built from the registries (_buildDevices), rules carried over 1-to-1 from the prototype:
- only devices with an area from the room list are shown;
- hidden: entry_type=service, integrations from EXCLUDED_DOMAINS, model=Group, scenes, bridges, myheat sub-devices, duplicates by "name|area";
- a device with a
lock.*entity always getsmdi:lock(TTLock locks in the registry are named "Dom"/"Terrasa"/"Kladovka" [House/Terrace/Storeroom] — unrecognizable by name); - lamps (mdi:lightbulb) with ≥2 in a room collapse into a group
mdi:lightbulb-group(click → menu: the whole group + individual lamps).
Live data
- Temperature: an entity with device_class=temperature / °C /
_temperature$→ label on the right. - LQI (zigbee): the average over
*_linkqualityentities → label under the icon; color vialqiColor(): ≤40 red → ≥180 green (hsl gradient). The room average is shown in the room tooltip. - Icon state classes: on (yellow), open (orange: cover/valve/lock/binary_sensor of problem classes), unavail (transparency).
Sizes
icon_size in the config = % of the visible plan area width (default 2.5). Implementation:
.stage { container-type: inline-size } + sizes in cqw. Legacy px values (>8) are ignored.
Sticky header
.head { position: sticky; top: var(--header-height, 56px) }; it is MANDATORY that
ha-card { overflow: visible } — overflow: hidden breaks sticky.
Device markers (v1.6.0+)
config.markers[]: {id, binding:'device:<id>'|'entity:<eid>'|'virtual', space?, area?, hidden?, name?, icon?, model?, link?, description?, pdfs:[{name,url}]}. A hybrid: auto-discovered HA devices
appear on their own; a marker with binding=device:<id> overrides them (metadata/rebinding/hiding),
entity:<eid> — for groups/helpers, virtual — a manual icon without HA. The marker id = device_id /
lg_<eid> / v_<rand> (preserves the position in the layout). The binding picker excludes already-placed
references and duplicates by name|area. Manual files: houseplan/file/set → /config/houseplan/files/<id>/,
served from /houseplan_files/files/.
Server-side configuration (v1.3.0+)
.storage/houseplan.config (Store):
{ "spaces": [{ "id","title","plan_url","aspect","view_box":[4],"rooms":[{"id","name","area","x","y","w","h"}] }],
"device_overrides": {"<device_id>": {"hidden","icon","name"}},
"virtual_devices": [{"id","space","name","icon","x","y","note?","entity_id?"}],
"settings": {"exclude_integrations":[],"group_lights":true} }
All coordinates are normalized (0..1 of the space plan); the render space is
1000 × 1000/aspect. Layout v2: {device_id: {"s": space, "x", "y"}} (normalized).
Plan files: <config>/houseplan/plans/<space>.<ext> → URL /houseplan_files/plans/….
If the server config is empty, the card falls back to the legacy bundle (the dacha) and shows a
"To server" migration button in edit mode. The dacha was migrated on 2026-07-04.
Markup editor (v1.4.0+)
State inside the card: _markup (mode), _tool (draw/erase/delroom), _path (the current outline,
vertices on the GRID_N=60 grid). Clicks on the stage → _svgPoint→_snap. Each pair of points adds a
segment to space.segments (dedup by key, saved via config/set with debounce). The outline is closed
= a click on the first vertex → area select (hass.areas) + name → room {poly}. Polygon rooms and
rectangles are rendered uniformly (hit-test: point-in-polygon / rect).
Integration WS API
| Command | Parameters | Response |
|---|---|---|
houseplan/layout/get |
— | {layout: {device_id: {x,y}}} |
houseplan/layout/set |
layout |
{ok} (admin_only optional) |
houseplan/layout/update |
device_id, pos |
{ok} |
houseplan/config/get |
— | {config, rev} |
houseplan/config/set |
config, expected_rev? |
{ok, rev} / err conflict; event houseplan_config_updated |
houseplan/plan/set |
space_id, ext (svg/png/jpg/webp), data (b64, ≤8 MB) |
{ok, url} |
houseplan/file/set |
marker_id, filename, data (b64) |
{ok,url,name} (legacy, WS limit) |
File uploads go over HTTP (not WS, which has a message-size limit): POST /api/houseplan/upload
(multipart: marker_id + file), HomeAssistantView, requires_auth. Served from /houseplan_files/files/.