diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index cdbea2ca346..f1c4ad729c6 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -84,8 +84,7 @@ class AbstractConfig(ABC): unsub_func() self._unsub_proactive_report = None - @callback - def should_expose(self, entity_id): + async def should_expose(self, entity_id): """If an entity should be exposed.""" return False diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index eb23b09627e..ee9ef61787b 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -103,7 +103,7 @@ async def async_api_discovery( discovery_endpoints = [ alexa_entity.serialize_discovery() for alexa_entity in async_get_entities(hass, config) - if config.should_expose(alexa_entity.entity_id) + if await config.should_expose(alexa_entity.entity_id) ] return directive.response( diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index 4dd154ea11f..7aa929abf2c 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -30,7 +30,7 @@ class AlexaDirective: self.entity = self.entity_id = self.endpoint = self.instance = None - def load_entity(self, hass, config): + async def load_entity(self, hass, config): """Set attributes related to the entity for this request. Sets these attributes when self.has_endpoint is True: @@ -49,7 +49,7 @@ class AlexaDirective: self.entity_id = _endpoint_id.replace("#", ".") self.entity = hass.states.get(self.entity_id) - if not self.entity or not config.should_expose(self.entity_id): + if not self.entity or not await config.should_expose(self.entity_id): raise AlexaInvalidEndpointError(_endpoint_id) self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 24229507877..6c2da5c01c1 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -34,7 +34,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True await config.set_authorized(True) if directive.has_endpoint: - directive.load_entity(hass, config) + await directive.load_entity(hass, config) funct_ref = HANDLERS.get((directive.namespace, directive.name)) if funct_ref: diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 9be7381adb6..5c7dd4d1402 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -60,7 +60,7 @@ class AlexaConfig(AbstractConfig): """Return an identifier for the user that represents this config.""" return "" - def should_expose(self, entity_id): + async def should_expose(self, entity_id): """If an entity should be exposed.""" if not self._config[CONF_FILTER].empty_filter: return self._config[CONF_FILTER](entity_id) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index a189c364c02..b9e1426bbc1 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -64,7 +64,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): if new_state.domain not in ENTITY_ADAPTERS: return - if not smart_home_config.should_expose(changed_entity): + if not await smart_home_config.should_expose(changed_entity): _LOGGER.debug("Not exposing %s because filtered by config", changed_entity) return diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 9c691ebed55..06d6589204b 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -257,14 +257,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): and entity_supported(self.hass, entity_id) ) - def should_expose(self, entity_id): + async def should_expose(self, entity_id): """If an entity should be exposed.""" if not self._config[CONF_FILTER].empty_filter: if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False return self._config[CONF_FILTER](entity_id) - return async_should_expose(self.hass, CLOUD_ALEXA, entity_id) + return await async_should_expose(self.hass, CLOUD_ALEXA, entity_id) @callback def async_invalidate_access_token(self): @@ -423,7 +423,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): is_enabled = self.enabled for entity in alexa_entities.async_get_entities(self.hass, self): - if is_enabled and self.should_expose(entity.entity_id): + if is_enabled and await self.should_expose(entity.entity_id): to_update.append(entity.entity_id) else: to_remove.append(entity.entity_id) @@ -482,7 +482,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): entity_id = event.data["entity_id"] - if not self.should_expose(entity_id): + if not await self.should_expose(entity_id): return action = event.data["action"] diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 29b9c62ea1d..4c8ebfbd9e9 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -222,9 +222,9 @@ class CloudGoogleConfig(AbstractConfig): self._handle_device_registry_updated, ) - def should_expose(self, state): + async def should_expose(self, state): """If a state object should be exposed.""" - return self._should_expose_entity_id(state.entity_id) + return await self._should_expose_entity_id(state.entity_id) def _should_expose_legacy(self, entity_id): """If an entity ID should be exposed.""" @@ -258,14 +258,14 @@ class CloudGoogleConfig(AbstractConfig): and _supported_legacy(self.hass, entity_id) ) - def _should_expose_entity_id(self, entity_id): + async def _should_expose_entity_id(self, entity_id): """If an entity should be exposed.""" if not self._config[CONF_FILTER].empty_filter: if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False return self._config[CONF_FILTER](entity_id) - return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id) + return await async_should_expose(self.hass, CLOUD_GOOGLE, entity_id) @property def agent_user_id(self): @@ -358,8 +358,7 @@ class CloudGoogleConfig(AbstractConfig): """Handle updated preferences.""" self.async_schedule_google_sync_all() - @callback - def _handle_entity_registry_updated(self, event: Event) -> None: + async def _handle_entity_registry_updated(self, event: Event) -> None: """Handle when entity registry updated.""" if ( not self.enabled @@ -376,13 +375,12 @@ class CloudGoogleConfig(AbstractConfig): entity_id = event.data["entity_id"] - if not self._should_expose_entity_id(entity_id): + if not await self._should_expose_entity_id(entity_id): return self.async_schedule_google_sync_all() - @callback - def _handle_device_registry_updated(self, event: Event) -> None: + async def _handle_device_registry_updated(self, event: Event) -> None: """Handle when device registry updated.""" if ( not self.enabled @@ -396,13 +394,15 @@ class CloudGoogleConfig(AbstractConfig): return # Check if any exposed entity uses the device area - 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( - er.async_get(self.hass), event.data["device_id"] - ) + used = False + for entity_entry in er.async_entries_for_device( + 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 self.async_schedule_google_sync_all() diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index f156acfd568..b27a6ebee02 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -94,7 +94,7 @@ CONFIG_SCHEMA = vol.Schema( def _get_agent_manager(hass: HomeAssistant) -> AgentManager: """Get the active agent.""" manager = AgentManager(hass) - manager.async_setup() + hass.async_create_task(manager.async_setup()) return manager @@ -393,9 +393,9 @@ class AgentManager: self._agents: dict[str, AbstractConversationAgent] = {} self._builtin_agent_init_lock = asyncio.Lock() - def async_setup(self) -> None: + async def async_setup(self) -> None: """Set up the conversation agents.""" - async_setup_default_agent(self.hass) + await async_setup_default_agent(self.hass) async def async_get_agent( self, agent_id: str | None = None diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index d347140af2e..d57de76f5e0 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -73,23 +73,20 @@ def _get_language_variations(language: str) -> Iterable[str]: yield lang -@core.callback -def async_setup(hass: core.HomeAssistant) -> None: +async def async_setup(hass: core.HomeAssistant) -> None: """Set up entity registry listener for the default agent.""" entity_registry = er.async_get(hass) for entity_id in entity_registry.entities: - async_should_expose(hass, DOMAIN, entity_id) + await async_should_expose(hass, DOMAIN, entity_id) - @core.callback - def async_handle_entity_registry_changed(event: core.Event) -> None: + async def async_handle_entity_registry_changed(event: core.Event) -> None: """Set expose flag on newly created entities.""" if event.data["action"] == "create": - async_should_expose(hass, DOMAIN, event.data["entity_id"]) + await async_should_expose(hass, DOMAIN, event.data["entity_id"]) hass.bus.async_listen( er.EVENT_ENTITY_REGISTRY_UPDATED, async_handle_entity_registry_changed, - run_immediately=True, ) @@ -157,7 +154,7 @@ class DefaultAgent(AbstractConversationAgent): conversation_id, ) - slot_lists = self._make_slot_lists() + slot_lists = await self._make_slot_lists() result = await self.hass.async_add_executor_job( self._recognize, @@ -486,7 +483,7 @@ class DefaultAgent(AbstractConversationAgent): """Handle updated preferences.""" self._slot_lists = None - def _make_slot_lists(self) -> dict[str, SlotList]: + async def _make_slot_lists(self) -> dict[str, SlotList]: """Create slot lists with areas and entity names/aliases.""" if self._slot_lists is not None: return self._slot_lists @@ -496,7 +493,7 @@ class DefaultAgent(AbstractConversationAgent): entities = [ entity for entity in entity_registry.entities.values() - if async_should_expose(self.hass, DOMAIN, entity.entity_id) + if await async_should_expose(self.hass, DOMAIN, entity.entity_id) ] devices = dr.async_get(self.hass) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index e194242df91..d192b2514de 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -175,7 +175,7 @@ class AbstractConfig(ABC): """Get agent user ID from context.""" @abstractmethod - def should_expose(self, state) -> bool: + async def should_expose(self, state) -> bool: """Return if entity should be exposed.""" def should_2fa(self, state): @@ -535,16 +535,14 @@ class GoogleEntity: ] return self._traits - @callback - def should_expose(self): + async def should_expose(self): """If entity should be exposed.""" - return self.config.should_expose(self.state) + return await self.config.should_expose(self.state) - @callback - def should_expose_local(self) -> bool: + async def should_expose_local(self) -> bool: """Return if the entity should be exposed locally.""" return ( - self.should_expose() + await self.should_expose() and get_google_type( self.state.domain, self.state.attributes.get(ATTR_DEVICE_CLASS) ) @@ -587,7 +585,7 @@ class GoogleEntity: trait.might_2fa(domain, features, device_class) for trait in self.traits() ) - def sync_serialize(self, agent_user_id, instance_uuid): + async def sync_serialize(self, agent_user_id, instance_uuid): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync @@ -623,7 +621,7 @@ class GoogleEntity: device["name"]["nicknames"].extend(entity_entry.aliases) # Add local SDK info if enabled - if self.config.is_local_sdk_active and self.should_expose_local(): + if self.config.is_local_sdk_active and await self.should_expose_local(): device["otherDeviceIds"] = [{"deviceId": self.entity_id}] device["customData"] = { "webhookId": self.config.get_local_webhook_id(agent_user_id), diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 84d5e4a3364..4aadc9c4002 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -111,7 +111,7 @@ class GoogleConfig(AbstractConfig): """Return if states should be proactively reported.""" return self._config.get(CONF_REPORT_STATE) - def should_expose(self, state) -> bool: + async def should_expose(self, state) -> bool: """Return if entity should be exposed.""" expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) exposed_domains = self._config.get(CONF_EXPOSED_DOMAINS) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 737b54c8b1e..c0a65cbfa7a 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -63,7 +63,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig if not new_state: return - if not google_config.should_expose(new_state): + if not await google_config.should_expose(new_state): return 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) for entity in async_get_entities(hass, google_config): - if not entity.should_expose(): + if not await entity.should_expose(): continue try: diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 1b1b443baac..798743a447d 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -87,11 +87,11 @@ async def async_devices_sync_response(hass, config, agent_user_id): devices = [] for entity in entities: - if not entity.should_expose(): + if not await entity.should_expose(): continue try: - devices.append(entity.sync_serialize(agent_user_id, instance_uuid)) + devices.append(await entity.sync_serialize(agent_user_id, instance_uuid)) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error serializing %s", entity.entity_id) @@ -318,7 +318,7 @@ async def async_devices_reachable( "devices": [ entity.reachable_device_serialize() for entity in async_get_entities(hass, data.config) - if entity.entity_id in google_ids and entity.should_expose_local() + if entity.entity_id in google_ids and await entity.should_expose_local() ] } diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 987a4317ba8..45646b72b7f 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -343,7 +343,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no ) exposed_entities = ExposedEntities(hass) - await exposed_entities.async_initialize() + await exposed_entities.async_load() hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities return True diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index b246c26e91c..9217e073fe4 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping import dataclasses +from itertools import chain from typing import Any import voluptuous as vol @@ -14,6 +15,11 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.exceptions import HomeAssistantError 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.storage import Store @@ -77,25 +83,58 @@ class AssistantPreferences: return {"expose_new": self.expose_new} -class ExposedEntities: - """Control assistant settings.""" +@dataclasses.dataclass(frozen=True) +class ExposedEntity: + """An exposed entity without a unique_id.""" + + assistants: dict[str, dict[str, Any]] + + def to_json(self, entity_id: str) -> dict[str, Any]: + """Return a JSON serializable representation for storage.""" + return { + "assistants": self.assistants, + "id": entity_id, + } + + +class SerializedExposedEntities(SerializedStorageCollection): + """Serialized exposed entities storage storage collection.""" + + assistants: dict[str, dict[str, Any]] + + +class ExposedEntitiesIDManager(IDManager): + """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. + + Settings for entities without a unique_id are stored in the store. + Settings for entities with a unique_id are stored in the entity registry. + """ _assistants: dict[str, AssistantPreferences] def __init__(self, hass: HomeAssistant) -> None: """Initialize.""" - self._hass = hass - self._listeners: dict[str, list[Callable[[], None]]] = {} - self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store( - hass, STORAGE_VERSION, STORAGE_KEY + super().__init__( + Store(hass, STORAGE_VERSION, STORAGE_KEY), ExposedEntitiesIDManager() ) + self._listeners: dict[str, list[Callable[[], None]]] = {} - async def async_initialize(self) -> None: + async def async_load(self) -> None: """Finish initializing.""" - 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_set) - await self.async_load() + await super().async_load() + 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_set) + websocket_api.async_register_command(self.hass, ws_list_exposed_entities) @callback def async_listen_entity_updates( @@ -104,17 +143,18 @@ class ExposedEntities: """Listen for updates to entity expose settings.""" self._listeners.setdefault(assistant, []).append(listener) - @callback - def async_expose_entity( + async def async_expose_entity( self, assistant: str, entity_id: str, should_expose: bool ) -> None: """Expose an entity to an assistant. 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)): - raise HomeAssistantError("Unknown entity") + return await self._async_expose_legacy_entity( + assistant, entity_id, should_expose + ) assistant_options: Mapping[str, Any] if ( @@ -129,6 +169,34 @@ class ExposedEntities: for listener in self._listeners.get(assistant, []): listener() + async def _async_expose_legacy_entity( + self, assistant: str, entity_id: str, should_expose: bool + ) -> None: + """Expose an entity to an assistant. + + Notify listeners if expose flag was changed. + """ + if ( + (exposed_entity := self.data.get(entity_id)) + and (assistant_options := exposed_entity.assistants.get(assistant, {})) + and assistant_options.get("should_expose") == should_expose + ): + return + + if exposed_entity: + await self.async_update_item( + entity_id, {"assistants": {assistant: {"should_expose": should_expose}}} + ) + else: + await self.async_create_item( + { + "entity_id": entity_id, + "assistants": {assistant: {"should_expose": should_expose}}, + } + ) + for listener in self._listeners.get(assistant, []): + listener() + @callback def async_get_expose_new_entities(self, assistant: str) -> bool: """Check if new entities are exposed to an assistant.""" @@ -147,9 +215,14 @@ class ExposedEntities: self, assistant: str ) -> dict[str, Mapping[str, Any]]: """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]] = {} + options: Mapping | None + for entity_id, exposed_entity in self.data.items(): + if options := exposed_entity.assistants.get(assistant): + result[entity_id] = options + for entity_id, entry in entity_registry.entities.items(): if options := entry.options.get(assistant): result[entity_id] = options @@ -159,31 +232,33 @@ class ExposedEntities: @callback def async_get_entity_settings(self, entity_id: str) -> dict[str, Mapping[str, Any]]: """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]] = {} - if not (registry_entry := entity_registry.async_get(entity_id)): + assistant_settings: Mapping + if registry_entry := entity_registry.async_get(entity_id): + assistant_settings = registry_entry.options + elif exposed_entity := self.data.get(entity_id): + assistant_settings = exposed_entity.assistants + else: raise HomeAssistantError("Unknown entity") for assistant in KNOWN_ASSISTANTS: - if options := registry_entry.options.get(assistant): + if options := assistant_settings.get(assistant): result[assistant] = options return result - @callback - def async_should_expose(self, assistant: str, entity_id: str) -> bool: + async def async_should_expose(self, assistant: str, entity_id: str) -> bool: """Return True if an entity should be exposed to an assistant.""" should_expose: bool if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: 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)): - # Entities which are not in the entity registry are not exposed - return False - + return await self._async_should_expose_legacy_entity(assistant, entity_id) if assistant in registry_entry.options: if "should_expose" in registry_entry.options[assistant]: should_expose = registry_entry.options[assistant]["should_expose"] @@ -202,11 +277,43 @@ class ExposedEntities: return should_expose + async def _async_should_expose_legacy_entity( + self, assistant: str, entity_id: str + ) -> bool: + """Return True if an entity should be exposed to an assistant.""" + should_expose: bool + + if ( + exposed_entity := self.data.get(entity_id) + ) and assistant in exposed_entity.assistants: + if "should_expose" in exposed_entity.assistants[assistant]: + 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 + + if exposed_entity: + await self.async_update_item( + entity_id, {"assistants": {assistant: {"should_expose": should_expose}}} + ) + else: + await self.async_create_item( + { + "entity_id": entity_id, + "assistants": {assistant: {"should_expose": should_expose}}, + } + ) + + return should_expose + def _is_default_exposed( - self, entity_id: str, registry_entry: er.RegistryEntry + self, entity_id: str, registry_entry: er.RegistryEntry | None ) -> bool: """Return True if an entity is exposed by default.""" - if ( + if registry_entry and ( registry_entry.entity_category is not None or registry_entry.hidden_by is not None ): @@ -216,7 +323,7 @@ class ExposedEntities: if domain in DEFAULT_EXPOSED_DOMAINS: return True - device_class = get_device_class(self._hass, entity_id) + device_class = get_device_class(self.hass, entity_id) if ( domain == "binary_sensor" and device_class in DEFAULT_EXPOSED_BINARY_SENSOR_DEVICE_CLASSES @@ -228,37 +335,71 @@ class ExposedEntities: return False - async def async_load(self) -> None: + async def _process_create_data(self, data: dict) -> dict: + """Validate the config is valid.""" + return data + + @callback + 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: + """Return a new updated item.""" + new_assistant_settings: dict[str, Any] = update_data["assistants"] + old_assistant_settings = item.assistants + for assistant, old_settings in old_assistant_settings.items(): + new_settings = new_assistant_settings.get(assistant, {}) + new_assistant_settings[assistant] = old_settings | new_settings + return dataclasses.replace(item, assistants=new_assistant_settings) + + def _create_item(self, item_id: str, data: dict) -> ExposedEntity: + """Create an item from validated config.""" + del data["entity_id"] + return ExposedEntity(**data) + + def _deserialize_item(self, data: dict) -> ExposedEntity: + """Create an item from its serialized representation.""" + del data["entity_id"] + return ExposedEntity(**data) + + 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: """Load from the store.""" - data = await self._store.async_load() + data = await super()._async_load_data() assistants: dict[str, AssistantPreferences] = {} - if data: + if data and "assistants" in data: for domain, preferences in data["assistants"].items(): assistants[domain] = AssistantPreferences(**preferences) self._assistants = assistants - @callback - def _async_schedule_save(self) -> None: - """Schedule saving the preferences.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - - @callback - def _data_to_save(self) -> dict[str, dict[str, dict[str, Any]]]: - """Return data to store in a file.""" - data = {} - - data["assistants"] = { - domain: preferences.to_json() - for domain, preferences in self._assistants.items() - } + if data and "items" not in data: + return None # type: ignore[unreachable] return data + @callback + def _data_to_save(self) -> SerializedExposedEntities: + """Return JSON-compatible date for storing to file.""" + base_data = super()._base_data_to_save() + return { + "items": base_data["items"], + "assistants": { + domain: preferences.to_json() + for domain, preferences in self._assistants.items() + }, + } + -@callback @websocket_api.require_admin @websocket_api.websocket_command( { @@ -268,11 +409,11 @@ class ExposedEntities: vol.Required("should_expose"): bool, } ) -def ws_expose_entity( +@websocket_api.async_response +async def ws_expose_entity( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Expose an entity to an assistant.""" - entity_registry = er.async_get(hass) entity_ids: str = msg["entity_ids"] if blocked := next( @@ -288,28 +429,40 @@ def ws_expose_entity( ) return - if unknown := next( - ( - entity_id - for entity_id in entity_ids - if entity_id not in entity_registry.entities - ), - None, - ): - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, f"can't expose '{unknown}'" - ) - return - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] for entity_id in entity_ids: for assistant in msg["assistants"]: - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( assistant, entity_id, msg["should_expose"] ) connection.send_result(msg["id"]) +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "homeassistant/expose_entity/list", + } +) +def ws_list_exposed_entities( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Expose an entity to an assistant.""" + result: dict[str, Any] = {} + + exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + entity_registry = er.async_get(hass) + for entity_id in chain(exposed_entities.data, entity_registry.entities): + result[entity_id] = {} + entity_settings = async_get_entity_settings(hass, entity_id) + for assistant, settings in entity_settings.items(): + if "should_expose" not in settings: + continue + result[entity_id][assistant] = settings["should_expose"] + connection.send_result(msg["id"], {"exposed_entities": result}) + + @callback @websocket_api.require_admin @websocket_api.websocket_command( @@ -372,8 +525,7 @@ def async_get_entity_settings( return exposed_entities.async_get_entity_settings(entity_id) -@callback -def async_expose_entity( +async def async_expose_entity( hass: HomeAssistant, assistant: str, entity_id: str, @@ -381,11 +533,12 @@ def async_expose_entity( ) -> None: """Get assistant expose settings for an entity.""" exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] - exposed_entities.async_expose_entity(assistant, entity_id, should_expose) + await exposed_entities.async_expose_entity(assistant, entity_id, should_expose) -@callback -def async_should_expose(hass: HomeAssistant, assistant: str, entity_id: str) -> bool: +async def async_should_expose( + hass: HomeAssistant, assistant: str, entity_id: str +) -> bool: """Return True if an entity should be exposed to an assistant.""" exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] - return exposed_entities.async_should_expose(assistant, entity_id) + return await exposed_entities.async_should_expose(assistant, entity_id) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index ef64a86c6e8..6e6dffd2337 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -138,6 +138,6 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: for assistant, settings in expose_settings.items(): if (should_expose := settings.get("should_expose")) is None: continue - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( hass, assistant, switch_entity_id, should_expose ) diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index a73271bdc83..d2e3995b85e 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -111,7 +111,7 @@ class BaseEntity(Entity): return registry.async_update_entity(self.entity_id, name=wrapped_switch.name) - def copy_expose_settings() -> None: + async def copy_expose_settings() -> None: """Copy assistant expose settings from the wrapped entity. Also unexpose the wrapped entity if exposed. @@ -122,15 +122,15 @@ class BaseEntity(Entity): for assistant, settings in expose_settings.items(): if (should_expose := settings.get("should_expose")) is None: continue - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( self.hass, assistant, self.entity_id, should_expose ) - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( self.hass, assistant, self._switch_entity_id, False ) copy_custom_name(wrapped_switch) - copy_expose_settings() + await copy_expose_settings() class BaseToggleEntity(BaseEntity, ToggleEntity): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 601f59fd118..36cc005bf2f 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2450,13 +2450,18 @@ async def test_exclude_filters(hass: HomeAssistant) -> None: hass.states.async_set("cover.deny", "off", {"friendly_name": "Blocked cover"}) alexa_config = MockConfig(hass) - alexa_config.should_expose = entityfilter.generate_filter( + filter = entityfilter.generate_filter( include_domains=[], include_entities=[], exclude_domains=["script"], 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) await hass.async_block_till_done() @@ -2481,13 +2486,18 @@ async def test_include_filters(hass: HomeAssistant) -> None: hass.states.async_set("group.allow", "off", {"friendly_name": "Allowed group"}) alexa_config = MockConfig(hass) - alexa_config.should_expose = entityfilter.generate_filter( + filter = entityfilter.generate_filter( include_domains=["automation", "group"], include_entities=["script.deny"], exclude_domains=[], 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) await hass.async_block_till_done() @@ -2506,13 +2516,18 @@ async def test_never_exposed_entities(hass: HomeAssistant) -> None: hass.states.async_set("group.allow", "off", {"friendly_name": "Allowed group"}) alexa_config = MockConfig(hass) - alexa_config.should_expose = entityfilter.generate_filter( + filter = entityfilter.generate_filter( include_domains=["group"], include_entities=[], exclude_domains=[], 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) await hass.async_block_till_done() diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index f0202e3d958..8d37eba219a 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -370,6 +370,7 @@ async def test_websocket_update_orientation_prefs( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera ) -> None: """Test updating camera preferences.""" + await async_setup_component(hass, "homeassistant", {}) client = await hass_ws_client(hass) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 8001411ac71..46a778f5e31 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1888,6 +1888,7 @@ async def test_failed_cast_other_url( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test warning when casting from internal_url fails.""" + await async_setup_component(hass, "homeassistant", {}) with assert_setup_component(1, tts.DOMAIN): assert await async_setup_component( hass, @@ -1911,6 +1912,7 @@ async def test_failed_cast_internal_url( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test warning when casting from internal_url fails.""" + await async_setup_component(hass, "homeassistant", {}) await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -1939,6 +1941,7 @@ async def test_failed_cast_external_url( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test warning when casting from external_url fails.""" + await async_setup_component(hass, "homeassistant", {}) await async_process_ha_core_config( hass, {"external_url": "http://example.com:8123"}, @@ -1969,6 +1972,7 @@ async def test_failed_cast_tts_base_url( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test warning when casting from tts.base_url fails.""" + await async_setup_component(hass, "homeassistant", {}) with assert_setup_component(1, tts.DOMAIN): assert await async_setup_component( hass, diff --git a/tests/components/climate/test_recorder.py b/tests/components/climate/test_recorder.py index 435d1378e84..b6acf375f2e 100644 --- a/tests/components/climate/test_recorder.py +++ b/tests/components/climate/test_recorder.py @@ -29,6 +29,7 @@ from tests.components.recorder.common import async_wait_recording_done async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test climate registered attributes to be excluded.""" now = dt_util.utcnow() + await async_setup_component(hass, "homeassistant", {}) await async_setup_component( hass, climate.DOMAIN, {climate.DOMAIN: {"platform": "demo"}} ) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 0e1f941ab64..bf4890e92dd 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -18,7 +18,6 @@ from homeassistant.components.homeassistant.exposed_entities import ( ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.setup import async_setup_component @@ -39,10 +38,10 @@ def expose_new(hass, expose_new): exposed_entities.async_set_expose_new_entities("cloud.alexa", expose_new) -def expose_entity(hass, entity_id, should_expose): +async def expose_entity(hass, entity_id, should_expose): """Expose an entity to Alexa.""" exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] - exposed_entities.async_expose_entity("cloud.alexa", entity_id, should_expose) + await exposed_entities.async_expose_entity("cloud.alexa", entity_id, should_expose) async def test_alexa_config_expose_entity_prefs( @@ -96,36 +95,35 @@ async def test_alexa_config_expose_entity_prefs( alexa_report_state=False, ) expose_new(hass, True) - expose_entity(hass, entity_entry5.entity_id, False) + await expose_entity(hass, entity_entry5.entity_id, False) conf = alexa_config.CloudAlexaConfig( hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub ) await conf.async_initialize() - # can't expose an entity which is not in the entity registry - with pytest.raises(HomeAssistantError): - expose_entity(hass, "light.kitchen", True) - assert not conf.should_expose("light.kitchen") + # an entity which is not in the entity registry can be exposed + await expose_entity(hass, "light.kitchen", True) + assert await conf.should_expose("light.kitchen") # categorized and hidden entities should not be exposed - assert not conf.should_expose(entity_entry1.entity_id) - assert not conf.should_expose(entity_entry2.entity_id) - assert not conf.should_expose(entity_entry3.entity_id) - assert not conf.should_expose(entity_entry4.entity_id) + assert not await conf.should_expose(entity_entry1.entity_id) + assert not await conf.should_expose(entity_entry2.entity_id) + assert not await conf.should_expose(entity_entry3.entity_id) + assert not await conf.should_expose(entity_entry4.entity_id) # this has been hidden - assert not conf.should_expose(entity_entry5.entity_id) + assert not await conf.should_expose(entity_entry5.entity_id) # exposed by default - assert conf.should_expose(entity_entry6.entity_id) + assert await conf.should_expose(entity_entry6.entity_id) - expose_entity(hass, entity_entry5.entity_id, True) - assert conf.should_expose(entity_entry5.entity_id) + await expose_entity(hass, entity_entry5.entity_id, True) + assert await conf.should_expose(entity_entry5.entity_id) - expose_entity(hass, entity_entry5.entity_id, None) - assert not conf.should_expose(entity_entry5.entity_id) + await expose_entity(hass, entity_entry5.entity_id, None) + assert not await conf.should_expose(entity_entry5.entity_id) assert "alexa" not in hass.config.components await hass.async_block_till_done() assert "alexa" in hass.config.components - assert not conf.should_expose(entity_entry5.entity_id) + assert not await conf.should_expose(entity_entry5.entity_id) async def test_alexa_config_report_state( @@ -370,7 +368,7 @@ async def test_alexa_update_expose_trigger_sync( await conf.async_initialize() with patch_sync_helper() as (to_update, to_remove): - expose_entity(hass, light_entry.entity_id, True) + await expose_entity(hass, light_entry.entity_id, True) await hass.async_block_till_done() async_fire_time_changed(hass, fire_all=True) await hass.async_block_till_done() @@ -380,9 +378,9 @@ async def test_alexa_update_expose_trigger_sync( assert to_remove == [] with patch_sync_helper() as (to_update, to_remove): - expose_entity(hass, light_entry.entity_id, False) - expose_entity(hass, binary_sensor_entry.entity_id, True) - expose_entity(hass, sensor_entry.entity_id, True) + await expose_entity(hass, light_entry.entity_id, False) + await expose_entity(hass, binary_sensor_entry.entity_id, True) + await expose_entity(hass, sensor_entry.entity_id, True) await hass.async_block_till_done() async_fire_time_changed(hass, fire_all=True) await hass.async_block_till_done() @@ -588,7 +586,7 @@ async def test_alexa_config_migrate_expose_entity_prefs( alexa_report_state=False, alexa_settings_version=1, ) - expose_entity(hass, entity_migrated.entity_id, False) + await expose_entity(hass, entity_migrated.entity_id, False) cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.unknown"] = { PREF_SHOULD_EXPOSE: True diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index d1e1a8ce112..9bca4b79340 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -265,13 +265,13 @@ async def test_google_config_expose_entity( state = State(entity_entry.entity_id, "on") gconf = await cloud_client.get_google_config() - assert gconf.should_expose(state) + assert await gconf.should_expose(state) - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( "cloud.google_assistant", entity_entry.entity_id, False ) - assert not gconf.should_expose(state) + assert not await gconf.should_expose(state) async def test_google_config_should_2fa( diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 8b927f7f3aa..51e8de98301 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -21,7 +21,6 @@ from homeassistant.components.homeassistant.exposed_entities import ( ) from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EntityCategory from homeassistant.core import CoreState, HomeAssistant, State -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -47,10 +46,10 @@ def expose_new(hass, expose_new): exposed_entities.async_set_expose_new_entities("cloud.google_assistant", expose_new) -def expose_entity(hass, entity_id, should_expose): +async def expose_entity(hass, entity_id, should_expose): """Expose an entity to Google.""" exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( "cloud.google_assistant", entity_id, should_expose ) @@ -151,7 +150,7 @@ async def test_google_update_expose_trigger_sync( with patch.object(config, "async_sync_entities") as mock_sync, patch.object( ga_helpers, "SYNC_DELAY", 0 ): - expose_entity(hass, light_entry.entity_id, True) + await expose_entity(hass, light_entry.entity_id, True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow()) await hass.async_block_till_done() @@ -161,9 +160,9 @@ async def test_google_update_expose_trigger_sync( with patch.object(config, "async_sync_entities") as mock_sync, patch.object( ga_helpers, "SYNC_DELAY", 0 ): - expose_entity(hass, light_entry.entity_id, False) - expose_entity(hass, binary_sensor_entry.entity_id, True) - expose_entity(hass, sensor_entry.entity_id, True) + await expose_entity(hass, light_entry.entity_id, False) + await expose_entity(hass, binary_sensor_entry.entity_id, True) + await expose_entity(hass, sensor_entry.entity_id, True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow()) await hass.async_block_till_done() @@ -385,7 +384,7 @@ async def test_google_config_expose_entity_prefs( ) expose_new(hass, True) - expose_entity(hass, entity_entry5.entity_id, False) + await expose_entity(hass, entity_entry5.entity_id, False) state = State("light.kitchen", "on") state_config = State(entity_entry1.entity_id, "on") @@ -395,25 +394,24 @@ async def test_google_config_expose_entity_prefs( state_not_exposed = State(entity_entry5.entity_id, "on") state_exposed_default = State(entity_entry6.entity_id, "on") - # can't expose an entity which is not in the entity registry - with pytest.raises(HomeAssistantError): - expose_entity(hass, "light.kitchen", True) - assert not mock_conf.should_expose(state) + # an entity which is not in the entity registry can be exposed + await expose_entity(hass, "light.kitchen", True) + assert await mock_conf.should_expose(state) # categorized and hidden entities should not be exposed - assert not mock_conf.should_expose(state_config) - assert not mock_conf.should_expose(state_diagnostic) - assert not mock_conf.should_expose(state_hidden_integration) - assert not mock_conf.should_expose(state_hidden_user) + assert not await mock_conf.should_expose(state_config) + assert not await mock_conf.should_expose(state_diagnostic) + assert not await mock_conf.should_expose(state_hidden_integration) + assert not await mock_conf.should_expose(state_hidden_user) # this has been hidden - assert not mock_conf.should_expose(state_not_exposed) + assert not await mock_conf.should_expose(state_not_exposed) # exposed by default - assert mock_conf.should_expose(state_exposed_default) + assert await mock_conf.should_expose(state_exposed_default) - expose_entity(hass, entity_entry5.entity_id, True) - assert mock_conf.should_expose(state_not_exposed) + await expose_entity(hass, entity_entry5.entity_id, True) + assert await mock_conf.should_expose(state_not_exposed) - expose_entity(hass, entity_entry5.entity_id, None) - assert not mock_conf.should_expose(state_not_exposed) + await expose_entity(hass, entity_entry5.entity_id, None) + assert not await mock_conf.should_expose(state_not_exposed) def test_enabled_requires_valid_sub( @@ -537,7 +535,7 @@ async def test_google_config_migrate_expose_entity_prefs( google_report_state=False, google_settings_version=1, ) - expose_entity(hass, entity_migrated.entity_id, False) + await expose_entity(hass, entity_migrated.entity_id, False) cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.unknown"] = { PREF_SHOULD_EXPOSE: True diff --git a/tests/components/color_extractor/test_service.py b/tests/components/color_extractor/test_service.py index a928be477fb..b1236af89fb 100644 --- a/tests/components/color_extractor/test_service.py +++ b/tests/components/color_extractor/test_service.py @@ -32,6 +32,12 @@ LIGHT_ENTITY = "light.kitchen_lights" CLOSE_THRESHOLD = 10 +@pytest.fixture(autouse=True) +async def setup_homeassistant(hass: HomeAssistant): + """Set up the homeassistant integration.""" + await async_setup_component(hass, "homeassistant", {}) + + def _close_enough(actual_rgb, testing_rgb): """Validate the given RGB value is in acceptable tolerance.""" # Convert the given RGB values to hue / saturation and then back again diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index 6eadb068054..fb455da945b 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -51,7 +51,9 @@ def expose_new(hass, expose_new): exposed_entities.async_set_expose_new_entities(conversation.DOMAIN, expose_new) -def expose_entity(hass, entity_id, should_expose): +async def expose_entity(hass, entity_id, should_expose): """Expose an entity to the default agent.""" exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] - exposed_entities.async_expose_entity(conversation.DOMAIN, entity_id, should_expose) + await exposed_entities.async_expose_entity( + conversation.DOMAIN, entity_id, should_expose + ) diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 44bb3111987..daba360f7bf 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -108,7 +108,7 @@ async def test_exposed_areas( hass.states.async_set(bedroom_light.entity_id, "on") # Hide the bedroom light - expose_entity(hass, bedroom_light.entity_id, False) + await expose_entity(hass, bedroom_light.entity_id, False) result = await conversation.async_converse( hass, "turn on lights in the kitchen", None, Context(), None diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 6b981cbbdfa..b6bb4aad68f 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -680,7 +680,7 @@ async def test_http_processing_intent_entity_exposed( } # Unexpose the entity - expose_entity(hass, "light.kitchen", False) + await expose_entity(hass, "light.kitchen", False) await hass.async_block_till_done() client = await hass_client() @@ -730,7 +730,7 @@ async def test_http_processing_intent_entity_exposed( } # Now expose the entity - expose_entity(hass, "light.kitchen", True) + await expose_entity(hass, "light.kitchen", True) await hass.async_block_till_done() client = await hass_client() @@ -845,7 +845,7 @@ async def test_http_processing_intent_conversion_not_expose_new( } # Expose the entity - expose_entity(hass, "light.kitchen", True) + await expose_entity(hass, "light.kitchen", True) await hass.async_block_till_done() resp = await client.post( diff --git a/tests/components/demo/conftest.py b/tests/components/demo/conftest.py index 96032c12018..a6182289a86 100644 --- a/tests/components/demo/conftest.py +++ b/tests/components/demo/conftest.py @@ -1,3 +1,14 @@ """demo conftest.""" +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 from tests.components.light.conftest import mock_light_profiles # noqa: F401 + + +@pytest.fixture(autouse=True) +async def setup_homeassistant(hass: HomeAssistant): + """Set up the homeassistant integration.""" + await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py index 9b73957ef71..5d294ec3bba 100644 --- a/tests/components/emulated_kasa/test_init.py +++ b/tests/components/emulated_kasa/test_init.py @@ -2,6 +2,8 @@ import math from unittest.mock import AsyncMock, Mock, patch +import pytest + from homeassistant.components import emulated_kasa from homeassistant.components.emulated_kasa.const import ( CONF_POWER, @@ -132,6 +134,12 @@ CONFIG_SENSOR = { } +@pytest.fixture(autouse=True) +async def setup_homeassistant(hass: HomeAssistant): + """Set up the homeassistant integration.""" + await async_setup_component(hass, "homeassistant", {}) + + def nested_value(ndict, *keys): """Return a nested dict value or None if it doesn't exist.""" if len(keys) == 0: diff --git a/tests/components/fan/test_recorder.py b/tests/components/fan/test_recorder.py index 90cf2f59ee0..f60a06cb4c5 100644 --- a/tests/components/fan/test_recorder.py +++ b/tests/components/fan/test_recorder.py @@ -19,6 +19,7 @@ from tests.components.recorder.common import async_wait_recording_done async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test fan registered attributes to be excluded.""" now = dt_util.utcnow() + await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "demo"}}) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 2122818bbb4..29ad5e6710a 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -58,7 +58,7 @@ class MockConfig(helpers.AbstractConfig): """Get agent user ID making request.""" return context.user_id - def should_expose(self, state): + async def should_expose(self, state): """Expose it all.""" return self._should_expose is None or self._should_expose(state) diff --git a/tests/components/google_assistant/test_diagnostics.py b/tests/components/google_assistant/test_diagnostics.py index 3f7b536f163..5f033319c44 100644 --- a/tests/components/google_assistant/test_diagnostics.py +++ b/tests/components/google_assistant/test_diagnostics.py @@ -20,6 +20,7 @@ async def test_diagnostics( await setup.async_setup_component( hass, switch.DOMAIN, {"switch": [{"platform": "demo"}]} ) + await async_setup_component(hass, "homeassistant", {}) await async_setup_component( hass, diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 793db076c79..cda3ded25be 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -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")) - serialized = entity.sync_serialize(None, "mock-uuid") + serialized = await entity.sync_serialize(None, "mock-uuid") assert "otherDeviceIds" not in serialized assert "customData" not in serialized config.async_enable_local_sdk() - serialized = entity.sync_serialize("mock-user-id", "abcdef") + serialized = await entity.sync_serialize("mock-user-id", "abcdef") assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] assert serialized["customData"] == { "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", return_value=device_type, ): - serialized = entity.sync_serialize(None, "mock-uuid") + serialized = await entity.sync_serialize(None, "mock-uuid") assert "otherDeviceIds" not in serialized assert "customData" not in serialized diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index b7dc880ede0..5297d6b29e5 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -257,7 +257,9 @@ async def test_should_expose(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert ( - config.should_expose(State(DOMAIN + ".mock", "mock", {"view": "not None"})) + await config.should_expose( + State(DOMAIN + ".mock", "mock", {"view": "not None"}) + ) is False ) @@ -265,7 +267,10 @@ async def test_should_expose(hass: HomeAssistant) -> None: # Wait for google_assistant.helpers.async_initialize.sync_google to be called await hass.async_block_till_done() - assert config.should_expose(State(CLOUD_NEVER_EXPOSED_ENTITIES[0], "mock")) is False + assert ( + await config.should_expose(State(CLOUD_NEVER_EXPOSED_ENTITIES[0], "mock")) + is False + ) async def test_missing_service_account(hass: HomeAssistant) -> None: diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 28b7080b730..ff673ddfe24 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -452,6 +452,7 @@ async def test_execute( ) -> None: """Test an execute command.""" await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() await hass.services.async_call( @@ -635,6 +636,7 @@ async def test_execute_times_out( orig_execute_limit = sh.EXECUTE_LIMIT sh.EXECUTE_LIMIT = 0.02 # Decrease timeout to 20ms await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() await hass.services.async_call( @@ -907,7 +909,7 @@ async def test_serialize_input_boolean(hass: HomeAssistant) -> None: """Test serializing an input boolean entity.""" state = State("input_boolean.bla", "on") entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) - result = entity.sync_serialize(None, "mock-uuid") + result = await entity.sync_serialize(None, "mock-uuid") assert result == { "id": "input_boolean.bla", "attributes": {}, diff --git a/tests/components/group/conftest.py b/tests/components/group/conftest.py index e26e98598e6..3aefbfacdf8 100644 --- a/tests/components/group/conftest.py +++ b/tests/components/group/conftest.py @@ -1,2 +1,13 @@ """group conftest.""" +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + from tests.components.light.conftest import mock_light_profiles # noqa: F401 + + +@pytest.fixture(autouse=True) +async def setup_homeassistant(hass: HomeAssistant): + """Set up the homeassistant integration.""" + await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/group/test_recorder.py b/tests/components/group/test_recorder.py index 5dbe1ee5618..3ca965ec998 100644 --- a/tests/components/group/test_recorder.py +++ b/tests/components/group/test_recorder.py @@ -3,6 +3,8 @@ from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components import group from homeassistant.components.group import ATTR_AUTO, ATTR_ENTITY_ID, ATTR_ORDER from homeassistant.components.recorder import Recorder @@ -16,6 +18,11 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done +@pytest.fixture(autouse=True) +async def setup_homeassistant(): + """Override the fixture in group.conftest.""" + + async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test number registered attributes to be excluded.""" now = dt_util.utcnow() diff --git a/tests/components/homeassistant/test_exposed_entities.py b/tests/components/homeassistant/test_exposed_entities.py index 1aa98ab423f..7b3e627011a 100644 --- a/tests/components/homeassistant/test_exposed_entities.py +++ b/tests/components/homeassistant/test_exposed_entities.py @@ -4,13 +4,13 @@ import pytest from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, ExposedEntities, + ExposedEntity, async_get_assistant_settings, async_listen_entity_updates, async_should_expose, ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -31,7 +31,7 @@ async def test_load_preferences(hass: HomeAssistant) -> None: assert list(exposed_entities._assistants) == ["test1", "test2"] exposed_entities2 = ExposedEntities(hass) - await flush_store(exposed_entities._store) + await flush_store(exposed_entities.store) await exposed_entities2.async_load() assert exposed_entities._assistants == exposed_entities2._assistants @@ -50,6 +50,9 @@ async def test_expose_entity( entry1 = entity_registry.async_get_or_create("test", "test", "unique1") entry2 = entity_registry.async_get_or_create("test", "test", "unique2") + exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + assert len(exposed_entities.data) == 0 + # Set options await ws_client.send_json_auto_id( { @@ -67,6 +70,7 @@ async def test_expose_entity( assert entry1.options == {"cloud.alexa": {"should_expose": True}} entry2 = entity_registry.async_get(entry2.entity_id) assert entry2.options == {} + assert len(exposed_entities.data) == 0 # Update options await ws_client.send_json_auto_id( @@ -91,6 +95,7 @@ async def test_expose_entity( "cloud.alexa": {"should_expose": False}, "cloud.google_assistant": {"should_expose": False}, } + assert len(exposed_entities.data) == 0 async def test_expose_entity_unknown( @@ -103,6 +108,7 @@ async def test_expose_entity_unknown( await hass.async_block_till_done() exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + assert len(exposed_entities.data) == 0 # Set options await ws_client.send_json_auto_id( @@ -115,14 +121,41 @@ async def test_expose_entity_unknown( ) response = await ws_client.receive_json() - assert not response["success"] - assert response["error"] == { - "code": "not_found", - "message": "can't expose 'test.test'", + assert response["success"] + + assert len(exposed_entities.data) == 1 + assert exposed_entities.data == { + "test.test": ExposedEntity({"cloud.alexa": {"should_expose": True}}) } - with pytest.raises(HomeAssistantError): - exposed_entities.async_expose_entity("cloud.alexa", "test.test", True) + # Update options + await ws_client.send_json_auto_id( + { + "type": "homeassistant/expose_entity", + "assistants": ["cloud.alexa", "cloud.google_assistant"], + "entity_ids": ["test.test", "test.test2"], + "should_expose": False, + } + ) + + response = await ws_client.receive_json() + assert response["success"] + + assert len(exposed_entities.data) == 2 + assert exposed_entities.data == { + "test.test": ExposedEntity( + { + "cloud.alexa": {"should_expose": False}, + "cloud.google_assistant": {"should_expose": False}, + } + ), + "test.test2": ExposedEntity( + { + "cloud.alexa": {"should_expose": False}, + "cloud.google_assistant": {"should_expose": False}, + } + ), + } async def test_expose_entity_blocked( @@ -178,7 +211,7 @@ async def test_expose_new_entities( assert response["result"] == {"expose_new": False} # Check if exposed - should be False - assert async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False + assert await async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False # Expose new entities to Alexa await ws_client.send_json_auto_id( @@ -201,10 +234,12 @@ async def test_expose_new_entities( assert response["result"] == {"expose_new": expose_new} # Check again if exposed - should still be False - assert async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False + assert await async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False # Check if exposed - should be True - assert async_should_expose(hass, "cloud.alexa", entry2.entity_id) == expose_new + assert ( + await async_should_expose(hass, "cloud.alexa", entry2.entity_id) == expose_new + ) async def test_listen_updates( @@ -226,21 +261,21 @@ async def test_listen_updates( entry = entity_registry.async_get_or_create("climate", "test", "unique1") # Call for another assistant - listener not called - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( "cloud.google_assistant", entry.entity_id, True ) assert len(calls) == 0 # Call for our assistant - listener called - exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) + await exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) assert len(calls) == 1 # Settings not changed - listener not called - exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) + await exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) assert len(calls) == 1 # Settings changed - listener called - exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, False) + await exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, False) assert len(calls) == 2 @@ -258,7 +293,7 @@ async def test_get_assistant_settings( assert async_get_assistant_settings(hass, "cloud.alexa") == {} - exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) + await exposed_entities.async_expose_entity("cloud.alexa", entry.entity_id, True) assert async_get_assistant_settings(hass, "cloud.alexa") == { "climate.test_unique1": {"should_expose": True} } @@ -287,40 +322,44 @@ async def test_should_expose( assert response["success"] # Unknown entity is not exposed - assert async_should_expose(hass, "test.test", "test.test") is False + assert await async_should_expose(hass, "test.test", "test.test") is False # Blocked entity is not exposed entry_blocked = entity_registry.async_get_or_create( "group", "test", "unique", suggested_object_id="all_locks" ) assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] - assert async_should_expose(hass, "cloud.alexa", entry_blocked.entity_id) is False + assert ( + await async_should_expose(hass, "cloud.alexa", entry_blocked.entity_id) is False + ) # Lock is exposed lock1 = entity_registry.async_get_or_create("lock", "test", "unique1") assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] - assert async_should_expose(hass, "cloud.alexa", lock1.entity_id) is True + assert await async_should_expose(hass, "cloud.alexa", lock1.entity_id) is True # Hidden entity is not exposed lock2 = entity_registry.async_get_or_create( "lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER ) assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] - assert async_should_expose(hass, "cloud.alexa", lock2.entity_id) is False + assert await async_should_expose(hass, "cloud.alexa", lock2.entity_id) is False # Entity with category is not exposed lock3 = entity_registry.async_get_or_create( "lock", "test", "unique3", entity_category=EntityCategory.CONFIG ) assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] - assert async_should_expose(hass, "cloud.alexa", lock3.entity_id) is False + assert await async_should_expose(hass, "cloud.alexa", lock3.entity_id) is False # Binary sensor without device class is not exposed binarysensor1 = entity_registry.async_get_or_create( "binary_sensor", "test", "unique1" ) assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] - assert async_should_expose(hass, "cloud.alexa", binarysensor1.entity_id) is False + assert ( + await async_should_expose(hass, "cloud.alexa", binarysensor1.entity_id) is False + ) # Binary sensor with certain device class is exposed binarysensor2 = entity_registry.async_get_or_create( @@ -330,12 +369,14 @@ async def test_should_expose( original_device_class="door", ) assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] - assert async_should_expose(hass, "cloud.alexa", binarysensor2.entity_id) is True + assert ( + await async_should_expose(hass, "cloud.alexa", binarysensor2.entity_id) is True + ) # Sensor without device class is not exposed sensor1 = entity_registry.async_get_or_create("sensor", "test", "unique1") assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] - assert async_should_expose(hass, "cloud.alexa", sensor1.entity_id) is False + assert await async_should_expose(hass, "cloud.alexa", sensor1.entity_id) is False # Sensor with certain device class is exposed sensor2 = entity_registry.async_get_or_create( @@ -345,4 +386,58 @@ async def test_should_expose( original_device_class="temperature", ) assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] - assert async_should_expose(hass, "cloud.alexa", sensor2.entity_id) is True + assert await async_should_expose(hass, "cloud.alexa", sensor2.entity_id) is True + + +async def test_list_exposed_entities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test list exposed entities.""" + ws_client = await hass_ws_client(hass) + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + entry1 = entity_registry.async_get_or_create("test", "test", "unique1") + entry2 = entity_registry.async_get_or_create("test", "test", "unique2") + + # Set options for registered entities + await ws_client.send_json_auto_id( + { + "type": "homeassistant/expose_entity", + "assistants": ["cloud.alexa", "cloud.google_assistant"], + "entity_ids": [entry1.entity_id, entry2.entity_id], + "should_expose": True, + } + ) + response = await ws_client.receive_json() + assert response["success"] + + # Set options for entities not in the entity registry + await ws_client.send_json_auto_id( + { + "type": "homeassistant/expose_entity", + "assistants": ["cloud.alexa", "cloud.google_assistant"], + "entity_ids": [ + "test.test", + "test.test2", + ], + "should_expose": False, + } + ) + response = await ws_client.receive_json() + assert response["success"] + + # List exposed entities + await ws_client.send_json_auto_id({"type": "homeassistant/expose_entity/list"}) + response = await ws_client.receive_json() + assert response["success"] + assert response["result"] == { + "exposed_entities": { + "test.test": {"cloud.alexa": False, "cloud.google_assistant": False}, + "test.test2": {"cloud.alexa": False, "cloud.google_assistant": False}, + "test.test_unique1": {"cloud.alexa": True, "cloud.google_assistant": True}, + "test.test_unique2": {"cloud.alexa": True, "cloud.google_assistant": True}, + }, + } diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 3de10491f39..b925fcb341c 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -427,6 +427,7 @@ async def test_options_flow_devices( demo_config_entry = MockConfigEntry(domain="domain") demo_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "demo", {"demo": {}}) assert await async_setup_component(hass, "homekit", {"homekit": {}}) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 58babc0ccb0..69e2aa2c8e2 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -319,6 +319,7 @@ async def test_config_entry_with_trigger_accessory( entity_registry: er.EntityRegistry, ) -> None: """Test generating diagnostics for a bridge config entry with a trigger accessory.""" + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "demo", {"demo": {}}) hk_driver.publish = MagicMock() diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 773dd171511..0b74763c6a7 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -747,6 +747,7 @@ async def test_homekit_start_with_a_device( entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "demo", {"demo": {}}) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 2bb9a4972a3..5a1d42352fe 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -81,6 +81,7 @@ async def test_bridge_with_triggers( an above or below additional configuration which we have no way to input, we ignore them. """ + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "demo", {"demo": {}}) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_triggers.py b/tests/components/homekit/test_type_triggers.py index fd77499ff09..0374f3f1e94 100644 --- a/tests/components/homekit/test_type_triggers.py +++ b/tests/components/homekit/test_type_triggers.py @@ -25,6 +25,7 @@ async def test_programmable_switch_button_fires_on_trigger( demo_config_entry = MockConfigEntry(domain="domain") demo_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "demo", {"demo": {}}) await hass.async_block_till_done() hass.states.async_set("light.ceiling_lights", STATE_OFF) diff --git a/tests/components/light/test_recorder.py b/tests/components/light/test_recorder.py index c139ff4cf4a..2c85dac0bd4 100644 --- a/tests/components/light/test_recorder.py +++ b/tests/components/light/test_recorder.py @@ -26,6 +26,7 @@ from tests.components.recorder.common import async_wait_recording_done async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test light registered attributes to be excluded.""" now = dt_util.utcnow() + assert await async_setup_component(hass, "homeassistant", {}) await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {"platform": "demo"}} ) diff --git a/tests/components/number/test_recorder.py b/tests/components/number/test_recorder.py index 5b382f2540d..d996a67f93b 100644 --- a/tests/components/number/test_recorder.py +++ b/tests/components/number/test_recorder.py @@ -18,6 +18,7 @@ from tests.components.recorder.common import async_wait_recording_done async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test number registered attributes to be excluded.""" + assert await async_setup_component(hass, "homeassistant", {}) await async_setup_component( hass, number.DOMAIN, {number.DOMAIN: {"platform": "demo"}} ) diff --git a/tests/components/select/test_recorder.py b/tests/components/select/test_recorder.py index 4fb6f60e0f6..903d24d39bb 100644 --- a/tests/components/select/test_recorder.py +++ b/tests/components/select/test_recorder.py @@ -19,6 +19,7 @@ from tests.components.recorder.common import async_wait_recording_done async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test select registered attributes to be excluded.""" now = dt_util.utcnow() + assert await async_setup_component(hass, "homeassistant", {}) await async_setup_component( hass, select.DOMAIN, {select.DOMAIN: {"platform": "demo"}} ) diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index a77ee314088..2254abc08f9 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -1,4 +1,6 @@ """The tests for the Light Switch platform.""" +import pytest + from homeassistant.components.light import ( ATTR_COLOR_MODE, ATTR_SUPPORTED_COLOR_MODES, @@ -12,6 +14,12 @@ from . import common as switch_common from tests.components.light import common +@pytest.fixture(autouse=True) +async def setup_homeassistant(hass: HomeAssistant): + """Set up the homeassistant integration.""" + await async_setup_component(hass, "homeassistant", {}) + + async def test_default_state(hass: HomeAssistant) -> None: """Test light switch default state.""" await async_setup_component( diff --git a/tests/components/switch_as_x/conftest.py b/tests/components/switch_as_x/conftest.py index f722292fc89..d324f7a0c54 100644 --- a/tests/components/switch_as_x/conftest.py +++ b/tests/components/switch_as_x/conftest.py @@ -6,6 +6,15 @@ from unittest.mock import AsyncMock, patch import pytest +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +async def setup_homeassistant(hass: HomeAssistant): + """Set up the homeassistant integration.""" + await async_setup_component(hass, "homeassistant", {}) + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index fac744d0c0e..ad7bf732dcc 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -702,7 +702,7 @@ async def test_import_expose_settings_1( original_name="ABC", ) for assistant, should_expose in EXPOSE_SETTINGS.items(): - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( hass, assistant, switch_entity_entry.entity_id, should_expose ) @@ -760,7 +760,7 @@ async def test_import_expose_settings_2( original_name="ABC", ) for assistant, should_expose in EXPOSE_SETTINGS.items(): - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( hass, assistant, switch_entity_entry.entity_id, should_expose ) @@ -785,7 +785,7 @@ async def test_import_expose_settings_2( suggested_object_id="abc", ) for assistant, should_expose in EXPOSE_SETTINGS.items(): - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( 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", ) for assistant, should_expose in EXPOSE_SETTINGS.items(): - exposed_entities.async_expose_entity( + await exposed_entities.async_expose_entity( hass, assistant, switch_as_x_entity_entry.entity_id, should_expose ) diff --git a/tests/components/text/test_recorder.py b/tests/components/text/test_recorder.py index f695ce11117..54134ee501a 100644 --- a/tests/components/text/test_recorder.py +++ b/tests/components/text/test_recorder.py @@ -19,6 +19,7 @@ from tests.components.recorder.common import async_wait_recording_done async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test siren registered attributes to be excluded.""" now = dt_util.utcnow() + assert await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, text.DOMAIN, {text.DOMAIN: {"platform": "demo"}}) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))