Don't use storage collection helper in ExposedEntities (#92396)

* Don't use storage collection helper in ExposedEntities

* Fix tests
This commit is contained in:
Erik Montnemery 2023-05-03 12:39:22 +02:00 committed by GitHub
parent 7aa94f97c0
commit 4860a8d1e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 248 additions and 299 deletions

View file

@ -84,7 +84,8 @@ class AbstractConfig(ABC):
unsub_func() unsub_func()
self._unsub_proactive_report = None self._unsub_proactive_report = None
async def should_expose(self, entity_id): @callback
def should_expose(self, entity_id):
"""If an entity should be exposed.""" """If an entity should be exposed."""
return False return False

View file

@ -103,7 +103,7 @@ async def async_api_discovery(
discovery_endpoints = [ discovery_endpoints = [
alexa_entity.serialize_discovery() alexa_entity.serialize_discovery()
for alexa_entity in async_get_entities(hass, config) for alexa_entity in async_get_entities(hass, config)
if await config.should_expose(alexa_entity.entity_id) if config.should_expose(alexa_entity.entity_id)
] ]
return directive.response( return directive.response(

View file

@ -30,7 +30,7 @@ class AlexaDirective:
self.entity = self.entity_id = self.endpoint = self.instance = None self.entity = self.entity_id = self.endpoint = self.instance = None
async def load_entity(self, hass, config): def load_entity(self, hass, config):
"""Set attributes related to the entity for this request. """Set attributes related to the entity for this request.
Sets these attributes when self.has_endpoint is True: Sets these attributes when self.has_endpoint is True:
@ -49,7 +49,7 @@ class AlexaDirective:
self.entity_id = _endpoint_id.replace("#", ".") self.entity_id = _endpoint_id.replace("#", ".")
self.entity = hass.states.get(self.entity_id) self.entity = hass.states.get(self.entity_id)
if not self.entity or not await config.should_expose(self.entity_id): if not self.entity or not config.should_expose(self.entity_id):
raise AlexaInvalidEndpointError(_endpoint_id) raise AlexaInvalidEndpointError(_endpoint_id)
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity)

View file

@ -34,7 +34,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
await config.set_authorized(True) await config.set_authorized(True)
if directive.has_endpoint: if directive.has_endpoint:
await directive.load_entity(hass, config) directive.load_entity(hass, config)
funct_ref = HANDLERS.get((directive.namespace, directive.name)) funct_ref = HANDLERS.get((directive.namespace, directive.name))
if funct_ref: if funct_ref:

View file

@ -60,7 +60,8 @@ class AlexaConfig(AbstractConfig):
"""Return an identifier for the user that represents this config.""" """Return an identifier for the user that represents this config."""
return "" return ""
async def should_expose(self, entity_id): @core.callback
def should_expose(self, entity_id):
"""If an entity should be exposed.""" """If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter: if not self._config[CONF_FILTER].empty_filter:
return self._config[CONF_FILTER](entity_id) return self._config[CONF_FILTER](entity_id)

View file

@ -64,7 +64,7 @@ async def async_enable_proactive_mode(hass, smart_home_config):
if new_state.domain not in ENTITY_ADAPTERS: if new_state.domain not in ENTITY_ADAPTERS:
return return
if not await smart_home_config.should_expose(changed_entity): if not smart_home_config.should_expose(changed_entity):
_LOGGER.debug("Not exposing %s because filtered by config", changed_entity) _LOGGER.debug("Not exposing %s because filtered by config", changed_entity)
return return

View file

@ -257,14 +257,15 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
and entity_supported(self.hass, entity_id) and entity_supported(self.hass, entity_id)
) )
async def should_expose(self, entity_id): @callback
def should_expose(self, entity_id):
"""If an entity should be exposed.""" """If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter: if not self._config[CONF_FILTER].empty_filter:
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False return False
return self._config[CONF_FILTER](entity_id) return self._config[CONF_FILTER](entity_id)
return await async_should_expose(self.hass, CLOUD_ALEXA, entity_id) return async_should_expose(self.hass, CLOUD_ALEXA, entity_id)
@callback @callback
def async_invalidate_access_token(self): def async_invalidate_access_token(self):
@ -423,7 +424,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
is_enabled = self.enabled is_enabled = self.enabled
for entity in alexa_entities.async_get_entities(self.hass, self): for entity in alexa_entities.async_get_entities(self.hass, self):
if is_enabled and await self.should_expose(entity.entity_id): if is_enabled and self.should_expose(entity.entity_id):
to_update.append(entity.entity_id) to_update.append(entity.entity_id)
else: else:
to_remove.append(entity.entity_id) to_remove.append(entity.entity_id)
@ -482,7 +483,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
entity_id = event.data["entity_id"] entity_id = event.data["entity_id"]
if not await self.should_expose(entity_id): if not self.should_expose(entity_id):
return return
action = event.data["action"] action = event.data["action"]

View file

@ -222,9 +222,9 @@ class CloudGoogleConfig(AbstractConfig):
self._handle_device_registry_updated, self._handle_device_registry_updated,
) )
async def should_expose(self, state): def should_expose(self, state):
"""If a state object should be exposed.""" """If a state object should be exposed."""
return await self._should_expose_entity_id(state.entity_id) return self._should_expose_entity_id(state.entity_id)
def _should_expose_legacy(self, entity_id): def _should_expose_legacy(self, entity_id):
"""If an entity ID should be exposed.""" """If an entity ID should be exposed."""
@ -258,14 +258,14 @@ class CloudGoogleConfig(AbstractConfig):
and _supported_legacy(self.hass, entity_id) and _supported_legacy(self.hass, entity_id)
) )
async def _should_expose_entity_id(self, entity_id): def _should_expose_entity_id(self, entity_id):
"""If an entity should be exposed.""" """If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter: if not self._config[CONF_FILTER].empty_filter:
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False return False
return self._config[CONF_FILTER](entity_id) return self._config[CONF_FILTER](entity_id)
return await async_should_expose(self.hass, CLOUD_GOOGLE, entity_id) return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
@property @property
def agent_user_id(self): def agent_user_id(self):
@ -358,7 +358,8 @@ class CloudGoogleConfig(AbstractConfig):
"""Handle updated preferences.""" """Handle updated preferences."""
self.async_schedule_google_sync_all() self.async_schedule_google_sync_all()
async def _handle_entity_registry_updated(self, event: Event) -> None: @callback
def _handle_entity_registry_updated(self, event: Event) -> None:
"""Handle when entity registry updated.""" """Handle when entity registry updated."""
if ( if (
not self.enabled not self.enabled
@ -375,11 +376,12 @@ class CloudGoogleConfig(AbstractConfig):
entity_id = event.data["entity_id"] entity_id = event.data["entity_id"]
if not await self._should_expose_entity_id(entity_id): if not self._should_expose_entity_id(entity_id):
return return
self.async_schedule_google_sync_all() self.async_schedule_google_sync_all()
@callback
async def _handle_device_registry_updated(self, event: Event) -> None: async def _handle_device_registry_updated(self, event: Event) -> None:
"""Handle when device registry updated.""" """Handle when device registry updated."""
if ( if (
@ -394,15 +396,13 @@ class CloudGoogleConfig(AbstractConfig):
return return
# Check if any exposed entity uses the device area # Check if any exposed entity uses the device area
used = False if not any(
entity_entry.area_id is None
and self._should_expose_entity_id(entity_entry.entity_id)
for entity_entry in er.async_entries_for_device( for entity_entry in er.async_entries_for_device(
er.async_get(self.hass), event.data["device_id"] er.async_get(self.hass), event.data["device_id"]
)
): ):
if entity_entry.area_id is None and await self._should_expose_entity_id(
entity_entry.entity_id
):
used = True
if not used:
return return
self.async_schedule_google_sync_all() self.async_schedule_google_sync_all()

View file

@ -94,7 +94,7 @@ CONFIG_SCHEMA = vol.Schema(
def _get_agent_manager(hass: HomeAssistant) -> AgentManager: def _get_agent_manager(hass: HomeAssistant) -> AgentManager:
"""Get the active agent.""" """Get the active agent."""
manager = AgentManager(hass) manager = AgentManager(hass)
hass.async_create_task(manager.async_setup()) manager.async_setup()
return manager return manager
@ -393,9 +393,9 @@ class AgentManager:
self._agents: dict[str, AbstractConversationAgent] = {} self._agents: dict[str, AbstractConversationAgent] = {}
self._builtin_agent_init_lock = asyncio.Lock() self._builtin_agent_init_lock = asyncio.Lock()
async def async_setup(self) -> None: def async_setup(self) -> None:
"""Set up the conversation agents.""" """Set up the conversation agents."""
await async_setup_default_agent(self.hass) async_setup_default_agent(self.hass)
async def async_get_agent( async def async_get_agent(
self, agent_id: str | None = None self, agent_id: str | None = None

View file

@ -73,20 +73,23 @@ def _get_language_variations(language: str) -> Iterable[str]:
yield lang yield lang
async def async_setup(hass: core.HomeAssistant) -> None: @core.callback
def async_setup(hass: core.HomeAssistant) -> None:
"""Set up entity registry listener for the default agent.""" """Set up entity registry listener for the default agent."""
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for entity_id in entity_registry.entities: for entity_id in entity_registry.entities:
await async_should_expose(hass, DOMAIN, entity_id) async_should_expose(hass, DOMAIN, entity_id)
async def async_handle_entity_registry_changed(event: core.Event) -> None: @core.callback
def async_handle_entity_registry_changed(event: core.Event) -> None:
"""Set expose flag on newly created entities.""" """Set expose flag on newly created entities."""
if event.data["action"] == "create": if event.data["action"] == "create":
await async_should_expose(hass, DOMAIN, event.data["entity_id"]) async_should_expose(hass, DOMAIN, event.data["entity_id"])
hass.bus.async_listen( hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED, er.EVENT_ENTITY_REGISTRY_UPDATED,
async_handle_entity_registry_changed, async_handle_entity_registry_changed,
run_immediately=True,
) )
@ -154,7 +157,7 @@ class DefaultAgent(AbstractConversationAgent):
conversation_id, conversation_id,
) )
slot_lists = await self._make_slot_lists() slot_lists = self._make_slot_lists()
result = await self.hass.async_add_executor_job( result = await self.hass.async_add_executor_job(
self._recognize, self._recognize,
@ -483,7 +486,7 @@ class DefaultAgent(AbstractConversationAgent):
"""Handle updated preferences.""" """Handle updated preferences."""
self._slot_lists = None self._slot_lists = None
async def _make_slot_lists(self) -> dict[str, SlotList]: def _make_slot_lists(self) -> dict[str, SlotList]:
"""Create slot lists with areas and entity names/aliases.""" """Create slot lists with areas and entity names/aliases."""
if self._slot_lists is not None: if self._slot_lists is not None:
return self._slot_lists return self._slot_lists
@ -493,7 +496,7 @@ class DefaultAgent(AbstractConversationAgent):
entities = [ entities = [
entity entity
for entity in entity_registry.entities.values() for entity in entity_registry.entities.values()
if await async_should_expose(self.hass, DOMAIN, entity.entity_id) if async_should_expose(self.hass, DOMAIN, entity.entity_id)
] ]
devices = dr.async_get(self.hass) devices = dr.async_get(self.hass)

View file

@ -175,7 +175,7 @@ class AbstractConfig(ABC):
"""Get agent user ID from context.""" """Get agent user ID from context."""
@abstractmethod @abstractmethod
async def should_expose(self, state) -> bool: def should_expose(self, state) -> bool:
"""Return if entity should be exposed.""" """Return if entity should be exposed."""
def should_2fa(self, state): def should_2fa(self, state):
@ -535,14 +535,16 @@ class GoogleEntity:
] ]
return self._traits return self._traits
async def should_expose(self): @callback
def should_expose(self):
"""If entity should be exposed.""" """If entity should be exposed."""
return await self.config.should_expose(self.state) return self.config.should_expose(self.state)
async def should_expose_local(self) -> bool: @callback
def should_expose_local(self) -> bool:
"""Return if the entity should be exposed locally.""" """Return if the entity should be exposed locally."""
return ( return (
await self.should_expose() self.should_expose()
and get_google_type( and get_google_type(
self.state.domain, self.state.attributes.get(ATTR_DEVICE_CLASS) self.state.domain, self.state.attributes.get(ATTR_DEVICE_CLASS)
) )
@ -585,7 +587,7 @@ class GoogleEntity:
trait.might_2fa(domain, features, device_class) for trait in self.traits() trait.might_2fa(domain, features, device_class) for trait in self.traits()
) )
async def sync_serialize(self, agent_user_id, instance_uuid): def sync_serialize(self, agent_user_id, instance_uuid):
"""Serialize entity for a SYNC response. """Serialize entity for a SYNC response.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync https://developers.google.com/actions/smarthome/create-app#actiondevicessync
@ -621,7 +623,7 @@ class GoogleEntity:
device["name"]["nicknames"].extend(entity_entry.aliases) device["name"]["nicknames"].extend(entity_entry.aliases)
# Add local SDK info if enabled # Add local SDK info if enabled
if self.config.is_local_sdk_active and await self.should_expose_local(): if self.config.is_local_sdk_active and self.should_expose_local():
device["otherDeviceIds"] = [{"deviceId": self.entity_id}] device["otherDeviceIds"] = [{"deviceId": self.entity_id}]
device["customData"] = { device["customData"] = {
"webhookId": self.config.get_local_webhook_id(agent_user_id), "webhookId": self.config.get_local_webhook_id(agent_user_id),

View file

@ -111,7 +111,7 @@ class GoogleConfig(AbstractConfig):
"""Return if states should be proactively reported.""" """Return if states should be proactively reported."""
return self._config.get(CONF_REPORT_STATE) return self._config.get(CONF_REPORT_STATE)
async def should_expose(self, state) -> bool: def should_expose(self, state) -> bool:
"""Return if entity should be exposed.""" """Return if entity should be exposed."""
expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT)
exposed_domains = self._config.get(CONF_EXPOSED_DOMAINS) exposed_domains = self._config.get(CONF_EXPOSED_DOMAINS)

View file

@ -63,7 +63,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig
if not new_state: if not new_state:
return return
if not await google_config.should_expose(new_state): if not google_config.should_expose(new_state):
return return
entity = GoogleEntity(hass, google_config, new_state) entity = GoogleEntity(hass, google_config, new_state)
@ -115,7 +115,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig
checker = await create_checker(hass, DOMAIN, extra_significant_check) checker = await create_checker(hass, DOMAIN, extra_significant_check)
for entity in async_get_entities(hass, google_config): for entity in async_get_entities(hass, google_config):
if not await entity.should_expose(): if not entity.should_expose():
continue continue
try: try:

View file

@ -87,11 +87,11 @@ async def async_devices_sync_response(hass, config, agent_user_id):
devices = [] devices = []
for entity in entities: for entity in entities:
if not await entity.should_expose(): if not entity.should_expose():
continue continue
try: try:
devices.append(await entity.sync_serialize(agent_user_id, instance_uuid)) devices.append(entity.sync_serialize(agent_user_id, instance_uuid))
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error serializing %s", entity.entity_id) _LOGGER.exception("Error serializing %s", entity.entity_id)
@ -318,7 +318,7 @@ async def async_devices_reachable(
"devices": [ "devices": [
entity.reachable_device_serialize() entity.reachable_device_serialize()
for entity in async_get_entities(hass, data.config) for entity in async_get_entities(hass, data.config)
if entity.entity_id in google_ids and await entity.should_expose_local() if entity.entity_id in google_ids and entity.should_expose_local()
] ]
} }

View file

@ -343,7 +343,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no
) )
exposed_entities = ExposedEntities(hass) exposed_entities = ExposedEntities(hass)
await exposed_entities.async_load() await exposed_entities.async_initialize()
hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities
return True return True

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
import dataclasses import dataclasses
from itertools import chain from itertools import chain
from typing import Any from typing import Any, TypedDict
import voluptuous as vol import voluptuous as vol
@ -15,11 +15,6 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.collection import (
IDManager,
SerializedStorageCollection,
StorageCollection,
)
from homeassistant.helpers.entity import get_device_class from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
@ -89,30 +84,21 @@ class ExposedEntity:
assistants: dict[str, dict[str, Any]] assistants: dict[str, dict[str, Any]]
def to_json(self, entity_id: str) -> dict[str, Any]: def to_json(self) -> dict[str, Any]:
"""Return a JSON serializable representation for storage.""" """Return a JSON serializable representation for storage."""
return { return {
"assistants": self.assistants, "assistants": self.assistants,
"id": entity_id,
} }
class SerializedExposedEntities(SerializedStorageCollection): class SerializedExposedEntities(TypedDict):
"""Serialized exposed entities storage storage collection.""" """Serialized exposed entities storage storage collection."""
assistants: dict[str, dict[str, Any]] assistants: dict[str, dict[str, Any]]
exposed_entities: dict[str, dict[str, Any]]
class ExposedEntitiesIDManager(IDManager): class ExposedEntities:
"""ID manager for tags."""
def generate_id(self, suggestion: str) -> str:
"""Generate an ID."""
assert not self.has_id(suggestion)
return suggestion
class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities]):
"""Control assistant settings. """Control assistant settings.
Settings for entities without a unique_id are stored in the store. Settings for entities without a unique_id are stored in the store.
@ -120,21 +106,23 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
""" """
_assistants: dict[str, AssistantPreferences] _assistants: dict[str, AssistantPreferences]
entities: dict[str, ExposedEntity]
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize.""" """Initialize."""
super().__init__( self._hass = hass
Store(hass, STORAGE_VERSION, STORAGE_KEY), ExposedEntitiesIDManager()
)
self._listeners: dict[str, list[Callable[[], None]]] = {} self._listeners: dict[str, list[Callable[[], None]]] = {}
self._store: Store[SerializedExposedEntities] = Store(
hass, STORAGE_VERSION, STORAGE_KEY
)
async def async_load(self) -> None: async def async_initialize(self) -> None:
"""Finish initializing.""" """Finish initializing."""
await super().async_load() websocket_api.async_register_command(self._hass, ws_expose_entity)
websocket_api.async_register_command(self.hass, ws_expose_entity) websocket_api.async_register_command(self._hass, ws_expose_new_entities_get)
websocket_api.async_register_command(self.hass, ws_expose_new_entities_get) websocket_api.async_register_command(self._hass, ws_expose_new_entities_set)
websocket_api.async_register_command(self.hass, ws_expose_new_entities_set) websocket_api.async_register_command(self._hass, ws_list_exposed_entities)
websocket_api.async_register_command(self.hass, ws_list_exposed_entities) await self._async_load_data()
@callback @callback
def async_listen_entity_updates( def async_listen_entity_updates(
@ -143,18 +131,17 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
"""Listen for updates to entity expose settings.""" """Listen for updates to entity expose settings."""
self._listeners.setdefault(assistant, []).append(listener) self._listeners.setdefault(assistant, []).append(listener)
async def async_expose_entity( @callback
def async_expose_entity(
self, assistant: str, entity_id: str, should_expose: bool self, assistant: str, entity_id: str, should_expose: bool
) -> None: ) -> None:
"""Expose an entity to an assistant. """Expose an entity to an assistant.
Notify listeners if expose flag was changed. Notify listeners if expose flag was changed.
""" """
entity_registry = er.async_get(self.hass) entity_registry = er.async_get(self._hass)
if not (registry_entry := entity_registry.async_get(entity_id)): if not (registry_entry := entity_registry.async_get(entity_id)):
return await self._async_expose_legacy_entity( return self._async_expose_legacy_entity(assistant, entity_id, should_expose)
assistant, entity_id, should_expose
)
assistant_options: Mapping[str, Any] assistant_options: Mapping[str, Any]
if ( if (
@ -169,7 +156,7 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
for listener in self._listeners.get(assistant, []): for listener in self._listeners.get(assistant, []):
listener() listener()
async def _async_expose_legacy_entity( def _async_expose_legacy_entity(
self, assistant: str, entity_id: str, should_expose: bool self, assistant: str, entity_id: str, should_expose: bool
) -> None: ) -> None:
"""Expose an entity to an assistant. """Expose an entity to an assistant.
@ -177,23 +164,20 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
Notify listeners if expose flag was changed. Notify listeners if expose flag was changed.
""" """
if ( if (
(exposed_entity := self.data.get(entity_id)) (exposed_entity := self.entities.get(entity_id))
and (assistant_options := exposed_entity.assistants.get(assistant, {})) and (assistant_options := exposed_entity.assistants.get(assistant, {}))
and assistant_options.get("should_expose") == should_expose and assistant_options.get("should_expose") == should_expose
): ):
return return
if exposed_entity: if exposed_entity:
await self.async_update_item( new_exposed_entity = self._update_exposed_entity(
entity_id, {"assistants": {assistant: {"should_expose": should_expose}}} assistant, entity_id, should_expose
) )
else: else:
await self.async_create_item( new_exposed_entity = self._new_exposed_entity(assistant, should_expose)
{ self.entities[entity_id] = new_exposed_entity
"entity_id": entity_id, self._async_schedule_save()
"assistants": {assistant: {"should_expose": should_expose}},
}
)
for listener in self._listeners.get(assistant, []): for listener in self._listeners.get(assistant, []):
listener() listener()
@ -215,11 +199,11 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
self, assistant: str self, assistant: str
) -> dict[str, Mapping[str, Any]]: ) -> dict[str, Mapping[str, Any]]:
"""Get all entity expose settings for an assistant.""" """Get all entity expose settings for an assistant."""
entity_registry = er.async_get(self.hass) entity_registry = er.async_get(self._hass)
result: dict[str, Mapping[str, Any]] = {} result: dict[str, Mapping[str, Any]] = {}
options: Mapping | None options: Mapping | None
for entity_id, exposed_entity in self.data.items(): for entity_id, exposed_entity in self.entities.items():
if options := exposed_entity.assistants.get(assistant): if options := exposed_entity.assistants.get(assistant):
result[entity_id] = options result[entity_id] = options
@ -232,13 +216,13 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
@callback @callback
def async_get_entity_settings(self, entity_id: str) -> dict[str, Mapping[str, Any]]: def async_get_entity_settings(self, entity_id: str) -> dict[str, Mapping[str, Any]]:
"""Get assistant expose settings for an entity.""" """Get assistant expose settings for an entity."""
entity_registry = er.async_get(self.hass) entity_registry = er.async_get(self._hass)
result: dict[str, Mapping[str, Any]] = {} result: dict[str, Mapping[str, Any]] = {}
assistant_settings: Mapping assistant_settings: Mapping
if registry_entry := entity_registry.async_get(entity_id): if registry_entry := entity_registry.async_get(entity_id):
assistant_settings = registry_entry.options assistant_settings = registry_entry.options
elif exposed_entity := self.data.get(entity_id): elif exposed_entity := self.entities.get(entity_id):
assistant_settings = exposed_entity.assistants assistant_settings = exposed_entity.assistants
else: else:
raise HomeAssistantError("Unknown entity") raise HomeAssistantError("Unknown entity")
@ -249,16 +233,17 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
return result return result
async def async_should_expose(self, assistant: str, entity_id: str) -> bool: @callback
def async_should_expose(self, assistant: str, entity_id: str) -> bool:
"""Return True if an entity should be exposed to an assistant.""" """Return True if an entity should be exposed to an assistant."""
should_expose: bool should_expose: bool
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False return False
entity_registry = er.async_get(self.hass) entity_registry = er.async_get(self._hass)
if not (registry_entry := entity_registry.async_get(entity_id)): if not (registry_entry := entity_registry.async_get(entity_id)):
return await self._async_should_expose_legacy_entity(assistant, entity_id) return self._async_should_expose_legacy_entity(assistant, entity_id)
if assistant in registry_entry.options: if assistant in registry_entry.options:
if "should_expose" in registry_entry.options[assistant]: if "should_expose" in registry_entry.options[assistant]:
should_expose = registry_entry.options[assistant]["should_expose"] should_expose = registry_entry.options[assistant]["should_expose"]
@ -277,14 +262,14 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
return should_expose return should_expose
async def _async_should_expose_legacy_entity( def _async_should_expose_legacy_entity(
self, assistant: str, entity_id: str self, assistant: str, entity_id: str
) -> bool: ) -> bool:
"""Return True if an entity should be exposed to an assistant.""" """Return True if an entity should be exposed to an assistant."""
should_expose: bool should_expose: bool
if ( if (
exposed_entity := self.data.get(entity_id) exposed_entity := self.entities.get(entity_id)
) and assistant in exposed_entity.assistants: ) and assistant in exposed_entity.assistants:
if "should_expose" in exposed_entity.assistants[assistant]: if "should_expose" in exposed_entity.assistants[assistant]:
should_expose = exposed_entity.assistants[assistant]["should_expose"] should_expose = exposed_entity.assistants[assistant]["should_expose"]
@ -296,16 +281,13 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
should_expose = False should_expose = False
if exposed_entity: if exposed_entity:
await self.async_update_item( new_exposed_entity = self._update_exposed_entity(
entity_id, {"assistants": {assistant: {"should_expose": should_expose}}} assistant, entity_id, should_expose
) )
else: else:
await self.async_create_item( new_exposed_entity = self._new_exposed_entity(assistant, should_expose)
{ self.entities[entity_id] = new_exposed_entity
"entity_id": entity_id, self._async_schedule_save()
"assistants": {assistant: {"should_expose": should_expose}},
}
)
return should_expose return should_expose
@ -323,7 +305,7 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
if domain in DEFAULT_EXPOSED_DOMAINS: if domain in DEFAULT_EXPOSED_DOMAINS:
return True return True
device_class = get_device_class(self.hass, entity_id) device_class = get_device_class(self._hass, entity_id)
if ( if (
domain == "binary_sensor" domain == "binary_sensor"
and device_class in DEFAULT_EXPOSED_BINARY_SENSOR_DEVICE_CLASSES and device_class in DEFAULT_EXPOSED_BINARY_SENSOR_DEVICE_CLASSES
@ -335,73 +317,66 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
return False return False
async def _process_create_data(self, data: dict) -> dict: def _update_exposed_entity(
"""Validate the config is valid.""" self,
return data assistant: str,
entity_id: str,
@callback should_expose: bool,
def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
entity_id: str = info["entity_id"]
return entity_id
async def _update_data(
self, item: ExposedEntity, update_data: dict
) -> ExposedEntity: ) -> ExposedEntity:
"""Return a new updated item.""" """Update an exposed entity."""
new_assistant_settings: dict[str, Any] = update_data["assistants"] entity = self.entities[entity_id]
old_assistant_settings = item.assistants assistants = dict(entity.assistants)
for assistant, old_settings in old_assistant_settings.items(): old_settings = assistants.get(assistant, {})
new_settings = new_assistant_settings.get(assistant, {}) assistants[assistant] = old_settings | {"should_expose": should_expose}
new_assistant_settings[assistant] = old_settings | new_settings return ExposedEntity(assistants)
return dataclasses.replace(item, assistants=new_assistant_settings)
def _create_item(self, item_id: str, data: dict) -> ExposedEntity: def _new_exposed_entity(self, assistant: str, should_expose: bool) -> ExposedEntity:
"""Create an item from validated config.""" """Create a new exposed entity."""
return ExposedEntity( return ExposedEntity(
assistants=data["assistants"], assistants={assistant: {"should_expose": should_expose}},
) )
def _deserialize_item(self, data: dict) -> ExposedEntity:
"""Create an item from its serialized representation."""
return ExposedEntity(
assistants=data["assistants"],
)
def _serialize_item(self, item_id: str, item: ExposedEntity) -> dict:
"""Return the serialized representation of an item for storing."""
return item.to_json(item_id)
async def _async_load_data(self) -> SerializedExposedEntities | None: async def _async_load_data(self) -> SerializedExposedEntities | None:
"""Load from the store.""" """Load from the store."""
data = await super()._async_load_data() data = await self._store.async_load()
assistants: dict[str, AssistantPreferences] = {} assistants: dict[str, AssistantPreferences] = {}
exposed_entities: dict[str, ExposedEntity] = {}
if data and "assistants" in data: if data:
for domain, preferences in data["assistants"].items(): for domain, preferences in data["assistants"].items():
assistants[domain] = AssistantPreferences(**preferences) assistants[domain] = AssistantPreferences(**preferences)
self._assistants = assistants if data and "exposed_entities" in data:
for entity_id, preferences in data["exposed_entities"].items():
exposed_entities[entity_id] = ExposedEntity(**preferences)
if data and "items" not in data: self._assistants = assistants
return None # type: ignore[unreachable] self.entities = exposed_entities
return data return data
@callback
def _async_schedule_save(self) -> None:
"""Schedule saving the preferences."""
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
@callback @callback
def _data_to_save(self) -> SerializedExposedEntities: def _data_to_save(self) -> SerializedExposedEntities:
"""Return JSON-compatible date for storing to file.""" """Return JSON-compatible date for storing to file."""
base_data = super()._base_data_to_save()
return { return {
"items": base_data["items"],
"assistants": { "assistants": {
domain: preferences.to_json() domain: preferences.to_json()
for domain, preferences in self._assistants.items() for domain, preferences in self._assistants.items()
}, },
"exposed_entities": {
entity_id: entity.to_json()
for entity_id, entity in self.entities.items()
},
} }
@callback
@websocket_api.require_admin @websocket_api.require_admin
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
@ -411,8 +386,7 @@ class ExposedEntities(StorageCollection[ExposedEntity, SerializedExposedEntities
vol.Required("should_expose"): bool, vol.Required("should_expose"): bool,
} }
) )
@websocket_api.async_response def ws_expose_entity(
async def ws_expose_entity(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None: ) -> None:
"""Expose an entity to an assistant.""" """Expose an entity to an assistant."""
@ -434,7 +408,7 @@ async def ws_expose_entity(
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
for entity_id in entity_ids: for entity_id in entity_ids:
for assistant in msg["assistants"]: for assistant in msg["assistants"]:
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
assistant, entity_id, msg["should_expose"] assistant, entity_id, msg["should_expose"]
) )
connection.send_result(msg["id"]) connection.send_result(msg["id"])
@ -455,7 +429,7 @@ def ws_list_exposed_entities(
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for entity_id in chain(exposed_entities.data, entity_registry.entities): for entity_id in chain(exposed_entities.entities, entity_registry.entities):
result[entity_id] = {} result[entity_id] = {}
entity_settings = async_get_entity_settings(hass, entity_id) entity_settings = async_get_entity_settings(hass, entity_id)
for assistant, settings in entity_settings.items(): for assistant, settings in entity_settings.items():
@ -527,7 +501,8 @@ def async_get_entity_settings(
return exposed_entities.async_get_entity_settings(entity_id) return exposed_entities.async_get_entity_settings(entity_id)
async def async_expose_entity( @callback
def async_expose_entity(
hass: HomeAssistant, hass: HomeAssistant,
assistant: str, assistant: str,
entity_id: str, entity_id: str,
@ -535,12 +510,11 @@ async def async_expose_entity(
) -> None: ) -> None:
"""Get assistant expose settings for an entity.""" """Get assistant expose settings for an entity."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
await exposed_entities.async_expose_entity(assistant, entity_id, should_expose) exposed_entities.async_expose_entity(assistant, entity_id, should_expose)
async def async_should_expose( @callback
hass: HomeAssistant, assistant: str, entity_id: str def async_should_expose(hass: HomeAssistant, assistant: str, entity_id: str) -> bool:
) -> bool:
"""Return True if an entity should be exposed to an assistant.""" """Return True if an entity should be exposed to an assistant."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
return await exposed_entities.async_should_expose(assistant, entity_id) return exposed_entities.async_should_expose(assistant, entity_id)

View file

@ -138,6 +138,6 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
for assistant, settings in expose_settings.items(): for assistant, settings in expose_settings.items():
if (should_expose := settings.get("should_expose")) is None: if (should_expose := settings.get("should_expose")) is None:
continue continue
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
hass, assistant, switch_entity_id, should_expose hass, assistant, switch_entity_id, should_expose
) )

View file

@ -111,7 +111,7 @@ class BaseEntity(Entity):
return return
registry.async_update_entity(self.entity_id, name=wrapped_switch.name) registry.async_update_entity(self.entity_id, name=wrapped_switch.name)
async def copy_expose_settings() -> None: def copy_expose_settings() -> None:
"""Copy assistant expose settings from the wrapped entity. """Copy assistant expose settings from the wrapped entity.
Also unexpose the wrapped entity if exposed. Also unexpose the wrapped entity if exposed.
@ -122,15 +122,15 @@ class BaseEntity(Entity):
for assistant, settings in expose_settings.items(): for assistant, settings in expose_settings.items():
if (should_expose := settings.get("should_expose")) is None: if (should_expose := settings.get("should_expose")) is None:
continue continue
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
self.hass, assistant, self.entity_id, should_expose self.hass, assistant, self.entity_id, should_expose
) )
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
self.hass, assistant, self._switch_entity_id, False self.hass, assistant, self._switch_entity_id, False
) )
copy_custom_name(wrapped_switch) copy_custom_name(wrapped_switch)
await copy_expose_settings() copy_expose_settings()
class BaseToggleEntity(BaseEntity, ToggleEntity): class BaseToggleEntity(BaseEntity, ToggleEntity):

