# Filtering: hide versus delete Agreed with the owner 2026-07-29. This document is the source of truth for the mechanism; the code follows it. ## Principle Whether a device is on the plan is an EXPLICIT, per-device fact. The bottom-left actions have deliberately different meanings: Hide/Show is a reversible presentation flag, while Delete removes the plan object and every plan-level contribution. The old on-the-fly filtering algorithm survives only as the SEEDER of initial hidden flags. ## Data model - `marker.hidden: true` — hidden from the plan. For an auto device without a marker, hiding creates a stub marker (this mechanism predates this spec). - `marker.hidden: false` (marker present) — explicitly VISIBLE: the seeder never touches a device that has any marker, so unhiding must KEEP the stub marker. That is the re-seed protection. - `marker.removed: true` — a minimal binding tombstone. It is not a marker and is never built, rendered, shown as a ghost or aggregated. It exists only so automatic discovery cannot immediately recreate the deleted device. The same binding remains available in Add; saving it again replaces the tombstone and starts with a fresh position. - No marker — never evaluated by the seeder yet, or a plain physical device. - `settings.filter_seeded: true` — this config has been materialised. - `settings.show_all` — removed (deleted during materialisation). The old toggle was shared config state; the new one is a local editor tool. ## Seeding "Non-physical" = the old filter rules: excluded integration domains, model "Group", scene-like models, bridges, myheat children, and individual lamp devices in an area covered by a light group (when group folding is on). The seeder runs on the editing client (write permission required) whenever devices rebuild, and creates `hidden: true` stub markers for non-physical devices in BOUND areas that have NO marker. It is idempotent: marked devices are never revisited. It fires on: 1. first load of a config without `filter_seeded` (materialises the current behaviour; nothing changes visually, the flags become real and editable); 2. an area newly bound to the plan; 3. a new device appearing in a bound area — non-physical ones are hidden silently (no red dot); physical ones keep the red-dot flow. Until a config is seeded (`filter_seeded` absent), `buildDevices` applies the LEGACY runtime filter, so a read-only client on an old config sees exactly the old behaviour until an editing client materialises it. ## Behaviour | State | Renders | Show hidden | Room/light/climate data | Add picker | |---|---:|---:|---:|---:| | visible marker/device | yes | — | yes | no duplicate | | `hidden: true` | no | ghost | LQI/climate yes, visible light no | no duplicate | | `removed: true` | no | no | no | yes | - Hidden devices ARE built (flagged `hidden`), but not rendered in any mode, except the device editor with "Show hidden devices" on — there they render ghosted (translucent, dashed) and clicking opens the dialog, where the bottom-left "Show" action restores it after saving. - "Show hidden devices" (rename of "Show all") is LOCAL, ephemeral state of the current tab. - Room LQI counts hidden devices (owner's decision). - Hidden devices are NOT content for the CONTENT FRAME (docs/CANVAS.md §4, audit DEV-2C947-01). The frame is presentation: an object the plan does not draw may not decide what the plan opens on. Hiding a marker that had once been dragged into the yard used to leave the visible house a dot in the corner of a frame 112x too wide — on the full card and on `houseplan-space-card` alike. They keep their place in the auto-grid roster (so a visible neighbour does not move when one is hidden) and in every aggregation listed here; only the frame stops seeing them, ghosts in the device editor included — reaching a ghost is the pan slack's job (§5). - Light fill and glow do NOT count hidden devices — an invisible device casts no visible light (owner's decision). Room climate is registry-wide and unaffected, as before. - The bottom-left "Hide" / "Show" action appears in the dialog of every existing device kind, virtual included; changing it is applied by "Save". - "Delete" appears beside Hide/Show for every existing marker. It asks for confirmation and commits immediately. HA device/entity markers leave only the tombstone above; virtual markers are removed outright. - Deleting a generated light-group binding also removes that group from the plan's light-source set. On a legacy unseeded config its formerly folded member lamps may consequently appear as ordinary devices. This is deliberate delete semantics, not restoration of a hidden group. - Deletion removes the marker layout, pending activity state, attachments and saved vacuum trails. A late drag from a stale browser tab is ignored by the server while the tombstone exists. - A deleted **device binding** is excluded from room LQI, light-source resolution, room light statistics/fill/Glow, registry-wide climate averages and explicit room sources. An **entity binding** tombstone suppresses that standalone plan object; it does not remove the same entity from the data of a still-live parent HA device. Tombstones are binding-scoped, not mutations of the HA registry. - References in openings, live text and another marker's persisted `controls` are retained but become inactive (no lock/contact badge, `—`, or control action respectively). Adding the binding again restores them without reconstructing configuration or performing an unrelated Save first. - Duplicate names are still numbered, light groups still fold — those are aggregation, not hiding. ## What a marker SHOWS 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 **resolved light sources** (`resolvedLightSources`): external `controls` plus its own primary controllable entity when `is_light` is set (an `entity:*` marker's bound entity and a `device:*` marker's child entities are excluded from the external list), otherwise automatic `light.*` only when `light` is the device's resolved functional role. An auxiliary LED/display light on a media player or appliance does not turn the whole marker into a lamp. This exact set also feeds Glow, Light fill, room light stats and group toggle; For compatibility, a pre-v1.60 marker that lists its own `switch.*` in `controls` is interpreted as `is_light`. Marker Save converts either binding; Optimize Plans can convert the losslessly identifiable `entity:switch.*` case (a `device:*` marker needs the HA registry and is therefore left to Save). External controls remain additive, so migration does not change Glow, Light fill or room statistics. The dialog preserves their ordered raw list, including duplicates and temporarily unknown targets; runtime consumers separately de-duplicate and keep only currently controllable entities. 3. otherwise the device's **resolved state role** (`resolvedDeviceStateEntities`): functional device domains first, then semantic binary signals, then switches, then passive readings together. `primaryEntity` is only the first entity of this same set for actions which require one target; it no longer defines marker availability by itself. For `climate.*`, a recognized real `hvac_action`/equivalent action remains authoritative: `idle` stays neutral even while the selected mode is `heat`, while `heating`, `cooling`, `preheating` and `defrosting` are working. Unknown vendor mode-like values in action attributes are ignored instead of suppressing the normal enabled-mode fallback. If the integration exposes no recognized action, the current non-off state is matched against HA's `hvac_modes` (plus the standard modes) and used as the best available enabled/working approximation. Rule 1 was added 2026-08-04 on the owner's report: his Aqara «Roller shade driver E1» curtains ship the `cover.*` hidden by the integration and a visible `switch.*_reverse_direction`, so `primaryEntity` picked the service switch — the plan showed no ring while a curtain travelled, no `curtains` / `curtains-closed` morph, and a yellow «включено» plate whenever the reverse-direction option happened to be on. The tap had already been taught to find the cover among ALL the device's entities (2026-08-04, the same `coverEntityOf`); the indication now follows it. **Why the cover is FIRST and not third** (audit DEV-1DA1-01, fixed the same day). It went in below `controls` and the lit light at first, and that left the contract below («у штор не должно быть жёлтой подложки НИКОГДА») with two holes big enough to walk through: a mixed device — a lamp that also ships a blind — told «Открыть/закрыть» went yellow off its own lit `light.*`, and a curtain marker with a bound wall switch went yellow off `controls`. In both the early `return 'on'` never reached the cover branch, so the travelling curtain also lost its breathing ring, and in glow fill (where the renderer strips `on` from a shining source) it was left with no indicator at all — while the tap still drove the cover. A rule that «шторы никогда не жёлтые» cannot have exceptions decided by the neighbours in the entity list. **Why it hangs on the explicit action and not on «the device has a cover».** Choosing «Открыть/закрыть» in the marker dialog is the only statement the card has that means *this marker IS the curtain* — and the dialog offers that option for exactly the devices where a cover exists. Tying the indication to it keeps one answer to «what is this marker»: the option offered, the entity tapped and the state shown are the same entity, decided in one place. Nothing changes behind the user's back for a mixed device (a lamp that also owns a blind, a TRV with a service switch) that was NOT told it is a curtain: it keeps its primary and the same controls/light/primary precedence until its owner says otherwise. Two edges follow from the wording: a marker set to «Открыть/закрыть» whose device carries no `cover.*` at all falls through to rules 2–3 (the statement is only as strong as the entity behind it), and a curtain left on «Инфо-карточка» still indicates its primary — one click in the dialog away, and the honest reading of what the marker was told it is. ### A media player is powered, not "working" (owner 2026-08-07) The resolved role stays `media_player.*` for every TV, receiver, speaker and soundbar; no model/name exception is involved. Its HA transport states (`on`, `idle`, `playing`, `paused`, `standby`, etc.) all produce a neutral marker with no running effect. Explicit `off` deliberately reuses the existing `.dev.unavail` faded presentation used by `unknown` / `unavailable`; it does not add another visual status. When a marker resolves several media entities, it fades only if none is currently available and powered. The media role also outranks auxiliary `light.*`/`switch.*` entities belonging to the same physical device. Status LEDs, display illumination and vendor options therefore neither paint the media marker nor enter room light aggregates automatically. Explicit marker `controls` or `is_light` remains the user override for a real light source. ### A cover is never painted (owner 2026-08-04) «У штор не должно быть жёлтой подложки НИКОГДА, индикация открыто/закрыто за счёт морфинга иконки.» For the `cover` domain — and for the cover an «Открыть/закрыть» 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 | `.activity-transition` breathes in Icon + activity | open glyph | | `unknown` / no state | neutral | — | base icon, no morph | | `unavailable` | neutral, faded (`.unavail`) | — | base icon | Until this the domain shared one branch with `valve` and wore `.dev.open` — an orange FILLED badge (`--hp-open`), not a mere border — while open or opening. The open/closed story is now told by the icon alone (`stateIcon` / `COVER_ICONS`), so the morph has to be exhaustive: every device class maps its two states to two DIFFERENT glyphs, and a cover with no `device_class` at all (z2m ships plenty) morphs within the family of its own base icon — `mdi:roller-shade` (what the name rule «штор|curtain|blind|shade» hands out), `mdi:garage-variant`, `mdi:blinds-horizontal`, `mdi:door`. The one place a hand-picked icon is not final: a cover whose custom icon IS one of those pair members morphs inside that pair — never traded for another family — because otherwise choosing an icon would silently switch the marker's only indicator off. WHAT KEEPS THE FRAME. `.dev.open` is untouched everywhere else: door / window / garage_door / opening binary sensors, an unlocked `lock`, and `valve`. A valve is deliberately left out of the owner's rule — no icon pair morphs for it, so the frame is the only thing it has to say «открыт» with. If the owner ever wants the two domains to read alike, a valve needs an icon pair first.