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.
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:
- first load of a config without
filter_seeded(materialises the current behaviour; nothing changes visually, the flags become real and editable); - an area newly bound to the plan;
- 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-cardalike. 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:
- the marker's bound controls, if it has any (a stateless remote or a virtual wall switch mirrors what it drives, not itself);
- a lit light among its entities (owner's principle 2026-07-29: the glow spot and the badge may never disagree);
- 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); - 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.