# 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`): bound `controls` first (other targets only: an `entity:*` marker's bound entity and a `device:*` marker's child entities are excluded), otherwise its primary controllable entity when `is_light` is set, 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; 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. 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.