mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
Avoid mutating registry when rendering assist_exposed_entities
async_should_expose persists a derived should_expose value into the entity registry (or the legacy store) whenever no explicit setting exists, firing registry-updated events and scheduling a save. Calling it for every entity while rendering a template meant the first render could materialize defaults for potentially every entity, so template rendering was not side-effect free. Add a read-only async_get_should_expose that returns the same result (including the expose-new-entities default) without persisting it, and use it from the template extension. The default computation is factored into a shared helper so the caching and read-only paths stay in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -253,10 +253,9 @@ class ExposedEntities:
|
||||
should_expose = registry_entry.options[assistant]["should_expose"]
|
||||
return should_expose
|
||||
|
||||
if self.async_get_expose_new_entities(assistant):
|
||||
should_expose = self._is_default_exposed(entity_id, registry_entry)
|
||||
else:
|
||||
should_expose = False
|
||||
should_expose = self._async_compute_default_should_expose(
|
||||
assistant, entity_id, registry_entry
|
||||
)
|
||||
|
||||
assistant_options: ReadOnlyDict[str, Any] | dict[str, Any]
|
||||
assistant_options = registry_entry.options.get(assistant, {})
|
||||
@@ -267,6 +266,27 @@ class ExposedEntities:
|
||||
|
||||
return should_expose
|
||||
|
||||
@callback
|
||||
def async_get_should_expose(self, assistant: str, entity_id: str) -> bool:
|
||||
"""Return True if an entity should be exposed, without persisting defaults."""
|
||||
should_expose: bool
|
||||
|
||||
entity_registry = er.async_get(self._hass)
|
||||
if registry_entry := entity_registry.async_get(entity_id):
|
||||
options: Mapping[str, Any] = registry_entry.options.get(assistant, {})
|
||||
if "should_expose" in options:
|
||||
should_expose = options["should_expose"]
|
||||
return should_expose
|
||||
return self._async_compute_default_should_expose(
|
||||
assistant, entity_id, registry_entry
|
||||
)
|
||||
if (
|
||||
exposed_entity := self.entities.get(entity_id)
|
||||
) and "should_expose" in exposed_entity.assistants.get(assistant, {}):
|
||||
should_expose = exposed_entity.assistants[assistant]["should_expose"]
|
||||
return should_expose
|
||||
return self._async_compute_default_should_expose(assistant, entity_id, None)
|
||||
|
||||
def _async_should_expose_legacy_entity(
|
||||
self, assistant: str, entity_id: str
|
||||
) -> bool:
|
||||
@@ -280,10 +300,9 @@ class ExposedEntities:
|
||||
should_expose = exposed_entity.assistants[assistant]["should_expose"]
|
||||
return should_expose
|
||||
|
||||
if self.async_get_expose_new_entities(assistant):
|
||||
should_expose = self._is_default_exposed(entity_id, None)
|
||||
else:
|
||||
should_expose = False
|
||||
should_expose = self._async_compute_default_should_expose(
|
||||
assistant, entity_id, None
|
||||
)
|
||||
|
||||
if exposed_entity:
|
||||
new_exposed_entity = self._update_exposed_entity(
|
||||
@@ -298,6 +317,15 @@ class ExposedEntities:
|
||||
|
||||
return should_expose
|
||||
|
||||
@callback
|
||||
def _async_compute_default_should_expose(
|
||||
self, assistant: str, entity_id: str, registry_entry: er.RegistryEntry | None
|
||||
) -> bool:
|
||||
"""Compute default exposure for an entity without persisting it."""
|
||||
if self.async_get_expose_new_entities(assistant):
|
||||
return self._is_default_exposed(entity_id, registry_entry)
|
||||
return False
|
||||
|
||||
def _is_default_exposed(
|
||||
self, entity_id: str, registry_entry: er.RegistryEntry | None
|
||||
) -> bool:
|
||||
@@ -518,6 +546,15 @@ def async_should_expose(hass: HomeAssistant, assistant: str, entity_id: str) ->
|
||||
return exposed_entities.async_should_expose(assistant, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_should_expose(
|
||||
hass: HomeAssistant, assistant: str, entity_id: str
|
||||
) -> bool:
|
||||
"""Return True if an entity should be exposed, without persisting defaults."""
|
||||
exposed_entities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
return exposed_entities.async_get_should_expose(assistant, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_set_assistant_option(
|
||||
hass: HomeAssistant, assistant: str, entity_id: str, option: str, value: Any
|
||||
|
||||
@@ -35,7 +35,7 @@ class ExposedEntitiesExtension(BaseTemplateExtension):
|
||||
# extension is imported very early during bootstrap.
|
||||
from homeassistant.components.homeassistant.exposed_entities import ( # noqa: PLC0415
|
||||
DATA_EXPOSED_ENTITIES,
|
||||
async_should_expose,
|
||||
async_get_should_expose,
|
||||
)
|
||||
|
||||
exposed_entities = self.hass.data[DATA_EXPOSED_ENTITIES]
|
||||
@@ -47,5 +47,5 @@ class ExposedEntitiesExtension(BaseTemplateExtension):
|
||||
for entity_id in dict.fromkeys(
|
||||
chain(exposed_entities.entities, entity_registry.entities)
|
||||
)
|
||||
if async_should_expose(self.hass, "conversation", entity_id)
|
||||
if async_get_should_expose(self.hass, "conversation", entity_id)
|
||||
]
|
||||
|
||||
@@ -58,6 +58,24 @@ async def test_assist_exposed_entities_only_conversation(
|
||||
assert info.rate_limit is None
|
||||
|
||||
|
||||
async def test_assist_exposed_entities_default_exposure_is_read_only(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test entities exposed only by default are returned without mutating state."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
# conversation exposes new entities by default and "light" is a default
|
||||
# exposed domain, so this entity is exposed without any explicit setting.
|
||||
entry = entity_registry.async_get_or_create("light", "test", "entity1")
|
||||
|
||||
info = render_to_info(hass, "{{ assist_exposed_entities() }}")
|
||||
assert_result_info(info, [entry.entity_id])
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Rendering must not persist the derived should_expose value.
|
||||
assert "conversation" not in entity_registry.async_get(entry.entity_id).options
|
||||
|
||||
|
||||
async def test_assist_exposed_entities_not_in_registry(hass: HomeAssistant) -> None:
|
||||
"""Test assist_exposed_entities includes entities not in the registry."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
Reference in New Issue
Block a user