Files
houseplan-card/docs/FILTERING.md
T
Matysh 1da1aba625
Validate / hacs (push) Failing after 1m8s
Validate / hassfest (push) Failing after 1m6s
Validate / frontend (push) Successful in 2m26s
Validate / backend (push) Failing after 7m59s
Validate / smoke (push) Failing after 9m32s
Curtains never wear a coloured plate
Owner's contract, 2026-08-04, verbatim: «у штор не должно быть жёлтой подложки
никогда, индикация открыто/закрыто за счёт морфинга иконки».

WHAT 'open' WAS. `.dev.open` is not a border — it is the badge FILLED with
--hp-open (#ff9f43), border and glyph colour included: a solid orange plate,
one step down from the yellow «включено» one. Covers shared a branch with
`valve` and took it in `open` AND `opening`, so a travelling curtain wore the
orange plate UNDER the breathing ring the owner approved a day earlier — the
plate he had just said should stay neutral while it moves, kept for the state
it stopped in. Since de53d53 an «Открыть/закрыть» marker reads its cover
wherever that entity sits, so the paint had just reached every curtain that
had the action set, his own included.

WHAT IT IS NOW. `_stateClass` returns no plate class for the `cover` domain in
any state: closed, open, ajar (HA reports a positioned cover as plain 'open'),
opening and closing all keep the neutral badge, and motion is the `.covermove`
ring alone. Open/closed is told by the ICON — which makes the morph the only
signal there is, so it had to stop having holes:

- `awning` mapped BOTH states to `mdi:awning-outline` — one glyph for open and
  closed, i.e. no indication at all for that class. Now outline (retracted) ->
  `mdi:awning` (extended).
- a cover with NO device_class (z2m ships plenty) only morphed if its icon
  happened to be in a device_class pair — and the icons the card itself hands
  out are not: the name rule «штор|curtain|blind|shade» gives `mdi:roller-shade`,
  «ворота|garage|gate» gives `mdi:garage-variant`. Those, plus
  `mdi:blinds-horizontal` and `mdi:door`, are now recognised as pairs on the
  base icon (COVER_ICON_ALIASES — base-icon matching only, never picked by
  device_class, so nothing is swapped for a guess).
- a hand-picked icon still wins outright everywhere, with ONE exception: a
  cover whose custom icon IS one of those pair members morphs inside THAT pair
  (`mdi:curtains` <-> `mdi:curtains-closed`) — never traded for another family.
  Without it, choosing an icon would silently switch the marker's only
  indicator off.

WHAT KEEPS THE FRAME, deliberately: door / window / garage_door / opening
binary sensors, an unlocked lock — and `valve`, which parts ways with `cover`
here. No icon pair morphs for a valve, so the frame is the only thing it has
to say «открыт» with; sweeping it along would have left those markers mute for
a rule that names the curtains. If the two domains should ever read alike, a
valve needs an icon pair first (docs/FILTERING.md).

smoke_cover_no_plate.mjs walks one curtain through closed / open / ajar /
opening / closing and reads the COMPUTED plate colour against probes of
--hp-bg, --hp-on and --hp-open: neutral every time, never yellow, never
orange, no 'on'/'open' class, the breathing ring in the two travelling states
and nowhere else. It also checks the morph for all ten classes both ways, the
no-device_class and custom-icon paths, and — the point of the whole bottom
half — that an unlocked lock and an open window sensor STILL come out orange
(and a locked lock neutral again, so the frame still means something). 13
checks are red on the parent commit. The unit suite gains a loop that fails
any class mapping both states to one glyph. smoke_cover_tap and
smoke_cover_not_primary flip their «open frame» assertions to the new
contract; docs/FILTERING.md gets the state table and the valve reasoning,
docs/TESTING.md the checklist item. shot_cover_states.mjs captures the four
states side by side.
2026-08-04 04:38:23 +03:00

7.8 KiB

Filtering: the explicit "hide from plan" flag

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 "Hide from plan" checkbox, stored as marker.hidden. The old on-the-fly filtering algorithm survives only as the SEEDER of those flags — it decides the initial value once, and the user owns the flag from then on.

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.
  • 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

  • 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 to untick.
  • "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 checkbox appears in the dialog of EVERY device kind, virtual included.
  • "Remove from plan" disappears for auto/entity devices (the checkbox is the one way to hide); a virtual device's "Delete" remains a real deletion.
  • Duplicate names are still numbered, light groups still fold — those are aggregation, not hiding.

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:

  1. the marker's bound controls, if it has any (a stateless remote or a virtual wall switch mirrors what it drives, not itself);
  2. a lit light among its entities (owner's principle 2026-07-29: the glow spot and the badge may never disagree);
  3. 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);
  4. otherwise the primary entity (primaryEntity).

Rule 3 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 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): those keep their primary until their owner says otherwise, and even with the action chosen a lit light still wins rule 2. The cost is that 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 cover is never painted (owner 2026-08-04)

«У штор не должно быть жёлтой подложки НИКОГДА, индикация открыто/закрыто за счёт морфинга иконки.» For the cover domain — and for the cover an «Открыть/закрыть» marker indicates, rule 3 above — _stateClass returns no plate class 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
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.