Allow exposing entities not in the entity registry to assistants (#92363)

This commit is contained in:
Erik Montnemery 2023-05-02 22:08:09 +02:00 committed by GitHub
parent cc4e741cfa
commit e3c16e634b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 563 additions and 224 deletions

View file

@ -84,8 +84,7 @@ class AbstractConfig(ABC):
unsub_func() unsub_func()
self._unsub_proactive_report = None self._unsub_proactive_report = None
@callback async def should_expose(self, entity_id):
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 config.should_expose(alexa_entity.entity_id) if await 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
def load_entity(self, hass, config): async 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 config.should_expose(self.entity_id): if not self.entity or not await 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:
directive.load_entity(hass, config) await 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,7 @@ 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 ""
def should_expose(self, entity_id): async 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 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) _LOGGER.debug("Not exposing %s because filtered by config", changed_entity)
return return

View file

@ -257,14 +257,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
and entity_supported(self.hass, entity_id) 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 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 async_should_expose(self.hass, CLOUD_ALEXA, entity_id) return await 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 +423,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 self.should_expose(entity.entity_id): if is_enabled and await 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 +482,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
entity_id = event.data["entity_id"] entity_id = event.data["entity_id"]
if not self.should_expose(entity_id): if not await 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,
) )
def should_expose(self, state): async def should_expose(self, state):
"""If a state object should be exposed.""" """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): 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)
) )
def _should_expose_entity_id(self, entity_id): async 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 async_should_expose(self.hass, CLOUD_GOOGLE, entity_id) return await async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
@property @property
def agent_user_id(self): def agent_user_id(self):
@ -358,8 +358,7 @@ class CloudGoogleConfig(AbstractConfig):
"""Handle updated preferences.""" """Handle updated preferences."""
self.async_schedule_google_sync_all() self.async_schedule_google_sync_all()
@callback async def _handle_entity_registry_updated(self, event: Event) -> None:
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
@ -376,13 +375,12 @@ class CloudGoogleConfig(AbstractConfig):
entity_id = event.data["entity_id"] 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 return
self.async_schedule_google_sync_all() self.async_schedule_google_sync_all()
@callback async def _handle_device_registry_updated(self, event: Event) -> None:
def _handle_device_registry_updated(self, event: Event) -> None:
"""Handle when device registry updated.""" """Handle when device registry updated."""
if ( if (
not self.enabled not self.enabled
@ -396,13 +394,15 @@ class CloudGoogleConfig(AbstractConfig):
return return
# Check if any exposed entity uses the device area # Check if any exposed entity uses the device area
if not any( used = False
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)
manager.async_setup() hass.async_create_task(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()
def async_setup(self) -> None: async def async_setup(self) -> None:
"""Set up the conversation agents.""" """Set up the conversation agents."""
async_setup_default_agent(self.hass) await 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,23 +73,20 @@ def _get_language_variations(language: str) -> Iterable[str]:
yield lang yield lang
@core.callback async def async_setup(hass: core.HomeAssistant) -> None:
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:
async_should_expose(hass, DOMAIN, entity_id) await async_should_expose(hass, DOMAIN, entity_id)
@core.callback async def async_handle_entity_registry_changed(event: core.Event) -> None:
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":
async_should_expose(hass, DOMAIN, event.data["entity_id"]) await 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,
) )
@ -157,7 +154,7 @@ class DefaultAgent(AbstractConversationAgent):
conversation_id, conversation_id,
) )
slot_lists = self._make_slot_lists() slot_lists = await self._make_slot_lists()
result = await self.hass.async_add_executor_job( result = await self.hass.async_add_executor_job(
self._recognize, self._recognize,
@ -486,7 +483,7 @@ class DefaultAgent(AbstractConversationAgent):
"""Handle updated preferences.""" """Handle updated preferences."""
self._slot_lists = None 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.""" """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
@ -496,7 +493,7 @@ class DefaultAgent(AbstractConversationAgent):
entities = [ entities = [
entity entity
for entity in entity_registry.entities.values() 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) 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
def should_expose(self, state) -> bool: async 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,16 +535,14 @@ class GoogleEntity:
] ]
return self._traits return self._traits
@callback async def should_expose(self):
def should_expose(self):
"""If entity should be exposed.""" """If entity should be exposed."""
return self.config.should_expose(self.state) return await self.config.should_expose(self.state)
@callback async def should_expose_local(self) -> bool:
def should_expose_local(self) -> bool:
"""Return if the entity should be exposed locally.""" """Return if the entity should be exposed locally."""
return ( return (
self.should_expose() await 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)
) )
@ -587,7 +585,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()
) )
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. """Serialize entity for a SYNC response.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync https://developers.google.com/actions/smarthome/create-app#actiondevicessync
@ -623,7 +621,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 self.should_expose_local(): if self.config.is_local_sdk_active and await 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)
def should_expose(self, state) -> bool: async 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 google_config.should_expose(new_state): if not await 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 entity.should_expose(): if not await 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 entity.should_expose(): if not await entity.should_expose():
continue continue
try: 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 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 entity.should_expose_local() if entity.entity_id in google_ids and await 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_initialize() await exposed_entities.async_load()
hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities
return True return True

