6 files
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
|
||||
This module registers versioned ``ha_mcp_tools/*`` WebSocket commands that the
|
||||
ha-mcp server calls in-process (same HA core, no REST/WS round-trips) behind a
|
||||
capability gate. It registers twenty-three commands. It advertises twenty-five
|
||||
capabilities: twenty-two command capabilities plus three additive flags
|
||||
(dashboards_doc_search, search_visibility, and search_entity_membership);
|
||||
capability gate. It registers twenty-three commands. It advertises twenty-six
|
||||
capabilities: twenty-two command capabilities plus four additive flags
|
||||
(dashboards_doc_search, search_visibility, search_entity_membership, and
|
||||
search_visibility_allowlist_authorization);
|
||||
the info handshake carries no capability entry:
|
||||
|
||||
* ``ha_mcp_tools/info`` — the handshake: ``schema_version`` + ``capabilities[]``
|
||||
@@ -111,10 +112,17 @@ the info handshake carries no capability entry:
|
||||
:func:`_visibility_hidden_set` mirrors the server's ``hidden_entity_ids``, with
|
||||
the Assist dimension delegated to core's ``async_should_expose``), so
|
||||
``ha_search`` can route through the component even with an active filter. A
|
||||
degraded dimension (unknown ``exclude_category`` / empty-registry allowlist /
|
||||
unavailable Assist) fails open, and :func:`_visibility_warnings` returns the
|
||||
resolver-parity ``visibility_warnings`` the response carries so the filtering is
|
||||
not silently incomplete.
|
||||
second ``search_visibility_allowlist_authorization`` capability declares that
|
||||
this component understands the ``allowlist_authorization`` wire key, which the
|
||||
server sends only to a component advertising it and which selects the revised
|
||||
precedence (an allowlist match authorizes past category, HA-hidden, and Assist
|
||||
filters). Without the key the component keeps the legacy conjunctive
|
||||
precedence, so it stays correct against an older released server that still
|
||||
resolves the old way in its own outbound scan. A degraded dimension (unknown
|
||||
``exclude_category`` / empty-registry allowlist / unavailable Assist) fails
|
||||
open, and :func:`_visibility_warnings` returns the resolver-parity
|
||||
``visibility_warnings`` carried by the response so filtering is not silently
|
||||
incomplete.
|
||||
* ``ha_mcp_tools/server_entry`` — the component locates its OWN server config
|
||||
entry (``entry.data[CONF_ENTRY_TYPE] == server``, the one marker key it reads
|
||||
from ``entry.data``) and returns ``{entry_id, channel, pip_spec}`` (channel /
|
||||
@@ -268,7 +276,7 @@ from collections.abc import Collection, Mapping
|
||||
from dataclasses import dataclass
|
||||
from difflib import SequenceMatcher
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import voluptuous as vol
|
||||
import yaml # type: ignore[import-untyped]
|
||||
@@ -376,6 +384,11 @@ CAPABILITIES: list[str] = [
|
||||
# would ignore the param is never sent it (param-sniffing is banned for
|
||||
# routing; the CAPABILITIES flag is what the server gates on).
|
||||
"search_visibility",
|
||||
# A semantic flag on search_visibility: this component understands the
|
||||
# ``allowlist_authorization`` wire key (revised allowlist precedence). The
|
||||
# server sends that key only to a component advertising this flag, and with
|
||||
# an active allowlist requires the flag or falls back to its legacy resolver.
|
||||
"search_visibility_allowlist_authorization",
|
||||
"server_entry",
|
||||
# The WRITE counterpart of ``server_entry`` (Phase 3). The server gates its
|
||||
# ``ha_dev_manage_server(update_source)`` embedded-mode direct-write on this; an
|
||||
@@ -644,10 +657,13 @@ def _info_schema() -> dict[Any, Any]:
|
||||
|
||||
|
||||
# The nine hide dimensions ``VisibilityConfig.to_wire`` emits, split by wire type
|
||||
# (seven id/name lists, two bool flags). Kept in lockstep with the server's
|
||||
# ``to_wire`` and :func:`_visibility_hidden_set`; a new dimension is a new component
|
||||
# capability, added on BOTH sides (see :func:`_visibility_param_schema`). The union
|
||||
# is pinned equal to the server resolver's key set by the cross-seam contract test.
|
||||
# (seven id/name lists, two bool flags), plus the optional
|
||||
# ``allowlist_authorization`` precedence flag the server adds for a component that
|
||||
# advertises ``search_visibility_allowlist_authorization``. Kept in lockstep with
|
||||
# the server's ``to_wire`` / ``VisibilityWire`` and :func:`_visibility_hidden_set`;
|
||||
# a new dimension is a new component capability, added on BOTH sides (see
|
||||
# :func:`_visibility_param_schema`). The union is pinned equal to the server
|
||||
# resolver's key set by the cross-seam contract test.
|
||||
_VISIBILITY_LIST_KEYS = (
|
||||
"exclude_categories",
|
||||
"deny_entity_ids",
|
||||
@@ -657,18 +673,25 @@ _VISIBILITY_LIST_KEYS = (
|
||||
"allow_areas",
|
||||
"allow_labels",
|
||||
)
|
||||
_VISIBILITY_BOOL_KEYS = ("exclude_hidden", "respect_assist_exposure")
|
||||
_VISIBILITY_BOOL_KEYS = (
|
||||
"exclude_hidden",
|
||||
"respect_assist_exposure",
|
||||
"allowlist_authorization",
|
||||
)
|
||||
|
||||
|
||||
def _visibility_param_schema() -> Any:
|
||||
"""Voluptuous schema for the ``search`` ``visibility`` dict — exactly the nine keys.
|
||||
"""Voluptuous schema for the ``search`` ``visibility`` dict — exactly the ten keys.
|
||||
|
||||
Enumerating the known dimensions (PREVENT_EXTRA is voluptuous' default for a
|
||||
nested ``Schema``) makes an unknown key a loud ``invalid_format`` command error
|
||||
rather than a silent drop. If a newer server emits a tenth dimension to this
|
||||
1.2.0 component, the server's error taxonomy converts that into a legacy fallback
|
||||
with the filter STILL correctly applied — structural fail-closed for free —
|
||||
instead of partial, unwarned filtering. Built at call time so it honors the
|
||||
The nine hide dimensions plus the ``allowlist_authorization`` precedence flag
|
||||
(sent only when this component advertises
|
||||
``search_visibility_allowlist_authorization``; absent means legacy
|
||||
precedence). Enumerating the known keys (PREVENT_EXTRA is voluptuous' default
|
||||
for a nested ``Schema``) makes an unknown key a loud ``invalid_format`` command
|
||||
error rather than a silent drop. If a newer server emits an eleventh key to
|
||||
this component, the server's error taxonomy converts that into a legacy
|
||||
fallback with the filter STILL correctly applied — structural fail-closed for
|
||||
free — instead of partial, unwarned filtering. Built at call time so it honors the
|
||||
monkeypatched ``vol`` in the unit suite.
|
||||
"""
|
||||
schema: dict[Any, Any] = {vol.Optional(key): [str] for key in _VISIBILITY_LIST_KEYS}
|
||||
@@ -693,15 +716,13 @@ def _search_schema() -> dict[Any, Any]:
|
||||
int, vol.Range(min=1, max=MAX_RESULTS)
|
||||
),
|
||||
vol.Optional("offset", default=0): vol.All(int, vol.Range(min=0)),
|
||||
# Opt-in entity-visibility filter (``search_visibility`` capability): the
|
||||
# server's raw VisibilityConfig hide dimensions. When present and
|
||||
# non-empty, hidden entities are excluded from the entity results before
|
||||
# counts/pagination, mirroring the legacy ``load_hidden_set`` hard-exclude
|
||||
# so ``ha_search`` can drop its "filter active -> legacy only" gate. Gated
|
||||
# by the CAPABILITIES flag, so an old component never receives it. The nine
|
||||
# known keys are enumerated so an unknown dimension fails loudly (the server
|
||||
# then falls back to legacy with the filter applied) — see
|
||||
# :func:`_visibility_param_schema`.
|
||||
# Opt-in entity visibility for component search. The component advertises
|
||||
# ``search_visibility`` and ``search_visibility_allowlist_authorization``;
|
||||
# the server reads those flags before sending these raw VisibilityConfig
|
||||
# dimensions, and adds ``allowlist_authorization`` only for the second flag.
|
||||
# The ten known keys are enumerated so an unknown one fails loudly and the
|
||||
# server falls back to its own filtered legacy path.
|
||||
# See ``_visibility_param_schema``.
|
||||
vol.Optional("visibility"): _visibility_param_schema(),
|
||||
}
|
||||
|
||||
@@ -1110,23 +1131,30 @@ def _do_search(
|
||||
# :func:`_visibility_hidden_set`.
|
||||
if isinstance(visibility, Mapping) and visibility:
|
||||
states_list = _iter_states(hass)
|
||||
# Probe Assist availability once (only when the config asks for it) so a
|
||||
# requested-but-unavailable Assist dimension both skips its hiding and
|
||||
# surfaces the resolver-parity degradation warning.
|
||||
assist_available = (
|
||||
_assist_exposure_available(hass)
|
||||
if visibility.get("respect_assist_exposure")
|
||||
else True
|
||||
inventory = _visibility_inventory(view, states_list, visibility)
|
||||
assist_applies = (
|
||||
bool(visibility.get("respect_assist_exposure"))
|
||||
and not inventory.allowlist.authorized
|
||||
)
|
||||
# Short-circuit the probe unless Assist applies. True on the skipped
|
||||
# path means there is no Assist degradation to report; the hidden-set
|
||||
# helper does not consult Assist while an authorizing allowlist is
|
||||
# active (a legacy wire keeps Assist in play, so the probe still runs).
|
||||
assist_available = not assist_applies or _assist_exposure_available(hass)
|
||||
hidden = _visibility_hidden_set(
|
||||
view,
|
||||
states_list,
|
||||
visibility,
|
||||
lambda eid: _assist_should_expose(hass, eid),
|
||||
assist_available=assist_available,
|
||||
inventory=inventory,
|
||||
)
|
||||
visibility_warnings = _visibility_warnings(
|
||||
view, states_list, visibility, assist_available=assist_available
|
||||
view,
|
||||
states_list,
|
||||
visibility,
|
||||
assist_available=assist_available,
|
||||
inventory=inventory,
|
||||
)
|
||||
if hidden:
|
||||
scored_entities = [
|
||||
@@ -2614,6 +2642,7 @@ def _all_entity_entries(view: _RegistryView) -> list[Any]:
|
||||
try:
|
||||
return list(entities.values())
|
||||
except Exception: # pragma: no cover - defensive
|
||||
_LOGGER.warning("entity registry enumeration failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
@@ -5001,6 +5030,67 @@ _ALLOWLIST_REGISTRY_EMPTY_WARNING = (
|
||||
)
|
||||
|
||||
|
||||
class _AllowlistState(NamedTuple):
|
||||
"""Effective restrict-mode activity, registry degradation, and precedence.
|
||||
|
||||
``authorized`` is ``active`` plus the ``allowlist_authorization`` wire flag:
|
||||
an allow match then authorizes the entity past the category, HA-hidden, and
|
||||
Assist filters. Without the flag the legacy conjunctive semantics apply even
|
||||
while ``active`` is True (an older server resolves that way itself).
|
||||
"""
|
||||
|
||||
active: bool
|
||||
degraded: bool
|
||||
authorized: bool
|
||||
|
||||
|
||||
class _VisibilityInventory(NamedTuple):
|
||||
"""Registry/state indexes and allowlist state shared by one search."""
|
||||
|
||||
registry_by_id: dict[str, Any]
|
||||
state_ids: set[str]
|
||||
allowlist: _AllowlistState
|
||||
|
||||
|
||||
def _visibility_allowlist_state(
|
||||
registry_by_id: Mapping[str, Any],
|
||||
state_ids: set[str],
|
||||
visibility: Mapping[str, Any],
|
||||
) -> _AllowlistState:
|
||||
"""Resolve allowlist activity from one precomputed search inventory.
|
||||
|
||||
``degraded`` deliberately remains true when an explicit ``allow_entity_ids``
|
||||
list keeps restrict mode active: only the registry-derived allow dimensions
|
||||
degraded, so callers still warn while the explicit ID allowlist remains in
|
||||
force. This differs from the resolver's pre-fetch predicate, which asks only
|
||||
whether degradation would make Assist relevant again.
|
||||
|
||||
``authorized`` additionally requires the ``allowlist_authorization`` wire flag:
|
||||
an active allowlist alone keeps the legacy conjunctive precedence.
|
||||
"""
|
||||
entity_ids_active = bool(visibility.get("allow_entity_ids"))
|
||||
registry_dimensions_active = bool(
|
||||
visibility.get("allow_areas") or visibility.get("allow_labels")
|
||||
)
|
||||
degraded = registry_dimensions_active and not registry_by_id and bool(state_ids)
|
||||
active = entity_ids_active or (registry_dimensions_active and not degraded)
|
||||
authorized = active and bool(visibility.get("allowlist_authorization"))
|
||||
return _AllowlistState(active, degraded, authorized)
|
||||
|
||||
|
||||
def _visibility_inventory(
|
||||
view: _RegistryView, states: Any, visibility: Mapping[str, Any]
|
||||
) -> _VisibilityInventory:
|
||||
"""Build the registry/state inventory once for one visibility computation."""
|
||||
registry_by_id = _registry_index_by_id(view)
|
||||
state_ids = _state_entity_ids(states)
|
||||
return _VisibilityInventory(
|
||||
registry_by_id,
|
||||
state_ids,
|
||||
_visibility_allowlist_state(registry_by_id, state_ids, visibility),
|
||||
)
|
||||
|
||||
|
||||
def _unknown_categories_warning(unknown_categories: set[str]) -> str:
|
||||
"""The resolver's unknown-``exclude_categories`` warning text (byte-identical)."""
|
||||
return (
|
||||
@@ -5016,14 +5106,17 @@ def _visibility_hidden_set(
|
||||
should_expose_fn: Any,
|
||||
*,
|
||||
assist_available: bool = True,
|
||||
inventory: _VisibilityInventory | None = None,
|
||||
) -> set[str]:
|
||||
"""Compute the opt-in hidden entity_id set, mirroring the server's resolver.
|
||||
|
||||
A pure replication of ``visibility.resolver.hidden_entity_ids`` over the live
|
||||
``_RegistryView`` + ``states`` (rather than the WS ``{success, result}``
|
||||
payloads the server passes): a conjunction of independent hide dimensions —
|
||||
the deny list, the category / hidden / area / label excludes (area/labels are
|
||||
device-inherited), the allow-list restrict mode, and the Assist dimension. The
|
||||
payloads the server passes), including its precedence: concrete deny and
|
||||
area/label excludes win; an allowlist match authorizes past the broad category,
|
||||
Home Assistant hidden-state, and Assist filters when the wire opted into that
|
||||
revised precedence with ``allowlist_authorization`` (without the flag those
|
||||
filters still apply to a matched entity, matching the older server). The
|
||||
Assist dimension delegates to the injectable ``should_expose_fn(entity_id) ->
|
||||
bool`` (:func:`_assist_should_expose` in production — a READ-ONLY reconstruction
|
||||
of core's ``async_should_expose`` from the explicit exposure map + expose_new +
|
||||
@@ -5033,11 +5126,11 @@ def _visibility_hidden_set(
|
||||
computed defaults back, which this fast path must not do (see
|
||||
:func:`_assist_should_expose`).
|
||||
|
||||
``should_expose_fn`` is consulted only when ``respect_assist_exposure`` is set
|
||||
AND ``assist_available`` is True. When the config requests the Assist dimension
|
||||
but the exposure machinery is unavailable (``assist_available=False``), the
|
||||
dimension is SKIPPED — hiding nothing by Assist — mirroring the resolver's
|
||||
"skip the dimension when its data is unavailable" fail-open (the paired
|
||||
``should_expose_fn`` is consulted only when ``respect_assist_exposure`` is set,
|
||||
no AUTHORIZING allowlist is active, and ``assist_available`` is True. When the config
|
||||
requests the Assist dimension but the exposure machinery is unavailable
|
||||
(``assist_available=False``), the dimension is SKIPPED — hiding nothing by
|
||||
Assist — mirroring the resolver's fail-open behavior (the paired
|
||||
degradation warning is surfaced by :func:`_visibility_warnings`). Kept a
|
||||
standalone pure function so it is unit-testable without the full search
|
||||
pipeline.
|
||||
@@ -5052,29 +5145,25 @@ def _visibility_hidden_set(
|
||||
allow_areas = set(visibility.get("allow_areas") or [])
|
||||
allow_labels = set(visibility.get("allow_labels") or [])
|
||||
respect_assist = bool(visibility.get("respect_assist_exposure"))
|
||||
# ``assist_active`` gates the per-entity Assist should_expose sub-check (skipped
|
||||
# when the config asks for Assist but its data is unavailable). The allow/assist
|
||||
# loop below is guarded by ``allow_active or respect_assist`` — a structural
|
||||
# mirror of the resolver's guard, not a functional requirement: when Assist is
|
||||
# requested-but-unavailable and no allowlist is set, the loop runs but hides
|
||||
# nothing (both sub-checks are inert). Kept identical so the two paths match.
|
||||
assist_active = respect_assist and assist_available
|
||||
allow_active = bool(allow_areas or allow_labels or allow_entity_ids)
|
||||
|
||||
registry_by_id = _registry_index_by_id(view)
|
||||
if inventory is None:
|
||||
inventory = _visibility_inventory(view, states, visibility)
|
||||
registry_by_id = inventory.registry_by_id
|
||||
# states-only entity universe (YAML/template entities absent from the registry
|
||||
# that the allow / Assist dimensions must still be able to hide).
|
||||
state_ids = {
|
||||
eid for eid in (getattr(s, "entity_id", None) for s in states or []) if eid
|
||||
}
|
||||
|
||||
# Fail-open guard (mirrors the resolver): an area/label allowlist needs registry
|
||||
# data to match; if the registry is empty but there are states-only candidates,
|
||||
# restrict mode would blank everything, so drop those allow dimensions.
|
||||
if (allow_areas or allow_labels) and not registry_by_id and state_ids:
|
||||
state_ids = inventory.state_ids
|
||||
allow_active = inventory.allowlist.active
|
||||
authorized = inventory.allowlist.authorized
|
||||
# Fail-open guard: registry-derived allow dimensions cannot match when the
|
||||
# registry is empty but states-only candidates exist.
|
||||
if inventory.allowlist.degraded:
|
||||
allow_areas = set()
|
||||
allow_labels = set()
|
||||
allow_active = bool(allow_entity_ids)
|
||||
|
||||
# An authorizing allowlist skips the broad Assist filter; a legacy wire keeps
|
||||
# it. Only a full degradation (registry-derived dimensions dropped and no
|
||||
# allow_entity_ids left) makes ``active`` False and re-enables Assist.
|
||||
assist_active = respect_assist and assist_available and not authorized
|
||||
|
||||
hidden: set[str] = set(denied)
|
||||
_apply_visibility_excludes(
|
||||
@@ -5086,8 +5175,9 @@ def _visibility_hidden_set(
|
||||
exclude_areas,
|
||||
exclude_labels,
|
||||
hidden,
|
||||
automatic_excludes_active=not authorized,
|
||||
)
|
||||
if allow_active or respect_assist:
|
||||
if allow_active or assist_active:
|
||||
_apply_visibility_allow_assist(
|
||||
view,
|
||||
registry_by_id,
|
||||
@@ -5099,6 +5189,7 @@ def _visibility_hidden_set(
|
||||
assist_active,
|
||||
should_expose_fn,
|
||||
hidden,
|
||||
allow_authorizes=authorized,
|
||||
)
|
||||
return hidden
|
||||
|
||||
@@ -5109,6 +5200,7 @@ def _visibility_warnings(
|
||||
visibility: Mapping[str, Any],
|
||||
*,
|
||||
assist_available: bool = True,
|
||||
inventory: _VisibilityInventory | None = None,
|
||||
) -> list[str]:
|
||||
"""Degradation warnings for a visibility computation, mirroring the resolver.
|
||||
|
||||
@@ -5133,19 +5225,16 @@ def _visibility_warnings(
|
||||
if unknown:
|
||||
warnings.append(_unknown_categories_warning(unknown))
|
||||
|
||||
allow_areas = set(visibility.get("allow_areas") or [])
|
||||
allow_labels = set(visibility.get("allow_labels") or [])
|
||||
registry_by_id = _registry_index_by_id(view)
|
||||
state_ids = {
|
||||
eid for eid in (getattr(s, "entity_id", None) for s in states or []) if eid
|
||||
}
|
||||
# Empty-registry allowlist fail-open: same guard as _visibility_hidden_set —
|
||||
# only fires when there are states-only candidates the restrict mode would
|
||||
# otherwise blank.
|
||||
if (allow_areas or allow_labels) and not registry_by_id and state_ids:
|
||||
if inventory is None:
|
||||
inventory = _visibility_inventory(view, states, visibility)
|
||||
if inventory.allowlist.degraded:
|
||||
warnings.append(_ALLOWLIST_REGISTRY_EMPTY_WARNING)
|
||||
|
||||
if bool(visibility.get("respect_assist_exposure")) and not assist_available:
|
||||
if (
|
||||
visibility.get("respect_assist_exposure")
|
||||
and not inventory.allowlist.authorized
|
||||
and not assist_available
|
||||
):
|
||||
warnings.append(_ASSIST_UNAVAILABLE_WARNING)
|
||||
|
||||
return warnings
|
||||
@@ -5161,6 +5250,15 @@ def _registry_index_by_id(view: _RegistryView) -> dict[str, Any]:
|
||||
return index
|
||||
|
||||
|
||||
def _state_entity_ids(states: Any) -> set[str]:
|
||||
"""Entity IDs present in the live state-machine snapshot."""
|
||||
return {
|
||||
eid
|
||||
for eid in (getattr(state, "entity_id", None) for state in states or [])
|
||||
if eid
|
||||
}
|
||||
|
||||
|
||||
def _apply_visibility_excludes(
|
||||
view: _RegistryView,
|
||||
registry_by_id: dict[str, Any],
|
||||
@@ -5170,22 +5268,27 @@ def _apply_visibility_excludes(
|
||||
exclude_areas: set[str],
|
||||
exclude_labels: set[str],
|
||||
hidden: set[str],
|
||||
*,
|
||||
automatic_excludes_active: bool,
|
||||
) -> None:
|
||||
"""Add the exclude-dimension hits to ``hidden`` (registry-derived, deny-first).
|
||||
"""Add automatic and hard exclude hits to ``hidden``.
|
||||
|
||||
Mirrors the resolver's exclude loop. The empty-set dimensions are inert (``x in
|
||||
set()`` / ``set() & x`` are falsy), so an inactive dimension hides nothing
|
||||
without a guard; only ``exclude_hidden`` is a bool flag and keeps its guard.
|
||||
Category/HA-hidden filters are skipped for an AUTHORIZING allowlist; concrete
|
||||
area/label exclusions remain hard conflicts and always apply. The empty-set
|
||||
dimensions are inert (``x in set()`` / ``set() & x`` are falsy), so an inactive
|
||||
dimension hides nothing without a guard; only ``exclude_hidden`` is a bool flag
|
||||
and keeps its guard.
|
||||
"""
|
||||
for eid, entry in registry_by_id.items():
|
||||
if eid in denied:
|
||||
continue
|
||||
if _enum_value(getattr(entry, "entity_category", None)) in categories:
|
||||
hidden.add(eid)
|
||||
continue
|
||||
if exclude_hidden and getattr(entry, "hidden_by", None) is not None:
|
||||
hidden.add(eid)
|
||||
continue
|
||||
if automatic_excludes_active:
|
||||
if _enum_value(getattr(entry, "entity_category", None)) in categories:
|
||||
hidden.add(eid)
|
||||
continue
|
||||
if exclude_hidden and getattr(entry, "hidden_by", None) is not None:
|
||||
hidden.add(eid)
|
||||
continue
|
||||
if _effective_area_for_entry(view, entry) in exclude_areas:
|
||||
hidden.add(eid)
|
||||
continue
|
||||
@@ -5204,24 +5307,28 @@ def _apply_visibility_allow_assist(
|
||||
assist_active: bool,
|
||||
should_expose_fn: Any,
|
||||
hidden: set[str],
|
||||
*,
|
||||
allow_authorizes: bool,
|
||||
) -> None:
|
||||
"""Add allow-restrict + Assist hits to ``hidden`` over registry + states.
|
||||
|
||||
These conjunctive filters must reach states-only entities, so they iterate the
|
||||
full candidate universe. Mirrors the resolver's allow/assist loop. ``should_expose_fn``
|
||||
is consulted only when ``assist_active`` (Assist requested AND its data
|
||||
available); an unavailable-Assist request degrades to no Assist hiding here,
|
||||
with the warning surfaced separately.
|
||||
Both filters reach states-only entities. A nonmatching entity is always hidden
|
||||
while the allowlist is active. With ``allow_authorizes`` a match skips the
|
||||
Assist check; on a legacy wire a matched entity still faces it, so
|
||||
``should_expose_fn`` is consulted whenever Assist is requested and available.
|
||||
"""
|
||||
for eid in registry_by_id.keys() | state_ids:
|
||||
if eid in hidden:
|
||||
continue
|
||||
entry = registry_by_id.get(eid)
|
||||
if allow_active and not _entity_allowed(
|
||||
view, eid, entry, allow_entity_ids, allow_areas, allow_labels
|
||||
):
|
||||
hidden.add(eid)
|
||||
continue
|
||||
if allow_active:
|
||||
if not _entity_allowed(
|
||||
view, eid, entry, allow_entity_ids, allow_areas, allow_labels
|
||||
):
|
||||
hidden.add(eid)
|
||||
continue
|
||||
if allow_authorizes:
|
||||
continue
|
||||
if assist_active and not should_expose_fn(eid):
|
||||
hidden.add(eid)
|
||||
|
||||
@@ -5391,6 +5498,7 @@ def _assist_exposure_available(hass: HomeAssistant) -> bool:
|
||||
DATA_EXPOSED_ENTITIES,
|
||||
)
|
||||
except Exception:
|
||||
_LOGGER.warning("Assist exposure support import failed", exc_info=True)
|
||||
return False
|
||||
data = getattr(hass, "data", None)
|
||||
return isinstance(data, Mapping) and DATA_EXPOSED_ENTITIES in data
|
||||
|
||||
Reference in New Issue
Block a user