diff --git a/README.md b/README.md index 6b99fec..b4f22ed 100755 --- a/README.md +++ b/README.md @@ -133,6 +133,21 @@ Rooms may not overlap: a click strictly inside an existing room, or an outline t - **Split** — click a room, then two points on its walls; the chord cuts it in two. The bigger part stays the room it was (name, area, devices); the smaller one asks for a new name and area. +### Doors, windows and locks + +In markup mode the **"Opening"** tool places doors and windows: click next to a wall and the +opening snaps onto it. Pick the type, the **length in real centimetres** (defaults: door 90 cm, +window 120 cm), an open/close sensor and — for doors — a **lock entity**. + +With a sensor bound, the plan comes alive: the door leaf swings on its hinge and the swing arc +draws itself in as the real door opens; a window opens its two casements. While open, the moving +parts take an accent colour. A door with a lock shows a padlock badge next to it — green when +locked, orange when unlocked. For safety the lock can **not** be toggled from the plan; a click +on the opening shows a status card with both states instead. + +Openings are easy to adjust later: hovering one highlights it, you can **drag it along the +walls** (it slides around corners too), and a **double click opens its properties**. + ### Step 3. Devices appear by themselves As soon as you save a room bound to an area, **the devices of that area are automatically laid out inside the outline**. These are the same devices shown on the **Settings → Devices → (filtered by the room)** page — only the meaningful ones, without service records, bridges and duplicates. @@ -178,6 +193,12 @@ You can also place a **single entity** (not just a whole device): start typing i 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**. +The same dialog controls how the device looks on the plan. **Display** switches between the +icon badge, an animated **presence ripple** (pulsing rings while the entity is active, a faint +dot when idle — great for motion sensors) or both, with a per-device ring colour and size. The +**icon size** (×0.5–3) and **rotation** are also per-device, so a wall valve can be small and +turned the way it is mounted. + ![Adding a device manually](docs/images/07-marker-dialog.png) --- diff --git a/README.ru.md b/README.ru.md index 2ad5b96..cd388f7 100644 --- a/README.ru.md +++ b/README.ru.md @@ -135,6 +135,21 @@ title: План дома - **Разделить** — кликните комнату, затем две точки на её стенах; хорда разрежет её надвое. Бо́льшая часть остаётся прежней комнатой (имя, зона, устройства), меньшая просит новое имя и зону. +### Двери, окна и замки + +В режиме разметки инструмент **«Проём»** ставит двери и окна: кликните рядом со стеной — проём +примагнитится к ней. Выберите тип, **длину в реальных сантиметрах** (по умолчанию дверь 90 см, +окно 120 см), датчик открытия и — для двери — **замок**. + +С привязанным датчиком план оживает: створка двери поворачивается на петле, и дуга распахивания +дорисовывается по мере открытия настоящей двери; окно раскрывает две створки. Пока открыто, +подвижные части подсвечены акцентным цветом. У двери с замком рядом отображается замочек — +зелёный, когда заперто, оранжевый, когда нет. Ради безопасности замок с плана **нельзя** +переключить — клик по проёму показывает карточку с обоими статусами. + +Проёмы легко поправить позже: при наведении проём подсвечивается, его можно **перетащить вдоль +стен** (в том числе за угол), а **двойной клик открывает свойства**. + ### Шаг 3. Устройства появляются сами Как только вы сохранили комнату с привязкой к зоне, **устройства этой зоны автоматически расставляются внутри контура**. Берутся те же устройства, что показаны на странице **Настройки → Устройства → (фильтр по нужной комнате)** — только осмысленные, без служебных записей, мостов и дубликатов. @@ -182,6 +197,12 @@ title: План дома Не всё нужно оставлять на автоматику. Кнопкой **+** в шапке можно поставить на план любое устройство, группу или **виртуальную точку** (например, «Вентиль на вводе», которого нет как устройства). Задайте имя, иконку, модель, ссылку, описание и при желании приложите **PDF-инструкцию**. +В этом же диалоге настраивается вид устройства на плане. **Отображение** переключает значок, +анимированную **пульсацию присутствия** (расходящиеся кольца, пока сущность активна, и тусклая +точка в покое — идеально для датчиков движения) или то и другое сразу, с цветом и размером колец +на устройство. **Размер значка** (×0,5–3) и **поворот** — тоже индивидуальные: вентиль на стене +может быть маленьким и повёрнутым так, как он установлен. + ![Добавление устройства вручную](docs/images/07-marker-dialog.png) --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6b1e726..a6e767e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -86,6 +86,15 @@ Built from the registries (`_buildDevices`), rules carried over 1-to-1 from the ## Device markers (v1.6.0+) +Per-marker appearance (v1.22.0): `display: badge|ripple|icon_ripple` (+`ripple_color`, +`ripple_size`) draws presence-style pulsing rings gated by the pure `isActiveState` — +deliberately independent of the card-wide `live_states` toggle, with `unavailable` counting as +idle. `size` (icon multiplier via the `--dev-size` CSS var — value badges scale along) and +`angle` rotate/scale a single icon. Room drawing shows a live **ruler** (`segmentCm` + +`formatLength`, metres or feet+inches by `hass.config.unit_system`); the scale is per-space +`cell_cm` (default 5 cm per grid cell). + + `config.markers[]`: `{id, binding:'device:'|'entity:'|'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:` overrides them (metadata/rebinding/hiding), @@ -109,6 +118,18 @@ Plan files: `/houseplan/plans/.` → URL `/houseplan_files/p 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. +## Room geometry rules (v1.19–v1.21) + +A **line is never an entity of its own**: walls are *derived* from room outlines +(`roomEdges`, deduped by `segKey`), so an abandoned outline leaves nothing behind and deleting +a room keeps the walls its neighbours still contribute. Rooms may not overlap +(`pointStrictlyInside` + `roomsOverlap`; being ON a shared wall is legal — real neighbouring +walls overlap collinearly rather than match exactly). **Merge/Split** use boolean geometry from +**polyclip-ts** (chosen over `polygon-clipping`, whose ESM build exports only a default while +its types declare named exports — breaking either tsc or the runtime): merge accepts a pair only +when the union collapses into one hole-free outline; split cuts wall-to-wall with a chord, the +bigger part keeps the room identity (name/area/devices). + ## Markup editor (v1.4.0+) State inside the card: `_markup` (mode), `_tool` (draw/delroom), `_path` (the current outline, @@ -127,6 +148,26 @@ While drawing, the length of the current segment follows the cursor (`_fmtLen` `formatLength`): metres, or feet+inches when `hass.config.unit_system` is imperial. The scale is per-space `cell_cm` — cm represented by one grid cell (default 5, so 240 cells ≈ 12 m). +## Doors & windows (v1.23.0+) + +`space.openings[]` — plan geometry, **not** markers: an opening needs an angle, a length and a +wall, while markers are free points whose positions live in the layout store. Model: +`{id, type: door|window, x, y, angle, length, contact?, lock?, invert?, flip_h?, flip_v?}` +(normalized coords; `length` normalized by plan width). Placement snaps onto the nearest +**derived** wall via `snapToWall` (logic.ts) — the angle is normalized to [-90, 90) because two +rooms share a wall with opposite edge directions, and without that a drag across segment +boundaries flips the hinge. The opening then keeps **absolute coordinates**, so editing, merging +or deleting rooms never breaks it. + +Rendering (after easy-floorplan, MIT): SVG symbol at the origin (jambs + hinged leaf + a +quarter-circle arc revealed via `stroke-dashoffset`), translated/rotated onto the wall; windows +are two casement leaves. `openingAmount` (pure) maps the contact state to 0..1: no sensor → +door drawn open / window closed (static-plan convention); `unavailable`/`unknown` freeze that +default. The lock renders as an HTML padlock badge (`.oplock`) in the device layer; a lock is +**never** toggled from the plan (TOGGLE_FORBIDDEN_DOMAINS rule). View-mode UX: hover outline, +drag along walls (continuous re-snap, saved on release), click → status card (250 ms timer), +double click → properties dialog. In markup mode the "Opening" tool handles clicks instead. + ## Integration WS API | Command | Parameters | Response | diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d4ff981..da97354 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -69,6 +69,15 @@ cp dist/houseplan-card.js custom_components/houseplan/frontend/ ("InvalidStateError: Transition was aborted", hui-view is not created for 1–2 min). Cured by repeating the SPA navigation: pushState + a location-changed event, or just waiting. +## Dependency and cache gotchas + +- **polygon-clipping is a trap**: its `.d.ts` declares named exports but the ESM build has only + a default export — tsc or the runtime breaks, whichever you appease. Use **polyclip-ts** + (proper ESM + native types; same results, +~50 KB bundle via bignumber.js). +- **Redeploying the same version keeps the resource URL** (`/houseplan_files/houseplan-card.js?v=X`), + so browsers may serve the previous bundle from cache. Bump the version for anything users must + pick up, or hard-refresh (Ctrl+Shift+R) when testing a hotfix redeploy. + ## Release Tag `vX.Y.Z` + GitHub Release → the workflow `.github/workflows/release.yml` builds and attaches diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3dbee48..32b3805 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -14,27 +14,27 @@ This roadmap replaces them. Track progress in `custom_components/houseplan/quality_scale.yaml` (done/exempt + comment). -- [ ] `runtime-data`: move `hass.data[DOMAIN]` (stores, entry, write lock) to typed +- [x] `runtime-data`: move `hass.data[DOMAIN]` (stores, entry, write lock) to typed `entry.runtime_data` (`type HouseplanConfigEntry = ConfigEntry[HouseplanData]`). Keep WS command registration in `async_setup` (commands must survive entry reloads). -- [ ] `config-entry-unloading` (Silver, do it now): real `async_unload_entry` — unregister +- [x] `config-entry-unloading` (Silver, do it now): real `async_unload_entry` — unregister update listeners via `entry.async_on_unload`; document what stays (static paths cannot be unregistered — exempt comment). -- [ ] `test-before-setup`: verify storage is loadable/writable in `async_setup_entry`, +- [x] `test-before-setup`: verify storage is loadable/writable in `async_setup_entry`, raise `ConfigEntryNotReady` on failure. -- [ ] `unique-config-entry`: replace the manual `_async_current_entries()` check with +- [x] `unique-config-entry`: replace the manual `_async_current_entries()` check with `single_config_entry: true` in manifest. -- [ ] `config-flow-test-coverage` + backend tests for websocket_api (pytest-homeassistant-custom-component: +- [x] `config-flow-test-coverage` + backend tests for websocket_api (pytest-homeassistant-custom-component: conflict/rev, admin_only enforcement, layout point ops, upload limits). -- [ ] Storage versioning: `Store(minor_version=...)` + `_async_migrate_func` skeleton **now**, +- [x] Storage versioning: `Store(minor_version=...)` + `_async_migrate_func` skeleton **now**, before the config schema evolves further (Bronze-adjacent, cheap while schema is stable). -- [ ] `quality_scale.yaml` with honest exempt list (no entities ⇒ entity rules N/A, no polling, no deps). +- [x] `quality_scale.yaml` with honest exempt list (no entities ⇒ entity rules N/A, no polling, no deps). ## Phase 8 — Quality Scale: Silver + selected Gold - [ ] `test-coverage` ≥95% backend; frontend: extract remaining pure logic (view math, marker resolution) into `logic.ts`/`devices.ts` and cover with node:test. -- [ ] `diagnostics.py`: config + layout dump with `async_redact_data` (redact names/links/PDF paths). +- [x] `diagnostics.py`: config + layout dump with `async_redact_data` (redact names/links/PDF paths). - [ ] `reconfiguration-flow` + richer **options flow**: admin_only, curation defaults (exclude domains — UI editable, replacing the hardcoded EXCLUDED_DOMAINS fallback), LQI thresholds, group_lights default. diff --git a/docs/STATUS.md b/docs/STATUS.md index 16cb812..898a20d 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -9,16 +9,16 @@ > (versions, publication, infrastructure), DEVELOPMENT.md for new gotchas, > ARCHITECTURE.md for design changes, ROADMAP.md when plans move. -## Snapshot (2026-07-05) +## Snapshot (2026-07-17) | Item | State | |---|---| | Version | **v1.23.1** 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.23.1** (latest published 2026-07-17, bundle auto-attached by release.yml) | | 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 | | 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.23.1**, installed *via HACS* (custom repo). The user has real rooms, 12 openings (doors/windows with TTLock locks and contact sensors) and live fills configured | | 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 | 77 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) | @@ -67,6 +67,27 @@ space-geometry/space-render + module-level config cache (config-store). - **v1.16.1** — space-card renders room fills as configured on the full card (snapshot), no longer omitted; +shared areaLqi(). +- **v1.17.x** — entity markers get auto icon/temp (issue #1); correct resource URL documented + (issue #2); humidity badge (gated on device_class, not the icon). +- **v1.18.x** — live ruler while drawing rooms (metres / feet+inches) + per-space scale + `cell_cm`; visibility fix (the badge lived in the markup-hidden devlayer). +- **v1.19.0** — a line is never an entity of its own: walls derived from room outlines + (`roomEdges`), unfinished contours persist nothing, Erase tool removed, `space.segments` + stripped on save. +- **v1.20.0** — rooms may not overlap (strictly-inside clicks refused, overlapping contours + refused at close; shared walls stay legal). +- **v1.21.x** — merge & split rooms (boolean geometry via polyclip-ts; merge = union collapses + to one hole-free outline; split = wall-to-wall chord, bigger part keeps identity) + UX fixes. +- **v1.22.0** — presence ripples (badge/ripple/icon_ripple + colour/size, `isActiveState`), + per-device icon size/rotation (`--dev-size`), one-click HACS badge. NOTE: sources were lost + in a sandbox reset after deploy and restored from conversation patches — push immediately + after building, never wait for verification. +- **v1.23.0** — doors & windows: "Opening" markup tool (snap onto derived walls, absolute + coords), length in real cm, contact sensor + lock, animated leaf/arc, padlock badge, status + card; lock never toggled from the plan. +- **v1.23.1** — openings UX: hover outline + grab cursor, drag along walls (angle normalized + to [-90,90) so the hinge never flips), click=status / double-click=properties, thicker hit + strip. Release v1.23.1 published on GitHub (covers v1.22.0–v1.23.1). ## Where things live diff --git a/docs/TESTING.md b/docs/TESTING.md index d1d562c..dab49d8 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -210,6 +210,18 @@ require hands on real hardware — they remain for the human pass. - [ ] `show_button: false` hides the footer - [ ] Full card honours `#space=` on load and on hashchange; invalid id ignored [auto] +## Presence ripples / per-device icon (v1.22.0) + +- [ ] Marker dialog → Display = "Ripple only": the icon badge disappears, rings pulse while the + entity is on, and collapse to a faint dot when it goes off +- [ ] Display = "Icon + ripple": both the icon and the rings are drawn +- [ ] Ripple colour and size (×2..×8) apply per device +- [ ] An entity going `unavailable` stops the pulsing (idle dot), never leaves it running +- [ ] Icon size ×0.5..×3 and rotation 0..350° apply per device; the temp/humidity badges + scale with the icon +- [ ] Ripples still work with the card-wide "live states" toggle OFF (they are opt-in per device) +- [ ] With OS "reduce motion" enabled, rings do not animate + ## Doors & windows (v1.23.0) - [ ] Markup → "Opening": a click away from any wall shows a toast; near a wall — the dialog