View file

@ -2450,18 +2450,13 @@ async def test_exclude_filters(hass: HomeAssistant) -> None:
hass.states.async_set("cover.deny", "off", {"friendly_name": "Blocked cover"}) hass.states.async_set("cover.deny", "off", {"friendly_name": "Blocked cover"})
alexa_config = MockConfig(hass) alexa_config = MockConfig(hass)
filter = entityfilter.generate_filter( alexa_config.should_expose = entityfilter.generate_filter(
include_domains=[], include_domains=[],
include_entities=[], include_entities=[],
exclude_domains=["script"], exclude_domains=["script"],
exclude_entities=["cover.deny"], exclude_entities=["cover.deny"],
) )
async def mock_should_expose(entity_id):
return filter(entity_id)
alexa_config.should_expose = mock_should_expose
msg = await smart_home.async_handle_message(hass, alexa_config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -2486,18 +2481,13 @@ async def test_include_filters(hass: HomeAssistant) -> None:
hass.states.async_set("group.allow", "off", {"friendly_name": "Allowed group"}) hass.states.async_set("group.allow", "off", {"friendly_name": "Allowed group"})
alexa_config = MockConfig(hass) alexa_config = MockConfig(hass)
filter = entityfilter.generate_filter( alexa_config.should_expose = entityfilter.generate_filter(
include_domains=["automation", "group"], include_domains=["automation", "group"],
include_entities=["script.deny"], include_entities=["script.deny"],
exclude_domains=[], exclude_domains=[],
exclude_entities=[], exclude_entities=[],
) )
async def mock_should_expose(entity_id):
return filter(entity_id)
alexa_config.should_expose = mock_should_expose
msg = await smart_home.async_handle_message(hass, alexa_config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -2516,18 +2506,13 @@ async def test_never_exposed_entities(hass: HomeAssistant) -> None:
hass.states.async_set("group.allow", "off", {"friendly_name": "Allowed group"}) hass.states.async_set("group.allow", "off", {"friendly_name": "Allowed group"})
alexa_config = MockConfig(hass) alexa_config = MockConfig(hass)
filter = entityfilter.generate_filter( alexa_config.should_expose = entityfilter.generate_filter(
include_domains=["group"], include_domains=["group"],
include_entities=[], include_entities=[],
exclude_domains=[], exclude_domains=[],
exclude_entities=[], exclude_entities=[],
) )
async def mock_should_expose(entity_id):
return filter(entity_id)
alexa_config.should_expose = mock_should_expose
msg = await smart_home.async_handle_message(hass, alexa_config, request) msg = await smart_home.async_handle_message(hass, alexa_config, request)
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -38,10 +38,10 @@ def expose_new(hass, expose_new):
exposed_entities.async_set_expose_new_entities("cloud.alexa", expose_new) exposed_entities.async_set_expose_new_entities("cloud.alexa", expose_new)
async def expose_entity(hass, entity_id, should_expose): def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to Alexa.""" """Expose an entity to Alexa."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
await exposed_entities.async_expose_entity("cloud.alexa", entity_id, should_expose) exposed_entities.async_expose_entity("cloud.alexa", entity_id, should_expose)
async def test_alexa_config_expose_entity_prefs( async def test_alexa_config_expose_entity_prefs(
@ -95,35 +95,35 @@ async def test_alexa_config_expose_entity_prefs(
alexa_report_state=False, alexa_report_state=False,
) )
expose_new(hass, True) expose_new(hass, True)
await expose_entity(hass, entity_entry5.entity_id, False) expose_entity(hass, entity_entry5.entity_id, False)
conf = alexa_config.CloudAlexaConfig( conf = alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
) )
await conf.async_initialize() await conf.async_initialize()
# an entity which is not in the entity registry can be exposed # an entity which is not in the entity registry can be exposed
await expose_entity(hass, "light.kitchen", True) expose_entity(hass, "light.kitchen", True)
assert await conf.should_expose("light.kitchen") assert conf.should_expose("light.kitchen")
# categorized and hidden entities should not be exposed # categorized and hidden entities should not be exposed
assert not await conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry1.entity_id)
assert not await conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry2.entity_id)
assert not await conf.should_expose(entity_entry3.entity_id) assert not conf.should_expose(entity_entry3.entity_id)
assert not await conf.should_expose(entity_entry4.entity_id) assert not conf.should_expose(entity_entry4.entity_id)
# this has been hidden # this has been hidden
assert not await conf.should_expose(entity_entry5.entity_id) assert not conf.should_expose(entity_entry5.entity_id)
# exposed by default # exposed by default
assert await conf.should_expose(entity_entry6.entity_id) assert conf.should_expose(entity_entry6.entity_id)
await expose_entity(hass, entity_entry5.entity_id, True) expose_entity(hass, entity_entry5.entity_id, True)
assert await conf.should_expose(entity_entry5.entity_id) assert conf.should_expose(entity_entry5.entity_id)
await expose_entity(hass, entity_entry5.entity_id, None) expose_entity(hass, entity_entry5.entity_id, None)
assert not await conf.should_expose(entity_entry5.entity_id) assert not conf.should_expose(entity_entry5.entity_id)
assert "alexa" not in hass.config.components assert "alexa" not in hass.config.components
await hass.async_block_till_done() await hass.async_block_till_done()
assert "alexa" in hass.config.components assert "alexa" in hass.config.components
assert not await conf.should_expose(entity_entry5.entity_id) assert not conf.should_expose(entity_entry5.entity_id)
async def test_alexa_config_report_state( async def test_alexa_config_report_state(
@ -368,7 +368,7 @@ async def test_alexa_update_expose_trigger_sync(
await conf.async_initialize() await conf.async_initialize()
with patch_sync_helper() as (to_update, to_remove): with patch_sync_helper() as (to_update, to_remove):
await expose_entity(hass, light_entry.entity_id, True) expose_entity(hass, light_entry.entity_id, True)
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_time_changed(hass, fire_all=True) async_fire_time_changed(hass, fire_all=True)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -378,9 +378,9 @@ async def test_alexa_update_expose_trigger_sync(
assert to_remove == [] assert to_remove == []
with patch_sync_helper() as (to_update, to_remove): with patch_sync_helper() as (to_update, to_remove):
await expose_entity(hass, light_entry.entity_id, False) expose_entity(hass, light_entry.entity_id, False)
await expose_entity(hass, binary_sensor_entry.entity_id, True) expose_entity(hass, binary_sensor_entry.entity_id, True)
await expose_entity(hass, sensor_entry.entity_id, True) expose_entity(hass, sensor_entry.entity_id, True)
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_time_changed(hass, fire_all=True) async_fire_time_changed(hass, fire_all=True)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -586,7 +586,7 @@ async def test_alexa_config_migrate_expose_entity_prefs(
alexa_report_state=False, alexa_report_state=False,
alexa_settings_version=1, alexa_settings_version=1,
) )
await expose_entity(hass, entity_migrated.entity_id, False) expose_entity(hass, entity_migrated.entity_id, False)
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.unknown"] = { cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.unknown"] = {
PREF_SHOULD_EXPOSE: True PREF_SHOULD_EXPOSE: True

View file

@ -265,13 +265,13 @@ async def test_google_config_expose_entity(
state = State(entity_entry.entity_id, "on") state = State(entity_entry.entity_id, "on")
gconf = await cloud_client.get_google_config() gconf = await cloud_client.get_google_config()
assert await gconf.should_expose(state) assert gconf.should_expose(state)
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
"cloud.google_assistant", entity_entry.entity_id, False "cloud.google_assistant", entity_entry.entity_id, False
) )
assert not await gconf.should_expose(state) assert not gconf.should_expose(state)
async def test_google_config_should_2fa( async def test_google_config_should_2fa(

View file

@ -46,10 +46,10 @@ def expose_new(hass, expose_new):
exposed_entities.async_set_expose_new_entities("cloud.google_assistant", expose_new) exposed_entities.async_set_expose_new_entities("cloud.google_assistant", expose_new)
async def expose_entity(hass, entity_id, should_expose): def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to Google.""" """Expose an entity to Google."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
"cloud.google_assistant", entity_id, should_expose "cloud.google_assistant", entity_id, should_expose
) )
@ -150,7 +150,7 @@ async def test_google_update_expose_trigger_sync(
with patch.object(config, "async_sync_entities") as mock_sync, patch.object( with patch.object(config, "async_sync_entities") as mock_sync, patch.object(
ga_helpers, "SYNC_DELAY", 0 ga_helpers, "SYNC_DELAY", 0
): ):
await expose_entity(hass, light_entry.entity_id, True) expose_entity(hass, light_entry.entity_id, True)
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow()) async_fire_time_changed(hass, utcnow())
await hass.async_block_till_done() await hass.async_block_till_done()
@ -160,9 +160,9 @@ async def test_google_update_expose_trigger_sync(
with patch.object(config, "async_sync_entities") as mock_sync, patch.object( with patch.object(config, "async_sync_entities") as mock_sync, patch.object(
ga_helpers, "SYNC_DELAY", 0 ga_helpers, "SYNC_DELAY", 0
): ):
await expose_entity(hass, light_entry.entity_id, False) expose_entity(hass, light_entry.entity_id, False)
await expose_entity(hass, binary_sensor_entry.entity_id, True) expose_entity(hass, binary_sensor_entry.entity_id, True)
await expose_entity(hass, sensor_entry.entity_id, True) expose_entity(hass, sensor_entry.entity_id, True)
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow()) async_fire_time_changed(hass, utcnow())
await hass.async_block_till_done() await hass.async_block_till_done()
@ -384,7 +384,7 @@ async def test_google_config_expose_entity_prefs(
) )
expose_new(hass, True) expose_new(hass, True)
await expose_entity(hass, entity_entry5.entity_id, False) expose_entity(hass, entity_entry5.entity_id, False)
state = State("light.kitchen", "on") state = State("light.kitchen", "on")
state_config = State(entity_entry1.entity_id, "on") state_config = State(entity_entry1.entity_id, "on")
@ -395,23 +395,23 @@ async def test_google_config_expose_entity_prefs(
state_exposed_default = State(entity_entry6.entity_id, "on") state_exposed_default = State(entity_entry6.entity_id, "on")
# an entity which is not in the entity registry can be exposed # an entity which is not in the entity registry can be exposed
await expose_entity(hass, "light.kitchen", True) expose_entity(hass, "light.kitchen", True)
assert await mock_conf.should_expose(state) assert mock_conf.should_expose(state)
# categorized and hidden entities should not be exposed # categorized and hidden entities should not be exposed
assert not await mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_config)
assert not await mock_conf.should_expose(state_diagnostic) assert not mock_conf.should_expose(state_diagnostic)
assert not await mock_conf.should_expose(state_hidden_integration) assert not mock_conf.should_expose(state_hidden_integration)
assert not await mock_conf.should_expose(state_hidden_user) assert not mock_conf.should_expose(state_hidden_user)
# this has been hidden # this has been hidden
assert not await mock_conf.should_expose(state_not_exposed) assert not mock_conf.should_expose(state_not_exposed)
# exposed by default # exposed by default
assert await mock_conf.should_expose(state_exposed_default) assert mock_conf.should_expose(state_exposed_default)
await expose_entity(hass, entity_entry5.entity_id, True) expose_entity(hass, entity_entry5.entity_id, True)
assert await mock_conf.should_expose(state_not_exposed) assert mock_conf.should_expose(state_not_exposed)
await expose_entity(hass, entity_entry5.entity_id, None) expose_entity(hass, entity_entry5.entity_id, None)
assert not await mock_conf.should_expose(state_not_exposed) assert not mock_conf.should_expose(state_not_exposed)
def test_enabled_requires_valid_sub( def test_enabled_requires_valid_sub(
@ -535,7 +535,7 @@ async def test_google_config_migrate_expose_entity_prefs(
google_report_state=False, google_report_state=False,
google_settings_version=1, google_settings_version=1,
) )
await expose_entity(hass, entity_migrated.entity_id, False) expose_entity(hass, entity_migrated.entity_id, False)
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.unknown"] = { cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.unknown"] = {
PREF_SHOULD_EXPOSE: True PREF_SHOULD_EXPOSE: True

View file

@ -51,9 +51,7 @@ def expose_new(hass, expose_new):
exposed_entities.async_set_expose_new_entities(conversation.DOMAIN, expose_new) exposed_entities.async_set_expose_new_entities(conversation.DOMAIN, expose_new)
async def expose_entity(hass, entity_id, should_expose): def expose_entity(hass, entity_id, should_expose):
"""Expose an entity to the default agent.""" """Expose an entity to the default agent."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(conversation.DOMAIN, entity_id, should_expose)
conversation.DOMAIN, entity_id, should_expose
)

View file

@ -108,7 +108,7 @@ async def test_exposed_areas(
hass.states.async_set(bedroom_light.entity_id, "on") hass.states.async_set(bedroom_light.entity_id, "on")
# Hide the bedroom light # Hide the bedroom light
await expose_entity(hass, bedroom_light.entity_id, False) expose_entity(hass, bedroom_light.entity_id, False)
result = await conversation.async_converse( result = await conversation.async_converse(
hass, "turn on lights in the kitchen", None, Context(), None hass, "turn on lights in the kitchen", None, Context(), None

View file

@ -680,7 +680,7 @@ async def test_http_processing_intent_entity_exposed(
} }
# Unexpose the entity # Unexpose the entity
await expose_entity(hass, "light.kitchen", False) expose_entity(hass, "light.kitchen", False)
await hass.async_block_till_done() await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
@ -730,7 +730,7 @@ async def test_http_processing_intent_entity_exposed(
} }
# Now expose the entity # Now expose the entity
await expose_entity(hass, "light.kitchen", True) expose_entity(hass, "light.kitchen", True)
await hass.async_block_till_done() await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
@ -845,7 +845,7 @@ async def test_http_processing_intent_conversion_not_expose_new(
} }
# Expose the entity # Expose the entity
await expose_entity(hass, "light.kitchen", True) expose_entity(hass, "light.kitchen", True)
await hass.async_block_till_done() await hass.async_block_till_done()
resp = await client.post( resp = await client.post(

View file

@ -58,7 +58,7 @@ class MockConfig(helpers.AbstractConfig):
"""Get agent user ID making request.""" """Get agent user ID making request."""
return context.user_id return context.user_id
async def should_expose(self, state): def should_expose(self, state):
"""Expose it all.""" """Expose it all."""
return self._should_expose is None or self._should_expose(state) return self._should_expose is None or self._should_expose(state)

View file

@ -49,13 +49,13 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass: HomeAssistant)
) )
entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights"))
serialized = await entity.sync_serialize(None, "mock-uuid") serialized = entity.sync_serialize(None, "mock-uuid")
assert "otherDeviceIds" not in serialized assert "otherDeviceIds" not in serialized
assert "customData" not in serialized assert "customData" not in serialized
config.async_enable_local_sdk() config.async_enable_local_sdk()
serialized = await entity.sync_serialize("mock-user-id", "abcdef") serialized = entity.sync_serialize("mock-user-id", "abcdef")
assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}]
assert serialized["customData"] == { assert serialized["customData"] == {
"httpPort": 1234, "httpPort": 1234,
@ -68,7 +68,7 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass: HomeAssistant)
"homeassistant.components.google_assistant.helpers.get_google_type", "homeassistant.components.google_assistant.helpers.get_google_type",
return_value=device_type, return_value=device_type,
): ):
serialized = await entity.sync_serialize(None, "mock-uuid") serialized = entity.sync_serialize(None, "mock-uuid")
assert "otherDeviceIds" not in serialized assert "otherDeviceIds" not in serialized
assert "customData" not in serialized assert "customData" not in serialized

View file

@ -257,9 +257,7 @@ async def test_should_expose(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
assert ( assert (
await config.should_expose( config.should_expose(State(DOMAIN + ".mock", "mock", {"view": "not None"}))
State(DOMAIN + ".mock", "mock", {"view": "not None"})
)
is False is False
) )
@ -267,10 +265,7 @@ async def test_should_expose(hass: HomeAssistant) -> None:
# Wait for google_assistant.helpers.async_initialize.sync_google to be called # Wait for google_assistant.helpers.async_initialize.sync_google to be called
await hass.async_block_till_done() await hass.async_block_till_done()
assert ( assert config.should_expose(State(CLOUD_NEVER_EXPOSED_ENTITIES[0], "mock")) is False
await config.should_expose(State(CLOUD_NEVER_EXPOSED_ENTITIES[0], "mock"))
is False
)
async def test_missing_service_account(hass: HomeAssistant) -> None: async def test_missing_service_account(hass: HomeAssistant) -> None:

View file

@ -909,7 +909,7 @@ async def test_serialize_input_boolean(hass: HomeAssistant) -> None:
"""Test serializing an input boolean entity.""" """Test serializing an input boolean entity."""
state = State("input_boolean.bla", "on") state = State("input_boolean.bla", "on")
entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) entity = sh.GoogleEntity(hass, BASIC_CONFIG, state)
result = await entity.sync_serialize(None, "mock-uuid") result = entity.sync_serialize(None, "mock-uuid")
assert result == { assert result == {
"id": "input_boolean.bla", "id": "input_boolean.bla",
"attributes": {}, "attributes": {},

View file

@ -101,21 +101,21 @@ async def test_load_preferences(hass: HomeAssistant) -> None:
exposed_entities.async_set_expose_new_entities("test1", True) exposed_entities.async_set_expose_new_entities("test1", True)
exposed_entities.async_set_expose_new_entities("test2", False) exposed_entities.async_set_expose_new_entities("test2", False)
await exposed_entities.async_expose_entity("test1", "light.kitchen", True) exposed_entities.async_expose_entity("test1", "light.kitchen", True)
await exposed_entities.async_expose_entity("test1", "light.living_room", True) exposed_entities.async_expose_entity("test1", "light.living_room", True)
await exposed_entities.async_expose_entity("test2", "light.kitchen", True) exposed_entities.async_expose_entity("test2", "light.kitchen", True)
await exposed_entities.async_expose_entity("test2", "light.kitchen", True) exposed_entities.async_expose_entity("test2", "light.kitchen", True)
assert list(exposed_entities._assistants) == ["test1", "test2"] assert list(exposed_entities._assistants) == ["test1", "test2"]
assert list(exposed_entities.data) == ["light.kitchen", "light.living_room"] assert list(exposed_entities.entities) == ["light.kitchen", "light.living_room"]
await flush_store(exposed_entities.store) await flush_store(exposed_entities._store)
exposed_entities2 = ExposedEntities(hass) exposed_entities2 = ExposedEntities(hass)
await exposed_entities2.async_load() await exposed_entities2.async_initialize()
assert exposed_entities._assistants == exposed_entities2._assistants assert exposed_entities._assistants == exposed_entities2._assistants
assert exposed_entities.data == exposed_entities2.data assert exposed_entities.entities == exposed_entities2.entities
async def test_expose_entity( async def test_expose_entity(
@ -132,7 +132,7 @@ async def test_expose_entity(
entry2 = entity_registry.async_get_or_create("test", "test", "unique2") entry2 = entity_registry.async_get_or_create("test", "test", "unique2")
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
assert len(exposed_entities.data) == 0 assert len(exposed_entities.entities) == 0
# Set options # Set options
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -151,7 +151,7 @@ async def test_expose_entity(
assert entry1.options == {"cloud.alexa": {"should_expose": True}} assert entry1.options == {"cloud.alexa": {"should_expose": True}}
entry2 = entity_registry.async_get(entry2.entity_id) entry2 = entity_registry.async_get(entry2.entity_id)
assert entry2.options == {} assert entry2.options == {}
assert len(exposed_entities.data) == 0 assert len(exposed_entities.entities) == 0
# Update options # Update options
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -176,7 +176,7 @@ async def test_expose_entity(
"cloud.alexa": {"should_expose": False}, "cloud.alexa": {"should_expose": False},
"cloud.google_assistant": {"should_expose": False}, "cloud.google_assistant": {"should_expose": False},
} }
assert len(exposed_entities.data) == 0 assert len(exposed_entities.entities) == 0
async def test_expose_entity_unknown( async def test_expose_entity_unknown(
@ -189,7 +189,7 @@ async def test_expose_entity_unknown(
await hass.async_block_till_done() await hass.async_block_till_done()
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
assert len(exposed_entities.data) == 0 assert len(exposed_entities.entities) == 0
# Set options # Set options
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -204,8 +204,8 @@ async def test_expose_entity_unknown(
response = await ws_client.receive_json() response = await ws_client.receive_json()
assert response["success"] assert response["success"]
assert len(exposed_entities.data) == 1 assert len(exposed_entities.entities) == 1
assert exposed_entities.data == { assert exposed_entities.entities == {
"test.test": ExposedEntity({"cloud.alexa": {"should_expose": True}}) "test.test": ExposedEntity({"cloud.alexa": {"should_expose": True}})
} }
@ -222,8 +222,8 @@ async def test_expose_entity_unknown(
response = await ws_client.receive_json() response = await ws_client.receive_json()
assert response["success"] assert response["success"]
assert len(exposed_entities.data) == 2 assert len(exposed_entities.entities) == 2
assert exposed_entities.data == { assert exposed_entities.entities == {
"test.test": ExposedEntity( "test.test": ExposedEntity(
{ {
"cloud.alexa": {"should_expose": False}, "cloud.alexa": {"should_expose": False},
@ -292,7 +292,7 @@ async def test_expose_new_entities(
assert response["result"] == {"expose_new": False} assert response["result"] == {"expose_new": False}
# Check if exposed - should be False # Check if exposed - should be False
assert await async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False assert async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False
# Expose new entities to Alexa # Expose new entities to Alexa
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -315,12 +315,10 @@ async def test_expose_new_entities(
assert response["result"] == {"expose_new": expose_new} assert response["result"] == {"expose_new": expose_new}
# Check again if exposed - should still be False # Check again if exposed - should still be False
assert await async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False assert async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False
# Check if exposed - should be True # Check if exposed - should be True
assert ( assert async_should_expose(hass, "cloud.alexa", entry2.entity_id) == expose_new
await async_should_expose(hass, "cloud.alexa", entry2.entity_id) == expose_new
)
async def test_listen_updates( async def test_listen_updates(
@ -342,21 +340,21 @@ async def test_listen_updates(
entry = entity_registry.async_get_or_create("climate", "test", "unique1") entry = entity_registry.async_get_or_create("climate", "test", "unique1")
# Call for another assistant - listener not called # Call for another assistant - listener not called
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
"cloud.google_assistant", entry.entity_id, True "cloud.google_assistant", entry.entity_id, True
) )
assert len(calls) == 0 assert len(calls) == 0
# Call for our assistant - listener called # Call for our assistant - listener called
await exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
assert len(calls) == 1 assert len(calls) == 1
# Settings not changed - listener not called # Settings not changed - listener not called
await exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
assert len(calls) == 1 assert len(calls) == 1
# Settings changed - listener called # Settings changed - listener called
await exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, False) exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, False)
assert len(calls) == 2 assert len(calls) == 2
@ -375,10 +373,8 @@ async def test_get_assistant_settings(
assert async_get_assistant_settings(hass, "cloud.alexa") == {} assert async_get_assistant_settings(hass, "cloud.alexa") == {}
await exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True)
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity("cloud.alexa", "light.not_in_registry", True)
"cloud.alexa", "light.not_in_registry", True
)
assert async_get_assistant_settings(hass, "cloud.alexa") == snapshot assert async_get_assistant_settings(hass, "cloud.alexa") == snapshot
assert async_get_assistant_settings(hass, "cloud.google_assistant") == snapshot assert async_get_assistant_settings(hass, "cloud.google_assistant") == snapshot
@ -412,45 +408,38 @@ async def test_should_expose(
assert response["success"] assert response["success"]
# Unknown entity is not exposed # Unknown entity is not exposed
assert await async_should_expose(hass, "test.test", "test.test") is False assert async_should_expose(hass, "test.test", "test.test") is False
# Blocked entity is not exposed # Blocked entity is not exposed
assert await async_should_expose(hass, "cloud.alexa", entities["blocked"]) is False assert async_should_expose(hass, "cloud.alexa", entities["blocked"]) is False
# Lock is exposed # Lock is exposed
assert await async_should_expose(hass, "cloud.alexa", entities["lock"]) is True assert async_should_expose(hass, "cloud.alexa", entities["lock"]) is True
# Binary sensor without device class is not exposed # Binary sensor without device class is not exposed
assert ( assert async_should_expose(hass, "cloud.alexa", entities["binary_sensor"]) is False
await async_should_expose(hass, "cloud.alexa", entities["binary_sensor"])
is False
)
# Binary sensor with certain device class is exposed # Binary sensor with certain device class is exposed
assert ( assert async_should_expose(hass, "cloud.alexa", entities["door_sensor"]) is True
await async_should_expose(hass, "cloud.alexa", entities["door_sensor"]) is True
)
# Sensor without device class is not exposed # Sensor without device class is not exposed
assert await async_should_expose(hass, "cloud.alexa", entities["sensor"]) is False assert async_should_expose(hass, "cloud.alexa", entities["sensor"]) is False
# Sensor with certain device class is exposed # Sensor with certain device class is exposed
assert ( assert (
await async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True
is True
) )
# The second time we check, it should load it from storage # The second time we check, it should load it from storage
assert ( assert (
await async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True
is True
) )
# Check with a different assistant # Check with a different assistant
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
exposed_entities.async_set_expose_new_entities("cloud.no_default_expose", False) exposed_entities.async_set_expose_new_entities("cloud.no_default_expose", False)
assert ( assert (
await async_should_expose( async_should_expose(
hass, "cloud.no_default_expose", entities["temperature_sensor"] hass, "cloud.no_default_expose", entities["temperature_sensor"]
) )
is False is False
@ -481,13 +470,13 @@ async def test_should_expose_hidden_categorized(
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
"lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER "lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER
) )
assert await async_should_expose(hass, "cloud.alexa", "lock.test_unique2") is False assert async_should_expose(hass, "cloud.alexa", "lock.test_unique2") is False
# Entity with category is not exposed # Entity with category is not exposed
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
"lock", "test", "unique3", entity_category=EntityCategory.CONFIG "lock", "test", "unique3", entity_category=EntityCategory.CONFIG
) )
assert await async_should_expose(hass, "cloud.alexa", "lock.test_unique3") is False assert async_should_expose(hass, "cloud.alexa", "lock.test_unique3") is False
async def test_list_exposed_entities( async def test_list_exposed_entities(
@ -555,8 +544,8 @@ async def test_listeners(
callbacks = [] callbacks = []
exposed_entities.async_listen_entity_updates("test1", lambda: callbacks.append(1)) exposed_entities.async_listen_entity_updates("test1", lambda: callbacks.append(1))
await async_expose_entity(hass, "test1", "light.kitchen", True) async_expose_entity(hass, "test1", "light.kitchen", True)
assert len(callbacks) == 1 assert len(callbacks) == 1
entry1 = entity_registry.async_get_or_create("switch", "test", "unique1") entry1 = entity_registry.async_get_or_create("switch", "test", "unique1")
await async_expose_entity(hass, "test1", entry1.entity_id, True) async_expose_entity(hass, "test1", entry1.entity_id, True)

View file

@ -702,7 +702,7 @@ async def test_import_expose_settings_1(
original_name="ABC", original_name="ABC",
) )
for assistant, should_expose in EXPOSE_SETTINGS.items(): for assistant, should_expose in EXPOSE_SETTINGS.items():
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
hass, assistant, switch_entity_entry.entity_id, should_expose hass, assistant, switch_entity_entry.entity_id, should_expose
) )
@ -760,7 +760,7 @@ async def test_import_expose_settings_2(
original_name="ABC", original_name="ABC",
) )
for assistant, should_expose in EXPOSE_SETTINGS.items(): for assistant, should_expose in EXPOSE_SETTINGS.items():
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
hass, assistant, switch_entity_entry.entity_id, should_expose hass, assistant, switch_entity_entry.entity_id, should_expose
) )
@ -785,7 +785,7 @@ async def test_import_expose_settings_2(
suggested_object_id="abc", suggested_object_id="abc",
) )
for assistant, should_expose in EXPOSE_SETTINGS.items(): for assistant, should_expose in EXPOSE_SETTINGS.items():
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
hass, assistant, switch_as_x_entity_entry.entity_id, not should_expose hass, assistant, switch_as_x_entity_entry.entity_id, not should_expose
) )
@ -850,7 +850,7 @@ async def test_restore_expose_settings(
suggested_object_id="abc", suggested_object_id="abc",
) )
for assistant, should_expose in EXPOSE_SETTINGS.items(): for assistant, should_expose in EXPOSE_SETTINGS.items():
await exposed_entities.async_expose_entity( exposed_entities.async_expose_entity(
hass, assistant, switch_as_x_entity_entry.entity_id, should_expose hass, assistant, switch_as_x_entity_entry.entity_id, should_expose
) )