View file

@ -3,6 +3,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 typing import Any from typing import Any
import voluptuous as vol 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.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
@ -77,25 +83,58 @@ class AssistantPreferences:
return {"expose_new": self.expose_new} return {"expose_new": self.expose_new}
class ExposedEntities: @dataclasses.dataclass(frozen=True)
"""Control assistant settings.""" 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] _assistants: dict[str, AssistantPreferences]
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize.""" """Initialize."""
self._hass = hass super().__init__(
self._listeners: dict[str, list[Callable[[], None]]] = {} Store(hass, STORAGE_VERSION, STORAGE_KEY), ExposedEntitiesIDManager()
self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
hass, STORAGE_VERSION, STORAGE_KEY
) )
self._listeners: dict[str, list[Callable[[], None]]] = {}
async def async_initialize(self) -> None: async def async_load(self) -> None:
"""Finish initializing.""" """Finish initializing."""
websocket_api.async_register_command(self._hass, ws_expose_entity) await super().async_load()
websocket_api.async_register_command(self._hass, ws_expose_new_entities_get) websocket_api.async_register_command(self.hass, ws_expose_entity)
websocket_api.async_register_command(self._hass, ws_expose_new_entities_set) websocket_api.async_register_command(self.hass, ws_expose_new_entities_get)
await self.async_load() websocket_api.async_register_command(self.hass, ws_expose_new_entities_set)
websocket_api.async_register_command(self.hass, ws_list_exposed_entities)
@callback @callback
def async_listen_entity_updates( def async_listen_entity_updates(
@ -104,17 +143,18 @@ class ExposedEntities:
"""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)
@callback async def async_expose_entity(
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)):
raise HomeAssistantError("Unknown entity") return await self._async_expose_legacy_entity(
assistant, entity_id, should_expose
)
assistant_options: Mapping[str, Any] assistant_options: Mapping[str, Any]
if ( if (
@ -129,6 +169,34 @@ class ExposedEntities:
for listener in self._listeners.get(assistant, []): for listener in self._listeners.get(assistant, []):
listener() 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 @callback
def async_get_expose_new_entities(self, assistant: str) -> bool: def async_get_expose_new_entities(self, assistant: str) -> bool:
"""Check if new entities are exposed to an assistant.""" """Check if new entities are exposed to an assistant."""
@ -147,9 +215,14 @@ class ExposedEntities:
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
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(): for entity_id, entry in entity_registry.entities.items():
if options := entry.options.get(assistant): if options := entry.options.get(assistant):
result[entity_id] = options result[entity_id] = options
@ -159,31 +232,33 @@ class ExposedEntities:
@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]] = {}
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") raise HomeAssistantError("Unknown entity")
for assistant in KNOWN_ASSISTANTS: for assistant in KNOWN_ASSISTANTS:
if options := registry_entry.options.get(assistant): if options := assistant_settings.get(assistant):
result[assistant] = options result[assistant] = options
return result return result
@callback async def async_should_expose(self, assistant: str, entity_id: str) -> bool:
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)):
# Entities which are not in the entity registry are not exposed return await self._async_should_expose_legacy_entity(assistant, entity_id)
return False
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"]
@ -202,11 +277,43 @@ class ExposedEntities:
return should_expose 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( def _is_default_exposed(
self, entity_id: str, registry_entry: er.RegistryEntry self, entity_id: str, registry_entry: er.RegistryEntry | None
) -> bool: ) -> bool:
"""Return True if an entity is exposed by default.""" """Return True if an entity is exposed by default."""
if ( if registry_entry and (
registry_entry.entity_category is not None registry_entry.entity_category is not None
or registry_entry.hidden_by is not None or registry_entry.hidden_by is not None
): ):
@ -216,7 +323,7 @@ class ExposedEntities:
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
@ -228,37 +335,71 @@ class ExposedEntities:
return False 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.""" """Load from the store."""
data = await self._store.async_load() data = await super()._async_load_data()
assistants: dict[str, AssistantPreferences] = {} assistants: dict[str, AssistantPreferences] = {}
if data: if data and "assistants" in 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 self._assistants = assistants
@callback if data and "items" not in data:
def _async_schedule_save(self) -> None: return None # type: ignore[unreachable]
"""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()
}
return data 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.require_admin
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
@ -268,11 +409,11 @@ class ExposedEntities:
vol.Required("should_expose"): bool, 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] hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None: ) -> None:
"""Expose an entity to an assistant.""" """Expose an entity to an assistant."""
entity_registry = er.async_get(hass)
entity_ids: str = msg["entity_ids"] entity_ids: str = msg["entity_ids"]
if blocked := next( if blocked := next(
@ -288,28 +429,40 @@ def ws_expose_entity(
) )
return 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] 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"]:
exposed_entities.async_expose_entity( await 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"])
@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 @callback
@websocket_api.require_admin @websocket_api.require_admin
@websocket_api.websocket_command( @websocket_api.websocket_command(
@ -372,8 +525,7 @@ def async_get_entity_settings(
return exposed_entities.async_get_entity_settings(entity_id) return exposed_entities.async_get_entity_settings(entity_id)
@callback async def async_expose_entity(
def async_expose_entity(
hass: HomeAssistant, hass: HomeAssistant,
assistant: str, assistant: str,
entity_id: str, entity_id: str,
@ -381,11 +533,12 @@ 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]
exposed_entities.async_expose_entity(assistant, entity_id, should_expose) await exposed_entities.async_expose_entity(assistant, entity_id, should_expose)
@callback async def async_should_expose(
def async_should_expose(hass: HomeAssistant, assistant: str, entity_id: str) -> bool: hass: HomeAssistant, 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."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] 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)

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
exposed_entities.async_expose_entity( await 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)
def copy_expose_settings() -> None: async 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
exposed_entities.async_expose_entity( await exposed_entities.async_expose_entity(
self.hass, assistant, self.entity_id, should_expose 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 self.hass, assistant, self._switch_entity_id, False
) )
copy_custom_name(wrapped_switch) copy_custom_name(wrapped_switch)
copy_expose_settings() await copy_expose_settings()
class BaseToggleEntity(BaseEntity, ToggleEntity): class BaseToggleEntity(BaseEntity, ToggleEntity):

View file

@ -2450,13 +2450,18 @@ 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)
alexa_config.should_expose = entityfilter.generate_filter( filter = 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()
@ -2481,13 +2486,18 @@ 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)
alexa_config.should_expose = entityfilter.generate_filter( filter = 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()
@ -2506,13 +2516,18 @@ 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)
alexa_config.should_expose = entityfilter.generate_filter( filter = 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

@ -370,6 +370,7 @@ async def test_websocket_update_orientation_prefs(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera
) -> None: ) -> None:
"""Test updating camera preferences.""" """Test updating camera preferences."""
await async_setup_component(hass, "homeassistant", {})
client = await hass_ws_client(hass) client = await hass_ws_client(hass)

View file

@ -1888,6 +1888,7 @@ async def test_failed_cast_other_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Test warning when casting from internal_url fails.""" """Test warning when casting from internal_url fails."""
await async_setup_component(hass, "homeassistant", {})
with assert_setup_component(1, tts.DOMAIN): with assert_setup_component(1, tts.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
@ -1911,6 +1912,7 @@ async def test_failed_cast_internal_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Test warning when casting from internal_url fails.""" """Test warning when casting from internal_url fails."""
await async_setup_component(hass, "homeassistant", {})
await async_process_ha_core_config( await async_process_ha_core_config(
hass, hass,
{"internal_url": "http://example.local:8123"}, {"internal_url": "http://example.local:8123"},
@ -1939,6 +1941,7 @@ async def test_failed_cast_external_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Test warning when casting from external_url fails.""" """Test warning when casting from external_url fails."""
await async_setup_component(hass, "homeassistant", {})
await async_process_ha_core_config( await async_process_ha_core_config(
hass, hass,
{"external_url": "http://example.com:8123"}, {"external_url": "http://example.com:8123"},
@ -1969,6 +1972,7 @@ async def test_failed_cast_tts_base_url(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Test warning when casting from tts.base_url fails.""" """Test warning when casting from tts.base_url fails."""
await async_setup_component(hass, "homeassistant", {})
with assert_setup_component(1, tts.DOMAIN): with assert_setup_component(1, tts.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,

View file

@ -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: async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test climate registered attributes to be excluded.""" """Test climate registered attributes to be excluded."""
now = dt_util.utcnow() now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component( await async_setup_component(
hass, climate.DOMAIN, {climate.DOMAIN: {"platform": "demo"}} hass, climate.DOMAIN, {climate.DOMAIN: {"platform": "demo"}}
) )

View file

@ -18,7 +18,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
) )
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.setup import async_setup_component 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) 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.""" """Expose an entity to Alexa."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] 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( async def test_alexa_config_expose_entity_prefs(
@ -96,36 +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)
expose_entity(hass, entity_entry5.entity_id, False) await 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()
# can't expose an entity which is not in the entity registry # an entity which is not in the entity registry can be exposed
with pytest.raises(HomeAssistantError): await expose_entity(hass, "light.kitchen", True)
expose_entity(hass, "light.kitchen", True) assert await conf.should_expose("light.kitchen")
assert not conf.should_expose("light.kitchen")
# categorized and hidden entities should not be exposed # categorized and hidden entities should not be exposed
assert not conf.should_expose(entity_entry1.entity_id) assert not await conf.should_expose(entity_entry1.entity_id)
assert not conf.should_expose(entity_entry2.entity_id) assert not await conf.should_expose(entity_entry2.entity_id)
assert not conf.should_expose(entity_entry3.entity_id) assert not await conf.should_expose(entity_entry3.entity_id)
assert not conf.should_expose(entity_entry4.entity_id) assert not await conf.should_expose(entity_entry4.entity_id)
# this has been hidden # 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 # 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) await expose_entity(hass, entity_entry5.entity_id, True)
assert conf.should_expose(entity_entry5.entity_id) assert await conf.should_expose(entity_entry5.entity_id)
expose_entity(hass, entity_entry5.entity_id, None) await expose_entity(hass, entity_entry5.entity_id, None)
assert not conf.should_expose(entity_entry5.entity_id) assert not await 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 conf.should_expose(entity_entry5.entity_id) assert not await conf.should_expose(entity_entry5.entity_id)
async def test_alexa_config_report_state( async def test_alexa_config_report_state(
@ -370,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):
expose_entity(hass, light_entry.entity_id, True) await 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()
@ -380,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):
expose_entity(hass, light_entry.entity_id, False) await expose_entity(hass, light_entry.entity_id, False)
expose_entity(hass, binary_sensor_entry.entity_id, True) await expose_entity(hass, binary_sensor_entry.entity_id, True)
expose_entity(hass, sensor_entry.entity_id, True) await 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()
@ -588,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,
) )
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"] = { 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 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 "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( async def test_google_config_should_2fa(

View file

@ -21,7 +21,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
) )
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EntityCategory from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EntityCategory
from homeassistant.core import CoreState, HomeAssistant, State 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.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow 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) 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.""" """Expose an entity to Google."""
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] 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 "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( with patch.object(config, "async_sync_entities") as mock_sync, patch.object(
ga_helpers, "SYNC_DELAY", 0 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() 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()
@ -161,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
): ):
expose_entity(hass, light_entry.entity_id, False) await expose_entity(hass, light_entry.entity_id, False)
expose_entity(hass, binary_sensor_entry.entity_id, True) await expose_entity(hass, binary_sensor_entry.entity_id, True)
expose_entity(hass, sensor_entry.entity_id, True) await 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()
@ -385,7 +384,7 @@ async def test_google_config_expose_entity_prefs(
) )
expose_new(hass, True) 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 = State("light.kitchen", "on")
state_config = State(entity_entry1.entity_id, "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_not_exposed = State(entity_entry5.entity_id, "on")
state_exposed_default = State(entity_entry6.entity_id, "on") state_exposed_default = State(entity_entry6.entity_id, "on")
# can't expose an entity which is not in the entity registry # an entity which is not in the entity registry can be exposed
with pytest.raises(HomeAssistantError): await expose_entity(hass, "light.kitchen", True)
expose_entity(hass, "light.kitchen", True) assert await mock_conf.should_expose(state)
assert not mock_conf.should_expose(state)
# categorized and hidden entities should not be exposed # categorized and hidden entities should not be exposed
assert not mock_conf.should_expose(state_config) assert not await mock_conf.should_expose(state_config)
assert not mock_conf.should_expose(state_diagnostic) assert not await mock_conf.should_expose(state_diagnostic)
assert not mock_conf.should_expose(state_hidden_integration) assert not await mock_conf.should_expose(state_hidden_integration)
assert not mock_conf.should_expose(state_hidden_user) assert not await mock_conf.should_expose(state_hidden_user)
# this has been hidden # 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 # 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) await expose_entity(hass, entity_entry5.entity_id, True)
assert mock_conf.should_expose(state_not_exposed) assert await mock_conf.should_expose(state_not_exposed)
expose_entity(hass, entity_entry5.entity_id, None) await expose_entity(hass, entity_entry5.entity_id, None)
assert not mock_conf.should_expose(state_not_exposed) assert not await mock_conf.should_expose(state_not_exposed)
def test_enabled_requires_valid_sub( def test_enabled_requires_valid_sub(
@ -537,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,
) )
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"] = { cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.unknown"] = {
PREF_SHOULD_EXPOSE: True PREF_SHOULD_EXPOSE: True

View file

@ -32,6 +32,12 @@ LIGHT_ENTITY = "light.kitchen_lights"
CLOSE_THRESHOLD = 10 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): def _close_enough(actual_rgb, testing_rgb):
"""Validate the given RGB value is in acceptable tolerance.""" """Validate the given RGB value is in acceptable tolerance."""
# Convert the given RGB values to hue / saturation and then back again # Convert the given RGB values to hue / saturation and then back again

View file

@ -51,7 +51,9 @@ 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)
def expose_entity(hass, entity_id, should_expose): async 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]
exposed_entities.async_expose_entity(conversation.DOMAIN, entity_id, should_expose) await exposed_entities.async_expose_entity(
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
expose_entity(hass, bedroom_light.entity_id, False) await 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
expose_entity(hass, "light.kitchen", False) await 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
expose_entity(hass, "light.kitchen", True) await 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
expose_entity(hass, "light.kitchen", True) await 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

@ -1,3 +1,14 @@
"""demo conftest.""" """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.blueprint.conftest import stub_blueprint_populate # noqa: F401
from tests.components.light.conftest import mock_light_profiles # 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", {})

View file

@ -2,6 +2,8 @@
import math import math
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import pytest
from homeassistant.components import emulated_kasa from homeassistant.components import emulated_kasa
from homeassistant.components.emulated_kasa.const import ( from homeassistant.components.emulated_kasa.const import (
CONF_POWER, 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): def nested_value(ndict, *keys):
"""Return a nested dict value or None if it doesn't exist.""" """Return a nested dict value or None if it doesn't exist."""
if len(keys) == 0: if len(keys) == 0:

View file

@ -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: async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test fan registered attributes to be excluded.""" """Test fan registered attributes to be excluded."""
now = dt_util.utcnow() now = dt_util.utcnow()
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "demo"}}) await async_setup_component(hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "demo"}})
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))

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
def should_expose(self, state): async 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

