Files
houseplan-card/docs/ARCHITECTURE.md
T
Matysh 94fb4af14c feat v1.11.0: full English translation + en/ru UI localization
- 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.
2026-07-05 21:43:58 +03:00

7.9 KiB
Raw Blame History

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

  1. 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 (unlike add_extra_js_url, which remains a fallback for YAML mode). The user does not need to add the resource manually.
  2. Icon layout lives on the server. helpers.storage.Store(1, "houseplan.layout").storage/houseplan.layout. The card reads/writes via hass.callWS (houseplan/layout/get|set|update). Fallback — localStorage (when the integration is absent).
  3. No token. Everything comes from the frontend hass object: hass.states (reactive), hass.devices/entities/areas (registries). No direct WS connections.
  4. 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 the FLOOR_BG_RECT rectangle:
    • 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 gets mdi: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 *_linkquality entities → label under the icon; color via lqiColor(): ≤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/.