Files
frontend/test/common/controllers
Petar PetrovandAidan Timson 37de8c4b15 Delegate dashboard visibility conditions to core (#52904)
* Add dashboard visibility condition classifier, translator and splitter

Pure-logic foundation for delegating dashboard visibility conditions to
core (#52836).

- Add a VisibilityCondition type spanning the client-only lovelace
  conditions (screen, user, view_columns, location, time) and core
  automation conditions, alongside the existing lovelace Condition.
- translate.ts: classify conditions as client- or server-evaluated and
  translate server ones to core format (entity -> entity_id, state_not
  -> not-wrap, numeric bound coercion), preserving lovelace's
  not = not(AND) semantics.
- split.ts: split a tree into maximal server subtrees (one subscription
  each, sibling-grouped) plus a three-valued client combiner.

* Add reactive condition evaluator controller

Phase B of delegating dashboard visibility conditions to core (#52836).

- ConditionEvaluatorController opens one subscribe_condition per server
  subtree, evaluates client leaves locally, observes screen/time
  boundaries, and exposes a tri-state visible/hidden/unknown result plus
  error. It recomputes on push/listener/hass/context change, debounces
  re-subscription when the tree changes, and tears down on disconnect.
- Add observeConditionChanges to listeners.ts (notify-only, decoupled
  from checkConditionsMet) and widen extract.ts to the VisibilityCondition
  tree, factoring time-boundary scheduling into a shared helper.

* Harden condition translation for incomplete and odd numeric inputs

Follow-up to the adversarial review of the translator (#52836):

- Incomplete/garbage state conditions (no entity, no value, or an empty
  object) now translate to an always-false core condition instead of a
  schema-invalid `state`, matching checkConditionsMet and avoiding a
  broken grouped subscription.
- numeric_state bounds: coerce only finite numeric strings (incl. "" -> 0)
  to numbers, pass genuine entity-id references through, and drop junk or
  non-finite strings (matching lovelace's "NaN -> ignored" and never
  emitting a non-JSON-serializable Infinity).

* Fix condition evaluator controller lifecycle edge cases

Follow-up to the adversarial review of the controller (#52836):

- Key re-subscription on a structural signature of the condition tree
  rather than array reference identity, so a host re-deriving the array
  each render neither starves the debounce nor churns subscriptions.
- Reset the published result to `unknown` on host disconnect so a
  detached/reconnecting host never renders a stale, no-longer-live result.
- Read hass lazily in the time-boundary listeners so timezone changes are
  picked up on the next boundary instead of being pinned at subscribe time.

* Evaluate dashboard visibility through the reactive condition evaluator

Rework ConditionalListenerMixin to derive visibility from
ConditionEvaluatorController instead of evaluating checkConditionsMet
synchronously. Stateful conditions (state, numeric_state, template, sun,
zone, device, integration) are delegated to core via subscribe_condition;
client-only conditions (screen, user, view_columns, location, time) stay
local. The mixin re-feeds the evaluator on connect and on hass/config/
column changes, and drives _updateVisibility from its tri-state verdict.

Consumers (hui-card, hui-badge, hui-section, hui-heading-badge,
hui-view-sidebar, hui-conditional-base) now read the mixin's
_conditionsVisible(), which prefers the server-aware verdict and falls back
to an optimistic synchronous seed while a server subtree is pending — exact
for legacy lovelace conditions (no flash for existing dashboards) and hidden
for core-only conditions until the server reports.

- fold the host entity_id context into the evaluator path via
  addEntityToCondition, and read core-format entity_id in
  checkStateCondition / checkStateNumericCondition so seed and server agree
- addEntityToCondition no longer grafts a context entity onto an
  already-core condition that carries its own entity_id
- the conditional card/row now evaluates legacy {entity, state} conditions
- cache the entity-folded array so the evaluator's signature memo holds
  across hass updates, and drop the cached verdict when the tree changes by
  value so the seed is used for the new tree

* Add server condition types to the dashboard visibility editor

Let the visibility editor add and edit the core-format server condition
types (template, sun, zone, device) by embedding the automation condition
editors, which already speak core format. ha-card-condition-editor
dispatches these types to ha-automation-condition-editor; the existing
lovelace editors and the and/or/not containers are unchanged, and because
the logical editors nest ha-card-condition-editor, mixed trees dispatch
each child correctly.

- extend the add-condition menu with the new types (icons + labels)
- suppress the client-side live-test for server-class conditions (and any
  logical tree containing one); checkConditionsMet can't evaluate them, so
  the indicator stays neutral instead of showing a misleading failure

Server-backed live-test and read-both/write-new conversion of lovelace
state/numeric_state conditions are follow-ups.

* Evaluate the visibility condition editor live-test through the reactive evaluator

Drive the per-condition live-test indicator with the same
ConditionEvaluatorController the dashboard uses at runtime, so server-class
conditions (template/sun/zone/device and core-format state/numeric_state) get
a real subscribe_condition-backed verdict instead of a neutral indicator.
Client-only conditions stay evaluated locally and mixed logical trees combine
both via three-valued logic.

- Fold the card entity into the observed condition exactly as the runtime
  mixin does, and memoize the folded array so the evaluator's signature memo
  keeps hitting on hass-only updates.
- Map the evaluator verdict to the indicator: visible -> pass, hidden -> fail,
  pending -> unknown, server error -> invalid (raw error shown as the tooltip
  detail, localized label kept as the aria-label).
- Keep a client-side validity check for purely client trees so a malformed
  client-only config still surfaces as invalid.
- Recurse the no-entity (filter-mode) suppression so nested entity-less
  conditions are handled, and report an as-yet-unknown manual test as no
  result rather than a failure.
- Drop the now-unused invalid-config alert and its orphaned translation keys.

* Evaluate the visibility status banner server-side

Drive the card-level visibility summary banner with the same
ConditionEvaluatorController used by the per-condition live-test, so a set
containing server-class conditions reports its real visible/hidden verdict
instead of being flagged as an invalid configuration.

- Add a distinct "unknown" banner state for while a server result is still
  pending, separate from "invalid" (a genuine configuration error).
- Keep a client-side validity check for purely client trees, and fold the
  card entity into the observed conditions, matching the per-condition editor.
- Extract isPureClientCondition (every leaf client-side, as opposed to
  isClientCondition's any-leaf) so both consumers share one classifier.

* Edit dashboard state conditions via the core condition editors

Route `state` / `numeric_state` visibility conditions to the core automation
condition editors (outside entity-filter mode), so they share one editing
surface with the server-class types and are authored in core format.

- Read both: existing lovelace-format conditions (`entity`, `state_not`, …)
  are translated to core for display; a `state_not` shows as `not(state)`.
- Write new with touch-to-convert: opening a condition leaves it untouched;
  editing or adding one persists it in core format (`entity_id`, `state` list).
- Entity-filter mode keeps the lovelace no-entity syntax and editors.
- Register the core `state` / `numeric_state` editors (dynamicElement only
  renders a tag, it does not define the element) and widen the editor chain's
  condition arrays to the mixed visibility union.

* Remove the superseded client-only condition listeners

The dashboard now evaluates visibility through the reactive condition
evaluator (which uses `observeConditionChanges`), so the old synchronous
client-only listener path has no remaining callers.

- Delete `ConditionListenersController` and the `setupConditionListeners` /
  `setupMediaQueryListeners` / `setupTimeListeners` helpers.
- Keep `observeConditionChanges` and the shared time-boundary scheduler, and
  re-point their tests onto it so the scheduling edge cases stay covered.

* Pin the condition editor live-test for hidden and invalid configs

The per-row live-test set `invalid` (or hidden) directly but left its
evaluator callback unguarded, so the evaluator's torn-down `unknown`
result — fired ~500ms after observing `undefined` — clobbered the pinned
state until the next hass tick re-pinned it, causing a transient flicker.

Add the same `_override` guard the sibling visibility-status banner
already uses: the hidden / client-invalid branches set the result
directly and pin it, and the evaluator callback ignores results while
pinned.

* Remove the orphaned conditional-listener no-op methods

`addConditionalListener` / `clearConditionalListeners` became no-op
shims once the evaluator took over subscriptions and teardown; their only
callers were the listener-wiring removed in the migration. Drop both
methods, the mixin's now-redundant `disconnectedCallback` override, and
the stale `clearConditionalListeners()` call in hui-conditional-base.

* Accept server-evaluated conditions in validateConditionalConfig

The conditional card/row/element gate their config on
validateConditionalConfig, which only knew the client-side condition
types and rejected the server-evaluated ones (template, sun, zone,
device, and integration-provided conditions). Now that those are
authorable and delegated to core, accept them — core validates them —
so configuring one no longer throws "Invalid configuration".

* Evaluate conditional picture element visibility server-side

The picture-elements `conditional` element was the one visibility
consumer still evaluating fully client-side, so its stateful conditions
were not delegated to core (and server-class types it could not evaluate
fell through to a permanently-hidden result). Convert it to a
ReactiveElement driven by ConditionalListenerMixin, exactly like its
sibling hui-conditional-base, so the evaluator delegates stateful
conditions through subscribe_condition and evaluates the client-only
ones locally.

* Keep conditional cards mounted while hidden for server conditions

A conditional card gated on a server-evaluated condition (template, sun,
zone, device) never reappeared: hui-card removes a hidden child card from
the DOM, tearing down the evaluator, and the synchronous seed can revive a
client condition but not a server one, so the subscription was never
(re)opened and the server result that would show the card never arrived.

Set connectedWhileHidden so the conditional card/row stay mounted while
hidden and keep their subscriptions alive, like the other cards that must
keep working while hidden. The inner element is still unmounted when
hidden, so there is no extra render cost.

* Load config translations for the reused condition editors

The dashboard visibility editor reuses the automation condition editors
(state, numeric_state, template, sun, zone, device), which label their
form fields from the `config` translation fragment. The lovelace panel
never loads that fragment, so those labels rendered blank — e.g. the
numeric_state limit-type selectors and the above/below fields.

Load the `config` fragment when the conditions editor first renders, so
the embedded editors resolve their labels.

* Fold the card entity into entity-less conditions on read

A card with a host entity can carry entity-less state / numeric_state
visibility conditions that implicitly target that entity (folded in at
runtime). The editor translated them with an empty entity_id, so the
reused automation editor showed an empty, invalid entity field.

Fold the current-mode context entity into the displayed condition, and
recompute it when the entity context arrives (it can follow the
condition). Opening still does not rewrite the stored config; only
editing converts it to explicit core format.

* Demote the unused state/numeric_state condition editors to base classes

With the automation condition editors handling state / numeric_state in
the dashboard editor, the lovelace `ha-card-condition-state` and
`ha-card-condition-numeric_state` elements are no longer rendered. Their
only remaining role is as the base class for the entity-filter
`-no_entity` variants, so drop the now-dead custom-element registrations
(and the redundant side-effect imports) while keeping the shared logic.

* Format condition editors

* Fix condition editor indentation

* Invalidate the condition evaluator result as soon as the tree changes

When the observed tree changed by value, the controller kept the old
split and subscriptions alive until the debounced re-subscribe fired, so
it kept publishing (and the editor's Test action reported) the previous
tree's verdict, and a host that had already reset to unknown never got a
notification when the new tree settled on the same result.

Tear the old tree down when the signature changes and publish unknown
until the new subscription reports. Trees with nothing to subscribe to
re-evaluate immediately rather than waiting out the debounce.

Track a pending re-subscribe with its own flag: undefined is a valid
signature (observation cleared), so using it as the "nothing pending"
marker kept rescheduling the timer on every hass update and could leave
the previous subscription open indefinitely.

* Translate bound-less numeric_state conditions to a schema-valid core condition

Dropping a junk or non-finite bound could leave a numeric_state condition
with neither above nor below, which core's schema rejects; because sibling
server conditions are grouped into one subscription, that single invalid
leaf failed the whole group and hid the content. Lovelace merely ignored
such bounds and only required the value to be numeric, so express that
residual check as a template condition instead. An entity-less
numeric_state resolves to always-false, matching checkConditionsMet.

* Validate every visibility condition client-side and handle unsupported automation editor configs

validateConditionalConfig already accepts the server-evaluated types, so
gating it behind "every condition is client-only" only served to skip
validation as soon as a server condition was present; a malformed screen
condition next to a template condition was reported as hidden rather than
invalid. Run it for the whole set.

The embedded automation condition editors fire ui-mode-not-available when
an existing core config fails their UI struct. Fall back to YAML with the
struct warnings, as the automation condition row does, instead of
rendering a blank editor while still claiming UI support.

* Type dashboard visibility contracts as VisibilityCondition

Card, badge, section, view sidebar and heading badge visibility, plus the
conditional card, row and element conditions, still declared the
lovelace-only Condition type, so the newly supported template, sun, zone,
device and integration conditions were invalid at the config boundary and
needed casts throughout. Declare them as VisibilityCondition, widen
validateConditionalConfig accordingly and drop the casts that made
redundant. Entity-filter conditions stay lovelace-only.

* Subscribe when hass arrives after the conditions were observed

The subscribed signature was recorded before checking that hass was
available. A host that connected with its config set but received hass
later therefore never subscribed: the later observe saw the same
signature, found no split and stayed unknown for good. Only record the
signature once a subscription is actually opened.

* Seed pending visibility with a three-valued local evaluation

While a server result was pending, the mixin ran the whole tree through
the legacy client evaluator. Core-only leaves default to false there, so
not: [template] was inverted to visible until core replied, and core
state / numeric_state options such as for, match and value_template were
ignored, both contradicting the no-flash guarantee.

Evaluate locally only the leaves the legacy evaluator reproduces exactly
(client-only types, lovelace state / numeric_state, and core ones without
those options), keep every other leaf unknown through and / or / not, and
hide while the outcome still depends on an unknown leaf.

* Hide server-only condition types in entity-filter mode

The map and entity-filter cards still evaluate their conditions locally
with checkConditionsMet, where template / sun / zone / device fall through
to a failing state check and filter out every entity. Don't offer them in
the add menu for that mode.

* Leave conditions carrying enabled to the server in the local seed

Core treats a disabled condition as neutral (and enabled may be a
template), while the legacy evaluator ignores enabled altogether. A
pending not: [disabled failing state, ...] was therefore seeded visible
although core reports hidden. Treat any node carrying enabled as unknown.

* Reject filter-incompatible pasted conditions in entity-filter mode

The condition clipboard is shared across editors in the session, so a
template / sun / zone / device condition, or a core-format leaf pinned to
its own entity_id, could be pasted into a map or entity-filter editor,
where the local evaluator would filter out every entity or evaluate the
wrong one. Hide the paste entry and ignore the action for such clipboard
contents while in filter mode.

* Re-feed the section visibility evaluator after a strategy config resolves

_config is not reactive, and for a strategy section it is assigned after
the Lit update that last fed the evaluator, so _conditionsVisible kept
reading the verdict for the raw or previous config until the next hass
update. Re-run setupConditionalListeners right after assigning it.

* Hide content whenever a server condition subtree errors

An errored subtree was recorded as false and fed through the combinators,
so a client-side not inverted it and an invalid server condition could
show content; the runtime mixin only looks at the result, not the error.
Force the result to hidden while any subtree is in error.

* Honor enabled: false on visibility conditions

Core skips a disabled condition inside a compound: it neither passes nor
fails. Rebuilding logical conditions during translation dropped enabled
(and the other row metadata), so a disabled server-side group was
evaluated, and the mixed-tree split and the local seed ignored it as well.

Preserve the row metadata on translated logical conditions, skip disabled
nodes when splitting and when seeding, and leave a template-valued
enabled on a seeded node unknown since only core can render it.

* Type the conditional card conditions as VisibilityCondition

ConditionalCardConfig, the contract HuiConditionalBase actually consumes,
still declared the lovelace-only union, unlike the row and element
contracts.

* Keep a client-side node with a template-valued enabled unknown

Only core can render enabled: "{{ ... }}", and core never sees a mixed or
client-only node, so the split silently treated such a node as enabled
and let its children decide visibility. Keep it unknown instead, erring
toward hiding and matching the optimistic seed.

* Whitelist the condition types usable as entity filters

The filter-compatibility check only excluded the four built-in server
types, so an integration-provided condition copied from a visibility
editor could still be pasted into an entity-filter editor, where the
local evaluator cannot evaluate it and filters out every entity. Check
against the types checkConditionsMet supports instead.

* Fold the host entity into legacy entity-less conditions as well

A legacy condition such as { state: "on" } has no condition key, so
addEntityToCondition never stamped the card entity on it. That used to be
harmless because checkConditionsMet fell back to the evaluation context,
but the server translation has no context and turned the condition into
always-false once core reported. Treat the legacy shape as the state
condition it is, and make the helper generic over the visibility union so
callers no longer need casts.

* Keep the lovelace outcome for infinite numeric bounds

Number("1e400") is Infinity, which lovelace does not ignore: above +inf
or below -inf can never pass, while above -inf or below +inf always do.
Dropping such a bound could flip an untouched condition from hidden to
visible. Resolve the unsatisfiable cases to always-false and drop only the
vacuous ones. Infinity is not JSON-serializable, so neither is ever sent.

* Only leave a template-valued enabled unknown in the local seed

The check also matched enabled: true, so an explicitly enabled condition
was seeded unknown and the card hidden until core responded, exactly the
flash the seed is meant to avoid.

* Show no test chip while the condition evaluator reports an error

An invalid server condition evaluates to hidden plus an error; reading
only the result turned Test into a transient "not met" chip that
contradicted the invalid live indicator.

* Seed only core conditions the client compares identically

The optimistic seed still admitted core-format shapes the legacy evaluator
handles differently: an attribute comparison (core compares the raw value,
the client stringifies it) and an entity-valued numeric bound (core errors
when the entity is missing, the client ignores the bound). Leave those
unknown until core reports.

* Accept a single condition as the children of a logical condition

Core's LogicalCondition allows conditions to be one condition or a list,
but every traversal here assumed a list and crashed on the single shape.
Widen the type accordingly and route all traversals through one helper.

* Keep lovelace leaves with legacy-only semantics client-side

Core compares raw attribute values, only dereferences input_* comparison
values and errors on a missing bound entity, where lovelace stringifies
the attribute, resolves any existing entity and ignores the bound.
Delegating such existing conditions changed their visibility silently.

Classify a lovelace-format state / numeric_state leaf that relies on any
of those as client-side so it keeps its exact legacy evaluation; once the
user edits it, it is saved in core format and evaluated by core.

* Drop the pinned editor indicators when leaving an override branch

The empty and client-invalid branches pin the indicator and clear the
evaluator, which is then already unknown; when the condition becomes
valid the pending observation is unknown too, so no notification arrives
and the stale pinned state stayed visible until core first responded.

* Reject core's single-child logical shorthand as an entity filter

Filter consumers and the filter-mode editor expect the lovelace list
shape for logical children, so a pasted core condition using the
single-child shorthand caused a runtime error there. Require a list in
the filter-compatibility check and traverse defensively when looking for
no-entity conditions.

* Seed only core leaves whose entity is present and, for numeric_state, bounded

Core errors on a missing entity and rejects a bound-less numeric_state,
while the client evaluates a missing entity as unknown and passes any
numeric value, so both shapes could flash visible before the subscription
error arrived. Leave them unknown until core reports.

* Re-subscribe when the hass connection object is replaced

Subscriptions are bound to the connection they were opened on. An
unchanged tree observed with a hass carrying a different connection kept
the stale subscriptions; re-open them in that case while still not
churning on ordinary hass updates.

* Keep non-finite bounds distinguishable in the condition signature

JSON.stringify serializes Infinity, -Infinity and NaN alike as null, so
flipping a YAML .inf bound produced the same signature and the old
subscription stayed active.

* Seed lovelace leaves only while their target entity exists

Core reports an error for a condition on a missing entity, which hides the
tree, while the legacy evaluator compares against the literal unknown.
Seeding such a leaf optimistically could therefore flash the content
before the error arrived; leave it unknown instead.

* Do not seed a core state condition that compares against another entity

checkConditionsMet dereferences any existing entity used as a comparison
value, while core only dereferences input_* entities, so such a core leaf
could flash visible before the subscription reported false. Apply the
same entity-reference guard the legacy classification uses.

* Preserve row metadata when rebuilding lovelace leaves

Both the evaluation translation and the editor's display conversion
rebuilt lovelace state / numeric_state leaves without enabled, alias and
note, so editing a disabled legacy condition silently re-enabled it and a
template-valued enabled was dropped before reaching core.

* Surface template rendering errors from condition subscriptions

subscribe_condition events carry template_errors next to a result when a
template inside the condition failed to render. Forward them through the
evaluator's error so the editor flags the condition, without overriding
core's result the way a hard error does.

* Treat a null numeric bound as absent

Lovelace ignores a null bound (== null), but the translator only checked
for undefined and Number(null) turned it into an unintended 0. The editor
conversion now skips null bounds as well.

* Keep the and wrapper for a single-child not and row metadata on fallbacks

Core skips a disabled child, so a bare not over one child evaluates to
true where lovelace's not (¬ of the AND of its children) gives false; the
and wrapper restores that for any arity. The always-false and numeric
value fallbacks also dropped enabled, alias and note, so a template-disabled
leaf would have participated in its parent regardless.

* Treat an empty legacy entity as absent everywhere

checkStateCondition falls back to the host entity with entity || context,
so an existing condition with entity: "" targeted the card entity. The
entity fold, the optimistic seed, the editor conversion and the
translation used nullish checks instead, which kept the empty string and
turned such a condition into an error or an always-false leaf. Use the
same truthy fallback throughout.

* Append non-finite values to the condition signature instead of substituting

A marker string standing in for Infinity could in principle collide with
an equal user string. Keep plain JSON and append the ordered non-finite
values, which is unambiguous.

* Remove a stray brace rendered under every condition editor

A leftover interpolation terminator outside the template expression
rendered a literal "}" below the condition content. Also reject pasting a
condition pinned to an explicit entity into an entity filter, where the
fold-in would keep it and judge every candidate by the copied entity.

* Tidy the optimistic seed's leaf check

Condense its doc comment, take the states map rather than the whole hass
object, and simplify the bound checks.

* Tighten comments on dashboard condition evaluation.

---------

Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-09-08 12:53:50 +00:00
..