@ -20,6 +20,7 @@ async def test_diagnostics(
await setup.async_setup_component( await setup.async_setup_component(
hass, switch.DOMAIN, {"switch": [{"platform": "demo"}]} hass, switch.DOMAIN, {"switch": [{"platform": "demo"}]}
) )
await async_setup_component(hass, "homeassistant", {})
await async_setup_component( await async_setup_component(
hass, hass,

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 = entity.sync_serialize(None, "mock-uuid") serialized = await 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 = 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["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 = entity.sync_serialize(None, "mock-uuid") serialized = await 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,7 +257,9 @@ async def test_should_expose(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
assert ( assert (
config.should_expose(State(DOMAIN + ".mock", "mock", {"view": "not None"})) await config.should_expose(
State(DOMAIN + ".mock", "mock", {"view": "not None"})
)
is False 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 # 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 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: async def test_missing_service_account(hass: HomeAssistant) -> None:

View file

@ -452,6 +452,7 @@ async def test_execute(
) -> None: ) -> None:
"""Test an execute command.""" """Test an execute command."""
await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) await async_setup_component(hass, "light", {"light": {"platform": "demo"}})
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.services.async_call( await hass.services.async_call(
@ -635,6 +636,7 @@ async def test_execute_times_out(
orig_execute_limit = sh.EXECUTE_LIMIT orig_execute_limit = sh.EXECUTE_LIMIT
sh.EXECUTE_LIMIT = 0.02 # Decrease timeout to 20ms sh.EXECUTE_LIMIT = 0.02 # Decrease timeout to 20ms
await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) await async_setup_component(hass, "light", {"light": {"platform": "demo"}})
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.services.async_call( await hass.services.async_call(
@ -907,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 = entity.sync_serialize(None, "mock-uuid") result = await entity.sync_serialize(None, "mock-uuid")
assert result == { assert result == {
"id": "input_boolean.bla", "id": "input_boolean.bla",
"attributes": {}, "attributes": {},

View file

@ -1,2 +1,13 @@
"""group conftest.""" """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 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", {})

View file

@ -3,6 +3,8 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import pytest
from homeassistant.components import group from homeassistant.components import group
from homeassistant.components.group import ATTR_AUTO, ATTR_ENTITY_ID, ATTR_ORDER from homeassistant.components.group import ATTR_AUTO, ATTR_ENTITY_ID, ATTR_ORDER
from homeassistant.components.recorder import Recorder 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 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: async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test number registered attributes to be excluded.""" """Test number registered attributes to be excluded."""
now = dt_util.utcnow() now = dt_util.utcnow()

View file

@ -4,13 +4,13 @@ import pytest
from homeassistant.components.homeassistant.exposed_entities import ( from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES, DATA_EXPOSED_ENTITIES,
ExposedEntities, ExposedEntities,
ExposedEntity,
async_get_assistant_settings, async_get_assistant_settings,
async_listen_entity_updates, async_listen_entity_updates,
async_should_expose, async_should_expose,
) )
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, EntityCategory from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component 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"] assert list(exposed_entities._assistants) == ["test1", "test2"]
exposed_entities2 = ExposedEntities(hass) exposed_entities2 = ExposedEntities(hass)
await flush_store(exposed_entities._store) await flush_store(exposed_entities.store)
await exposed_entities2.async_load() await exposed_entities2.async_load()
assert exposed_entities._assistants == exposed_entities2._assistants 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") entry1 = entity_registry.async_get_or_create("test", "test", "unique1")
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]
assert len(exposed_entities.data) == 0
# Set options # Set options
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
{ {
@ -67,6 +70,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
# Update options # Update options
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -91,6 +95,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
async def test_expose_entity_unknown( async def test_expose_entity_unknown(
@ -103,6 +108,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
# Set options # Set options
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -115,14 +121,41 @@ async def test_expose_entity_unknown(
) )
response = await ws_client.receive_json() response = await ws_client.receive_json()
assert not response["success"] assert response["success"]
assert response["error"] == {
"code": "not_found", assert len(exposed_entities.data) == 1
"message": "can't expose 'test.test'", assert exposed_entities.data == {
"test.test": ExposedEntity({"cloud.alexa": {"should_expose": True}})
} }
with pytest.raises(HomeAssistantError): # Update options
exposed_entities.async_expose_entity("cloud.alexa", "test.test", True) 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( async def test_expose_entity_blocked(
@ -178,7 +211,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 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 # Expose new entities to Alexa
await ws_client.send_json_auto_id( await ws_client.send_json_auto_id(
@ -201,10 +234,12 @@ 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 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 # 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( async def test_listen_updates(
@ -226,21 +261,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
exposed_entities.async_expose_entity( await 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
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 assert len(calls) == 1
# Settings not changed - listener not called # 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 assert len(calls) == 1
# Settings changed - listener called # 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 assert len(calls) == 2
@ -258,7 +293,7 @@ async def test_get_assistant_settings(
assert async_get_assistant_settings(hass, "cloud.alexa") == {} 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") == { assert async_get_assistant_settings(hass, "cloud.alexa") == {
"climate.test_unique1": {"should_expose": True} "climate.test_unique1": {"should_expose": True}
} }
@ -287,40 +322,44 @@ async def test_should_expose(
assert response["success"] assert response["success"]
# Unknown entity is not exposed # 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 # Blocked entity is not exposed
entry_blocked = entity_registry.async_get_or_create( entry_blocked = entity_registry.async_get_or_create(
"group", "test", "unique", suggested_object_id="all_locks" "group", "test", "unique", suggested_object_id="all_locks"
) )
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] 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 # Lock is exposed
lock1 = entity_registry.async_get_or_create("lock", "test", "unique1") lock1 = entity_registry.async_get_or_create("lock", "test", "unique1")
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] 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 # Hidden entity is not exposed
lock2 = entity_registry.async_get_or_create( lock2 = entity_registry.async_get_or_create(
"lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER "lock", "test", "unique2", hidden_by=er.RegistryEntryHider.USER
) )
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] 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 # Entity with category is not exposed
lock3 = entity_registry.async_get_or_create( lock3 = entity_registry.async_get_or_create(
"lock", "test", "unique3", entity_category=EntityCategory.CONFIG "lock", "test", "unique3", entity_category=EntityCategory.CONFIG
) )
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] 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 # Binary sensor without device class is not exposed
binarysensor1 = entity_registry.async_get_or_create( binarysensor1 = entity_registry.async_get_or_create(
"binary_sensor", "test", "unique1" "binary_sensor", "test", "unique1"
) )
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] 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 # Binary sensor with certain device class is exposed
binarysensor2 = entity_registry.async_get_or_create( binarysensor2 = entity_registry.async_get_or_create(
@ -330,12 +369,14 @@ async def test_should_expose(
original_device_class="door", original_device_class="door",
) )
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] 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 # Sensor without device class is not exposed
sensor1 = entity_registry.async_get_or_create("sensor", "test", "unique1") sensor1 = entity_registry.async_get_or_create("sensor", "test", "unique1")
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] 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 # Sensor with certain device class is exposed
sensor2 = entity_registry.async_get_or_create( sensor2 = entity_registry.async_get_or_create(
@ -345,4 +386,58 @@ async def test_should_expose(
original_device_class="temperature", original_device_class="temperature",
) )
assert entry_blocked.entity_id == CLOUD_NEVER_EXPOSED_ENTITIES[0] 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},
},
}

View file

@ -427,6 +427,7 @@ async def test_options_flow_devices(
demo_config_entry = MockConfigEntry(domain="domain") demo_config_entry = MockConfigEntry(domain="domain")
demo_config_entry.add_to_hass(hass) 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, "demo", {"demo": {}})
assert await async_setup_component(hass, "homekit", {"homekit": {}}) assert await async_setup_component(hass, "homekit", {"homekit": {}})

View file

@ -319,6 +319,7 @@ async def test_config_entry_with_trigger_accessory(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test generating diagnostics for a bridge config entry with a trigger accessory.""" """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": {}}) assert await async_setup_component(hass, "demo", {"demo": {}})
hk_driver.publish = MagicMock() hk_driver.publish = MagicMock()

View file

@ -747,6 +747,7 @@ async def test_homekit_start_with_a_device(
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} 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": {}}) assert await async_setup_component(hass, "demo", {"demo": {}})
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -81,6 +81,7 @@ async def test_bridge_with_triggers(
an above or below additional configuration which we have no way an above or below additional configuration which we have no way
to input, we ignore them. to input, we ignore them.
""" """
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "demo", {"demo": {}}) assert await async_setup_component(hass, "demo", {"demo": {}})
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -25,6 +25,7 @@ async def test_programmable_switch_button_fires_on_trigger(
demo_config_entry = MockConfigEntry(domain="domain") demo_config_entry = MockConfigEntry(domain="domain")
demo_config_entry.add_to_hass(hass) 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, "demo", {"demo": {}})
await hass.async_block_till_done() await hass.async_block_till_done()
hass.states.async_set("light.ceiling_lights", STATE_OFF) hass.states.async_set("light.ceiling_lights", STATE_OFF)

View file

@ -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: async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test light registered attributes to be excluded.""" """Test light registered attributes to be excluded."""
now = dt_util.utcnow() now = dt_util.utcnow()
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component( await async_setup_component(
hass, light.DOMAIN, {light.DOMAIN: {"platform": "demo"}} hass, light.DOMAIN, {light.DOMAIN: {"platform": "demo"}}
) )

View file

@ -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: async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test number registered attributes to be excluded.""" """Test number registered attributes to be excluded."""
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component( await async_setup_component(
hass, number.DOMAIN, {number.DOMAIN: {"platform": "demo"}} hass, number.DOMAIN, {number.DOMAIN: {"platform": "demo"}}
) )

View file

@ -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: async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test select registered attributes to be excluded.""" """Test select registered attributes to be excluded."""
now = dt_util.utcnow() now = dt_util.utcnow()
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component( await async_setup_component(
hass, select.DOMAIN, {select.DOMAIN: {"platform": "demo"}} hass, select.DOMAIN, {select.DOMAIN: {"platform": "demo"}}
) )

View file

@ -1,4 +1,6 @@
"""The tests for the Light Switch platform.""" """The tests for the Light Switch platform."""
import pytest
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_COLOR_MODE, ATTR_COLOR_MODE,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
@ -12,6 +14,12 @@ from . import common as switch_common
from tests.components.light import 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: async def test_default_state(hass: HomeAssistant) -> None:
"""Test light switch default state.""" """Test light switch default state."""
await async_setup_component( await async_setup_component(

View file

@ -6,6 +6,15 @@ from unittest.mock import AsyncMock, patch
import pytest 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 @pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]: def mock_setup_entry() -> Generator[AsyncMock, None, None]:

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():
exposed_entities.async_expose_entity( await 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():
exposed_entities.async_expose_entity( await 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():
exposed_entities.async_expose_entity( await 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():
exposed_entities.async_expose_entity( await 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
) )

View file

@ -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: async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test siren registered attributes to be excluded.""" """Test siren registered attributes to be excluded."""
now = dt_util.utcnow() now = dt_util.utcnow()
assert await async_setup_component(hass, "homeassistant", {})
await async_setup_component(hass, text.DOMAIN, {text.DOMAIN: {"platform": "demo"}}) await async_setup_component(hass, text.DOMAIN, {text.DOMAIN: {"platform": "demo"}})
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))