mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
v1.59.0-beta.10: unify device visuals and wall refinements
This commit is contained in:
+17
-7
@@ -87,11 +87,16 @@ 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` +
|
||||
Per-marker appearance: `display: badge|icon_ripple|value`. All three use one semantic
|
||||
resolver (`src/device-visual.ts`) for availability, steady status and activity. `badge`
|
||||
shows the icon/morph and status plate; `icon_ripple` additionally shows a finite event,
|
||||
static presence, mechanical transition or actual-work ring; `value` replaces the icon
|
||||
with the HA-formatted numeric value. A critical alarm is red in every presentation.
|
||||
Legacy `display: ripple` is read as `icon_ripple` and rewritten on the next config save;
|
||||
the backend accepts it only for compatibility. `ripple_color` and `ripple_size` remain the
|
||||
stored names for the ordinary activity effect. `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).
|
||||
|
||||
@@ -386,12 +391,17 @@ hash falls back to the default.
|
||||
a per-light `clipPath` = zone polygons + doorway sectors, each contour a
|
||||
SEPARATE clipPath child (children union; subpaths of one nonzero path
|
||||
cancel on opposite windings — field bug v1.36.3). Radius: global
|
||||
`settings.glow_radius_cm` + per-marker `glow_radius_cm`.
|
||||
`settings.glow_radius_cm` + per-marker `glow_radius_cm`. On a thick wall the
|
||||
doorway sector is the angular intersection of its near- and far-face clear
|
||||
spans, so the jamb returns clip the spill as a real opening tunnel.
|
||||
- **Open boundaries** (v1.37, revised 2026-08): `space.open_spans` hold
|
||||
geometric virtual stretches; `room.open_to` remains the light-zone index
|
||||
derived from spans (legacy `open_to`-only configs expand to full
|
||||
`sharedBoundary` on read). Shared stretches drawn as a TRUE dash; outlines
|
||||
trimmed (`outlineWithout`/`cutSegments`). Geometry mutations clip one stored
|
||||
trimmed (`outlineWithout`/`cutSegments`). In View the dash group is painted
|
||||
before thick wall bodies, letting real jambs mask its centreline ends; in all
|
||||
editors it is painted after the bodies so saved spans and previews remain
|
||||
fully visible. Geometry mutations clip one stored
|
||||
span to **every** surviving shared segment. Adjacent pieces owned by different
|
||||
room pairs stay separate: their midpoints are the source of the corresponding
|
||||
`open_to` links after Split (`AUD-159B7-01`).
|
||||
|
||||
@@ -2,6 +2,44 @@
|
||||
|
||||
## Unreleased (dev)
|
||||
|
||||
## v1.59.0-beta.10 — 2026-08-05
|
||||
|
||||
Tenth pre-release of the 1.59 line: one coherent device-state language and a
|
||||
focused set of wall, doorway-light and background-label refinements.
|
||||
|
||||
- **One device-state language.** Marker plates and effects now come from one
|
||||
semantic resolver: yellow means actual work, orange means open/unlocked,
|
||||
unavailable is faded, and alarms are always red. The display list is reduced
|
||||
to Icon, Icon + activity and Value; the removed Ripple-only value migrates to
|
||||
Icon + activity on the next save. Activity distinguishes short events,
|
||||
persistent presence, mechanical travel and actual running, with
|
||||
reduced-motion fallbacks and no false event on first load or reconnect.
|
||||
Short effects also reset when a marker's effective source changes, while a
|
||||
cover's real opening/closing state takes precedence over the tap fallback.
|
||||
- **Clearer wall-drawing toolbar.** The Plan editor's former “Add” tool is now
|
||||
labelled “Walls”, and its new-wall thickness field sits immediately to the
|
||||
right of that button instead of after the rest of the toolbar.
|
||||
- **View-mode virtual walls sit behind thick wall bodies.** Their stored
|
||||
geometry still reaches the physical centreline, but in View the hatch masks
|
||||
the dash ends inside adjoining thick walls. All three editors keep saved
|
||||
dashes and live previews above the wall body so the complete span remains
|
||||
visible while editing.
|
||||
- **Door light respects thick-wall reveals.** Light now crosses a doorway only
|
||||
through the clear width of its physical wall tunnel. The near and far inner
|
||||
face spans jointly clip the spill, so the two jamb returns cast the expected
|
||||
cut-offs for an off-centre source; zero-thickness walls retain the previous
|
||||
doorway sector.
|
||||
- **Inline HA variables in decor text.** A label can now mix ordinary copy and
|
||||
multiple `{entity}` / `{entity:attribute}` references. Choosing a state or
|
||||
attribute inserts its token at the textarea caret. The separate unit field,
|
||||
one-slot hint, and preview are removed; old linked labels remain readable and
|
||||
migrate to inline tokens when edited.
|
||||
- **Canonical wall fragments.** Touching or overlapping virtual stretches on
|
||||
the same boundary and room pair now collapse into one `open_span`; Split
|
||||
pieces belonging to different room pairs remain separate. When removing the
|
||||
last virtual stretches leaves an original wall solid and uniformly thick,
|
||||
its atomic thickness entries collapse back to one whole-wall key.
|
||||
|
||||
## v1.59.0-beta.9 — 2026-08-05
|
||||
|
||||
Ninth pre-release of the 1.59 line: mixed virtual/thick resize integrity and
|
||||
|
||||
@@ -8,6 +8,48 @@
|
||||
|
||||
## Не выпущено (dev)
|
||||
|
||||
## v1.59.0-beta.10 — 2026-08-05
|
||||
|
||||
Десятый pre-release линейки 1.59: единая система состояния устройств и набор
|
||||
точечных улучшений стен, света через дверные проёмы и надписей подложки.
|
||||
|
||||
- **Единый язык состояния устройств.** Подложка и эффекты маркера теперь
|
||||
вычисляются одним семантическим resolver: жёлтый означает фактическую работу,
|
||||
оранжевый — открыто/разблокировано, недоступное устройство приглушается, а
|
||||
тревога всегда остаётся красной. В списке отображения остались «Значок»,
|
||||
«Значок + активность» и «Значение вместо иконки»; удалённый режим «Только
|
||||
пульсация» при следующем сохранении переходит в «Значок + активность».
|
||||
Активность различает короткое событие, постоянное присутствие, механическое
|
||||
движение и фактическую работу, не создавая ложного события при загрузке или
|
||||
восстановлении связи. Короткие эффекты также сбрасываются при смене
|
||||
фактического источника маркера, а реальное открытие/закрытие шторы имеет
|
||||
приоритет над временной реакцией на нажатие.
|
||||
- **Более понятная панель рисования стен.** Бывшая кнопка «Добавить» в
|
||||
редакторе плана теперь называется «Стены», а поле толщины новых стен стоит
|
||||
сразу справа от неё, а не после остальных инструментов панели.
|
||||
- **В режиме просмотра виртуальные стены находятся под телом толстой стены.**
|
||||
Их сохранённая геометрия по-прежнему доходит до физической осевой линии, но в
|
||||
View штриховка скрывает концы пунктира внутри примыкающих толстых стен. Во
|
||||
всех трёх редакторах сохранённый пунктир и живой preview остаются поверх тела
|
||||
стены, чтобы весь отрезок был виден во время правки.
|
||||
- **Свет из двери учитывает откосы толстой стены.** Теперь свет проходит через
|
||||
дверной проём только по свободной ширине его физического тоннеля. Ближний и
|
||||
дальний внутренние срезы совместно ограничивают пучок, поэтому оба стеновых
|
||||
откоса корректно отсекают лучи смещённого источника; для стены без толщины
|
||||
сохраняется прежний сектор проёма.
|
||||
- **Inline-переменные HA в тексте подложки.** Одна надпись теперь может
|
||||
сочетать обычный текст и несколько ссылок `{entity}` / `{entity:attribute}`.
|
||||
Выбор состояния или атрибута вставляет токен в позицию курсора textarea.
|
||||
Отдельное поле единицы, подсказка про один слот и предпросмотр удалены;
|
||||
старые связанные надписи продолжают читаться и при редактировании переходят
|
||||
на inline-токены.
|
||||
- **Канонические фрагменты стен.** Соседние или перекрывающиеся виртуальные
|
||||
отрезки на одной границе одной пары комнат теперь объединяются в один
|
||||
`open_span`; части Split, принадлежащие разным парам комнат, остаются
|
||||
раздельными. Если после удаления последних виртуальных участков исходная
|
||||
стена снова целиком реальная и имеет одну толщину, её атомарные ключи
|
||||
схлопываются обратно в один ключ цельной стены.
|
||||
|
||||
## v1.59.0-beta.9 — 2026-08-05
|
||||
|
||||
Девятый pre-release линейки 1.59: целостный resize смешанных
|
||||
|
||||
+8
-8
@@ -71,16 +71,16 @@ the old behaviour until an editing client materialises it.
|
||||
|
||||
## What a marker SHOWS
|
||||
|
||||
A marker's live indication — the yellow «on» plate, the «open» frame (never
|
||||
on a cover, see below), the breathing `covermove` ring, the state-morphed
|
||||
icon, the ripple — speaks for ONE entity of the device, resolved in this
|
||||
order (`_stateClass` / `_actEntity`):
|
||||
A marker's live indication — status plate, state-morphed icon and semantic
|
||||
activity — is derived by one resolver from one effective source set. Its
|
||||
source precedence is (`_visualSamples` / `_actEntity`):
|
||||
|
||||
1. the device's **cover**, when the marker's tap action is explicitly
|
||||
«Открыть/закрыть» (`tap_action: 'cover'` — `coverEntityOf`, the same helper
|
||||
and the same entity the tap drives). It wins over EVERYTHING below;
|
||||
2. the marker's bound **controls**, if it has any (a stateless remote or a
|
||||
virtual wall switch mirrors what it drives, not itself);
|
||||
virtual wall switch aggregates what it drives, not itself; any working
|
||||
target drives both the yellow plate and running activity);
|
||||
3. a **lit light** among its entities (owner's principle 2026-07-29: the glow
|
||||
spot and the badge may never disagree);
|
||||
4. otherwise the **primary** entity (`primaryEntity`).
|
||||
@@ -125,14 +125,14 @@ dialog away, and the honest reading of what the marker was told it is.
|
||||
|
||||
«У штор не должно быть жёлтой подложки НИКОГДА, индикация открыто/закрыто за
|
||||
счёт морфинга иконки.» For the `cover` domain — and for the cover an
|
||||
«Открыть/закрыть» marker indicates, rule 3 above — `_stateClass` returns no
|
||||
plate class in any state:
|
||||
«Открыть/закрыть» marker indicates, rule 1 above — the visual resolver returns
|
||||
no working/open plate in any state:
|
||||
|
||||
| cover state | plate | ring | icon |
|
||||
|---|---|---|---|
|
||||
| `closed` | neutral | — | closed glyph |
|
||||
| `open`, ajar (`open` + position) | neutral | — | open glyph |
|
||||
| `opening`, `closing` | neutral | `.covermove` breathes | open glyph |
|
||||
| `opening`, `closing` | neutral | `.activity-transition` breathes in Icon + activity | open glyph |
|
||||
| `unknown` / no state | neutral | — | base icon, no morph |
|
||||
| `unavailable` | neutral, faded (`.unavail`) | — | base icon |
|
||||
|
||||
|
||||
+45
-64
@@ -1,17 +1,18 @@
|
||||
# The text block — a decor label that can show an entity's state
|
||||
|
||||
Status: **implemented (dev, unreleased).** Code: `src/logic.ts`
|
||||
(`liveText`, `liveTextValue`, `decorTextScale`, `decorTextLines` — pure,
|
||||
unit-tested), `src/houseplan-card.ts` (`_renderDecorLayer`,
|
||||
(`liveText`, `liveTextReference`, `liveTextToken`, `liveTextValue`,
|
||||
`decorTextScale`, `decorTextLines` — pure, unit-tested),
|
||||
`src/houseplan-card.ts` (`_renderDecorLayer`,
|
||||
`_renderTextFrame`, `_renderDecorTextDialog`, the `_dt*` gestures),
|
||||
`custom_components/houseplan/validation.py` (`DECOR_SCHEMA`, text branch).
|
||||
Smokes: `demo/smoke_live_text.mjs`, `demo/smoke_decor_text.mjs`;
|
||||
`demo/smoke_decor.mjs` keeps the surrounding decor contract.
|
||||
|
||||
The shape: `{kind:'text', x, y, text, color, scale?, angle?, entity?, attr?,
|
||||
unit?}` — plus the legacy `size?` that older plans still carry. Everything
|
||||
after `color` is optional, so **every existing plan validates and renders
|
||||
unchanged and no migration runs**.
|
||||
The shape: `{kind:'text', x, y, text, color, scale?, angle?}` — plus legacy
|
||||
`size?`, `entity?`, `attr?`, and `unit?` fields that older plans may still
|
||||
carry. New live references are stored only inside `text`; existing linked
|
||||
labels render unchanged and migrate to inline references when edited.
|
||||
|
||||
## 1. Why a live label
|
||||
|
||||
@@ -27,28 +28,26 @@ happens to have a live number in it.
|
||||
The competing card (ha-floorplan) covers this with `text_set` and it is one of
|
||||
its most used features; our decor text was one field away from it.
|
||||
|
||||
## 2. The live value
|
||||
## 2. Inline HA variables
|
||||
|
||||
A decor text shape has three optional fields:
|
||||
`text` is both the visible copy and the complete template. It may contain any
|
||||
number of HA references mixed with ordinary text and line breaks:
|
||||
|
||||
- `entity` — an entity id whose state is substituted; absent = today's plain
|
||||
static label, byte-for-byte unchanged;
|
||||
- `attr` — an attribute name to read instead of `state` (battery level,
|
||||
current temperature of a climate, position of a cover); absent = the state;
|
||||
- `unit` — a suffix; absent = the entity's own `unit_of_measurement`, so
|
||||
`sensor.tank` needs no configuration to read *68 %*.
|
||||
- `{sensor.water_tank}` — the entity state;
|
||||
- `{climate.hall:current_temperature}` — one attribute;
|
||||
- `Бак {sensor.water_tank}, зал {climate.hall:current_temperature}` — several
|
||||
independent values in one label.
|
||||
|
||||
The `text` field keeps its present meaning and becomes the **template**: the
|
||||
placeholder `{}` is where the value lands. `Бак {}` → *Бак 68 %*. A template
|
||||
without a placeholder gets the value appended after a space, so a user who
|
||||
only picks an entity and types nothing sensible still sees something useful.
|
||||
**Only the first `{}` is replaced** — one label, one value.
|
||||
The editor writes the colon form because the boundary between entity and
|
||||
attribute is unambiguous. Hand-written `{climate.hall.current_temperature}` is
|
||||
accepted too: the first two dot-separated parts form the entity id and the
|
||||
rest is the attribute. Invalid brace contents stay literal, while a valid but
|
||||
missing entity or attribute renders as a dash.
|
||||
|
||||
`{}` was chosen over `{{ }}` deliberately: this is a substitution, not a
|
||||
template language. There is no expression, no condition, no arithmetic — the
|
||||
moment we accept `{{ states('x') | round(1) }}` we have signed up for the
|
||||
class of support load that fills the competitor's thread (138 posts of "my
|
||||
CSS/template does not work"). One value, one place, no syntax to get wrong.
|
||||
This remains substitution, not a template language: no expressions,
|
||||
conditions, arithmetic, Jinja, or nested braces. The 200-character limit is
|
||||
the limit of the saved template; each resolved value is still clipped to 60
|
||||
characters.
|
||||
|
||||
### 2.1. Rendering rules
|
||||
|
||||
@@ -77,17 +76,10 @@ CSS/template does not work"). One value, one place, no syntax to get wrong.
|
||||
formatter falls back to the raw state, byte-for-byte the pre-2026-08-05
|
||||
behaviour. Imperial/metric is not our business either — the value and the
|
||||
unit come from HA (docs/STYLING-HOOKS.md §6).
|
||||
- **The unit is inherited only for the STATE.** With an `attr` the entity's
|
||||
`unit_of_measurement` is not applied: it describes the state, and a
|
||||
`battery_level` read off a °C sensor must not come out as «73 °C». An
|
||||
explicit `unit` always wins; an empty one inherits.
|
||||
- **The unit appears exactly once.** HA's formatter normally appends the
|
||||
entity's unit itself, so the inherited one is not added a second time — and
|
||||
where it does not append it, ours still is, so the unit never silently
|
||||
disappears either. The rule: strip the entity's own `unit_of_measurement`
|
||||
if it is already the tail (exact trailing match, nothing else in the string
|
||||
is touched), then append the unit actually wanted — the user's explicit one,
|
||||
or the entity's own.
|
||||
- **Units belong to Home Assistant.** State variables use HA's formatted state,
|
||||
including its unit. Attribute variables use HA's attribute formatter and do
|
||||
not inherit the entity state's unit. There is no separate unit override in
|
||||
the new editor; a literal suffix can be typed immediately after the token.
|
||||
- The value is clipped to `LIVE_TEXT_VALUE_MAX` (60) characters: a caption is
|
||||
a caption, and an attribute that turns out to be a 4 KB string must not
|
||||
become the plan's wallpaper.
|
||||
@@ -119,7 +111,7 @@ size is not one of three opinions; it is whatever fits the place it is put in.
|
||||
newline is stored and rendered as a newline (one `<tspan>` per line, line
|
||||
height 1.2 em). The label **never wraps by itself** — a caption that reflows
|
||||
on every state change is a caption that jumps around the plan. A
|
||||
300-character line stays one line.
|
||||
200-character line stays one line.
|
||||
- **Multi-line blocks are centred**, horizontally (the decor layer's
|
||||
`text-anchor: middle`, which single-line labels already used) and
|
||||
vertically: the anchor `x/y` sits in the middle of the block, so adding a
|
||||
@@ -159,34 +151,24 @@ click opens its editor, and the corner/rotate handles appear.
|
||||
|
||||
## 5. The dialog
|
||||
|
||||
Under the text field:
|
||||
|
||||
- a **textarea** (line breaks are content now), saved with the button or
|
||||
Ctrl/⌘+Enter — plain Enter is a new line;
|
||||
- a hint that mentions `{}` **only when an entity is chosen** — an unlinked
|
||||
label must not be burdened with syntax it does not need;
|
||||
- an **entity picker** with a datalist of all entities — the same control
|
||||
style the vacuum source and the weather field use;
|
||||
- an **attribute field**, shown only once an entity is chosen, with a datalist
|
||||
of that entity's actual attribute names — the user should not have to know
|
||||
that a climate keeps `current_temperature`;
|
||||
- a **unit field** whose placeholder is the entity's own unit, so leaving it
|
||||
empty is the obvious right answer (the placeholder disappears once an
|
||||
attribute is chosen, because an attribute does not inherit it);
|
||||
- a **live preview** of the resulting label, rendered through the same
|
||||
`liveText` the plan uses: one substitution, one truth.
|
||||
|
||||
Clearing the entity clears the attribute and the unit with it — a save never
|
||||
leaves orphan fields in the config.
|
||||
- The textarea is the sole source of the label. It accepts ordinary copy,
|
||||
line breaks, and manually typed references in any order; Ctrl/⌘+Enter saves.
|
||||
- «Insert HA variable» contains an entity picker. After an entity is selected,
|
||||
the second control offers its state and actual attribute names.
|
||||
- Choosing the state or an attribute immediately inserts the complete token at
|
||||
the textarea's current selection/caret, then returns focus after the token.
|
||||
The user can continue typing or insert another variable, including one from
|
||||
another entity, until the 200-character field limit is reached.
|
||||
- There is no unit field, single-slot hint, or separate preview. The label on
|
||||
the plan is already the live preview and uses the same text template.
|
||||
|
||||
## 6. Backend
|
||||
|
||||
`DECOR_SCHEMA`, text branch: `entity` optional, `None` or an entity id
|
||||
(`^[a-z0-9_]+\.[a-z0-9_]+$`, ≤ 255); `attr` optional, `None` or a flat name
|
||||
≤ 64; `unit` optional, `None` or ≤ 16 characters; `scale` optional, finite,
|
||||
`0.15…20`; `angle` optional, finite, `-360…360`; `text` ≤ 200 characters,
|
||||
newlines included. Everything optional, so every existing plan validates
|
||||
unchanged. Tests: `tests_backend/test_validation.py`
|
||||
`DECOR_SCHEMA`, text branch: `text` ≤ 200 characters, newlines and inline
|
||||
references included; `scale` optional, finite, `0.15…20`; `angle` optional,
|
||||
finite, `-360…360`. Legacy `entity`/`attr`/`unit` remain accepted and bounded
|
||||
so old saved plans continue to validate and render. The frontend writes none
|
||||
of them after the label has been edited. Tests: `tests_backend/test_validation.py`
|
||||
(`test_decor_text_live_fields`, `test_decor_text_block_scale_and_angle`).
|
||||
|
||||
## 7. What this is not
|
||||
@@ -194,8 +176,7 @@ unchanged. Tests: `tests_backend/test_validation.py`
|
||||
- **Not a second device marker.** No tap action, no icon, no state class, no
|
||||
participation in room aggregation (LQI, climate averages) — it is a caption,
|
||||
not a device. A user who wants an interactive thing puts a marker.
|
||||
- **Not a template engine** (see §2).
|
||||
- **Not a multi-entity widget.** One label, one value. Two values are two
|
||||
labels; that stays honest and costs the user one drag.
|
||||
- **Not a template engine** (see §2). Multiple substitutions do not introduce
|
||||
expressions, conditions, or formatting rules.
|
||||
- **Not an auto-layout.** No wrapping, no shrink-to-fit: the size is set with
|
||||
the corners and the lines with the Enter key.
|
||||
|
||||
+8
-3
@@ -15,11 +15,11 @@
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| Version | **v1.59.0-beta.9** everywhere (manifest, const.py, package.json, CARD_VERSION) — **pre-release**, tag `v1.59.0-beta.9` on **`dev`**, GitHub Release with `prerelease=true`; `main` is not touched and nothing is copied to the home instance by hand — HACS delivers it on the beta channel. On top of beta.8: resize keeps partial `open_spans` and the atomic thickness keys of their solid remainders together in live preview, commit and Undo; real arms at virtual T-junctions receive the missing mitre, and the virtual dash/preview renders above thick real walls. Previous pre-release: v1.59.0-beta.8; previous stable: v1.58.0 |
|
||||
| Version | **v1.59.0-beta.10** everywhere (manifest, const.py, package.json, CARD_VERSION) — **pre-release**, tag `v1.59.0-beta.10` on **`dev`**, GitHub Release with `prerelease=true`; `main` is not touched and nothing is copied to the home instance by hand — HACS delivers it on the beta channel. On top of beta.9: unified device status/activity visuals; inline HA variables in decor text; doorway-light clipping by thick-wall reveals; view-only virtual-wall masking; a clearer wall toolbar; and canonical merging/compaction of wall fragments. Previous pre-release: v1.59.0-beta.9; previous stable: v1.58.0 |
|
||||
|
||||
| Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command. **Since 2026-08-04 there is also a pre-release track:** bump to `X.Y.Z-beta.N`, tag the `dev` commit, publish a GitHub Release with `prerelease=true` — `main` is not touched and nothing is copied to the home instance by hand; HACS delivers it on the beta channel |
|
||||
| Workflow | Owner's rule since 2026-08-05: ordinary fixes/features are made **locally, without tests and without commits**. Tests/build/smokes run only when the owner asks for a pre-release; then bump to `X.Y.Z-beta.N`, commit/tag the tested `dev` state and publish a GitHub Release with `prerelease=true`. `main` is not touched and nothing is copied to the home instance by hand; HACS delivers it on the beta channel |
|
||||
| GitHub | https://github.com/Matysh/houseplan-card — `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
|
||||
| CI | beta.8 repairs the stale general-settings smoke that left beta.7 Validate red (`111/112`) and adds exact-SHA release gating: `release.yml` withholds the asset until every matching Validate run finishes green. The full local gate is green; GitHub Validate and the release asset are verified after the tag is pushed |
|
||||
| CI | beta.10 passes the full local frontend, pure-backend and browser-smoke gate. Exact-SHA release gating remains active: `release.yml` withholds the asset until every matching Validate run finishes green; GitHub Validate and the release asset are verified after the tag is pushed |
|
||||
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled, mergeable clean, never drafted. Queue: 1212 open, 835 older than ours. Merge rate COLLAPSED: 75 in July but almost all in the first decade, 0 in the last week (checked 2026-07-29) — maintainers process in rare bursts; ETA unknowable, months at best. Nothing actionable on our side |
|
||||
| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), last direct copy was **v1.57.0**; from v1.58.0 on it updates itself through HACS by tag (no scp) |
|
||||
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
|
||||
@@ -69,6 +69,11 @@
|
||||
- **Yellow = working right now** (v1.51.0): climate by hvac_action, service
|
||||
switches can no longer become primary, glow pool and icon share one
|
||||
condition. Editor gestures on touch (pinch/pan) landed the same release.
|
||||
- **Unified device status/activity** (v1.59.0-beta.10, 2026-08-05): three display
|
||||
modes (Icon / Icon + activity / Value), one semantic resolver for yellow
|
||||
actual work, orange open/unlocked, unavailable and always-red alarms;
|
||||
activity distinguishes a short event, presence, mechanical travel and
|
||||
running. Legacy Ripple-only migrates to Icon + activity on the next save.
|
||||
|
||||
## Recent milestones (details in CHANGELOG.md)
|
||||
|
||||
|
||||
+14
-10
@@ -136,10 +136,10 @@
|
||||
- Drag иконок, снап к сетке, позиция per-space — Стенд: редактор устройств: перетащить иконку, F5 — позиция жива; у «Двора» своя раскладка.
|
||||
- ↺ сброс автолейаута — Стенд: не проверяется: кнопка Reset убрана в v1.33.2 (пункт в TESTING.md исторический).
|
||||
- Бейдж температуры, LQI с цветом — Стенд: под «Zigbee»-термометрами цифра LQI (цвет от красного к зелёному: Кабинет красный, Гостиная зелёный), температура на бейдже.
|
||||
- Живые состояния: свет жёлтый, открытая штора оранжевая, unavailable блёклый — Стенд: пощёлкать свет; открыть штору («Открыть шторы» или карточка); unavailable в демо нет — эмулировать нечем, но у робота после паузы всё живое.
|
||||
- Живые состояния: фактическая работа жёлтая, дверь/разблокированный замок/клапан оранжевые, штора нейтральная и меняет иконку, unavailable блёклый — Стенд: пощёлкать свет; открыть штору («Открыть шторы» или карточка); unavailable в демо нет — smoke.
|
||||
- Морфинг иконок по состоянию — Стенд: дверь/замок: открыть lock.front_door из карточки двери — иконка меняется; гаражные ворота во «Дворе».
|
||||
- «Значение вместо иконки» — Стенд: у термометра в диалоге выбрать display «значение» — на плане цифра.
|
||||
- RGB только в glow/ripple — Стенд: включить RGBWW-лампу в Гостиной цветом: бейдж жёлтый стандартный, цвет только в пятне glow.
|
||||
- RGB только в glow/activity — Стенд: включить RGBWW-лампу в Гостиной цветом: бейдж жёлтый стандартный, цвет только в пятне glow и activity-эффекте.
|
||||
- Красная пульсация тревоги — Стенд: включить `input_boolean.demo_leak` (Настройки → Устройства и службы → Помощники) — датчик на Кухне пульсирует красным; дым в Спальне сам вспыхивает каждые 10 минут на 30 с.
|
||||
- Стоимость рендера (geometry once) — Стенд: не проверяется руками — smoke_render_perf.
|
||||
- Тап vs drag открывания — Стенд: редактор плана, тап по двери = свойства, drag на место = ничего не записано.
|
||||
@@ -155,7 +155,7 @@
|
||||
- Tap запускает автоматизацию — Стенд: тап по «Открыть шторы» (Гостиная) → диалог подтверждения → штора едет; в диалоге маркера видно «Run» + поиск по automation/script/scene (есть скрипт «Открыть шторы», автоматизация «Вечерний сценарий», сцены «Кино», «Вечер в гостиной»); Esc = не выполнять.
|
||||
- Бейджи light-source в glow — Стенд: заливка glow (по умолчанию): у горящей лампы бейдж стандартный (индикатор — пятно), у розетки-«источника света» жёлтый.
|
||||
- Множитель размера иконки растит глиф — Стенд: size 3 у любого маркера.
|
||||
- Auto-grid parity, ripple ghost, hidden LQI parity, ghost без цифр — Стенд: скрыть «Zigbee»-термометр: LQI комнаты не меняется, «Схема» совпадает с планом; «Show hidden» в редакторе устройств показывает синий пунктирный призрак без значений.
|
||||
- Auto-grid parity, activity ghost, hidden LQI parity, ghost без цифр — Стенд: скрыть «Zigbee»-термометр: LQI комнаты не меняется, «Схема» совпадает с планом; «Show hidden» в редакторе устройств показывает синий пунктирный призрак без значений и эффектов.
|
||||
- Флаг «скрыть с плана» — Стенд: галка в любом диалоге устройства; скрытый исчезает из всех режимов и счётчика, но LQI комнаты держит.
|
||||
- «Жёлтый = работает» — Стенд: «Тёплый пол» в Кабинете: hvac_action=heating → жёлтая подложка. (Выключить нагрев можно, поставив target ниже 20° в карточке — бейдж гаснет.)
|
||||
- Жесты редактора на touch — Стенд: с телефона в редакторе плана: pinch = zoom, палец = pan, отпускание не ставит точку.
|
||||
@@ -316,15 +316,19 @@
|
||||
- show_button: false — Стенд: правкой YAML вида.
|
||||
- #space= диплинк — Стенд: открыть `…/plan#space=yard`.
|
||||
|
||||
## Presence ripples / per-device иконка
|
||||
## Состояние и активность устройства
|
||||
|
||||
- «Только пульсация»: бейдж исчезает, кольца при on — Стенд: у датчика движения «Movement Backyard» (Кухня) поставить display «пульсация» — он периодически on/off сам.
|
||||
- «Иконка + пульсация» — Стенд: там же.
|
||||
- Цвет и размер ×2..×8 — Стенд: там же.
|
||||
- unavailable останавливает пульс — Стенд: не проверяется (нет unavailable-сущностей в демо) — smoke.
|
||||
- В списке отображения ровно три режима: «Значок», «Значок + активность», «Значение вместо иконки»; «Только пульсация» отсутствует.
|
||||
- Motion в «Значок + активность» даёт три волны только при `off → on` и затихает примерно через 3,3 с, даже если датчик ещё `on`.
|
||||
- Occupancy/presence показывает спокойное статичное кольцо, пока присутствие активно.
|
||||
- Штора в `opening/closing` показывает дыхание только в «Значок + активность»; при прямом `closed ↔ open` без промежуточного состояния эффект держится примерно 3,3 с.
|
||||
- Работающий свет, реле, вентилятор, climate с активным `hvac_action`, плеер и пылесос получают жёлтую подложку; `automation = on` (включена) остаётся нейтральной.
|
||||
- Дверь/окно, разблокированный замок и открытый клапан — оранжевые, а не жёлтые.
|
||||
- Тревога всегда красная и пульсирует во всех трёх режимах; unavailable приглушён и не получает обычной активности.
|
||||
- Цвет и размер ×2..×8 применяются к обычной активности; тревога остаётся красной.
|
||||
- Размер ×0.5..×3 и поворот; бейджи масштабируются — Стенд: любой маркер.
|
||||
- Риплы при выключенных live-состояниях — Стенд: выключить «живые состояния» в конфиге карточки (правка вида) — риплы живут.
|
||||
- reduce motion — Стенд: включить в ОС тестера.
|
||||
- При выключенных live-состояниях обычные статус и активность выключены, критическая тревога остаётся.
|
||||
- reduce motion — Стенд: включить в ОС тестера; вместо анимации остаётся статичное кольцо.
|
||||
|
||||
## Двери и окна
|
||||
|
||||
|
||||
+94
-45
@@ -232,17 +232,23 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
- [ ] Drag anywhere (no edit mode), snaps to grid, persists after reload, per space
|
||||
- [ ] ↺ reset restores auto layout after confirm
|
||||
- [ ] Temperature badge on thermometers; LQI value under zigbee icons with red→green color
|
||||
- [ ] Live states: light on = yellow, open cover/lock/door = orange, unavailable = faded
|
||||
- [ ] Unified live states (dev, owner 2026-08-05): actual work is yellow;
|
||||
open door/window, unlocked lock and open valve are orange; covers stay
|
||||
neutral and morph their icon; unavailable is faded. The plate and the
|
||||
activity effect come from the same semantic resolver
|
||||
- [ ] State icons (v1.26.0): auto icons morph with state — door/window/garage open↔closed,
|
||||
lock locked↔unlocked, bulb on; custom icons and unavailable states never morph [manual]
|
||||
- [ ] display "Value instead of an icon": the marker shows the measurement (°/%/unit)
|
||||
as its body, small badges hidden; non-numeric fallback keeps the icon [manual]
|
||||
- [ ] RGB lights (v1.27.0, contract changed in v1.52.0): a lamp's colour lives
|
||||
in its glow spot and the ripple fallback ONLY — the icon/badge/border get
|
||||
no RGB tint; explicit ripple color still wins; off lights unchanged
|
||||
in its glow spot and the activity-effect fallback ONLY — the icon/badge/border get
|
||||
no RGB tint; explicit activity color still wins; off lights unchanged
|
||||
[auto: smoke_light_badges + smoke_rgb_alarm]
|
||||
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
|
||||
display mode; clears on 'off'; unavailable never alarms [manual]; reduced-motion static
|
||||
- [ ] Alarm pulse (v1.27.0, unified dev): leak/smoke/gas/CO/siren in `on`
|
||||
and an alarm control panel in `triggered`
|
||||
get a red plate and red pulse over every display mode and even with
|
||||
ordinary live-state dressing off; clears on 'off'; unavailable never
|
||||
alarms [manual]; reduced-motion is static
|
||||
- [ ] Render cost (v1.43.1, audit L1): geometry (space model, open pairs) is
|
||||
computed once per config change, not per HA state push — smoke asserts
|
||||
zero recomputations across 10 state pushes and recomputation after an
|
||||
@@ -299,9 +305,10 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
closed -> cover.open_cover, open (incl. ajar) -> cover.close_cover,
|
||||
opening/closing -> cover.stop_cover, no readable state -> cover.toggle;
|
||||
the «ask for confirmation» checkbox guards it like toggle/run.
|
||||
Indication: while travelling the icon breathes a soft yellow ring
|
||||
(.covermove, 2.2s, static under prefers-reduced-motion) and the plate
|
||||
stays NEUTRAL in EVERY state (2026-08-04, see the next item); the icon
|
||||
Indication: while travelling the icon breathes a soft activity ring
|
||||
(`.activity-transition`, 2.2s, static under prefers-reduced-motion) in
|
||||
display «Icon + activity» and the plate stays NEUTRAL in EVERY state
|
||||
(2026-08-04, see the next item); the icon
|
||||
morphs by state + device_class (blinds/shutter/curtain…), unknown state
|
||||
morphs nothing; no position percentages anywhere
|
||||
[auto: smoke_cover_tap + units resolveTapAction/coverService/stateIcon +
|
||||
@@ -312,7 +319,8 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
closing: the plate is the plain neutral badge every time — never the
|
||||
yellow «включено» one, never the orange «открыто» frame it used to wear
|
||||
while open — the icon is the only open/closed signal, and the breathing
|
||||
.covermove ring appears in the two travelling states and nowhere else.
|
||||
`.activity-transition` ring appears in the two travelling states and
|
||||
nowhere else when «Icon + activity» is selected.
|
||||
The morph is exhaustive: every device class gives two DIFFERENT glyphs
|
||||
(awning included), a cover with no device_class morphs within its own
|
||||
auto-icon family (mdi:roller-shade, mdi:garage-variant), a hand-picked
|
||||
@@ -334,7 +342,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
- [ ] Curtain INDICATION follows the same cover (dev, owner 2026-08-04): with
|
||||
«Open/close» chosen on that same Aqara marker, the plan shows the cover
|
||||
and not the service switch — the breathing ring while it travels
|
||||
(`covermove`, opening AND closing), the `mdi:curtains-closed` /
|
||||
(`activity-transition`, opening AND closing), the `mdi:curtains-closed` /
|
||||
`mdi:curtains` morph, a neutral plate throughout (the «открыто» frame
|
||||
was retired for covers later the same day), and no yellow plate when
|
||||
`switch.*_reverse_direction` happens to be on. The rule is exactly
|
||||
@@ -348,8 +356,8 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
device whose own `light.*` is ON, one whose bound `controls` switch is
|
||||
ON. In every cover state (closed / open / opening / closing) the plate
|
||||
stays neutral — never the yellow «включено», never the orange «открыто»
|
||||
— the travelling ring breathes, the icon morphs with the cover, and in
|
||||
glow fill the ring is still there (that is where a yellow-plated curtain
|
||||
— with «Icon + activity» the travelling ring breathes, the icon morphs
|
||||
with the cover, and in glow fill the ring is still there (that is where a yellow-plated curtain
|
||||
used to lose BOTH indicators). Untouched: the same mixed device without
|
||||
the explicit action is yellow again, a wall switch still mirrors its
|
||||
controls, and an «Открыть/закрыть» marker whose device has no `cover.*`
|
||||
@@ -357,7 +365,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
- [ ] Light-source badges (v1.52.0): in glow fill a lit lamp's badge stays
|
||||
standard (the spot is the indicator) and a lit socket stays yellow; in
|
||||
other fills a lit lamp is plain yellow with no RGB tint; morphing and
|
||||
the ripple colour fallback survive [auto: smoke_light_badges +
|
||||
the activity colour fallback survive [auto: smoke_light_badges +
|
||||
smoke_rgb_alarm]
|
||||
- [ ] Icon size multiplier scales the glyph (dev): set a marker's size to 3 —
|
||||
the icon inside grows with the badge instead of staying default
|
||||
@@ -365,8 +373,8 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
- [ ] Auto-grid parity (v1.51.2, HP-1511-01): with an empty layout, a visible
|
||||
device among hidden ones sits at the same spot on both cards
|
||||
[auto: smoke_hidden_flag autoGridParity]
|
||||
- [ ] Ripple ghost (v1.51.2, HP-1511-02): a hidden ripple-display marker shows
|
||||
its base icon, no pulse [auto: smoke_hidden_flag rippleGhost*]
|
||||
- [ ] Activity ghost (v1.51.2, unified dev): a hidden icon+activity marker
|
||||
shows its base icon and no effect [auto: smoke_hidden_flag rippleGhost*]
|
||||
- [ ] Hidden LQI parity (v1.51.1, HP-1510-01): a room whose only Zigbee
|
||||
devices are hidden paints the same lqi fill on the full and the static
|
||||
card [auto: smoke_hidden_flag lqiParity]
|
||||
@@ -378,7 +386,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
the count, still count toward room LQI, cast no glow/light fill; the
|
||||
device editor's "Show hidden" (local, per tab) shows them as BLUE
|
||||
dashed ghosts — distinct from a grey unavailable icon — with NO live
|
||||
state paint (no yellow, no alarm, no ripple);
|
||||
state paint (no yellow, no alarm, no activity);
|
||||
unticking keeps a hidden:false marker (re-seed protection); an old
|
||||
config materialises on first load by an editing client and legacy
|
||||
clients keep the old behaviour until then
|
||||
@@ -679,7 +687,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
tap action Toggle flips all bound lights/switches at once (any on → all
|
||||
off, all off → all on, one service call); the icon state (yellow badge)
|
||||
mirrors the targets, not the marker's own entity — the RGB tint is gone
|
||||
since v1.52.0, target colours reach only the glow/ripple; without explicit Toggle
|
||||
since v1.52.0, target colours reach only the glow/activity effect; without explicit Toggle
|
||||
the click opens info as usual; the info card lists targets with states;
|
||||
locks/other domains are filtered out of controls [auto: smoke_controls]
|
||||
- [ ] Glow fill (v1.35.0): fill mode "Light sources" — every room painted with
|
||||
@@ -869,17 +877,34 @@ require hands on real hardware — they remain for the human pass.
|
||||
- [ ] `show_button: false` hides the footer
|
||||
- [ ] Full card honours `#space=<id>` on load and on hashchange; invalid id ignored [manual]
|
||||
|
||||
## Presence ripples / per-device icon (v1.22.0)
|
||||
## Unified device status and activity (dev, owner 2026-08-05)
|
||||
|
||||
- [ ] 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
|
||||
- [ ] The Display list contains exactly Icon, Icon + activity, Value instead
|
||||
of an icon. Legacy `display: ripple` reads and saves back as `icon_ripple`
|
||||
- [ ] Icon shows state plate/morph but no ordinary activity effect; Icon +
|
||||
activity adds the semantic effect; Value keeps the state-coloured plate
|
||||
and hides ordinary activity
|
||||
- [ ] Motion/vibration/sound/contact rising edges render exactly three waves
|
||||
for about 3.3 s; initial load and recovery from unknown/unavailable do
|
||||
not fake an event; a rapid retrigger restarts it
|
||||
- [ ] Occupancy/presence is one calm static ring for the whole active state
|
||||
- [ ] Cover/lock/valve movement breathes until the travelling state ends;
|
||||
direct terminal `closed ↔ open` / `locked ↔ unlocked` without an
|
||||
intermediate state breathes for about 3.3 s
|
||||
- [ ] Actual work (light/switch/fan/humidifier on, active climate action,
|
||||
media playing, vacuum cleaning, script running) is yellow and slowly
|
||||
breathes in Icon + activity; `automation = on` is merely enabled and
|
||||
remains neutral
|
||||
- [ ] Controls aggregate their targets: any working target drives both the
|
||||
yellow plate and the running effect
|
||||
- [ ] Open contact/unlocked lock/open valve are orange; an open cover stays
|
||||
neutral because its icon morph carries that state
|
||||
- [ ] Unavailable suppresses ordinary activity. Alarm outranks all of it,
|
||||
remains red in every display mode and with ordinary live states off
|
||||
- [ ] Activity colour and size (×2..×8) apply per device; alarm ignores them
|
||||
- [ ] Icon size ×0.5..×3 and rotation 0..355° apply per device; the
|
||||
temp/humidity badges scale with the icon
|
||||
- [ ] With OS "reduce motion" enabled, activity/alarm rings are static
|
||||
|
||||
## Doors & windows (v1.23.0)
|
||||
|
||||
@@ -1132,25 +1157,30 @@ require hands on real hardware — they remain for the human pass.
|
||||
|
||||
## The text block on the plan (docs/LIVE-TEXT.md, dev, unreleased)
|
||||
|
||||
- [ ] **A label can show a live value**: in the Background editor place a text,
|
||||
pick an entity in the dialog and write `Бак {}` — the plan shows
|
||||
«Бак 68 %». Change the sensor: the label follows without any reload. A
|
||||
label with no entity is untouched, byte for byte
|
||||
[auto: smoke_live_text + unit logic.test]
|
||||
- [ ] **The unit comes from the entity**: leave the unit field empty and the
|
||||
entity's own `unit_of_measurement` is used (its placeholder shows which);
|
||||
type your own and it wins. Choose an ATTRIBUTE (say `battery_level` on a
|
||||
°C sensor) and the entity's unit is NOT inherited — an attribute is not
|
||||
the state [auto: smoke_live_text]
|
||||
- [ ] **One text, many HA variables**: write `Бак {sensor.tank}, зал
|
||||
{climate.hall:current_temperature}` — both values render and update
|
||||
independently. The hand-written dotted attribute form
|
||||
`{climate.hall.current_temperature}` works too; ordinary/invalid braces
|
||||
stay literal [auto: smoke_live_text + unit logic.test]
|
||||
- [ ] **Picker inserts at the caret**: put the cursor between two words, choose
|
||||
an entity and then its state/attribute — the full token appears exactly
|
||||
at that selection and focus returns immediately after it. Continue typing
|
||||
and insert another variable until the textarea's 200-character limit
|
||||
[auto: smoke_live_text]
|
||||
- [ ] **No separate unit/preview/single-slot UI**: the dialog has none of the
|
||||
old unit field, `{}` hint, or preview block. State units come from HA;
|
||||
attributes do not inherit the state unit, and a custom suffix is ordinary
|
||||
text after the token [manual]
|
||||
- [ ] **A dead sensor says so**: make the entity unavailable (or delete it) —
|
||||
the value becomes «—» and the rest of the caption stays. The dash carries
|
||||
no unit [auto: smoke_live_text]
|
||||
- [ ] **Nothing is rounded**: a sensor reporting `23.94781` shows `23.94781`.
|
||||
Rounding is the sensor's `display_precision`, not ours [auto:
|
||||
smoke_live_text]
|
||||
- [ ] **The preview is the truth**: the line under the dialog's fields is
|
||||
rendered by the same substitution the plan uses, so what it shows is what
|
||||
the plan will show [manual]
|
||||
- [ ] **Old links migrate on edit**: a stored beta.9 `text + entity/attr/unit`
|
||||
label still renders unchanged. Open it: the inline token is visible in
|
||||
the textarea; save it and only the new text template remains
|
||||
[auto: smoke_live_text]
|
||||
- [ ] **The same everywhere**: the label reads identically in View, in the
|
||||
editors and on a kiosk screen [auto: smoke_live_text]
|
||||
- [ ] **No font-size choice any more**: the dialog has no Small/Medium/Large.
|
||||
@@ -1427,9 +1457,9 @@ require hands on real hardware — they remain for the human pass.
|
||||
attribute goes through the attribute formatter (a climate's
|
||||
`current_temperature` is a number, not the climate's state)
|
||||
[auto: smoke_value_format + unit logic.test]
|
||||
- [ ] **Your own unit replaces the entity's**: type `проц.` in a label's unit
|
||||
field — the result is «68,4 проц.», never «68,4 % проц.»
|
||||
[auto: smoke_value_format]
|
||||
- [ ] **A literal suffix stays literal**: write an attribute token followed by
|
||||
` проц.` — the suffix is part of the text, with no hidden unit override
|
||||
and no duplicate appended by the label renderer [auto: smoke_live_text]
|
||||
- [ ] **An older Home Assistant is unchanged**: on an HA without
|
||||
`formatEntityState` the badge and the label print the raw state with the
|
||||
entity's unit appended, exactly as before — nothing is blank and nothing
|
||||
@@ -1451,6 +1481,11 @@ require hands on real hardware — they remain for the human pass.
|
||||
|
||||
## Wall thickness (docs/WALL-THICKNESS.md, Unreleased)
|
||||
|
||||
- [ ] **Walls + adjacent draw thickness**: the first Plan-editor tool is named
|
||||
“Walls” / «Стены» rather than “Add”; while it is active, its thickness
|
||||
field is the immediately following toolbar element, before Merge. The
|
||||
field still defaults to 15 cm and disappears when another tool is selected
|
||||
[auto: smoke_draw_wall_thickness]
|
||||
- [ ] **Tool + hover + input**: Plan editor → Wall thickness. Hover highlights
|
||||
the whole wall; click opens the cm/in field; empty/0 clears; Esc closes
|
||||
without applying; «Apply to all walls of this room» fills every allowed
|
||||
@@ -1462,6 +1497,11 @@ require hands on real hardware — they remain for the human pass.
|
||||
the body; the door swing is offset toward the inner face; with
|
||||
`hide_openings` the symbols hide but the cut remains
|
||||
[auto: smoke_wall_thickness]
|
||||
- [ ] **Door light uses the clear tunnel**: place an off-centre light beside a
|
||||
door in a thick wall. In the neighbouring room the glow is limited by
|
||||
sight lines through both the near and far inner-face corners; neither
|
||||
side crosses a solid jamb return. Clearing wall thickness restores the
|
||||
wider centreline-based sector [auto: test/logic.test.mjs; manual visual]
|
||||
- [ ] **Shared once / clear → line / resize re-keys**: one body for a shared
|
||||
wall; clearing thickness restores the centreline; resizing a thick wall
|
||||
keeps the thickness on the moved stretch, including both atomic solid
|
||||
@@ -1469,9 +1509,18 @@ require hands on real hardware — they remain for the human pass.
|
||||
[auto: smoke_wall_thickness + smoke_resize_virtual_thick]
|
||||
- [ ] **Virtual T-junction**: when two real thick arms from different room
|
||||
contours meet at an `open_span` endpoint, the outside corner is a clean
|
||||
mitre with no stair-step; the saved dash and the two-click rubber band
|
||||
paint above the real hatch right up to the centreline
|
||||
mitre with no stair-step. In every editor the saved dash and the two-click
|
||||
rubber band paint above the real hatch right up to the centreline; in
|
||||
View the same saved dash paints below the body, so each thick jamb masks
|
||||
its centreline end without shortening the stored span
|
||||
[auto: test/wall-thickness.test.mjs + smoke_resize_virtual_thick]
|
||||
- [ ] **Fragment normalisation**: draw adjacent/overlapping virtual stretches
|
||||
along the same pair of rooms — they persist and select as one span. Close
|
||||
the last virtual stretch on a uniformly thick wall — its saved `walls`
|
||||
data returns to one whole-edge key. Repeat across a Split boundary: spans
|
||||
owned by different room pairs must stay separate
|
||||
[auto: test/open-spans.test.mjs + test/wall-thickness.test.mjs +
|
||||
smoke_resize_virtual_thick]
|
||||
- [ ] **Unit + backend**: inset/mitre/bevel, key from either end, degrade,
|
||||
rekey, cm↔inches; `walls` schema bounds
|
||||
[auto: test/wall-thickness.test.mjs + tests_backend/test_validation.py]
|
||||
|
||||
+12
-6
@@ -48,8 +48,9 @@ Header in View: space tabs, device count, zoom cluster. Nothing else.
|
||||
|
||||
## Plan — geometry and appearance of the space
|
||||
|
||||
- Toolbar tools: Outline room (with a session wall-thickness field, default
|
||||
15 cm — docs/WALL-THICKNESS.md §6), Delete room, Merge, Split, Resize,
|
||||
- Toolbar tools: **Walls** / room outline (with its session wall-thickness field
|
||||
immediately on the right, default 15 cm — docs/WALL-THICKNESS.md §6), Delete
|
||||
room, Merge, Split, Resize,
|
||||
Opening (place / drag along walls / properties), Open boundary, Wall thickness
|
||||
(docs/WALL-THICKNESS.md — click a wall, set cm/inches from HA's unit system;
|
||||
empty/0 clears), Room labels (drag positions — labels are part of the plan).
|
||||
@@ -74,6 +75,10 @@ layer you cannot see is a layer you cannot edit.
|
||||
- **Virtual walls follow `show_borders`** (owner, 2026-08-05). They are walls —
|
||||
dashed ones. Drawing them on a space with no borders left a plan whose only
|
||||
walls were a few floating dashed stretches.
|
||||
- **Their geometry and their presentation are separate.** Every virtual span
|
||||
still ends on the real wall centreline. In View the thick wall body is
|
||||
painted over the dash ends, so they visually stop at its faces; editors paint
|
||||
the full dash (and live preview) over the body for unambiguous editing.
|
||||
- **`hide_openings` hides the symbol, not the opening.** Light still spills
|
||||
through it, the sun still enters at a window, a contact sensor still opens
|
||||
it, and the resize tool still anchors to it. Anything else would be a second
|
||||
@@ -85,7 +90,8 @@ layer you cannot see is a layer you cannot edit.
|
||||
## Devices — placement and marker configuration
|
||||
|
||||
- Icon dragging (ONLY here). Click on a device opens the **edit dialog directly**
|
||||
(binding, name, icon, size/angle, display badge/ripple + colors, tap override,
|
||||
(binding, name, icon, size/angle, display as icon / icon + activity / value,
|
||||
activity color and size, tap override,
|
||||
model/link/description/PDFs, room).
|
||||
- + add device/entity/virtual, the "Hide device from plan" checkbox (since
|
||||
v1.51.0 the one hiding mechanism, docs/FILTERING.md), ↺ reset layout,
|
||||
@@ -117,12 +123,12 @@ layer you cannot see is a layer you cannot edit.
|
||||
|
||||
1. State-reflecting icons (open/closed door variants etc., like core HA).
|
||||
2. `display: value` — show the measurement instead of an icon.
|
||||
3. Light color in the icon/ripple (RGB lights). *(Shipped in v1.27.0;
|
||||
superseded in v1.52.0: the colour lives in the glow spot and the ripple
|
||||
3. Light color in the activity effect (RGB lights). *(Shipped in v1.27.0;
|
||||
superseded in v1.52.0: the colour lives in the glow spot and the activity
|
||||
fallback only, the icon tint was removed by the owner's rule.)*
|
||||
4. Alarm visual (leak/smoke/doorbell): red pulse overlay.
|
||||
5. Rooms as sub-areas without an HA area + manual device placement by room id.
|
||||
6. Backlog (not planned): music notes for players, directional TV ripples.
|
||||
6. Backlog (not planned): music notes for players, directional TV effects.
|
||||
|
||||
## Implementation iterations
|
||||
|
||||
|
||||
+17
-4
@@ -18,7 +18,11 @@ Code: `src/wall-thickness.ts`, render in `src/houseplan-card.ts` /
|
||||
Per space: `walls: [{ key, cm }]`. Key = quantised midpoint + direction
|
||||
(modulo 180°). Config always stores centimetres. Open boundaries refuse
|
||||
thickness. One physical stretch has one thickness (atomic collinear spans when
|
||||
neighbours overlap only partially).
|
||||
neighbours overlap only partially). Atomic keys are storage only while a real
|
||||
geometric break requires them: once an original edge is entirely solid with
|
||||
one thickness, normalisation writes its single whole-edge key again. Likewise,
|
||||
touching/overlapping `open_spans` of the same room pair are stored as one span;
|
||||
pair ownership remains a hard boundary so Split can derive exact `open_to` links.
|
||||
|
||||
Degrade unmatched keys silently on write. Resize / undo / scale re-key all
|
||||
touched spans in the same transaction and keep `walls` in the resize snapshot.
|
||||
@@ -47,12 +51,19 @@ angle (mod 180°), then nearest span — never a perpendicular neighbour at a T.
|
||||
At an `open_span` endpoint, real arms owned by different room contours receive
|
||||
the same bounded mitre patch as arms from one contour; a virtual T therefore
|
||||
has one clean outer corner rather than two butt caps forming a step.
|
||||
The virtual segment itself remains centreline-based. View paints it before the
|
||||
wall body, which masks the part inside each adjoining thick jamb; editors paint
|
||||
it after the body so its full stored extent and live preview stay visible.
|
||||
|
||||
## 4. Floor, fills, light, area
|
||||
|
||||
- **Inner contour** = `inset(poly, half[])`.
|
||||
- Room fills and glow are clipped to the inner contour (not painted into the
|
||||
wall hatch).
|
||||
- Glow leaving through a door uses the clear rectangular opening tunnel: its
|
||||
sector is the intersection of the doorway spans at the near and far inner
|
||||
faces. The two jamb returns therefore clip off-axis light; a zero-depth wall
|
||||
keeps the original centreline sector.
|
||||
- Displayed **m²** = area of the inner contour (clean floor). Wall-length
|
||||
rulers and opening anchors stay on the centreline.
|
||||
- With no thickness, inner = poly (parity with pre-thickness behaviour).
|
||||
@@ -70,8 +81,9 @@ centreline/full-span wedge. `hide_openings` hides the symbol only.
|
||||
Plan-editor tool «Wall thickness»; hover whole wall; cm/in from HA; empty/0
|
||||
clears; apply-to-room. Hooks: `data-hp="wall"`. i18n en/ru.
|
||||
|
||||
**Draw with thickness.** The Draw toolbar carries a session thickness field
|
||||
(default **15 cm**, or inches when HA is imperial). Closing a new room outline
|
||||
**Draw with thickness.** The Plan toolbar's **Walls** button carries its session
|
||||
thickness field immediately on the right (default **15 cm**, or inches when HA
|
||||
is imperial). Closing a new room outline
|
||||
writes that cm onto every new edge that does not already have one; shared
|
||||
stretches that already carry a neighbour's thickness are left alone. Empty / 0
|
||||
leaves the room thin. Live thick preview follows the rubber-band while drawing.
|
||||
@@ -84,7 +96,8 @@ Decor-line thickness, per-side finish, auto-from-backdrop, plan-wide default.
|
||||
## 8. Testing
|
||||
|
||||
Unit: ring closed at corners; half-out; inner area; atomic partial shared;
|
||||
virtual-T mitre; angle-aware opening; whole and atomic rekey after edge/scale.
|
||||
virtual-T mitre; angle-aware opening; thick-door tunnel clipping; whole and
|
||||
atomic rekey after edge/scale.
|
||||
Browser: seamless frame; fill not in hatch; m² drops with thickness; a partial
|
||||
virtual stretch, its solid thick remainders and Undo move as one real resize;
|
||||
the virtual rubber band paints above the real body; sun starts at the room-side
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# Единая индикация состояния и активности устройств
|
||||
|
||||
Статус: **утверждено владельцем и реализовано локально 2026-08-05**.
|
||||
|
||||
## Цель
|
||||
|
||||
Заменить четыре пересекающиеся механики — цвет подложки, пользовательскую
|
||||
пульсацию, motion/presence-эффекты и движение штор — одной семантической
|
||||
системой. Визуал должен отвечать на три разных вопроса:
|
||||
|
||||
1. Доступно ли устройство.
|
||||
2. В каком устойчивом состоянии оно находится.
|
||||
3. Что оно делает прямо сейчас или что только что произошло.
|
||||
|
||||
## Режимы отображения
|
||||
|
||||
В UI остаются:
|
||||
|
||||
1. **Значок** — иконка, морфинг и статусная подложка без обычных эффектов
|
||||
активности.
|
||||
2. **Значок + активность** — то же плюс семантический эффект активности.
|
||||
3. **Значение вместо значка** — числовая плашка со статусным цветом; обычная
|
||||
активность не рисуется.
|
||||
|
||||
Опция **«Только пульсация» удаляется**. Legacy `display: ripple` читается как
|
||||
`icon_ripple`, а при следующем сохранении конфигурации переписывается в
|
||||
`icon_ripple`. Backend временно продолжает принимать старое значение.
|
||||
|
||||
## Модель визуального состояния
|
||||
|
||||
Для маркера вычисляется один результат:
|
||||
|
||||
```ts
|
||||
availability: 'available' | 'unavailable'
|
||||
status: 'neutral' | 'open' | 'working' | 'alarm'
|
||||
activity: 'none' | 'event' | 'presence' | 'transition' | 'running'
|
||||
```
|
||||
|
||||
- `availability` отвечает за приглушение недоступного устройства;
|
||||
- `status` управляет подложкой и морфингом;
|
||||
- `activity` управляет кольцом или волнами;
|
||||
- тревога имеет высший приоритет;
|
||||
- недоступность подавляет обычную активность.
|
||||
|
||||
## Визуальный язык
|
||||
|
||||
| Смысл | Подложка | Эффект в «Значок + активность» |
|
||||
|---|---|---|
|
||||
| Доступно, простаивает | нейтральная | нет |
|
||||
| Выполняет основную функцию | жёлтая | медленное дыхание |
|
||||
| Открыто / разблокировано | оранжевая | только событие/переход, не постоянный эффект |
|
||||
| Разовое срабатывание | текущая статусная | три расходящиеся волны, 3,3 с |
|
||||
| Присутствие | нейтральная | спокойное статичное кольцо |
|
||||
| Механическое движение | текущая статусная | дыхание до конца движения |
|
||||
| Недоступно | приглушённая | нет |
|
||||
| Тревога (протечка, дым, газ, CO, safety/tamper/problem, сирена, сработавшая охрана) | красная | быстрая красная пульсация при любом display |
|
||||
|
||||
`prefers-reduced-motion` заменяет анимацию статичным кольцом.
|
||||
|
||||
## Значение жёлтой подложки
|
||||
|
||||
Жёлтый означает только **фактическое выполнение основной функции**:
|
||||
|
||||
- `light/switch/fan/humidifier = on`;
|
||||
- climate: `hvac_action = heating/cooling/drying/fan`, но не просто выбранный
|
||||
режим `heat/cool/auto`;
|
||||
- media player: `playing`;
|
||||
- vacuum: `cleaning` (и работающий механизм при `returning`);
|
||||
- бытовая техника: `running/washing/rinsing/spinning/drying/heating/cooking`;
|
||||
- маркер с `controls`: работает хотя бы одна управляемая цель.
|
||||
|
||||
Не являются работой: открытая дверь, разблокированный замок, открытые шторы,
|
||||
presence/motion, `enabled`, `home`, `unknown`, `unavailable`, climate `idle`.
|
||||
|
||||
В glow-режиме у реального источника света жёлтая подложка может быть скрыта:
|
||||
его устойчивым индикатором служит световое пятно. Эффект активности остаётся.
|
||||
|
||||
## Семантика активности
|
||||
|
||||
### Event, 3,3 секунды
|
||||
|
||||
- motion: только засвидетельствованный `off -> on`, не весь cooldown;
|
||||
- vibration/shock/sound;
|
||||
- открытие двери/окна (`off -> on`), но не всё время открытого состояния;
|
||||
- button/event;
|
||||
- успешный запуск script/scene/automation без устойчивого состояния.
|
||||
|
||||
Первое состояние после загрузки и восстановление из `unknown/unavailable` не
|
||||
создают ложного события. Повторный детект перезапускает окно и анимацию.
|
||||
|
||||
### Presence
|
||||
|
||||
`occupancy/presence = on` даёт статичное кольцо до исчезновения присутствия.
|
||||
|
||||
### Transition
|
||||
|
||||
- cover: `opening/closing`;
|
||||
- lock: `locking/unlocking`;
|
||||
- valve: `opening/closing`;
|
||||
- vacuum: `returning`.
|
||||
|
||||
Если промежуточного состояния нет, прямой конечный переход
|
||||
`closed <-> open` / `locked <-> unlocked` показывает transition 3,3 секунды.
|
||||
|
||||
### Running
|
||||
|
||||
Пока устройство фактически работает, его подложка жёлтая, а в режиме
|
||||
«Значок + активность» вокруг неё медленно дышит кольцо.
|
||||
|
||||
## Источник состояния
|
||||
|
||||
Приоритет:
|
||||
|
||||
1. Явный специализированный источник (в будущей расширенной настройке).
|
||||
2. Cover, когда tap action явно задан как «Открыть/закрыть».
|
||||
3. `controls` — агрегированное состояние всех управляемых целей.
|
||||
4. Функциональная сущность устройства / включённый light.
|
||||
5. Primary entity.
|
||||
|
||||
Критические сущности устройства проверяются независимо и не могут быть
|
||||
скрыты менее важным primary state.
|
||||
|
||||
## Приоритет вывода
|
||||
|
||||
1. alarm;
|
||||
2. unavailable;
|
||||
3. event;
|
||||
4. transition;
|
||||
5. presence;
|
||||
6. running;
|
||||
7. open;
|
||||
8. neutral.
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Одна функция вычисляет подложку и activity; они не смотрят на разные
|
||||
сущности.
|
||||
- Обычная активность появляется только в `icon_ripple`.
|
||||
- Alarm отображается при любом display и не зависит от пользовательского цвета.
|
||||
- Hidden/ghost не получает live-чисел, морфинга, подложки или активности.
|
||||
- Static `houseplan-space-card` остаётся статической и не рисует live effects.
|
||||
- Пользовательские цвет и размер применяются к обычной активности, но не к
|
||||
красной тревоге.
|
||||
|
||||
## Реализация
|
||||
|
||||
- Чистая классификация сущностей, агрегация и распознавание переходов:
|
||||
`src/device-visual.ts`.
|
||||
- Выбор эффективных источников маркера, runtime-окно 3,3 с, миграция legacy
|
||||
display и рендер: `src/houseplan-card.ts`.
|
||||
- Общий слой колец и reduced-motion варианты: `src/styles.ts`.
|
||||
- UI оставляет три режима; backend сохраняет `ripple` только как входное
|
||||
legacy-значение до завершения миграционного периода.
|
||||
- Unit-спецификация классификатора подготовлена в
|
||||
`test/device-visual.test.mjs`; браузерные smokes переведены на новые классы.
|
||||
@@ -0,0 +1,465 @@
|
||||
# Свободные цепочки стен и разная толщина при рисовании — ТЗ
|
||||
|
||||
Статус: **согласовано и отложено** (2026-08-05).
|
||||
|
||||
> Реализацию не начинать до отдельного прямого запроса владельца проекта.
|
||||
> Этот документ фиксирует требования и принятые продуктовые решения для
|
||||
> будущей реализации.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Добавить возможность:
|
||||
|
||||
1. Рисовать одну цепочку стен, задавая отдельную толщину каждому следующему
|
||||
сегменту.
|
||||
2. Оставлять на плане незамкнутые стены, не принадлежащие комнате или контуру.
|
||||
3. Переключаться на другие инструменты без удаления уже нарисованной
|
||||
незамкнутой цепочки.
|
||||
4. После возвращения в инструмент или перезагрузки продолжать сохранённую
|
||||
цепочку с одного из её концов.
|
||||
5. Замыкать цепочку с выбором результата:
|
||||
- создать комнату;
|
||||
- оставить замкнутую цепочку самостоятельными стенами.
|
||||
|
||||
## 2. Текущее состояние и архитектурная причина изменения
|
||||
|
||||
Сейчас комната является единственным первичным источником геометрии стен:
|
||||
|
||||
- контур хранится в `rooms[].poly`;
|
||||
- стены вычисляются из рёбер комнат;
|
||||
- `walls[]` хранит толщину рёбер, но не самостоятельную геометрию;
|
||||
- незаконченный `_path` является временным состоянием редактора;
|
||||
- смена инструмента очищает `_path`;
|
||||
- сервер удаляет старое legacy-поле `segments`;
|
||||
- статическая карточка, проёмы, привязки и расчёт видимой области получают
|
||||
геометрию стен преимущественно из комнат.
|
||||
|
||||
Поэтому функция должна вводить новую сохраняемую сущность. Простое сохранение
|
||||
текущего `_path` не решает рендер, восстановление после перезагрузки,
|
||||
посегментную толщину и интеграцию с остальными инструментами.
|
||||
|
||||
## 3. Термины
|
||||
|
||||
- **Свободная цепочка стен** — упорядоченная открытая или замкнутая ломаная,
|
||||
которая не принадлежит комнате.
|
||||
- **Сегмент** — отрезок между двумя соседними вершинами; у замкнутой цепочки
|
||||
последний сегмент соединяет последнюю вершину с первой.
|
||||
- **Активная цепочка** — цепочка, которую пользователь сейчас продолжает.
|
||||
- **Линейная стена** — существующий вариант стены без объёмного тела.
|
||||
- **Толстая стена** — стена с положительной толщиной в сантиметрах.
|
||||
- **Виртуальный участок** — существующий `open_span`. Отсутствие толщины само
|
||||
по себе не превращает стену в виртуальную.
|
||||
|
||||
## 4. Модель данных
|
||||
|
||||
В конфигурацию пространства добавить необязательное поле `wall_paths`:
|
||||
|
||||
```ts
|
||||
interface WallPathCfg {
|
||||
id: string;
|
||||
points: Array<[number, number]>;
|
||||
closed?: boolean;
|
||||
segments: Array<{
|
||||
cm?: number;
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
Пример открытой цепочки:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "wp_01",
|
||||
"points": [[0.10, 0.20], [0.40, 0.20], [0.40, 0.55]],
|
||||
"segments": [{ "cm": 15 }, { "cm": 25 }]
|
||||
}
|
||||
```
|
||||
|
||||
Пример замкнутой самостоятельной цепочки:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "wp_02",
|
||||
"closed": true,
|
||||
"points": [[0.10, 0.20], [0.40, 0.20], [0.40, 0.55]],
|
||||
"segments": [{ "cm": 15 }, { "cm": 20 }, { "cm": 25 }]
|
||||
}
|
||||
```
|
||||
|
||||
Инварианты:
|
||||
|
||||
- открытая цепочка: `points.length >= 2` и
|
||||
`segments.length === points.length - 1`;
|
||||
- замкнутая цепочка: минимум три пригодные вершины и
|
||||
`segments.length === points.length`;
|
||||
- у замкнутой цепочки последний сегмент идёт от последней вершины к первой;
|
||||
- первая точка не дублируется в конце массива;
|
||||
- `cm` отсутствует для линейной стены без объёмного тела;
|
||||
- отсутствие `cm` не означает `open_span`;
|
||||
- соседние точки не совпадают;
|
||||
- `id` уникален в пределах пространства;
|
||||
- координаты нормализованы по тем же правилам, что и `rooms[].poly`;
|
||||
- старое корневое поле `segments` не возвращается и продолжает считаться
|
||||
legacy.
|
||||
|
||||
Толщина хранится внутри `wall_paths`, потому что существующий `walls[]`
|
||||
привязан к геометрии комнат и удаляет осиротевшие ключи при нормализации.
|
||||
|
||||
## 5. Рисование и посегментная толщина
|
||||
|
||||
### 5.1 Начало цепочки
|
||||
|
||||
1. Пользователь выбирает инструмент «Стены».
|
||||
2. Первый клик создаёт временную начальную точку.
|
||||
3. Второй клик создаёт первый сегмент и постоянную запись в `wall_paths`.
|
||||
4. Одиночная точка без сегмента не сохраняется и исчезает при смене
|
||||
инструмента или перезагрузке.
|
||||
|
||||
### 5.2 Значение поля толщины
|
||||
|
||||
Поле толщины означает **толщину следующего сегмента**:
|
||||
|
||||
- значение фиксируется при клике второй точки сегмента;
|
||||
- изменение поля не меняет уже созданные сегменты;
|
||||
- live-preview использует текущее значение;
|
||||
- пустое значение создаёт линейную стену;
|
||||
- конфиг всегда хранит сантиметры независимо от выбранной системы единиц;
|
||||
- допустимая толщина — 1–100 см;
|
||||
- нечисловое значение и значение вне диапазона показывают ошибку и не дают
|
||||
создать сегмент.
|
||||
|
||||
Пример: пользователь может последовательно создать сегменты 15 см, 25 см,
|
||||
линейный сегмент и снова 15 см в пределах одной цепочки.
|
||||
|
||||
## 6. Сохранение при смене инструмента
|
||||
|
||||
Как только создан первый сегмент:
|
||||
|
||||
- цепочка становится частью конфигурации пространства;
|
||||
- каждый следующий законченный сегмент сразу добавляется в локальную
|
||||
конфигурацию;
|
||||
- запись на сервер может оставаться debounce-записью;
|
||||
- перед сменой этажа, режима, пространства или размонтированием карточки
|
||||
ожидающая запись принудительно завершается;
|
||||
- переключение на другой инструмент прекращает активное рисование, но не
|
||||
удаляет цепочку;
|
||||
- цепочка остаётся видимой во всех предусмотренных режимах.
|
||||
|
||||
Активное состояние редактора не требуется хранить на сервере.
|
||||
|
||||
## 7. Продолжение цепочки
|
||||
|
||||
Принятое поведение:
|
||||
|
||||
- в той же сессии после возвращения в инструмент «Стены» автоматически
|
||||
продолжается последняя активная цепочка;
|
||||
- после перезагрузки пользователь выбирает конечную точку кликом;
|
||||
- конечные точки доступных цепочек подсвечиваются при наведении;
|
||||
- клик по последней точке продолжает цепочку в текущем направлении;
|
||||
- клик по первой точке разворачивает порядок точек и сегментных параметров,
|
||||
после чего продолжает цепочку с этого конца;
|
||||
- при продолжении поле толщины принимает значение крайнего сегмента выбранного
|
||||
конца, после чего пользователь может его изменить;
|
||||
- из середины сегмента и из внутренней вершины нельзя создавать ответвление.
|
||||
|
||||
### 7.1 Соединение двух цепочек
|
||||
|
||||
Разрешается соединение **конец с концом**:
|
||||
|
||||
- при необходимости одна или обе цепочки разворачиваются;
|
||||
- точки и параметры сегментов объединяются без потери соответствия;
|
||||
- одна исходная запись сохраняет `id`, вторая удаляется;
|
||||
- нулевой соединительный сегмент не создаётся;
|
||||
- соединение через середину сегмента не поддерживается;
|
||||
- если соединение приводит к замыканию, открывается диалог завершения,
|
||||
описанный ниже.
|
||||
|
||||
## 8. Замыкание цепочки
|
||||
|
||||
При клике по начальной точке активной цепочки с минимум тремя пригодными
|
||||
вершинами:
|
||||
|
||||
1. Проверяется замыкающий сегмент.
|
||||
2. Проверяются нулевая длина, самопересечения и недопустимые наложения.
|
||||
3. Открывается существующий диалог создания комнаты.
|
||||
4. Внизу диалога добавляется второе действие
|
||||
**«Оставить замкнутыми стенами»**.
|
||||
|
||||
До выбора результата исходная открытая цепочка остаётся в конфигурации, а
|
||||
замыкающий сегмент хранится как ожидающее локальное изменение. Это исключает
|
||||
потерю стен при отмене диалога или ошибке сохранения.
|
||||
|
||||
### 8.1 Основное действие: создать комнату
|
||||
|
||||
При обычном сохранении комнаты одной транзакцией:
|
||||
|
||||
- создаётся `room.poly`;
|
||||
- свободная цепочка удаляется из `wall_paths`;
|
||||
- толщина каждого сегмента переносится в существующую модель `walls[]`;
|
||||
- замыкающий сегмент получает толщину, выбранную перед замыканием;
|
||||
- если новый контур разделяет физическую стену с существующей комнатой,
|
||||
побеждает толщина уже существующей стены.
|
||||
|
||||
Одна физическая стена не должна иметь две конкурирующие толщины.
|
||||
|
||||
### 8.2 Второе действие: оставить замкнутыми стенами
|
||||
|
||||
При выборе «Оставить замкнутыми стенами»:
|
||||
|
||||
- комната не создаётся;
|
||||
- заливка, название и площадь не появляются;
|
||||
- замыкающий сегмент добавляется в `segments`;
|
||||
- цепочка сохраняется с `closed: true`;
|
||||
- активное рисование этой цепочки завершается.
|
||||
|
||||
Для продолжения замкнутой цепочки сначала потребуется удалить один из её
|
||||
сегментов. Перемещение вершин в первой версии не предусмотрено.
|
||||
|
||||
### 8.3 Отмена и ошибки
|
||||
|
||||
- отмена диалога возвращает пользователя к открытой сохранённой цепочке;
|
||||
- ошибка записи не удаляет исходный `wall_path`;
|
||||
- удаление `wall_path` и добавление комнаты выполняются атомарно в одном
|
||||
изменении локальной конфигурации.
|
||||
|
||||
## 9. Рендер
|
||||
|
||||
Свободные стены отображаются:
|
||||
|
||||
- в редакторе плана — всегда;
|
||||
- в остальных редакторах — по действующим правилам отображения границ;
|
||||
- в режиме просмотра — при включённом `show_borders`;
|
||||
- в основной и статической карточке.
|
||||
|
||||
### 9.1 Толстые стены
|
||||
|
||||
Для сегментов с толщиной необходимо:
|
||||
|
||||
- строить тело относительно центральной линии;
|
||||
- объединять тела свободных и комнатных стен перед отрисовкой;
|
||||
- использовать существующие заливку и штриховку стен;
|
||||
- корректно формировать L- и T-образные стыки;
|
||||
- объединять сегменты разной толщины без щелей;
|
||||
- ограничивать miter на острых углах и переходить к bevel;
|
||||
- завершать действительно свободный конец ровным поперечным срезом;
|
||||
- прятать окончание линейного сегмента под телом примыкающей толстой стены.
|
||||
|
||||
### 9.2 Виртуальные участки
|
||||
|
||||
Действующее правило сохраняется:
|
||||
|
||||
- в режиме просмотра виртуальный пунктир рисуется под телом стены;
|
||||
- в редакторах пунктир рисуется поверх тела полностью от центральной линии;
|
||||
- свободная линейная стена не является виртуальным участком.
|
||||
|
||||
### 9.3 Границы содержимого
|
||||
|
||||
`wall_paths` участвуют в расчёте видимой области с учётом половины толщины.
|
||||
План, состоящий только из свободных стен, должен корректно центрироваться и
|
||||
масштабироваться.
|
||||
|
||||
## 10. Влияние на комнаты, площадь и свет
|
||||
|
||||
До преобразования в комнату свободная цепочка:
|
||||
|
||||
- не создаёт заливку пола;
|
||||
- не создаёт площадь и подпись м²;
|
||||
- не создаёт название комнаты;
|
||||
- не связывается с HA area;
|
||||
- не изменяет внутренний контур существующей комнаты;
|
||||
- не вычитает площадь из комнаты;
|
||||
- не разделяет комнату на световые зоны;
|
||||
- не участвует в распространении света между комнатами;
|
||||
- не создаёт солнечные лучи или дверной тоннель.
|
||||
|
||||
Свободная стена является отображаемой физической геометрией, но не
|
||||
топологической границей помещения.
|
||||
|
||||
Автоматическое распознавание комнат из произвольного графа пересекающихся стен
|
||||
не входит в задачу. Комната создаётся только явным замыканием цепочки и выбором
|
||||
основного действия в диалоге.
|
||||
|
||||
## 11. Работа инструментов
|
||||
|
||||
### 11.1 Толщина стены
|
||||
|
||||
Инструмент позволяет выбрать сегмент свободной цепочки и изменить только его
|
||||
толщину. Действие «Применить ко всей цепочке» можно добавить позднее как
|
||||
необязательное улучшение.
|
||||
|
||||
### 11.2 Удаление
|
||||
|
||||
- единственный сегмент: удалить всю цепочку;
|
||||
- крайний сегмент: укоротить цепочку;
|
||||
- средний сегмент: разбить цепочку на две независимые цепочки;
|
||||
- одна часть сохраняет старый `id`, вторая получает новый;
|
||||
- часть без сегментов не сохраняется;
|
||||
- удаление сегмента замкнутой цепочки превращает её в открытую цепочку с
|
||||
`closed: false`.
|
||||
|
||||
### 11.3 Resize и перемещение вершин
|
||||
|
||||
Не входят в первую версию. Resize продолжает работать только с комнатами.
|
||||
Перемещение вершин свободных цепочек вынесено в следующий этап.
|
||||
|
||||
### 11.4 Merge и Split
|
||||
|
||||
Не считают свободные цепочки комнатами и не изменяют их автоматически.
|
||||
|
||||
### 11.5 Выравнивание
|
||||
|
||||
Глобальное выравнивание плана по сетке должно включать точки `wall_paths`, не
|
||||
нарушая соответствие сегментов и их толщин.
|
||||
|
||||
### 11.6 Привязка мебели
|
||||
|
||||
Свободные физические стены рекомендуется включить в привязку мебели, поскольку
|
||||
для пользователя они являются обычными стенами.
|
||||
|
||||
### 11.7 Двери и окна
|
||||
|
||||
Двери и окна устанавливаются **только в стены комнат**.
|
||||
|
||||
- свободные открытые и замкнутые цепочки не являются целью инструмента
|
||||
проёмов;
|
||||
- при наведении на них инструмент не показывает доступную привязку;
|
||||
- проёмы становятся доступны только после преобразования цепочки в комнату.
|
||||
|
||||
## 12. Undo, Escape и Reset
|
||||
|
||||
Рекомендуемая безопасная модель:
|
||||
|
||||
- `Ctrl+Z` или `Escape` удаляет последний сегмент, добавленный в текущей
|
||||
сессии редактирования;
|
||||
- при продолжении старой цепочки Undo не удаляет части, существовавшие до её
|
||||
выбора;
|
||||
- когда добавленных в сессии сегментов больше нет, следующее действие снимает
|
||||
активность с цепочки;
|
||||
- для новой цепочки отмена первого сегмента удаляет `wall_path`, оставляя
|
||||
временную стартовую точку;
|
||||
- следующая отмена удаляет стартовую точку.
|
||||
|
||||
Reset следует трактовать как «Отменить текущие изменения цепочки»:
|
||||
|
||||
- новая цепочка возвращается к состоянию до текущего рисования;
|
||||
- у продолженной цепочки удаляются только добавленные в этой сессии сегменты;
|
||||
- ранее сохранённая часть не удаляется;
|
||||
- полное удаление выполняется инструментом удаления.
|
||||
|
||||
## 13. Сохранение и конкурентные изменения
|
||||
|
||||
- каждый завершённый сегмент сразу попадает в локальную конфигурацию;
|
||||
- запись на сервер выполняется существующей сериализованной очередью;
|
||||
- ожидающий debounce принудительно завершается при уходе из контекста;
|
||||
- `activeWallPathId` является только состоянием редактора;
|
||||
- после перезагрузки продолжение начинается выбором конечной точки;
|
||||
- преобразование в комнату не должно сначала удалить цепочку и только потом
|
||||
пытаться сохранить комнату;
|
||||
- конфликты ревизий обрабатываются действующим механизмом конфигурации.
|
||||
|
||||
## 14. Серверная валидация и ограничения
|
||||
|
||||
Предлагаемые пределы:
|
||||
|
||||
- не более 500 цепочек на пространство;
|
||||
- не более 500 точек в одной цепочке;
|
||||
- не более 2000 свободных сегментов суммарно;
|
||||
- только конечные числовые координаты в допустимых границах холста;
|
||||
- толщина 1–100 см;
|
||||
- уникальные идентификаторы;
|
||||
- отсутствие совпадающих соседних точек;
|
||||
- точное соответствие количества сегментов типу цепочки;
|
||||
- минимум три пригодные вершины у замкнутой цепочки;
|
||||
- лишние или повреждённые поля отклоняются либо нормализуются единообразно.
|
||||
|
||||
Существующие конфигурации миграции не требуют: `wall_paths` является
|
||||
необязательным полем.
|
||||
|
||||
## 15. Геометрические и UX edge cases
|
||||
|
||||
Обязательно учесть:
|
||||
|
||||
- смену инструмента после единственной точки;
|
||||
- двойной клик в одной координате;
|
||||
- нулевой и очень короткий сегмент;
|
||||
- возврат в предыдущую точку;
|
||||
- попытку замкнуть цепочку с недостаточным количеством вершин;
|
||||
- самопересечение замыкающего или обычного сегмента;
|
||||
- прохождение свободной стены через существующую комнату;
|
||||
- полное или частичное совпадение с комнатной стеной;
|
||||
- полное или частичное совпадение двух свободных цепочек;
|
||||
- пересечение двух цепочек без общей вершины;
|
||||
- соединение двух цепочек с необходимостью развернуть обе;
|
||||
- попытку соединиться с серединой сегмента;
|
||||
- T-образный стык стен разной толщины;
|
||||
- переход толстая → линейная → толстая;
|
||||
- острый угол и ограничение miter;
|
||||
- удаление среднего сегмента открытой и замкнутой цепочки;
|
||||
- отмену диалога комнаты;
|
||||
- ошибку сервера при завершении комнаты;
|
||||
- изменение единиц измерения во время рисования;
|
||||
- координаты вне сетки;
|
||||
- достижение лимита точек или общего размера конфигурации;
|
||||
- план без комнат, содержащий только свободные стены;
|
||||
- скрытые границы в режиме просмотра;
|
||||
- конкурентное редактирование в другой вкладке.
|
||||
|
||||
## 16. Критерии готовности первой версии
|
||||
|
||||
1. Незамкнутая цепочка с хотя бы одним сегментом не исчезает при смене
|
||||
инструмента.
|
||||
2. Она сохраняется после перезагрузки карточки и Home Assistant.
|
||||
3. В той же сессии рисование продолжается автоматически.
|
||||
4. После перезагрузки цепочку можно продолжить с любого конца.
|
||||
5. Две цепочки можно соединить конец с концом.
|
||||
6. Ответвление из середины сегмента невозможно.
|
||||
7. Толщина задаётся отдельно каждому следующему сегменту.
|
||||
8. Сегменты разной толщины образуют корректное тело без щелей.
|
||||
9. При замыкании доступны «Создать комнату» и
|
||||
«Оставить замкнутыми стенами».
|
||||
10. Создание комнаты переносит толщины и атомарно удаляет свободную цепочку.
|
||||
11. При общей стене сохраняется ранее существовавшая толщина.
|
||||
12. Отмена диалога не удаляет свободную цепочку.
|
||||
13. Замкнутая самостоятельная цепочка сохраняется без комнаты, заливки и
|
||||
площади.
|
||||
14. Свободные стены видны в основной и статической карточке.
|
||||
15. Они учитываются при масштабировании и центрировании.
|
||||
16. Удаление среднего сегмента корректно разделяет цепочку.
|
||||
17. Двери и окна нельзя устанавливать в свободные стены.
|
||||
18. Старые конфигурации продолжают работать без миграции.
|
||||
|
||||
## 17. Не входит в первую версию
|
||||
|
||||
- перемещение вершин свободных стен;
|
||||
- Resize свободных цепочек;
|
||||
- ответвление из середины сегмента;
|
||||
- автоматическое распознавание комнат из сети стен;
|
||||
- установка дверей и окон в свободные стены;
|
||||
- участие свободных стен в комнатной топологии, площади и распространении
|
||||
света;
|
||||
- отдельные материалы, стили и свойства сторон свободной стены.
|
||||
|
||||
## 18. Предлагаемый порядок будущей реализации
|
||||
|
||||
1. Схема `wall_paths`, серверная валидация и чистые операции над цепочками.
|
||||
2. Посегментная толщина в инструменте «Стены».
|
||||
3. Сохранение, автоматическое продолжение и выбор конца после перезагрузки.
|
||||
4. Соединение цепочек конец с концом.
|
||||
5. Диалог замыкания и два варианта завершения.
|
||||
6. Геометрия тел и стыков стен разной толщины.
|
||||
7. Основная и статическая карточки, расчёт видимой области.
|
||||
8. Инструменты толщины, удаления, привязки и выравнивания.
|
||||
9. Документация и тестовый чек-лист.
|
||||
10. Прогон тестов только перед пре-релизом согласно принятому процессу.
|
||||
|
||||
## 19. Зафиксированные ответы владельца
|
||||
|
||||
| Вопрос | Решение |
|
||||
|---|---|
|
||||
| Что делать при замыкании? | Открывать диалог комнаты; внизу сразу добавить второе действие «Оставить замкнутыми стенами». |
|
||||
| Как продолжать цепочку? | В той же сессии продолжать автоматически; после перезагрузки выбирать конец кликом. |
|
||||
| Как соединять цепочки? | Соединять конец с концом; ответвление из середины сегмента не поддерживать. |
|
||||
| Разрешать двери и окна? | Нет, проёмы устанавливаются только в стены комнат. |
|
||||
| Нужны ли перемещение вершин и Resize? | Вынести в следующий этап; в первой версии оставить продолжение, изменение толщины и удаление сегментов. |
|
||||
| Какая толщина побеждает у общей стены? | Уже существующая физическая стена определяет толщину. |
|
||||
| Сохранять ли одиночную точку? | Нет, точка без сегмента остаётся временной. |
|
||||
Reference in New Issue
Block a user