Refactor handling of exposed entities for cloud Alexa and Google (#89877)
* Refactor handling of exposed entities for cloud Alexa * Tweak WS API * Validate assistant parameter * Address some review comments * Refactor handling of exposed entities for cloud Google * Raise when attempting to expose an unknown entity * Add tests * Adjust cloud tests * Allow getting expose new entities flag * Test Alexa migration * Test Google migration * Add WS command cloud/google_assistant/entities/get * Fix return value * Update typing * Address review comments * Rename async_get_exposed_entities to async_get_assistant_settingspull/90940/head^2
parent
0d84106947
commit
44c89a6b6c
|
@ -137,6 +137,7 @@ homeassistant.components.hardkernel.*
|
|||
homeassistant.components.hardware.*
|
||||
homeassistant.components.here_travel_time.*
|
||||
homeassistant.components.history.*
|
||||
homeassistant.components.homeassistant.exposed_entities
|
||||
homeassistant.components.homeassistant.triggers.event
|
||||
homeassistant.components.homeassistant_alerts.*
|
||||
homeassistant.components.homeassistant_hardware.*
|
||||
|
|
|
@ -20,6 +20,11 @@ from homeassistant.components.alexa import (
|
|||
errors as alexa_errors,
|
||||
state_report as alexa_state_report,
|
||||
)
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_get_assistant_settings,
|
||||
async_listen_entity_updates,
|
||||
async_should_expose,
|
||||
)
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.helpers import entity_registry as er, start
|
||||
|
@ -30,16 +35,17 @@ from homeassistant.util.dt import utcnow
|
|||
from .const import (
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_FILTER,
|
||||
PREF_ALEXA_DEFAULT_EXPOSE,
|
||||
PREF_ALEXA_ENTITY_CONFIGS,
|
||||
DOMAIN as CLOUD_DOMAIN,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
PREF_ENABLE_ALEXA,
|
||||
PREF_SHOULD_EXPOSE,
|
||||
)
|
||||
from .prefs import CloudPreferences
|
||||
from .prefs import ALEXA_SETTINGS_VERSION, CloudPreferences
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
|
||||
|
||||
# Time to wait when entity preferences have changed before syncing it to
|
||||
# the cloud.
|
||||
SYNC_DELAY = 1
|
||||
|
@ -64,7 +70,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||
self._cloud = cloud
|
||||
self._token = None
|
||||
self._token_valid = None
|
||||
self._cur_entity_prefs = prefs.alexa_entity_configs
|
||||
self._cur_entity_prefs = async_get_assistant_settings(hass, CLOUD_ALEXA)
|
||||
self._alexa_sync_unsub: Callable[[], None] | None = None
|
||||
self._endpoint = None
|
||||
|
||||
|
@ -115,10 +121,31 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||
"""Return an identifier for the user that represents this config."""
|
||||
return self._cloud_user
|
||||
|
||||
def _migrate_alexa_entity_settings_v1(self):
|
||||
"""Migrate alexa entity settings to entity registry options."""
|
||||
if not self._config[CONF_FILTER].empty_filter:
|
||||
# Don't migrate if there's a YAML config
|
||||
return
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
for entity_id, entry in entity_registry.entities.items():
|
||||
if CLOUD_ALEXA in entry.options:
|
||||
continue
|
||||
options = {"should_expose": self._should_expose_legacy(entity_id)}
|
||||
entity_registry.async_update_entity_options(entity_id, CLOUD_ALEXA, options)
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Initialize the Alexa config."""
|
||||
await super().async_initialize()
|
||||
|
||||
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
|
||||
if self._prefs.alexa_settings_version < 2:
|
||||
self._migrate_alexa_entity_settings_v1()
|
||||
await self._prefs.async_update(
|
||||
alexa_settings_version=ALEXA_SETTINGS_VERSION
|
||||
)
|
||||
|
||||
async def hass_started(hass):
|
||||
if self.enabled and ALEXA_DOMAIN not in self.hass.config.components:
|
||||
await async_setup_component(self.hass, ALEXA_DOMAIN, {})
|
||||
|
@ -126,19 +153,19 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||
start.async_at_start(self.hass, hass_started)
|
||||
|
||||
self._prefs.async_listen_updates(self._async_prefs_updated)
|
||||
async_listen_entity_updates(
|
||||
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
self._handle_entity_registry_updated,
|
||||
)
|
||||
|
||||
def should_expose(self, entity_id):
|
||||
def _should_expose_legacy(self, entity_id):
|
||||
"""If an entity should be exposed."""
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
if not self._config[CONF_FILTER].empty_filter:
|
||||
return self._config[CONF_FILTER](entity_id)
|
||||
|
||||
entity_configs = self._prefs.alexa_entity_configs
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
entity_expose = entity_config.get(PREF_SHOULD_EXPOSE)
|
||||
|
@ -160,6 +187,15 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||
|
||||
return not auxiliary_entity and split_entity_id(entity_id)[0] in default_expose
|
||||
|
||||
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)
|
||||
|
||||
@callback
|
||||
def async_invalidate_access_token(self):
|
||||
"""Invalidate access token."""
|
||||
|
@ -233,32 +269,30 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||
if not any(
|
||||
key in updated_prefs
|
||||
for key in (
|
||||
PREF_ALEXA_DEFAULT_EXPOSE,
|
||||
PREF_ALEXA_ENTITY_CONFIGS,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
PREF_ENABLE_ALEXA,
|
||||
)
|
||||
):
|
||||
return
|
||||
|
||||
# If we update just entity preferences, delay updating
|
||||
# as we might update more
|
||||
if updated_prefs == {PREF_ALEXA_ENTITY_CONFIGS}:
|
||||
if self._alexa_sync_unsub:
|
||||
self._alexa_sync_unsub()
|
||||
|
||||
self._alexa_sync_unsub = async_call_later(
|
||||
self.hass, SYNC_DELAY, self._sync_prefs
|
||||
)
|
||||
return
|
||||
|
||||
await self.async_sync_entities()
|
||||
|
||||
@callback
|
||||
def _async_exposed_entities_updated(self) -> None:
|
||||
"""Handle updated preferences."""
|
||||
# Delay updating as we might update more
|
||||
if self._alexa_sync_unsub:
|
||||
self._alexa_sync_unsub()
|
||||
|
||||
self._alexa_sync_unsub = async_call_later(
|
||||
self.hass, SYNC_DELAY, self._sync_prefs
|
||||
)
|
||||
|
||||
async def _sync_prefs(self, _now):
|
||||
"""Sync the updated preferences to Alexa."""
|
||||
self._alexa_sync_unsub = None
|
||||
old_prefs = self._cur_entity_prefs
|
||||
new_prefs = self._prefs.alexa_entity_configs
|
||||
new_prefs = async_get_assistant_settings(self.hass, CLOUD_ALEXA)
|
||||
|
||||
seen = set()
|
||||
to_update = []
|
||||
|
|
|
@ -19,6 +19,8 @@ PREF_USERNAME = "username"
|
|||
PREF_REMOTE_DOMAIN = "remote_domain"
|
||||
PREF_ALEXA_DEFAULT_EXPOSE = "alexa_default_expose"
|
||||
PREF_GOOGLE_DEFAULT_EXPOSE = "google_default_expose"
|
||||
PREF_ALEXA_SETTINGS_VERSION = "alexa_settings_version"
|
||||
PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
|
||||
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
|
||||
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female")
|
||||
DEFAULT_DISABLE_2FA = False
|
||||
|
|
|
@ -9,6 +9,10 @@ from hass_nabucasa.google_report_state import ErrorResponse
|
|||
|
||||
from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
|
||||
from homeassistant.components.google_assistant.helpers import AbstractConfig
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_listen_entity_updates,
|
||||
async_should_expose,
|
||||
)
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import (
|
||||
CoreState,
|
||||
|
@ -22,14 +26,18 @@ from homeassistant.setup import async_setup_component
|
|||
|
||||
from .const import (
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_FILTER,
|
||||
DEFAULT_DISABLE_2FA,
|
||||
DOMAIN as CLOUD_DOMAIN,
|
||||
PREF_DISABLE_2FA,
|
||||
PREF_SHOULD_EXPOSE,
|
||||
)
|
||||
from .prefs import CloudPreferences
|
||||
from .prefs import GOOGLE_SETTINGS_VERSION, CloudPreferences
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
|
||||
|
||||
|
||||
class CloudGoogleConfig(AbstractConfig):
|
||||
"""HA Cloud Configuration for Google Assistant."""
|
||||
|
@ -48,8 +56,6 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
self._user = cloud_user
|
||||
self._prefs = prefs
|
||||
self._cloud = cloud
|
||||
self._cur_entity_prefs = self._prefs.google_entity_configs
|
||||
self._cur_default_expose = self._prefs.google_default_expose
|
||||
self._sync_entities_lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
|
@ -89,10 +95,35 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
"""Return Cloud User account."""
|
||||
return self._user
|
||||
|
||||
def _migrate_google_entity_settings_v1(self):
|
||||
"""Migrate Google entity settings to entity registry options."""
|
||||
if not self._config[CONF_FILTER].empty_filter:
|
||||
# Don't migrate if there's a YAML config
|
||||
return
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
for entity_id, entry in entity_registry.entities.items():
|
||||
if CLOUD_GOOGLE in entry.options:
|
||||
continue
|
||||
options = {"should_expose": self._should_expose_legacy(entity_id)}
|
||||
if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None):
|
||||
options[PREF_DISABLE_2FA] = _2fa_disabled
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id, CLOUD_GOOGLE, options
|
||||
)
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Perform async initialization of config."""
|
||||
await super().async_initialize()
|
||||
|
||||
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
|
||||
if self._prefs.google_settings_version < 2:
|
||||
self._migrate_google_entity_settings_v1()
|
||||
await self._prefs.async_update(
|
||||
google_settings_version=GOOGLE_SETTINGS_VERSION
|
||||
)
|
||||
|
||||
async def hass_started(hass):
|
||||
if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components:
|
||||
await async_setup_component(self.hass, GOOGLE_DOMAIN, {})
|
||||
|
@ -109,7 +140,9 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
await self.async_disconnect_agent_user(agent_user_id)
|
||||
|
||||
self._prefs.async_listen_updates(self._async_prefs_updated)
|
||||
|
||||
async_listen_entity_updates(
|
||||
self.hass, CLOUD_GOOGLE, self._async_exposed_entities_updated
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
self._handle_entity_registry_updated,
|
||||
|
@ -123,14 +156,11 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
"""If a state object should be exposed."""
|
||||
return self._should_expose_entity_id(state.entity_id)
|
||||
|
||||
def _should_expose_entity_id(self, entity_id):
|
||||
def _should_expose_legacy(self, entity_id):
|
||||
"""If an entity ID should be exposed."""
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
if not self._config["filter"].empty_filter:
|
||||
return self._config["filter"](entity_id)
|
||||
|
||||
entity_configs = self._prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
entity_expose = entity_config.get(PREF_SHOULD_EXPOSE)
|
||||
|
@ -154,6 +184,15 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
|
||||
return not auxiliary_entity and split_entity_id(entity_id)[0] in default_expose
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
def agent_user_id(self):
|
||||
"""Return Agent User Id to use for query responses."""
|
||||
|
@ -168,11 +207,23 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
"""Get agent user ID making request."""
|
||||
return self.agent_user_id
|
||||
|
||||
def should_2fa(self, state):
|
||||
def _2fa_disabled_legacy(self, entity_id):
|
||||
"""If an entity should be checked for 2FA."""
|
||||
entity_configs = self._prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(state.entity_id, {})
|
||||
return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
return entity_config.get(PREF_DISABLE_2FA)
|
||||
|
||||
def should_2fa(self, state):
|
||||
"""If an entity should be checked for 2FA."""
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
registry_entry = entity_registry.async_get(state.entity_id)
|
||||
if not registry_entry:
|
||||
# Handle the entity has been removed
|
||||
return False
|
||||
|
||||
assistant_options = registry_entry.options.get(CLOUD_GOOGLE, {})
|
||||
return not assistant_options.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
||||
|
||||
async def async_report_state(self, message, agent_user_id: str):
|
||||
"""Send a state report to Google."""
|
||||
|
@ -218,14 +269,6 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
# So when we change it, we need to sync all entities.
|
||||
sync_entities = True
|
||||
|
||||
# If entity prefs are the same or we have filter in config.yaml,
|
||||
# don't sync.
|
||||
elif (
|
||||
self._cur_entity_prefs is not prefs.google_entity_configs
|
||||
or self._cur_default_expose is not prefs.google_default_expose
|
||||
) and self._config["filter"].empty_filter:
|
||||
self.async_schedule_google_sync_all()
|
||||
|
||||
if self.enabled and not self.is_local_sdk_active:
|
||||
self.async_enable_local_sdk()
|
||||
sync_entities = True
|
||||
|
@ -233,12 +276,14 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
self.async_disable_local_sdk()
|
||||
sync_entities = True
|
||||
|
||||
self._cur_entity_prefs = prefs.google_entity_configs
|
||||
self._cur_default_expose = prefs.google_default_expose
|
||||
|
||||
if sync_entities and self.hass.is_running:
|
||||
await self.async_sync_entities_all()
|
||||
|
||||
@callback
|
||||
def _async_exposed_entities_updated(self) -> None:
|
||||
"""Handle updated preferences."""
|
||||
self.async_schedule_google_sync_all()
|
||||
|
||||
@callback
|
||||
def _handle_entity_registry_updated(self, event: Event) -> None:
|
||||
"""Handle when entity registry updated."""
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""The HTTP api to control the cloud integration."""
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
import dataclasses
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
|
@ -22,22 +23,24 @@ from homeassistant.components.alexa import (
|
|||
from homeassistant.components.google_assistant import helpers as google_helpers
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.location import async_detect_location_info
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PREF_ALEXA_DEFAULT_EXPOSE,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
PREF_DISABLE_2FA,
|
||||
PREF_ENABLE_ALEXA,
|
||||
PREF_ENABLE_GOOGLE,
|
||||
PREF_GOOGLE_DEFAULT_EXPOSE,
|
||||
PREF_GOOGLE_REPORT_STATE,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
||||
PREF_TTS_DEFAULT_VOICE,
|
||||
REQUEST_TIMEOUT,
|
||||
)
|
||||
from .google_config import CLOUD_GOOGLE
|
||||
from .repairs import async_manage_legacy_subscription_issue
|
||||
from .subscription import async_subscription_info
|
||||
|
||||
|
@ -66,11 +69,11 @@ async def async_setup(hass):
|
|||
websocket_api.async_register_command(hass, websocket_remote_connect)
|
||||
websocket_api.async_register_command(hass, websocket_remote_disconnect)
|
||||
|
||||
websocket_api.async_register_command(hass, google_assistant_get)
|
||||
websocket_api.async_register_command(hass, google_assistant_list)
|
||||
websocket_api.async_register_command(hass, google_assistant_update)
|
||||
|
||||
websocket_api.async_register_command(hass, alexa_list)
|
||||
websocket_api.async_register_command(hass, alexa_update)
|
||||
websocket_api.async_register_command(hass, alexa_sync)
|
||||
|
||||
websocket_api.async_register_command(hass, thingtalk_convert)
|
||||
|
@ -350,8 +353,6 @@ async def websocket_subscription(
|
|||
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
||||
vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
|
||||
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
|
||||
vol.Optional(PREF_ALEXA_DEFAULT_EXPOSE): [str],
|
||||
vol.Optional(PREF_GOOGLE_DEFAULT_EXPOSE): [str],
|
||||
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
||||
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
|
||||
vol.Coerce(tuple), vol.In(MAP_VOICE)
|
||||
|
@ -523,6 +524,54 @@ async def websocket_remote_disconnect(
|
|||
connection.send_result(msg["id"], await _account_data(hass, cloud))
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@_require_cloud_login
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
"type": "cloud/google_assistant/entities/get",
|
||||
"entity_id": str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@_ws_handle_cloud_errors
|
||||
async def google_assistant_get(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Get data for a single google assistant entity."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
gconf = await cloud.client.get_google_config()
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_id: str = msg["entity_id"]
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if not entity_registry.async_is_registered(entity_id) or not state:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.const.ERR_NOT_FOUND,
|
||||
f"{entity_id} unknown or not in the entity registry",
|
||||
)
|
||||
return
|
||||
|
||||
entity = google_helpers.GoogleEntity(hass, gconf, state)
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity.is_supported():
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.const.ERR_NOT_SUPPORTED,
|
||||
f"{entity_id} not supported by Google assistant",
|
||||
)
|
||||
return
|
||||
|
||||
result = {
|
||||
"entity_id": entity.entity_id,
|
||||
"traits": [trait.name for trait in entity.traits()],
|
||||
"might_2fa": entity.might_2fa_traits(),
|
||||
}
|
||||
|
||||
connection.send_result(msg["id"], result)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@_require_cloud_login
|
||||
@websocket_api.websocket_command({"type": "cloud/google_assistant/entities"})
|
||||
|
@ -536,11 +585,14 @@ async def google_assistant_list(
|
|||
"""List all google assistant entities."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
gconf = await cloud.client.get_google_config()
|
||||
entity_registry = er.async_get(hass)
|
||||
entities = google_helpers.async_get_entities(hass, gconf)
|
||||
|
||||
result = []
|
||||
|
||||
for entity in entities:
|
||||
if not entity_registry.async_is_registered(entity.entity_id):
|
||||
continue
|
||||
result.append(
|
||||
{
|
||||
"entity_id": entity.entity_id,
|
||||
|
@ -558,8 +610,7 @@ async def google_assistant_list(
|
|||
{
|
||||
"type": "cloud/google_assistant/entities/update",
|
||||
"entity_id": str,
|
||||
vol.Optional("should_expose"): vol.Any(None, bool),
|
||||
vol.Optional("disable_2fa"): bool,
|
||||
vol.Optional(PREF_DISABLE_2FA): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
|
@ -569,17 +620,30 @@ async def google_assistant_update(
|
|||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Update google assistant config."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
changes = dict(msg)
|
||||
changes.pop("type")
|
||||
changes.pop("id")
|
||||
"""Update google assistant entity config."""
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_id: str = msg["entity_id"]
|
||||
|
||||
await cloud.client.prefs.async_update_google_entity_config(**changes)
|
||||
if not (registry_entry := entity_registry.async_get(entity_id)):
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.const.ERR_NOT_ALLOWED,
|
||||
f"can't configure {entity_id}",
|
||||
)
|
||||
return
|
||||
|
||||
connection.send_result(
|
||||
msg["id"], cloud.client.prefs.google_entity_configs.get(msg["entity_id"])
|
||||
disable_2fa = msg[PREF_DISABLE_2FA]
|
||||
assistant_options: Mapping[str, Any]
|
||||
if (
|
||||
assistant_options := registry_entry.options.get(CLOUD_GOOGLE, {})
|
||||
) and assistant_options.get(PREF_DISABLE_2FA) == disable_2fa:
|
||||
return
|
||||
|
||||
assistant_options = assistant_options | {PREF_DISABLE_2FA: disable_2fa}
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id, CLOUD_GOOGLE, assistant_options
|
||||
)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
|
@ -595,11 +659,14 @@ async def alexa_list(
|
|||
"""List all alexa entities."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
alexa_config = await cloud.client.get_alexa_config()
|
||||
entity_registry = er.async_get(hass)
|
||||
entities = alexa_entities.async_get_entities(hass, alexa_config)
|
||||
|
||||
result = []
|
||||
|
||||
for entity in entities:
|
||||
if not entity_registry.async_is_registered(entity.entity_id):
|
||||
continue
|
||||
result.append(
|
||||
{
|
||||
"entity_id": entity.entity_id,
|
||||
|
@ -611,35 +678,6 @@ async def alexa_list(
|
|||
connection.send_result(msg["id"], result)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@_require_cloud_login
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
"type": "cloud/alexa/entities/update",
|
||||
"entity_id": str,
|
||||
vol.Optional("should_expose"): vol.Any(None, bool),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@_ws_handle_cloud_errors
|
||||
async def alexa_update(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Update alexa entity config."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
changes = dict(msg)
|
||||
changes.pop("type")
|
||||
changes.pop("id")
|
||||
|
||||
await cloud.client.prefs.async_update_alexa_entity_config(**changes)
|
||||
|
||||
connection.send_result(
|
||||
msg["id"], cloud.client.prefs.alexa_entity_configs.get(msg["entity_id"])
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@_require_cloud_login
|
||||
@websocket_api.websocket_command({"type": "cloud/alexa/sync"})
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Home Assistant Cloud",
|
||||
"after_dependencies": ["google_assistant", "alexa"],
|
||||
"codeowners": ["@home-assistant/cloud"],
|
||||
"dependencies": ["http", "webhook"],
|
||||
"dependencies": ["homeassistant", "http", "webhook"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Preference management for cloud."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.components import webhook
|
||||
|
@ -18,9 +20,9 @@ from .const import (
|
|||
PREF_ALEXA_DEFAULT_EXPOSE,
|
||||
PREF_ALEXA_ENTITY_CONFIGS,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
PREF_ALEXA_SETTINGS_VERSION,
|
||||
PREF_CLOUD_USER,
|
||||
PREF_CLOUDHOOKS,
|
||||
PREF_DISABLE_2FA,
|
||||
PREF_ENABLE_ALEXA,
|
||||
PREF_ENABLE_GOOGLE,
|
||||
PREF_ENABLE_REMOTE,
|
||||
|
@ -29,14 +31,33 @@ from .const import (
|
|||
PREF_GOOGLE_LOCAL_WEBHOOK_ID,
|
||||
PREF_GOOGLE_REPORT_STATE,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
||||
PREF_GOOGLE_SETTINGS_VERSION,
|
||||
PREF_REMOTE_DOMAIN,
|
||||
PREF_SHOULD_EXPOSE,
|
||||
PREF_TTS_DEFAULT_VOICE,
|
||||
PREF_USERNAME,
|
||||
)
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_VERSION_MINOR = 2
|
||||
|
||||
ALEXA_SETTINGS_VERSION = 2
|
||||
GOOGLE_SETTINGS_VERSION = 2
|
||||
|
||||
|
||||
class CloudPreferencesStore(Store):
|
||||
"""Store entity registry data."""
|
||||
|
||||
async def _async_migrate_func(
|
||||
self, old_major_version: int, old_minor_version: int, old_data: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Migrate to the new version."""
|
||||
if old_major_version == 1:
|
||||
if old_minor_version < 2:
|
||||
old_data.setdefault(PREF_ALEXA_SETTINGS_VERSION, 1)
|
||||
old_data.setdefault(PREF_GOOGLE_SETTINGS_VERSION, 1)
|
||||
|
||||
return old_data
|
||||
|
||||
|
||||
class CloudPreferences:
|
||||
|
@ -45,7 +66,9 @@ class CloudPreferences:
|
|||
def __init__(self, hass):
|
||||
"""Initialize cloud prefs."""
|
||||
self._hass = hass
|
||||
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
self._store = CloudPreferencesStore(
|
||||
hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR
|
||||
)
|
||||
self._prefs = None
|
||||
self._listeners = []
|
||||
self.last_updated: set[str] = set()
|
||||
|
@ -79,14 +102,12 @@ class CloudPreferences:
|
|||
google_secure_devices_pin=UNDEFINED,
|
||||
cloudhooks=UNDEFINED,
|
||||
cloud_user=UNDEFINED,
|
||||
google_entity_configs=UNDEFINED,
|
||||
alexa_entity_configs=UNDEFINED,
|
||||
alexa_report_state=UNDEFINED,
|
||||
google_report_state=UNDEFINED,
|
||||
alexa_default_expose=UNDEFINED,
|
||||
google_default_expose=UNDEFINED,
|
||||
tts_default_voice=UNDEFINED,
|
||||
remote_domain=UNDEFINED,
|
||||
alexa_settings_version=UNDEFINED,
|
||||
google_settings_version=UNDEFINED,
|
||||
):
|
||||
"""Update user preferences."""
|
||||
prefs = {**self._prefs}
|
||||
|
@ -98,12 +119,10 @@ class CloudPreferences:
|
|||
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
|
||||
(PREF_CLOUDHOOKS, cloudhooks),
|
||||
(PREF_CLOUD_USER, cloud_user),
|
||||
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
|
||||
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
|
||||
(PREF_ALEXA_REPORT_STATE, alexa_report_state),
|
||||
(PREF_GOOGLE_REPORT_STATE, google_report_state),
|
||||
(PREF_ALEXA_DEFAULT_EXPOSE, alexa_default_expose),
|
||||
(PREF_GOOGLE_DEFAULT_EXPOSE, google_default_expose),
|
||||
(PREF_ALEXA_SETTINGS_VERSION, alexa_settings_version),
|
||||
(PREF_GOOGLE_SETTINGS_VERSION, google_settings_version),
|
||||
(PREF_TTS_DEFAULT_VOICE, tts_default_voice),
|
||||
(PREF_REMOTE_DOMAIN, remote_domain),
|
||||
):
|
||||
|
@ -112,53 +131,6 @@ class CloudPreferences:
|
|||
|
||||
await self._save_prefs(prefs)
|
||||
|
||||
async def async_update_google_entity_config(
|
||||
self,
|
||||
*,
|
||||
entity_id,
|
||||
disable_2fa=UNDEFINED,
|
||||
should_expose=UNDEFINED,
|
||||
):
|
||||
"""Update config for a Google entity."""
|
||||
entities = self.google_entity_configs
|
||||
entity = entities.get(entity_id, {})
|
||||
|
||||
changes = {}
|
||||
for key, value in (
|
||||
(PREF_DISABLE_2FA, disable_2fa),
|
||||
(PREF_SHOULD_EXPOSE, should_expose),
|
||||
):
|
||||
if value is not UNDEFINED:
|
||||
changes[key] = value
|
||||
|
||||
if not changes:
|
||||
return
|
||||
|
||||
updated_entity = {**entity, **changes}
|
||||
|
||||
updated_entities = {**entities, entity_id: updated_entity}
|
||||
await self.async_update(google_entity_configs=updated_entities)
|
||||
|
||||
async def async_update_alexa_entity_config(
|
||||
self, *, entity_id, should_expose=UNDEFINED
|
||||
):
|
||||
"""Update config for an Alexa entity."""
|
||||
entities = self.alexa_entity_configs
|
||||
entity = entities.get(entity_id, {})
|
||||
|
||||
changes = {}
|
||||
for key, value in ((PREF_SHOULD_EXPOSE, should_expose),):
|
||||
if value is not UNDEFINED:
|
||||
changes[key] = value
|
||||
|
||||
if not changes:
|
||||
return
|
||||
|
||||
updated_entity = {**entity, **changes}
|
||||
|
||||
updated_entities = {**entities, entity_id: updated_entity}
|
||||
await self.async_update(alexa_entity_configs=updated_entities)
|
||||
|
||||
async def async_set_username(self, username) -> bool:
|
||||
"""Set the username that is logged in."""
|
||||
# Logging out.
|
||||
|
@ -186,14 +158,12 @@ class CloudPreferences:
|
|||
"""Return dictionary version."""
|
||||
return {
|
||||
PREF_ALEXA_DEFAULT_EXPOSE: self.alexa_default_expose,
|
||||
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
|
||||
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
|
||||
PREF_CLOUDHOOKS: self.cloudhooks,
|
||||
PREF_ENABLE_ALEXA: self.alexa_enabled,
|
||||
PREF_ENABLE_GOOGLE: self.google_enabled,
|
||||
PREF_ENABLE_REMOTE: self.remote_enabled,
|
||||
PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose,
|
||||
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
|
||||
PREF_GOOGLE_REPORT_STATE: self.google_report_state,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
|
||||
PREF_TTS_DEFAULT_VOICE: self.tts_default_voice,
|
||||
|
@ -235,6 +205,11 @@ class CloudPreferences:
|
|||
"""Return Alexa Entity configurations."""
|
||||
return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {})
|
||||
|
||||
@property
|
||||
def alexa_settings_version(self):
|
||||
"""Return version of Alexa settings."""
|
||||
return self._prefs[PREF_ALEXA_SETTINGS_VERSION]
|
||||
|
||||
@property
|
||||
def google_enabled(self):
|
||||
"""Return if Google is enabled."""
|
||||
|
@ -255,6 +230,11 @@ class CloudPreferences:
|
|||
"""Return Google Entity configurations."""
|
||||
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
|
||||
|
||||
@property
|
||||
def google_settings_version(self):
|
||||
"""Return version of Google settings."""
|
||||
return self._prefs[PREF_GOOGLE_SETTINGS_VERSION]
|
||||
|
||||
@property
|
||||
def google_local_webhook_id(self):
|
||||
"""Return Google webhook ID to receive local messages."""
|
||||
|
@ -319,6 +299,7 @@ class CloudPreferences:
|
|||
return {
|
||||
PREF_ALEXA_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
||||
PREF_ALEXA_ENTITY_CONFIGS: {},
|
||||
PREF_ALEXA_SETTINGS_VERSION: ALEXA_SETTINGS_VERSION,
|
||||
PREF_CLOUD_USER: None,
|
||||
PREF_CLOUDHOOKS: {},
|
||||
PREF_ENABLE_ALEXA: True,
|
||||
|
@ -326,6 +307,7 @@ class CloudPreferences:
|
|||
PREF_ENABLE_REMOTE: False,
|
||||
PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
||||
PREF_GOOGLE_ENTITY_CONFIGS: {},
|
||||
PREF_GOOGLE_SETTINGS_VERSION: GOOGLE_SETTINGS_VERSION,
|
||||
PREF_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(),
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
||||
PREF_REMOTE_DOMAIN: None,
|
||||
|
|
|
@ -33,10 +33,12 @@ from homeassistant.helpers.service import (
|
|||
from homeassistant.helpers.template import async_load_custom_templates
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_EXPOSED_ENTITIES, DOMAIN
|
||||
from .exposed_entities import ExposedEntities
|
||||
|
||||
ATTR_ENTRY_ID = "entry_id"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = ha.DOMAIN
|
||||
SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
|
||||
SERVICE_RELOAD_CONFIG_ENTRY = "reload_config_entry"
|
||||
SERVICE_RELOAD_CUSTOM_TEMPLATES = "reload_custom_templates"
|
||||
|
@ -340,4 +342,8 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no
|
|||
hass, ha.DOMAIN, SERVICE_RELOAD_ALL, async_handle_reload_all
|
||||
)
|
||||
|
||||
exposed_entities = ExposedEntities(hass)
|
||||
await exposed_entities.async_initialize()
|
||||
hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities
|
||||
|
||||
return True
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
"""Constants for the Homeassistant integration."""
|
||||
import homeassistant.core as ha
|
||||
|
||||
DOMAIN = ha.DOMAIN
|
||||
|
||||
DATA_EXPOSED_ENTITIES = f"{DOMAIN}.exposed_entites"
|
|
@ -0,0 +1,351 @@
|
|||
"""Control which entities are exposed to voice assistants."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
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.entity import get_device_class
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import DATA_EXPOSED_ENTITIES, DOMAIN
|
||||
|
||||
KNOWN_ASSISTANTS = ("cloud.alexa", "cloud.google_assistant")
|
||||
|
||||
STORAGE_KEY = f"{DOMAIN}.exposed_entities"
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
SAVE_DELAY = 10
|
||||
|
||||
DEFAULT_EXPOSED_DOMAINS = {
|
||||
"climate",
|
||||
"cover",
|
||||
"fan",
|
||||
"humidifier",
|
||||
"light",
|
||||
"lock",
|
||||
"scene",
|
||||
"script",
|
||||
"switch",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
}
|
||||
|
||||
DEFAULT_EXPOSED_BINARY_SENSOR_DEVICE_CLASSES = {
|
||||
BinarySensorDeviceClass.DOOR,
|
||||
BinarySensorDeviceClass.GARAGE_DOOR,
|
||||
BinarySensorDeviceClass.LOCK,
|
||||
BinarySensorDeviceClass.MOTION,
|
||||
BinarySensorDeviceClass.OPENING,
|
||||
BinarySensorDeviceClass.PRESENCE,
|
||||
BinarySensorDeviceClass.WINDOW,
|
||||
}
|
||||
|
||||
DEFAULT_EXPOSED_SENSOR_DEVICE_CLASSES = {
|
||||
SensorDeviceClass.AQI,
|
||||
SensorDeviceClass.CO,
|
||||
SensorDeviceClass.CO2,
|
||||
SensorDeviceClass.HUMIDITY,
|
||||
SensorDeviceClass.PM10,
|
||||
SensorDeviceClass.PM25,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class AssistantPreferences:
|
||||
"""Preferences for an assistant."""
|
||||
|
||||
expose_new: bool
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
"""Return a JSON serializable representation for storage."""
|
||||
return {"expose_new": self.expose_new}
|
||||
|
||||
|
||||
class ExposedEntities:
|
||||
"""Control assistant settings."""
|
||||
|
||||
_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
|
||||
)
|
||||
|
||||
async def async_initialize(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()
|
||||
|
||||
@callback
|
||||
def async_listen_entity_updates(
|
||||
self, assistant: str, listener: Callable[[], None]
|
||||
) -> None:
|
||||
"""Listen for updates to entity expose settings."""
|
||||
self._listeners.setdefault(assistant, []).append(listener)
|
||||
|
||||
@callback
|
||||
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)
|
||||
if not (registry_entry := entity_registry.async_get(entity_id)):
|
||||
raise HomeAssistantError("Unknown entity")
|
||||
|
||||
assistant_options: Mapping[str, Any]
|
||||
if (
|
||||
assistant_options := registry_entry.options.get(assistant, {})
|
||||
) and assistant_options.get("should_expose") == should_expose:
|
||||
return
|
||||
|
||||
assistant_options = assistant_options | {"should_expose": should_expose}
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id, assistant, assistant_options
|
||||
)
|
||||
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."""
|
||||
if prefs := self._assistants.get(assistant):
|
||||
return prefs.expose_new
|
||||
return False
|
||||
|
||||
@callback
|
||||
def async_set_expose_new_entities(self, assistant: str, expose_new: bool) -> None:
|
||||
"""Enable an assistant to expose new entities."""
|
||||
self._assistants[assistant] = AssistantPreferences(expose_new=expose_new)
|
||||
self._async_schedule_save()
|
||||
|
||||
@callback
|
||||
def async_get_assistant_settings(
|
||||
self, assistant: str
|
||||
) -> dict[str, Mapping[str, Any]]:
|
||||
"""Get all entity expose settings for an assistant."""
|
||||
entity_registry = er.async_get(self._hass)
|
||||
result: dict[str, Mapping[str, Any]] = {}
|
||||
|
||||
for entity_id, entry in entity_registry.entities.items():
|
||||
if options := entry.options.get(assistant):
|
||||
result[entity_id] = options
|
||||
|
||||
return result
|
||||
|
||||
@callback
|
||||
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)
|
||||
if not (registry_entry := entity_registry.async_get(entity_id)):
|
||||
# Entities which are not in the entity registry are not exposed
|
||||
return False
|
||||
|
||||
if assistant in registry_entry.options:
|
||||
if "should_expose" in registry_entry.options[assistant]:
|
||||
should_expose = registry_entry.options[assistant]["should_expose"]
|
||||
return should_expose
|
||||
|
||||
if (prefs := self._assistants.get(assistant)) and prefs.expose_new:
|
||||
should_expose = self._is_default_exposed(entity_id, registry_entry)
|
||||
else:
|
||||
should_expose = False
|
||||
|
||||
assistant_options: Mapping[str, Any] = registry_entry.options.get(assistant, {})
|
||||
assistant_options = assistant_options | {"should_expose": should_expose}
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id, assistant, assistant_options
|
||||
)
|
||||
|
||||
return should_expose
|
||||
|
||||
def _is_default_exposed(
|
||||
self, entity_id: str, registry_entry: er.RegistryEntry
|
||||
) -> bool:
|
||||
"""Return True if an entity is exposed by default."""
|
||||
if (
|
||||
registry_entry.entity_category is not None
|
||||
or registry_entry.hidden_by is not None
|
||||
):
|
||||
return False
|
||||
|
||||
domain = split_entity_id(entity_id)[0]
|
||||
if domain in DEFAULT_EXPOSED_DOMAINS:
|
||||
return True
|
||||
|
||||
device_class = get_device_class(self._hass, entity_id)
|
||||
if (
|
||||
domain == "binary_sensor"
|
||||
and device_class in DEFAULT_EXPOSED_BINARY_SENSOR_DEVICE_CLASSES
|
||||
):
|
||||
return True
|
||||
|
||||
if domain == "sensor" and device_class in DEFAULT_EXPOSED_SENSOR_DEVICE_CLASSES:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load from the store."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
assistants: dict[str, AssistantPreferences] = {}
|
||||
|
||||
if 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()
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "homeassistant/expose_entity",
|
||||
vol.Required("assistants"): [vol.In(KNOWN_ASSISTANTS)],
|
||||
vol.Required("entity_ids"): [str],
|
||||
vol.Required("should_expose"): bool,
|
||||
}
|
||||
)
|
||||
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(
|
||||
(
|
||||
entity_id
|
||||
for entity_id in entity_ids
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
),
|
||||
None,
|
||||
):
|
||||
connection.send_error(
|
||||
msg["id"], websocket_api.const.ERR_NOT_ALLOWED, f"can't expose '{blocked}'"
|
||||
)
|
||||
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(
|
||||
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_new_entities/get",
|
||||
vol.Required("assistant"): vol.In(KNOWN_ASSISTANTS),
|
||||
}
|
||||
)
|
||||
def ws_expose_new_entities_get(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Check if new entities are exposed to an assistant."""
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
expose_new = exposed_entities.async_get_expose_new_entities(msg["assistant"])
|
||||
connection.send_result(msg["id"], {"expose_new": expose_new})
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "homeassistant/expose_new_entities/set",
|
||||
vol.Required("assistant"): vol.In(KNOWN_ASSISTANTS),
|
||||
vol.Required("expose_new"): bool,
|
||||
}
|
||||
)
|
||||
def ws_expose_new_entities_set(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Expose new entities to an assistatant."""
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
exposed_entities.async_set_expose_new_entities(msg["assistant"], msg["expose_new"])
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@callback
|
||||
def async_listen_entity_updates(
|
||||
hass: HomeAssistant, assistant: str, listener: Callable[[], None]
|
||||
) -> None:
|
||||
"""Listen for updates to entity expose settings."""
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
exposed_entities.async_listen_entity_updates(assistant, listener)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_assistant_settings(
|
||||
hass: HomeAssistant, assistant: str
|
||||
) -> dict[str, Mapping[str, Any]]:
|
||||
"""Get all entity expose settings for an assistant."""
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
return exposed_entities.async_get_assistant_settings(assistant)
|
||||
|
||||
|
||||
@callback
|
||||
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)
|
10
mypy.ini
10
mypy.ini
|
@ -1132,6 +1132,16 @@ disallow_untyped_defs = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.homeassistant.exposed_entities]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.homeassistant.triggers.event]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from homeassistant.components import cloud
|
||||
from homeassistant.components.cloud import const
|
||||
from homeassistant.components.cloud import const, prefs as cloud_prefs
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
|
@ -18,9 +18,11 @@ async def mock_cloud(hass, config=None):
|
|||
def mock_cloud_prefs(hass, prefs={}):
|
||||
"""Fixture for cloud component."""
|
||||
prefs_to_set = {
|
||||
const.PREF_ALEXA_SETTINGS_VERSION: cloud_prefs.ALEXA_SETTINGS_VERSION,
|
||||
const.PREF_ENABLE_ALEXA: True,
|
||||
const.PREF_ENABLE_GOOGLE: True,
|
||||
const.PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
||||
const.PREF_GOOGLE_SETTINGS_VERSION: cloud_prefs.GOOGLE_SETTINGS_VERSION,
|
||||
}
|
||||
prefs_to_set.update(prefs)
|
||||
hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set
|
||||
|
|
|
@ -6,10 +6,22 @@ import pytest
|
|||
|
||||
from homeassistant.components.alexa import errors
|
||||
from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config
|
||||
from homeassistant.components.cloud.const import (
|
||||
PREF_ALEXA_DEFAULT_EXPOSE,
|
||||
PREF_ALEXA_ENTITY_CONFIGS,
|
||||
PREF_SHOULD_EXPOSE,
|
||||
)
|
||||
from homeassistant.components.cloud.prefs import CloudPreferences
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
DATA_EXPOSED_ENTITIES,
|
||||
ExposedEntities,
|
||||
)
|
||||
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
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
@ -21,10 +33,23 @@ def cloud_stub():
|
|||
return Mock(is_logged_in=True, subscription_expired=False)
|
||||
|
||||
|
||||
def expose_new(hass, expose_new):
|
||||
"""Enable exposing new entities to Alexa."""
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
exposed_entities.async_set_expose_new_entities("cloud.alexa", expose_new)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
async def test_alexa_config_expose_entity_prefs(
|
||||
hass: HomeAssistant, cloud_prefs, cloud_stub, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test Alexa config should expose using prefs."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
entity_entry1 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
|
@ -53,54 +78,62 @@ async def test_alexa_config_expose_entity_prefs(
|
|||
suggested_object_id="hidden_user_light",
|
||||
hidden_by=er.RegistryEntryHider.USER,
|
||||
)
|
||||
entity_entry5 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_basement_id",
|
||||
suggested_object_id="basement",
|
||||
)
|
||||
entity_entry6 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_entrance_id",
|
||||
suggested_object_id="entrance",
|
||||
)
|
||||
|
||||
entity_conf = {"should_expose": False}
|
||||
await cloud_prefs.async_update(
|
||||
alexa_entity_configs={"light.kitchen": entity_conf},
|
||||
alexa_default_expose=["light"],
|
||||
alexa_enabled=True,
|
||||
alexa_report_state=False,
|
||||
)
|
||||
expose_new(hass, True)
|
||||
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")
|
||||
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)
|
||||
|
||||
entity_conf["should_expose"] = True
|
||||
assert 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)
|
||||
# this has been hidden
|
||||
assert not conf.should_expose(entity_entry5.entity_id)
|
||||
# exposed by default
|
||||
assert conf.should_expose(entity_entry6.entity_id)
|
||||
|
||||
entity_conf["should_expose"] = None
|
||||
assert 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)
|
||||
expose_entity(hass, entity_entry5.entity_id, True)
|
||||
assert conf.should_expose(entity_entry5.entity_id)
|
||||
|
||||
expose_entity(hass, entity_entry5.entity_id, None)
|
||||
assert not conf.should_expose(entity_entry5.entity_id)
|
||||
|
||||
assert "alexa" not in hass.config.components
|
||||
await cloud_prefs.async_update(
|
||||
alexa_default_expose=["sensor"],
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "alexa" in hass.config.components
|
||||
assert not conf.should_expose("light.kitchen")
|
||||
assert not conf.should_expose(entity_entry5.entity_id)
|
||||
|
||||
|
||||
async def test_alexa_config_report_state(
|
||||
hass: HomeAssistant, cloud_prefs, cloud_stub
|
||||
) -> None:
|
||||
"""Test Alexa config should expose using prefs."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
await cloud_prefs.async_update(
|
||||
alexa_report_state=False,
|
||||
)
|
||||
|
@ -134,6 +167,8 @@ async def test_alexa_config_invalidate_token(
|
|||
hass: HomeAssistant, cloud_prefs, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test Alexa config should expose using prefs."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
aioclient_mock.post(
|
||||
"https://example/access_token",
|
||||
json={
|
||||
|
@ -181,10 +216,18 @@ async def test_alexa_config_fail_refresh_token(
|
|||
hass: HomeAssistant,
|
||||
cloud_prefs,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
entity_registry: er.EntityRegistry,
|
||||
reject_reason,
|
||||
expected_exception,
|
||||
) -> None:
|
||||
"""Test Alexa config failing to refresh token."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
# Enable exposing new entities to Alexa
|
||||
expose_new(hass, True)
|
||||
# Register a fan entity
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
"fan", "test", "unique", suggested_object_id="test_fan"
|
||||
)
|
||||
|
||||
aioclient_mock.post(
|
||||
"https://example/access_token",
|
||||
|
@ -216,7 +259,7 @@ async def test_alexa_config_fail_refresh_token(
|
|||
assert conf.should_report_state is False
|
||||
assert conf.is_reporting_states is False
|
||||
|
||||
hass.states.async_set("fan.test_fan", "off")
|
||||
hass.states.async_set(entity_entry.entity_id, "off")
|
||||
|
||||
# Enable state reporting
|
||||
await cloud_prefs.async_update(alexa_report_state=True)
|
||||
|
@ -227,7 +270,7 @@ async def test_alexa_config_fail_refresh_token(
|
|||
assert conf.is_reporting_states is True
|
||||
|
||||
# Change states to trigger event listener
|
||||
hass.states.async_set("fan.test_fan", "on")
|
||||
hass.states.async_set(entity_entry.entity_id, "on")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Invalidate the token and try to fetch another
|
||||
|
@ -240,7 +283,7 @@ async def test_alexa_config_fail_refresh_token(
|
|||
)
|
||||
|
||||
# Change states to trigger event listener
|
||||
hass.states.async_set("fan.test_fan", "off")
|
||||
hass.states.async_set(entity_entry.entity_id, "off")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check state reporting is still wanted in cloud prefs, but disabled for Alexa
|
||||
|
@ -292,16 +335,30 @@ def patch_sync_helper():
|
|||
|
||||
|
||||
async def test_alexa_update_expose_trigger_sync(
|
||||
hass: HomeAssistant, cloud_prefs, cloud_stub
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, cloud_prefs, cloud_stub
|
||||
) -> None:
|
||||
"""Test Alexa config responds to updating exposed entities."""
|
||||
hass.states.async_set("binary_sensor.door", "on")
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
# Enable exposing new entities to Alexa
|
||||
expose_new(hass, True)
|
||||
# Register entities
|
||||
binary_sensor_entry = entity_registry.async_get_or_create(
|
||||
"binary_sensor", "test", "unique", suggested_object_id="door"
|
||||
)
|
||||
sensor_entry = entity_registry.async_get_or_create(
|
||||
"sensor", "test", "unique", suggested_object_id="temp"
|
||||
)
|
||||
light_entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
|
||||
hass.states.async_set(binary_sensor_entry.entity_id, "on")
|
||||
hass.states.async_set(
|
||||
"sensor.temp",
|
||||
sensor_entry.entity_id,
|
||||
"23",
|
||||
{"device_class": "temperature", "unit_of_measurement": "°C"},
|
||||
)
|
||||
hass.states.async_set("light.kitchen", "off")
|
||||
hass.states.async_set(light_entry.entity_id, "off")
|
||||
|
||||
await cloud_prefs.async_update(
|
||||
alexa_enabled=True,
|
||||
|
@ -313,34 +370,26 @@ async def test_alexa_update_expose_trigger_sync(
|
|||
await conf.async_initialize()
|
||||
|
||||
with patch_sync_helper() as (to_update, to_remove):
|
||||
await cloud_prefs.async_update_alexa_entity_config(
|
||||
entity_id="light.kitchen", should_expose=True
|
||||
)
|
||||
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()
|
||||
|
||||
assert conf._alexa_sync_unsub is None
|
||||
assert to_update == ["light.kitchen"]
|
||||
assert to_update == [light_entry.entity_id]
|
||||
assert to_remove == []
|
||||
|
||||
with patch_sync_helper() as (to_update, to_remove):
|
||||
await cloud_prefs.async_update_alexa_entity_config(
|
||||
entity_id="light.kitchen", should_expose=False
|
||||
)
|
||||
await cloud_prefs.async_update_alexa_entity_config(
|
||||
entity_id="binary_sensor.door", should_expose=True
|
||||
)
|
||||
await cloud_prefs.async_update_alexa_entity_config(
|
||||
entity_id="sensor.temp", should_expose=True
|
||||
)
|
||||
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 hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, fire_all=True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert conf._alexa_sync_unsub is None
|
||||
assert sorted(to_update) == ["binary_sensor.door", "sensor.temp"]
|
||||
assert to_remove == ["light.kitchen"]
|
||||
assert sorted(to_update) == [binary_sensor_entry.entity_id, sensor_entry.entity_id]
|
||||
assert to_remove == [light_entry.entity_id]
|
||||
|
||||
with patch_sync_helper() as (to_update, to_remove):
|
||||
await cloud_prefs.async_update(
|
||||
|
@ -350,56 +399,65 @@ async def test_alexa_update_expose_trigger_sync(
|
|||
|
||||
assert conf._alexa_sync_unsub is None
|
||||
assert to_update == []
|
||||
assert to_remove == ["binary_sensor.door", "sensor.temp", "light.kitchen"]
|
||||
assert to_remove == [
|
||||
binary_sensor_entry.entity_id,
|
||||
sensor_entry.entity_id,
|
||||
light_entry.entity_id,
|
||||
]
|
||||
|
||||
|
||||
async def test_alexa_entity_registry_sync(
|
||||
hass: HomeAssistant, mock_cloud_login, cloud_prefs
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_cloud_login,
|
||||
cloud_prefs,
|
||||
) -> None:
|
||||
"""Test Alexa config responds to entity registry."""
|
||||
# Enable exposing new entities to Alexa
|
||||
expose_new(hass, True)
|
||||
|
||||
await alexa_config.CloudAlexaConfig(
|
||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
|
||||
).async_initialize()
|
||||
|
||||
with patch_sync_helper() as (to_update, to_remove):
|
||||
hass.bus.async_fire(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "create", "entity_id": "light.kitchen"},
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert to_update == ["light.kitchen"]
|
||||
assert to_update == [entry.entity_id]
|
||||
assert to_remove == []
|
||||
|
||||
with patch_sync_helper() as (to_update, to_remove):
|
||||
hass.bus.async_fire(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "remove", "entity_id": "light.kitchen"},
|
||||
{"action": "remove", "entity_id": entry.entity_id},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert to_update == []
|
||||
assert to_remove == ["light.kitchen"]
|
||||
assert to_remove == [entry.entity_id]
|
||||
|
||||
with patch_sync_helper() as (to_update, to_remove):
|
||||
hass.bus.async_fire(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{
|
||||
"action": "update",
|
||||
"entity_id": "light.kitchen",
|
||||
"entity_id": entry.entity_id,
|
||||
"changes": ["entity_id"],
|
||||
"old_entity_id": "light.living_room",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert to_update == ["light.kitchen"]
|
||||
assert to_update == [entry.entity_id]
|
||||
assert to_remove == ["light.living_room"]
|
||||
|
||||
with patch_sync_helper() as (to_update, to_remove):
|
||||
hass.bus.async_fire(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "update", "entity_id": "light.kitchen", "changes": ["icon"]},
|
||||
{"action": "update", "entity_id": entry.entity_id, "changes": ["icon"]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -411,6 +469,7 @@ async def test_alexa_update_report_state(
|
|||
hass: HomeAssistant, cloud_prefs, cloud_stub
|
||||
) -> None:
|
||||
"""Test Alexa config responds to reporting state."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
await cloud_prefs.async_update(
|
||||
alexa_report_state=False,
|
||||
)
|
||||
|
@ -450,6 +509,7 @@ async def test_alexa_handle_logout(
|
|||
hass: HomeAssistant, cloud_prefs, cloud_stub
|
||||
) -> None:
|
||||
"""Test Alexa config responds to logging out."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
aconf = alexa_config.CloudAlexaConfig(
|
||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
||||
)
|
||||
|
@ -475,3 +535,118 @@ async def test_alexa_handle_logout(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_enable.return_value.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_alexa_config_migrate_expose_entity_prefs(
|
||||
hass: HomeAssistant,
|
||||
cloud_prefs: CloudPreferences,
|
||||
cloud_stub,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test migrating Alexa entity config."""
|
||||
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
entity_exposed = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_exposed",
|
||||
suggested_object_id="exposed",
|
||||
)
|
||||
|
||||
entity_migrated = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_migrated",
|
||||
suggested_object_id="migrated",
|
||||
)
|
||||
|
||||
entity_config = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_config",
|
||||
suggested_object_id="config",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
|
||||
entity_default = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_default",
|
||||
suggested_object_id="default",
|
||||
)
|
||||
|
||||
entity_blocked = entity_registry.async_get_or_create(
|
||||
"group",
|
||||
"test",
|
||||
"group_all_locks",
|
||||
suggested_object_id="all_locks",
|
||||
)
|
||||
assert entity_blocked.entity_id == "group.all_locks"
|
||||
|
||||
await cloud_prefs.async_update(
|
||||
alexa_enabled=True,
|
||||
alexa_report_state=False,
|
||||
alexa_settings_version=1,
|
||||
)
|
||||
expose_entity(hass, entity_migrated.entity_id, False)
|
||||
|
||||
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.unknown"] = {
|
||||
PREF_SHOULD_EXPOSE: True
|
||||
}
|
||||
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_exposed.entity_id] = {
|
||||
PREF_SHOULD_EXPOSE: True
|
||||
}
|
||||
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_migrated.entity_id] = {
|
||||
PREF_SHOULD_EXPOSE: True
|
||||
}
|
||||
conf = alexa_config.CloudAlexaConfig(
|
||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
||||
)
|
||||
await conf.async_initialize()
|
||||
|
||||
entity_exposed = entity_registry.async_get(entity_exposed.entity_id)
|
||||
assert entity_exposed.options == {"cloud.alexa": {"should_expose": True}}
|
||||
|
||||
entity_migrated = entity_registry.async_get(entity_migrated.entity_id)
|
||||
assert entity_migrated.options == {"cloud.alexa": {"should_expose": False}}
|
||||
|
||||
entity_config = entity_registry.async_get(entity_config.entity_id)
|
||||
assert entity_config.options == {"cloud.alexa": {"should_expose": False}}
|
||||
|
||||
entity_default = entity_registry.async_get(entity_default.entity_id)
|
||||
assert entity_default.options == {"cloud.alexa": {"should_expose": True}}
|
||||
|
||||
entity_blocked = entity_registry.async_get(entity_blocked.entity_id)
|
||||
assert entity_blocked.options == {"cloud.alexa": {"should_expose": False}}
|
||||
|
||||
|
||||
async def test_alexa_config_migrate_expose_entity_prefs_default_none(
|
||||
hass: HomeAssistant,
|
||||
cloud_prefs: CloudPreferences,
|
||||
cloud_stub,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test migrating Alexa entity config."""
|
||||
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
entity_default = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_default",
|
||||
suggested_object_id="default",
|
||||
)
|
||||
|
||||
await cloud_prefs.async_update(
|
||||
alexa_enabled=True,
|
||||
alexa_report_state=False,
|
||||
alexa_settings_version=1,
|
||||
)
|
||||
|
||||
cloud_prefs._prefs[PREF_ALEXA_DEFAULT_EXPOSE] = None
|
||||
conf = alexa_config.CloudAlexaConfig(
|
||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
||||
)
|
||||
await conf.async_initialize()
|
||||
|
||||
entity_default = entity_registry.async_get(entity_default.entity_id)
|
||||
assert entity_default.options == {"cloud.alexa": {"should_expose": True}}
|
||||
|
|
|
@ -13,8 +13,13 @@ from homeassistant.components.cloud.const import (
|
|||
PREF_ENABLE_ALEXA,
|
||||
PREF_ENABLE_GOOGLE,
|
||||
)
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
DATA_EXPOSED_ENTITIES,
|
||||
ExposedEntities,
|
||||
)
|
||||
from homeassistant.const import CONTENT_TYPE_JSON
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
@ -245,14 +250,25 @@ async def test_google_config_expose_entity(
|
|||
hass: HomeAssistant, mock_cloud_setup, mock_cloud_login
|
||||
) -> None:
|
||||
"""Test Google config exposing entity method uses latest config."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Enable exposing new entities to Google
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
exposed_entities.async_set_expose_new_entities("cloud.google_assistant", True)
|
||||
|
||||
# Register a light entity
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
|
||||
cloud_client = hass.data[DOMAIN].client
|
||||
state = State("light.kitchen", "on")
|
||||
state = State(entity_entry.entity_id, "on")
|
||||
gconf = await cloud_client.get_google_config()
|
||||
|
||||
assert gconf.should_expose(state)
|
||||
|
||||
await cloud_client.prefs.async_update_google_entity_config(
|
||||
entity_id="light.kitchen", should_expose=False
|
||||
exposed_entities.async_expose_entity(
|
||||
"cloud.google_assistant", entity_entry.entity_id, False
|
||||
)
|
||||
|
||||
assert not gconf.should_expose(state)
|
||||
|
@ -262,14 +278,21 @@ async def test_google_config_should_2fa(
|
|||
hass: HomeAssistant, mock_cloud_setup, mock_cloud_login
|
||||
) -> None:
|
||||
"""Test Google config disabling 2FA method uses latest config."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Register a light entity
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
|
||||
cloud_client = hass.data[DOMAIN].client
|
||||
gconf = await cloud_client.get_google_config()
|
||||
state = State("light.kitchen", "on")
|
||||
state = State(entity_entry.entity_id, "on")
|
||||
|
||||
assert gconf.should_2fa(state)
|
||||
|
||||
await cloud_client.prefs.async_update_google_entity_config(
|
||||
entity_id="light.kitchen", disable_2fa=True
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_entry.entity_id, "cloud.google_assistant", {"disable_2fa": True}
|
||||
)
|
||||
|
||||
assert not gconf.should_2fa(state)
|
||||
|
|
|
@ -6,11 +6,24 @@ from freezegun import freeze_time
|
|||
import pytest
|
||||
|
||||
from homeassistant.components.cloud import GACTIONS_SCHEMA
|
||||
from homeassistant.components.cloud.const import (
|
||||
PREF_DISABLE_2FA,
|
||||
PREF_GOOGLE_DEFAULT_EXPOSE,
|
||||
PREF_GOOGLE_ENTITY_CONFIGS,
|
||||
PREF_SHOULD_EXPOSE,
|
||||
)
|
||||
from homeassistant.components.cloud.google_config import CloudGoogleConfig
|
||||
from homeassistant.components.cloud.prefs import CloudPreferences
|
||||
from homeassistant.components.google_assistant import helpers as ga_helpers
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
DATA_EXPOSED_ENTITIES,
|
||||
ExposedEntities,
|
||||
)
|
||||
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
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
@ -28,10 +41,26 @@ def mock_conf(hass, cloud_prefs):
|
|||
)
|
||||
|
||||
|
||||
def expose_new(hass, expose_new):
|
||||
"""Enable exposing new entities to Google."""
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
exposed_entities.async_set_expose_new_entities("cloud.google_assistant", expose_new)
|
||||
|
||||
|
||||
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(
|
||||
"cloud.google_assistant", entity_id, should_expose
|
||||
)
|
||||
|
||||
|
||||
async def test_google_update_report_state(
|
||||
mock_conf, hass: HomeAssistant, cloud_prefs
|
||||
) -> None:
|
||||
"""Test Google config responds to updating preference."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
await mock_conf.async_initialize()
|
||||
await mock_conf.async_connect_agent_user("mock-user-id")
|
||||
|
||||
|
@ -51,6 +80,8 @@ async def test_google_update_report_state_subscription_expired(
|
|||
mock_conf, hass: HomeAssistant, cloud_prefs
|
||||
) -> None:
|
||||
"""Test Google config not reporting state when subscription has expired."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
await mock_conf.async_initialize()
|
||||
await mock_conf.async_connect_agent_user("mock-user-id")
|
||||
|
||||
|
@ -68,6 +99,8 @@ async def test_google_update_report_state_subscription_expired(
|
|||
|
||||
async def test_sync_entities(mock_conf, hass: HomeAssistant, cloud_prefs) -> None:
|
||||
"""Test sync devices."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
await mock_conf.async_initialize()
|
||||
await mock_conf.async_connect_agent_user("mock-user-id")
|
||||
|
||||
|
@ -88,6 +121,22 @@ async def test_google_update_expose_trigger_sync(
|
|||
hass: HomeAssistant, cloud_prefs
|
||||
) -> None:
|
||||
"""Test Google config responds to updating exposed entities."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Enable exposing new entities to Google
|
||||
expose_new(hass, True)
|
||||
# Register entities
|
||||
binary_sensor_entry = entity_registry.async_get_or_create(
|
||||
"binary_sensor", "test", "unique", suggested_object_id="door"
|
||||
)
|
||||
sensor_entry = entity_registry.async_get_or_create(
|
||||
"sensor", "test", "unique", suggested_object_id="temp"
|
||||
)
|
||||
light_entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
|
||||
with freeze_time(utcnow()):
|
||||
config = CloudGoogleConfig(
|
||||
hass,
|
||||
|
@ -102,9 +151,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
|
||||
):
|
||||
await cloud_prefs.async_update_google_entity_config(
|
||||
entity_id="light.kitchen", should_expose=True
|
||||
)
|
||||
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()
|
||||
|
@ -114,15 +161,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
|
||||
):
|
||||
await cloud_prefs.async_update_google_entity_config(
|
||||
entity_id="light.kitchen", should_expose=False
|
||||
)
|
||||
await cloud_prefs.async_update_google_entity_config(
|
||||
entity_id="binary_sensor.door", should_expose=True
|
||||
)
|
||||
await cloud_prefs.async_update_google_entity_config(
|
||||
entity_id="sensor.temp", should_expose=True
|
||||
)
|
||||
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 hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, utcnow())
|
||||
await hass.async_block_till_done()
|
||||
|
@ -134,6 +175,11 @@ async def test_google_entity_registry_sync(
|
|||
hass: HomeAssistant, mock_cloud_login, cloud_prefs
|
||||
) -> None:
|
||||
"""Test Google config responds to entity registry."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Enable exposing new entities to Google
|
||||
expose_new(hass, True)
|
||||
|
||||
config = CloudGoogleConfig(
|
||||
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
|
||||
)
|
||||
|
@ -146,9 +192,8 @@ async def test_google_entity_registry_sync(
|
|||
ga_helpers, "SYNC_DELAY", 0
|
||||
):
|
||||
# Created entity
|
||||
hass.bus.async_fire(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "create", "entity_id": "light.kitchen"},
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -157,7 +202,7 @@ async def test_google_entity_registry_sync(
|
|||
# Removed entity
|
||||
hass.bus.async_fire(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "remove", "entity_id": "light.kitchen"},
|
||||
{"action": "remove", "entity_id": entry.entity_id},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -168,7 +213,7 @@ async def test_google_entity_registry_sync(
|
|||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{
|
||||
"action": "update",
|
||||
"entity_id": "light.kitchen",
|
||||
"entity_id": entry.entity_id,
|
||||
"changes": ["entity_id"],
|
||||
},
|
||||
)
|
||||
|
@ -179,7 +224,7 @@ async def test_google_entity_registry_sync(
|
|||
# Entity registry updated with non-relevant changes
|
||||
hass.bus.async_fire(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "update", "entity_id": "light.kitchen", "changes": ["icon"]},
|
||||
{"action": "update", "entity_id": entry.entity_id, "changes": ["icon"]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -189,7 +234,7 @@ async def test_google_entity_registry_sync(
|
|||
hass.state = CoreState.starting
|
||||
hass.bus.async_fire(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "create", "entity_id": "light.kitchen"},
|
||||
{"action": "create", "entity_id": entry.entity_id},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -204,6 +249,10 @@ async def test_google_device_registry_sync(
|
|||
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
|
||||
)
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
# Enable exposing new entities to Google
|
||||
expose_new(hass, True)
|
||||
|
||||
entity_entry = ent_reg.async_get_or_create("light", "hue", "1234", device_id="1234")
|
||||
entity_entry = ent_reg.async_update_entity(entity_entry.entity_id, area_id="ABCD")
|
||||
|
||||
|
@ -293,6 +342,7 @@ async def test_google_config_expose_entity_prefs(
|
|||
hass: HomeAssistant, mock_conf, cloud_prefs, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test Google config should expose using prefs."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
entity_entry1 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
|
@ -321,45 +371,49 @@ async def test_google_config_expose_entity_prefs(
|
|||
suggested_object_id="hidden_user_light",
|
||||
hidden_by=er.RegistryEntryHider.USER,
|
||||
)
|
||||
|
||||
entity_conf = {"should_expose": False}
|
||||
await cloud_prefs.async_update(
|
||||
google_entity_configs={"light.kitchen": entity_conf},
|
||||
google_default_expose=["light"],
|
||||
entity_entry5 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_basement_id",
|
||||
suggested_object_id="basement",
|
||||
)
|
||||
entity_entry6 = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_entrance_id",
|
||||
suggested_object_id="entrance",
|
||||
)
|
||||
|
||||
expose_new(hass, True)
|
||||
expose_entity(hass, entity_entry5.entity_id, False)
|
||||
|
||||
state = State("light.kitchen", "on")
|
||||
state_config = State(entity_entry1.entity_id, "on")
|
||||
state_diagnostic = State(entity_entry2.entity_id, "on")
|
||||
state_hidden_integration = State(entity_entry3.entity_id, "on")
|
||||
state_hidden_user = State(entity_entry4.entity_id, "on")
|
||||
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)
|
||||
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)
|
||||
|
||||
entity_conf["should_expose"] = True
|
||||
assert 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)
|
||||
# this has been hidden
|
||||
assert not mock_conf.should_expose(state_not_exposed)
|
||||
# exposed by default
|
||||
assert mock_conf.should_expose(state_exposed_default)
|
||||
|
||||
entity_conf["should_expose"] = None
|
||||
assert 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)
|
||||
expose_entity(hass, entity_entry5.entity_id, True)
|
||||
assert mock_conf.should_expose(state_not_exposed)
|
||||
|
||||
await cloud_prefs.async_update(
|
||||
google_default_expose=["sensor"],
|
||||
)
|
||||
assert not mock_conf.should_expose(state)
|
||||
expose_entity(hass, entity_entry5.entity_id, None)
|
||||
assert not mock_conf.should_expose(state_not_exposed)
|
||||
|
||||
|
||||
def test_enabled_requires_valid_sub(
|
||||
|
@ -379,6 +433,7 @@ def test_enabled_requires_valid_sub(
|
|||
|
||||
async def test_setup_integration(hass: HomeAssistant, mock_conf, cloud_prefs) -> None:
|
||||
"""Test that we set up the integration if used."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
mock_conf._cloud.subscription_expired = False
|
||||
|
||||
assert "google_assistant" not in hass.config.components
|
||||
|
@ -423,3 +478,136 @@ async def test_google_handle_logout(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_enable.return_value.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_google_config_migrate_expose_entity_prefs(
|
||||
hass: HomeAssistant,
|
||||
cloud_prefs: CloudPreferences,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test migrating Google entity config."""
|
||||
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
entity_exposed = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_exposed",
|
||||
suggested_object_id="exposed",
|
||||
)
|
||||
|
||||
entity_no_2fa_exposed = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_no_2fa_exposed",
|
||||
suggested_object_id="no_2fa_exposed",
|
||||
)
|
||||
|
||||
entity_migrated = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_migrated",
|
||||
suggested_object_id="migrated",
|
||||
)
|
||||
|
||||
entity_config = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_config",
|
||||
suggested_object_id="config",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
|
||||
entity_default = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_default",
|
||||
suggested_object_id="default",
|
||||
)
|
||||
|
||||
entity_blocked = entity_registry.async_get_or_create(
|
||||
"group",
|
||||
"test",
|
||||
"group_all_locks",
|
||||
suggested_object_id="all_locks",
|
||||
)
|
||||
assert entity_blocked.entity_id == "group.all_locks"
|
||||
|
||||
await cloud_prefs.async_update(
|
||||
google_enabled=True,
|
||||
google_report_state=False,
|
||||
google_settings_version=1,
|
||||
)
|
||||
expose_entity(hass, entity_migrated.entity_id, False)
|
||||
|
||||
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.unknown"] = {
|
||||
PREF_SHOULD_EXPOSE: True
|
||||
}
|
||||
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_exposed.entity_id] = {
|
||||
PREF_SHOULD_EXPOSE: True
|
||||
}
|
||||
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_no_2fa_exposed.entity_id] = {
|
||||
PREF_SHOULD_EXPOSE: True,
|
||||
PREF_DISABLE_2FA: True,
|
||||
}
|
||||
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_migrated.entity_id] = {
|
||||
PREF_SHOULD_EXPOSE: True
|
||||
}
|
||||
conf = CloudGoogleConfig(
|
||||
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
|
||||
)
|
||||
await conf.async_initialize()
|
||||
|
||||
entity_exposed = entity_registry.async_get(entity_exposed.entity_id)
|
||||
assert entity_exposed.options == {"cloud.google_assistant": {"should_expose": True}}
|
||||
|
||||
entity_migrated = entity_registry.async_get(entity_migrated.entity_id)
|
||||
assert entity_migrated.options == {
|
||||
"cloud.google_assistant": {"should_expose": False}
|
||||
}
|
||||
|
||||
entity_no_2fa_exposed = entity_registry.async_get(entity_no_2fa_exposed.entity_id)
|
||||
assert entity_no_2fa_exposed.options == {
|
||||
"cloud.google_assistant": {"disable_2fa": True, "should_expose": True}
|
||||
}
|
||||
|
||||
entity_config = entity_registry.async_get(entity_config.entity_id)
|
||||
assert entity_config.options == {"cloud.google_assistant": {"should_expose": False}}
|
||||
|
||||
entity_default = entity_registry.async_get(entity_default.entity_id)
|
||||
assert entity_default.options == {"cloud.google_assistant": {"should_expose": True}}
|
||||
|
||||
entity_blocked = entity_registry.async_get(entity_blocked.entity_id)
|
||||
assert entity_blocked.options == {
|
||||
"cloud.google_assistant": {"should_expose": False}
|
||||
}
|
||||
|
||||
|
||||
async def test_google_config_migrate_expose_entity_prefs_default_none(
|
||||
hass: HomeAssistant,
|
||||
cloud_prefs: CloudPreferences,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test migrating Google entity config."""
|
||||
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
entity_default = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"test",
|
||||
"light_default",
|
||||
suggested_object_id="default",
|
||||
)
|
||||
|
||||
await cloud_prefs.async_update(
|
||||
google_enabled=True,
|
||||
google_report_state=False,
|
||||
google_settings_version=1,
|
||||
)
|
||||
|
||||
cloud_prefs._prefs[PREF_GOOGLE_DEFAULT_EXPOSE] = None
|
||||
conf = CloudGoogleConfig(
|
||||
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
|
||||
)
|
||||
await conf.async_initialize()
|
||||
|
||||
entity_default = entity_registry.async_get(entity_default.entity_id)
|
||||
assert entity_default.options == {"cloud.google_assistant": {"should_expose": True}}
|
||||
|
|
|
@ -15,6 +15,7 @@ from homeassistant.components.alexa.entities import LightCapabilities
|
|||
from homeassistant.components.cloud.const import DOMAIN
|
||||
from homeassistant.components.google_assistant.helpers import GoogleEntity
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.location import LocationInfo
|
||||
|
||||
from . import mock_cloud, mock_cloud_prefs
|
||||
|
@ -399,11 +400,9 @@ async def test_websocket_status(
|
|||
"alexa_enabled": True,
|
||||
"cloudhooks": {},
|
||||
"google_enabled": True,
|
||||
"google_entity_configs": {},
|
||||
"google_secure_devices_pin": None,
|
||||
"google_default_expose": None,
|
||||
"alexa_default_expose": None,
|
||||
"alexa_entity_configs": {},
|
||||
"alexa_report_state": True,
|
||||
"google_report_state": True,
|
||||
"remote_enabled": False,
|
||||
|
@ -520,8 +519,6 @@ async def test_websocket_update_preferences(
|
|||
"alexa_enabled": False,
|
||||
"google_enabled": False,
|
||||
"google_secure_devices_pin": "1234",
|
||||
"google_default_expose": ["light", "switch"],
|
||||
"alexa_default_expose": ["sensor", "media_player"],
|
||||
"tts_default_voice": ["en-GB", "male"],
|
||||
}
|
||||
)
|
||||
|
@ -531,8 +528,6 @@ async def test_websocket_update_preferences(
|
|||
assert not setup_api.google_enabled
|
||||
assert not setup_api.alexa_enabled
|
||||
assert setup_api.google_secure_devices_pin == "1234"
|
||||
assert setup_api.google_default_expose == ["light", "switch"]
|
||||
assert setup_api.alexa_default_expose == ["sensor", "media_player"]
|
||||
assert setup_api.tts_default_voice == ("en-GB", "male")
|
||||
|
||||
|
||||
|
@ -683,7 +678,11 @@ async def test_enabling_remote(
|
|||
|
||||
|
||||
async def test_list_google_entities(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
setup_api,
|
||||
mock_cloud_login,
|
||||
) -> None:
|
||||
"""Test that we can list Google entities."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
@ -699,9 +698,25 @@ async def test_list_google_entities(
|
|||
"homeassistant.components.google_assistant.helpers.async_get_entities",
|
||||
return_value=[entity, entity2],
|
||||
):
|
||||
await client.send_json({"id": 5, "type": "cloud/google_assistant/entities"})
|
||||
await client.send_json_auto_id({"type": "cloud/google_assistant/entities"})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert len(response["result"]) == 0
|
||||
|
||||
# Add the entities to the entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"cover", "test", "unique", suggested_object_id="garage"
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.google_assistant.helpers.async_get_entities",
|
||||
return_value=[entity, entity2],
|
||||
):
|
||||
await client.send_json_auto_id({"type": "cloud/google_assistant/entities"})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert len(response["result"]) == 2
|
||||
assert response["result"][0] == {
|
||||
|
@ -716,49 +731,118 @@ async def test_list_google_entities(
|
|||
}
|
||||
|
||||
|
||||
async def test_get_google_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
setup_api,
|
||||
mock_cloud_login,
|
||||
) -> None:
|
||||
"""Test that we can get a Google entity."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
# Test getting an unknown entity
|
||||
await client.send_json_auto_id(
|
||||
{"type": "cloud/google_assistant/entities/get", "entity_id": "light.kitchen"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"] == {
|
||||
"code": "not_found",
|
||||
"message": "light.kitchen unknown or not in the entity registry",
|
||||
}
|
||||
|
||||
# Test getting a blocked entity
|
||||
entity_registry.async_get_or_create(
|
||||
"group", "test", "unique", suggested_object_id="all_locks"
|
||||
)
|
||||
hass.states.async_set("group.all_locks", "bla")
|
||||
await client.send_json_auto_id(
|
||||
{"type": "cloud/google_assistant/entities/get", "entity_id": "group.all_locks"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"] == {
|
||||
"code": "not_supported",
|
||||
"message": "group.all_locks not supported by Google assistant",
|
||||
}
|
||||
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"cover", "test", "unique", suggested_object_id="garage"
|
||||
)
|
||||
hass.states.async_set("light.kitchen", "on")
|
||||
hass.states.async_set("cover.garage", "open", {"device_class": "garage"})
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{"type": "cloud/google_assistant/entities/get", "entity_id": "light.kitchen"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {
|
||||
"entity_id": "light.kitchen",
|
||||
"might_2fa": False,
|
||||
"traits": ["action.devices.traits.OnOff"],
|
||||
}
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{"type": "cloud/google_assistant/entities/get", "entity_id": "cover.garage"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {
|
||||
"entity_id": "cover.garage",
|
||||
"might_2fa": True,
|
||||
"traits": ["action.devices.traits.OpenClose"],
|
||||
}
|
||||
|
||||
|
||||
async def test_update_google_entity(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
setup_api,
|
||||
mock_cloud_login,
|
||||
) -> None:
|
||||
"""Test that we can update config of a Google entity."""
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json(
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"id": 5,
|
||||
"type": "cloud/google_assistant/entities/update",
|
||||
"entity_id": "light.kitchen",
|
||||
"should_expose": False,
|
||||
"disable_2fa": False,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
prefs = hass.data[DOMAIN].client.prefs
|
||||
assert prefs.google_entity_configs["light.kitchen"] == {
|
||||
"should_expose": False,
|
||||
"disable_2fa": False,
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"id": 6,
|
||||
"type": "cloud/google_assistant/entities/update",
|
||||
"entity_id": "light.kitchen",
|
||||
"should_expose": None,
|
||||
"type": "homeassistant/expose_entity",
|
||||
"assistants": ["cloud.google_assistant"],
|
||||
"entity_ids": [entry.entity_id],
|
||||
"should_expose": False,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
prefs = hass.data[DOMAIN].client.prefs
|
||||
assert prefs.google_entity_configs["light.kitchen"] == {
|
||||
"should_expose": None,
|
||||
"disable_2fa": False,
|
||||
}
|
||||
|
||||
assert entity_registry.async_get(entry.entity_id).options[
|
||||
"cloud.google_assistant"
|
||||
] == {"disable_2fa": False, "should_expose": False}
|
||||
|
||||
|
||||
async def test_list_alexa_entities(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
setup_api,
|
||||
mock_cloud_login,
|
||||
) -> None:
|
||||
"""Test that we can list Alexa entities."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
@ -769,9 +853,22 @@ async def test_list_alexa_entities(
|
|||
"homeassistant.components.alexa.entities.async_get_entities",
|
||||
return_value=[entity],
|
||||
):
|
||||
await client.send_json({"id": 5, "type": "cloud/alexa/entities"})
|
||||
await client.send_json_auto_id({"id": 5, "type": "cloud/alexa/entities"})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert len(response["result"]) == 0
|
||||
|
||||
# Add the entity to the entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.alexa.entities.async_get_entities",
|
||||
return_value=[entity],
|
||||
):
|
||||
await client.send_json_auto_id({"type": "cloud/alexa/entities"})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert len(response["result"]) == 1
|
||||
assert response["result"][0] == {
|
||||
|
@ -782,37 +879,31 @@ async def test_list_alexa_entities(
|
|||
|
||||
|
||||
async def test_update_alexa_entity(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_api, mock_cloud_login
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
setup_api,
|
||||
mock_cloud_login,
|
||||
) -> None:
|
||||
"""Test that we can update config of an Alexa entity."""
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique", suggested_object_id="kitchen"
|
||||
)
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json(
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"id": 5,
|
||||
"type": "cloud/alexa/entities/update",
|
||||
"entity_id": "light.kitchen",
|
||||
"type": "homeassistant/expose_entity",
|
||||
"assistants": ["cloud.alexa"],
|
||||
"entity_ids": [entry.entity_id],
|
||||
"should_expose": False,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
prefs = hass.data[DOMAIN].client.prefs
|
||||
assert prefs.alexa_entity_configs["light.kitchen"] == {"should_expose": False}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
"type": "cloud/alexa/entities/update",
|
||||
"entity_id": "light.kitchen",
|
||||
"should_expose": None,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
prefs = hass.data[DOMAIN].client.prefs
|
||||
assert prefs.alexa_entity_configs["light.kitchen"] == {"should_expose": None}
|
||||
assert entity_registry.async_get(entry.entity_id).options["cloud.alexa"] == {
|
||||
"should_expose": False
|
||||
}
|
||||
|
||||
|
||||
async def test_sync_alexa_entities_timeout(
|
||||
|
|
|
@ -0,0 +1,348 @@
|
|||
"""Test Home Assistant exposed entities helper."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
DATA_EXPOSED_ENTITIES,
|
||||
ExposedEntities,
|
||||
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
|
||||
|
||||
from tests.common import flush_store
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
async def test_load_preferences(hass: HomeAssistant) -> None:
|
||||
"""Make sure that we can load/save data correctly."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
assert exposed_entities._assistants == {}
|
||||
|
||||
exposed_entities.async_set_expose_new_entities("test1", True)
|
||||
exposed_entities.async_set_expose_new_entities("test2", False)
|
||||
|
||||
assert list(exposed_entities._assistants) == ["test1", "test2"]
|
||||
|
||||
exposed_entities2 = ExposedEntities(hass)
|
||||
await flush_store(exposed_entities._store)
|
||||
await exposed_entities2.async_load()
|
||||
|
||||
assert exposed_entities._assistants == exposed_entities2._assistants
|
||||
|
||||
|
||||
async def test_expose_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test expose entity."""
|
||||
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
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "homeassistant/expose_entity",
|
||||
"assistants": ["cloud.alexa"],
|
||||
"entity_ids": [entry1.entity_id],
|
||||
"should_expose": True,
|
||||
}
|
||||
)
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
entry1 = entity_registry.async_get(entry1.entity_id)
|
||||
assert entry1.options == {"cloud.alexa": {"should_expose": True}}
|
||||
entry2 = entity_registry.async_get(entry2.entity_id)
|
||||
assert entry2.options == {}
|
||||
|
||||
# Update options
|
||||
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": False,
|
||||
}
|
||||
)
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
entry1 = entity_registry.async_get(entry1.entity_id)
|
||||
assert entry1.options == {
|
||||
"cloud.alexa": {"should_expose": False},
|
||||
"cloud.google_assistant": {"should_expose": False},
|
||||
}
|
||||
entry2 = entity_registry.async_get(entry2.entity_id)
|
||||
assert entry2.options == {
|
||||
"cloud.alexa": {"should_expose": False},
|
||||
"cloud.google_assistant": {"should_expose": False},
|
||||
}
|
||||
|
||||
|
||||
async def test_expose_entity_unknown(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test behavior when exposing an unknown entity."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
|
||||
# Set options
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "homeassistant/expose_entity",
|
||||
"assistants": ["cloud.alexa"],
|
||||
"entity_ids": ["test.test"],
|
||||
"should_expose": True,
|
||||
}
|
||||
)
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"] == {
|
||||
"code": "not_found",
|
||||
"message": "can't expose 'test.test'",
|
||||
}
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
exposed_entities.async_expose_entity("cloud.alexa", "test.test", True)
|
||||
|
||||
|
||||
async def test_expose_entity_blocked(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test behavior when exposing a blocked entity."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Set options
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "homeassistant/expose_entity",
|
||||
"assistants": ["cloud.alexa"],
|
||||
"entity_ids": ["group.all_locks"],
|
||||
"should_expose": True,
|
||||
}
|
||||
)
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"] == {
|
||||
"code": "not_allowed",
|
||||
"message": "can't expose 'group.all_locks'",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expose_new", [True, False])
|
||||
async def test_expose_new_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
expose_new,
|
||||
) -> None:
|
||||
"""Test expose entity."""
|
||||
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("climate", "test", "unique1")
|
||||
entry2 = entity_registry.async_get_or_create("climate", "test", "unique2")
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "homeassistant/expose_new_entities/get",
|
||||
"assistant": "cloud.alexa",
|
||||
}
|
||||
)
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {"expose_new": False}
|
||||
|
||||
# Check if exposed - should be False
|
||||
assert async_should_expose(hass, "cloud.alexa", entry1.entity_id) is False
|
||||
|
||||
# Expose new entities to Alexa
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "homeassistant/expose_new_entities/set",
|
||||
"assistant": "cloud.alexa",
|
||||
"expose_new": expose_new,
|
||||
}
|
||||
)
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "homeassistant/expose_new_entities/get",
|
||||
"assistant": "cloud.alexa",
|
||||
}
|
||||
)
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
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
|
||||
|
||||
# Check if exposed - should be True
|
||||
assert async_should_expose(hass, "cloud.alexa", entry2.entity_id) == expose_new
|
||||
|
||||
|
||||
async def test_listen_updates(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test listen to updates."""
|
||||
calls = []
|
||||
|
||||
def listener():
|
||||
calls.append(None)
|
||||
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
async_listen_entity_updates(hass, "cloud.alexa", listener)
|
||||
|
||||
entry = entity_registry.async_get_or_create("climate", "test", "unique1")
|
||||
|
||||
# Call for another assistant - listener not called
|
||||
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)
|
||||
assert len(calls) == 1
|
||||
|
||||
# Settings not changed - listener not called
|
||||
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)
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
async def test_get_assistant_settings(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test get assistant settings."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
|
||||
entry = entity_registry.async_get_or_create("climate", "test", "unique1")
|
||||
|
||||
assert async_get_assistant_settings(hass, "cloud.alexa") == {}
|
||||
|
||||
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}
|
||||
}
|
||||
assert async_get_assistant_settings(hass, "cloud.google_assistant") == {}
|
||||
|
||||
|
||||
async def test_should_expose(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test expose entity."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Expose new entities to Alexa
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "homeassistant/expose_new_entities/set",
|
||||
"assistant": "cloud.alexa",
|
||||
"expose_new": True,
|
||||
}
|
||||
)
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
# Unknown entity is not exposed
|
||||
assert 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Binary sensor with certain device class is exposed
|
||||
binarysensor2 = entity_registry.async_get_or_create(
|
||||
"binary_sensor",
|
||||
"test",
|
||||
"unique2",
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Sensor with certain device class is exposed
|
||||
sensor2 = entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
"test",
|
||||
"unique2",
|
||||
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
|
Loading…
Reference in New Issue