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_settings
This commit is contained in:
parent
0d84106947
commit
44c89a6b6c
17 changed files with 1607 additions and 305 deletions
|
@ -137,6 +137,7 @@ homeassistant.components.hardkernel.*
|
||||||
homeassistant.components.hardware.*
|
homeassistant.components.hardware.*
|
||||||
homeassistant.components.here_travel_time.*
|
homeassistant.components.here_travel_time.*
|
||||||
homeassistant.components.history.*
|
homeassistant.components.history.*
|
||||||
|
homeassistant.components.homeassistant.exposed_entities
|
||||||
homeassistant.components.homeassistant.triggers.event
|
homeassistant.components.homeassistant.triggers.event
|
||||||
homeassistant.components.homeassistant_alerts.*
|
homeassistant.components.homeassistant_alerts.*
|
||||||
homeassistant.components.homeassistant_hardware.*
|
homeassistant.components.homeassistant_hardware.*
|
||||||
|
|
|
@ -20,6 +20,11 @@ from homeassistant.components.alexa import (
|
||||||
errors as alexa_errors,
|
errors as alexa_errors,
|
||||||
state_report as alexa_state_report,
|
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.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
||||||
from homeassistant.helpers import entity_registry as er, start
|
from homeassistant.helpers import entity_registry as er, start
|
||||||
|
@ -30,16 +35,17 @@ from homeassistant.util.dt import utcnow
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ENTITY_CONFIG,
|
CONF_ENTITY_CONFIG,
|
||||||
CONF_FILTER,
|
CONF_FILTER,
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE,
|
DOMAIN as CLOUD_DOMAIN,
|
||||||
PREF_ALEXA_ENTITY_CONFIGS,
|
|
||||||
PREF_ALEXA_REPORT_STATE,
|
PREF_ALEXA_REPORT_STATE,
|
||||||
PREF_ENABLE_ALEXA,
|
PREF_ENABLE_ALEXA,
|
||||||
PREF_SHOULD_EXPOSE,
|
PREF_SHOULD_EXPOSE,
|
||||||
)
|
)
|
||||||
from .prefs import CloudPreferences
|
from .prefs import ALEXA_SETTINGS_VERSION, CloudPreferences
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
|
||||||
|
|
||||||
# Time to wait when entity preferences have changed before syncing it to
|
# Time to wait when entity preferences have changed before syncing it to
|
||||||
# the cloud.
|
# the cloud.
|
||||||
SYNC_DELAY = 1
|
SYNC_DELAY = 1
|
||||||
|
@ -64,7 +70,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||||
self._cloud = cloud
|
self._cloud = cloud
|
||||||
self._token = None
|
self._token = None
|
||||||
self._token_valid = 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._alexa_sync_unsub: Callable[[], None] | None = None
|
||||||
self._endpoint = None
|
self._endpoint = None
|
||||||
|
|
||||||
|
@ -115,10 +121,31 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||||
"""Return an identifier for the user that represents this config."""
|
"""Return an identifier for the user that represents this config."""
|
||||||
return self._cloud_user
|
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):
|
async def async_initialize(self):
|
||||||
"""Initialize the Alexa config."""
|
"""Initialize the Alexa config."""
|
||||||
await super().async_initialize()
|
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):
|
async def hass_started(hass):
|
||||||
if self.enabled and ALEXA_DOMAIN not in self.hass.config.components:
|
if self.enabled and ALEXA_DOMAIN not in self.hass.config.components:
|
||||||
await async_setup_component(self.hass, ALEXA_DOMAIN, {})
|
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)
|
start.async_at_start(self.hass, hass_started)
|
||||||
|
|
||||||
self._prefs.async_listen_updates(self._async_prefs_updated)
|
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(
|
self.hass.bus.async_listen(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
self._handle_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 an entity should be exposed."""
|
||||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||||
return False
|
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_configs = self._prefs.alexa_entity_configs
|
||||||
entity_config = entity_configs.get(entity_id, {})
|
entity_config = entity_configs.get(entity_id, {})
|
||||||
entity_expose = entity_config.get(PREF_SHOULD_EXPOSE)
|
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
|
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
|
@callback
|
||||||
def async_invalidate_access_token(self):
|
def async_invalidate_access_token(self):
|
||||||
"""Invalidate access token."""
|
"""Invalidate access token."""
|
||||||
|
@ -233,32 +269,30 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||||
if not any(
|
if not any(
|
||||||
key in updated_prefs
|
key in updated_prefs
|
||||||
for key in (
|
for key in (
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE,
|
|
||||||
PREF_ALEXA_ENTITY_CONFIGS,
|
|
||||||
PREF_ALEXA_REPORT_STATE,
|
PREF_ALEXA_REPORT_STATE,
|
||||||
PREF_ENABLE_ALEXA,
|
PREF_ENABLE_ALEXA,
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
return
|
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()
|
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):
|
async def _sync_prefs(self, _now):
|
||||||
"""Sync the updated preferences to Alexa."""
|
"""Sync the updated preferences to Alexa."""
|
||||||
self._alexa_sync_unsub = None
|
self._alexa_sync_unsub = None
|
||||||
old_prefs = self._cur_entity_prefs
|
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()
|
seen = set()
|
||||||
to_update = []
|
to_update = []
|
||||||
|
|
|
@ -19,6 +19,8 @@ PREF_USERNAME = "username"
|
||||||
PREF_REMOTE_DOMAIN = "remote_domain"
|
PREF_REMOTE_DOMAIN = "remote_domain"
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE = "alexa_default_expose"
|
PREF_ALEXA_DEFAULT_EXPOSE = "alexa_default_expose"
|
||||||
PREF_GOOGLE_DEFAULT_EXPOSE = "google_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_TTS_DEFAULT_VOICE = "tts_default_voice"
|
||||||
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female")
|
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female")
|
||||||
DEFAULT_DISABLE_2FA = False
|
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 import DOMAIN as GOOGLE_DOMAIN
|
||||||
from homeassistant.components.google_assistant.helpers import AbstractConfig
|
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.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
CoreState,
|
CoreState,
|
||||||
|
@ -22,14 +26,18 @@ from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ENTITY_CONFIG,
|
CONF_ENTITY_CONFIG,
|
||||||
|
CONF_FILTER,
|
||||||
DEFAULT_DISABLE_2FA,
|
DEFAULT_DISABLE_2FA,
|
||||||
|
DOMAIN as CLOUD_DOMAIN,
|
||||||
PREF_DISABLE_2FA,
|
PREF_DISABLE_2FA,
|
||||||
PREF_SHOULD_EXPOSE,
|
PREF_SHOULD_EXPOSE,
|
||||||
)
|
)
|
||||||
from .prefs import CloudPreferences
|
from .prefs import GOOGLE_SETTINGS_VERSION, CloudPreferences
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
|
||||||
|
|
||||||
|
|
||||||
class CloudGoogleConfig(AbstractConfig):
|
class CloudGoogleConfig(AbstractConfig):
|
||||||
"""HA Cloud Configuration for Google Assistant."""
|
"""HA Cloud Configuration for Google Assistant."""
|
||||||
|
@ -48,8 +56,6 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
self._user = cloud_user
|
self._user = cloud_user
|
||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
self._cloud = cloud
|
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()
|
self._sync_entities_lock = asyncio.Lock()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -89,10 +95,35 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
"""Return Cloud User account."""
|
"""Return Cloud User account."""
|
||||||
return self._user
|
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):
|
async def async_initialize(self):
|
||||||
"""Perform async initialization of config."""
|
"""Perform async initialization of config."""
|
||||||
await super().async_initialize()
|
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):
|
async def hass_started(hass):
|
||||||
if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components:
|
if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components:
|
||||||
await async_setup_component(self.hass, GOOGLE_DOMAIN, {})
|
await async_setup_component(self.hass, GOOGLE_DOMAIN, {})
|
||||||
|
@ -109,7 +140,9 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
await self.async_disconnect_agent_user(agent_user_id)
|
await self.async_disconnect_agent_user(agent_user_id)
|
||||||
|
|
||||||
self._prefs.async_listen_updates(self._async_prefs_updated)
|
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(
|
self.hass.bus.async_listen(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
self._handle_entity_registry_updated,
|
self._handle_entity_registry_updated,
|
||||||
|
@ -123,14 +156,11 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
"""If a state object should be exposed."""
|
"""If a state object should be exposed."""
|
||||||
return self._should_expose_entity_id(state.entity_id)
|
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 an entity ID should be exposed."""
|
||||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not self._config["filter"].empty_filter:
|
|
||||||
return self._config["filter"](entity_id)
|
|
||||||
|
|
||||||
entity_configs = self._prefs.google_entity_configs
|
entity_configs = self._prefs.google_entity_configs
|
||||||
entity_config = entity_configs.get(entity_id, {})
|
entity_config = entity_configs.get(entity_id, {})
|
||||||
entity_expose = entity_config.get(PREF_SHOULD_EXPOSE)
|
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
|
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
|
@property
|
||||||
def agent_user_id(self):
|
def agent_user_id(self):
|
||||||
"""Return Agent User Id to use for query responses."""
|
"""Return Agent User Id to use for query responses."""
|
||||||
|
@ -168,11 +207,23 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
"""Get agent user ID making request."""
|
"""Get agent user ID making request."""
|
||||||
return self.agent_user_id
|
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."""
|
"""If an entity should be checked for 2FA."""
|
||||||
entity_configs = self._prefs.google_entity_configs
|
entity_configs = self._prefs.google_entity_configs
|
||||||
entity_config = entity_configs.get(state.entity_id, {})
|
entity_config = entity_configs.get(entity_id, {})
|
||||||
return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
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):
|
async def async_report_state(self, message, agent_user_id: str):
|
||||||
"""Send a state report to Google."""
|
"""Send a state report to Google."""
|
||||||
|
@ -218,14 +269,6 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
# So when we change it, we need to sync all entities.
|
# So when we change it, we need to sync all entities.
|
||||||
sync_entities = True
|
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:
|
if self.enabled and not self.is_local_sdk_active:
|
||||||
self.async_enable_local_sdk()
|
self.async_enable_local_sdk()
|
||||||
sync_entities = True
|
sync_entities = True
|
||||||
|
@ -233,12 +276,14 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
self.async_disable_local_sdk()
|
self.async_disable_local_sdk()
|
||||||
sync_entities = True
|
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:
|
if sync_entities and self.hass.is_running:
|
||||||
await self.async_sync_entities_all()
|
await self.async_sync_entities_all()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_exposed_entities_updated(self) -> None:
|
||||||
|
"""Handle updated preferences."""
|
||||||
|
self.async_schedule_google_sync_all()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_entity_registry_updated(self, event: Event) -> None:
|
def _handle_entity_registry_updated(self, event: Event) -> None:
|
||||||
"""Handle when entity registry updated."""
|
"""Handle when entity registry updated."""
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""The HTTP api to control the cloud integration."""
|
"""The HTTP api to control the cloud integration."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Mapping
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from http import HTTPStatus
|
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.google_assistant import helpers as google_helpers
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||||
|
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.util.location import async_detect_location_info
|
from homeassistant.util.location import async_detect_location_info
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE,
|
|
||||||
PREF_ALEXA_REPORT_STATE,
|
PREF_ALEXA_REPORT_STATE,
|
||||||
|
PREF_DISABLE_2FA,
|
||||||
PREF_ENABLE_ALEXA,
|
PREF_ENABLE_ALEXA,
|
||||||
PREF_ENABLE_GOOGLE,
|
PREF_ENABLE_GOOGLE,
|
||||||
PREF_GOOGLE_DEFAULT_EXPOSE,
|
|
||||||
PREF_GOOGLE_REPORT_STATE,
|
PREF_GOOGLE_REPORT_STATE,
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
||||||
PREF_TTS_DEFAULT_VOICE,
|
PREF_TTS_DEFAULT_VOICE,
|
||||||
REQUEST_TIMEOUT,
|
REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
from .google_config import CLOUD_GOOGLE
|
||||||
from .repairs import async_manage_legacy_subscription_issue
|
from .repairs import async_manage_legacy_subscription_issue
|
||||||
from .subscription import async_subscription_info
|
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_connect)
|
||||||
websocket_api.async_register_command(hass, websocket_remote_disconnect)
|
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_list)
|
||||||
websocket_api.async_register_command(hass, google_assistant_update)
|
websocket_api.async_register_command(hass, google_assistant_update)
|
||||||
|
|
||||||
websocket_api.async_register_command(hass, alexa_list)
|
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, alexa_sync)
|
||||||
|
|
||||||
websocket_api.async_register_command(hass, thingtalk_convert)
|
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_ENABLE_ALEXA): bool,
|
||||||
vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
|
vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
|
||||||
vol.Optional(PREF_GOOGLE_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_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
||||||
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
|
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
|
||||||
vol.Coerce(tuple), vol.In(MAP_VOICE)
|
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))
|
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
|
@websocket_api.require_admin
|
||||||
@_require_cloud_login
|
@_require_cloud_login
|
||||||
@websocket_api.websocket_command({"type": "cloud/google_assistant/entities"})
|
@websocket_api.websocket_command({"type": "cloud/google_assistant/entities"})
|
||||||
|
@ -536,11 +585,14 @@ async def google_assistant_list(
|
||||||
"""List all google assistant entities."""
|
"""List all google assistant entities."""
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
gconf = await cloud.client.get_google_config()
|
gconf = await cloud.client.get_google_config()
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
entities = google_helpers.async_get_entities(hass, gconf)
|
entities = google_helpers.async_get_entities(hass, gconf)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
|
if not entity_registry.async_is_registered(entity.entity_id):
|
||||||
|
continue
|
||||||
result.append(
|
result.append(
|
||||||
{
|
{
|
||||||
"entity_id": entity.entity_id,
|
"entity_id": entity.entity_id,
|
||||||
|
@ -558,8 +610,7 @@ async def google_assistant_list(
|
||||||
{
|
{
|
||||||
"type": "cloud/google_assistant/entities/update",
|
"type": "cloud/google_assistant/entities/update",
|
||||||
"entity_id": str,
|
"entity_id": str,
|
||||||
vol.Optional("should_expose"): vol.Any(None, bool),
|
vol.Optional(PREF_DISABLE_2FA): bool,
|
||||||
vol.Optional("disable_2fa"): bool,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@ -569,17 +620,30 @@ async def google_assistant_update(
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update google assistant config."""
|
"""Update google assistant entity config."""
|
||||||
cloud = hass.data[DOMAIN]
|
entity_registry = er.async_get(hass)
|
||||||
changes = dict(msg)
|
entity_id: str = msg["entity_id"]
|
||||||
changes.pop("type")
|
|
||||||
changes.pop("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(
|
disable_2fa = msg[PREF_DISABLE_2FA]
|
||||||
msg["id"], cloud.client.prefs.google_entity_configs.get(msg["entity_id"])
|
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
|
@websocket_api.require_admin
|
||||||
|
@ -595,11 +659,14 @@ async def alexa_list(
|
||||||
"""List all alexa entities."""
|
"""List all alexa entities."""
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
alexa_config = await cloud.client.get_alexa_config()
|
alexa_config = await cloud.client.get_alexa_config()
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
entities = alexa_entities.async_get_entities(hass, alexa_config)
|
entities = alexa_entities.async_get_entities(hass, alexa_config)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
|
if not entity_registry.async_is_registered(entity.entity_id):
|
||||||
|
continue
|
||||||
result.append(
|
result.append(
|
||||||
{
|
{
|
||||||
"entity_id": entity.entity_id,
|
"entity_id": entity.entity_id,
|
||||||
|
@ -611,35 +678,6 @@ async def alexa_list(
|
||||||
connection.send_result(msg["id"], result)
|
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
|
@websocket_api.require_admin
|
||||||
@_require_cloud_login
|
@_require_cloud_login
|
||||||
@websocket_api.websocket_command({"type": "cloud/alexa/sync"})
|
@websocket_api.websocket_command({"type": "cloud/alexa/sync"})
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "Home Assistant Cloud",
|
"name": "Home Assistant Cloud",
|
||||||
"after_dependencies": ["google_assistant", "alexa"],
|
"after_dependencies": ["google_assistant", "alexa"],
|
||||||
"codeowners": ["@home-assistant/cloud"],
|
"codeowners": ["@home-assistant/cloud"],
|
||||||
"dependencies": ["http", "webhook"],
|
"dependencies": ["homeassistant", "http", "webhook"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
"""Preference management for cloud."""
|
"""Preference management for cloud."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.auth.models import User
|
from homeassistant.auth.models import User
|
||||||
from homeassistant.components import webhook
|
from homeassistant.components import webhook
|
||||||
|
@ -18,9 +20,9 @@ from .const import (
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE,
|
PREF_ALEXA_DEFAULT_EXPOSE,
|
||||||
PREF_ALEXA_ENTITY_CONFIGS,
|
PREF_ALEXA_ENTITY_CONFIGS,
|
||||||
PREF_ALEXA_REPORT_STATE,
|
PREF_ALEXA_REPORT_STATE,
|
||||||
|
PREF_ALEXA_SETTINGS_VERSION,
|
||||||
PREF_CLOUD_USER,
|
PREF_CLOUD_USER,
|
||||||
PREF_CLOUDHOOKS,
|
PREF_CLOUDHOOKS,
|
||||||
PREF_DISABLE_2FA,
|
|
||||||
PREF_ENABLE_ALEXA,
|
PREF_ENABLE_ALEXA,
|
||||||
PREF_ENABLE_GOOGLE,
|
PREF_ENABLE_GOOGLE,
|
||||||
PREF_ENABLE_REMOTE,
|
PREF_ENABLE_REMOTE,
|
||||||
|
@ -29,14 +31,33 @@ from .const import (
|
||||||
PREF_GOOGLE_LOCAL_WEBHOOK_ID,
|
PREF_GOOGLE_LOCAL_WEBHOOK_ID,
|
||||||
PREF_GOOGLE_REPORT_STATE,
|
PREF_GOOGLE_REPORT_STATE,
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
||||||
|
PREF_GOOGLE_SETTINGS_VERSION,
|
||||||
PREF_REMOTE_DOMAIN,
|
PREF_REMOTE_DOMAIN,
|
||||||
PREF_SHOULD_EXPOSE,
|
|
||||||
PREF_TTS_DEFAULT_VOICE,
|
PREF_TTS_DEFAULT_VOICE,
|
||||||
PREF_USERNAME,
|
PREF_USERNAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
STORAGE_KEY = DOMAIN
|
STORAGE_KEY = DOMAIN
|
||||||
STORAGE_VERSION = 1
|
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:
|
class CloudPreferences:
|
||||||
|
@ -45,7 +66,9 @@ class CloudPreferences:
|
||||||
def __init__(self, hass):
|
def __init__(self, hass):
|
||||||
"""Initialize cloud prefs."""
|
"""Initialize cloud prefs."""
|
||||||
self._hass = hass
|
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._prefs = None
|
||||||
self._listeners = []
|
self._listeners = []
|
||||||
self.last_updated: set[str] = set()
|
self.last_updated: set[str] = set()
|
||||||
|
@ -79,14 +102,12 @@ class CloudPreferences:
|
||||||
google_secure_devices_pin=UNDEFINED,
|
google_secure_devices_pin=UNDEFINED,
|
||||||
cloudhooks=UNDEFINED,
|
cloudhooks=UNDEFINED,
|
||||||
cloud_user=UNDEFINED,
|
cloud_user=UNDEFINED,
|
||||||
google_entity_configs=UNDEFINED,
|
|
||||||
alexa_entity_configs=UNDEFINED,
|
|
||||||
alexa_report_state=UNDEFINED,
|
alexa_report_state=UNDEFINED,
|
||||||
google_report_state=UNDEFINED,
|
google_report_state=UNDEFINED,
|
||||||
alexa_default_expose=UNDEFINED,
|
|
||||||
google_default_expose=UNDEFINED,
|
|
||||||
tts_default_voice=UNDEFINED,
|
tts_default_voice=UNDEFINED,
|
||||||
remote_domain=UNDEFINED,
|
remote_domain=UNDEFINED,
|
||||||
|
alexa_settings_version=UNDEFINED,
|
||||||
|
google_settings_version=UNDEFINED,
|
||||||
):
|
):
|
||||||
"""Update user preferences."""
|
"""Update user preferences."""
|
||||||
prefs = {**self._prefs}
|
prefs = {**self._prefs}
|
||||||
|
@ -98,12 +119,10 @@ class CloudPreferences:
|
||||||
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
|
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
|
||||||
(PREF_CLOUDHOOKS, cloudhooks),
|
(PREF_CLOUDHOOKS, cloudhooks),
|
||||||
(PREF_CLOUD_USER, cloud_user),
|
(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_ALEXA_REPORT_STATE, alexa_report_state),
|
||||||
(PREF_GOOGLE_REPORT_STATE, google_report_state),
|
(PREF_GOOGLE_REPORT_STATE, google_report_state),
|
||||||
(PREF_ALEXA_DEFAULT_EXPOSE, alexa_default_expose),
|
(PREF_ALEXA_SETTINGS_VERSION, alexa_settings_version),
|
||||||
(PREF_GOOGLE_DEFAULT_EXPOSE, google_default_expose),
|
(PREF_GOOGLE_SETTINGS_VERSION, google_settings_version),
|
||||||
(PREF_TTS_DEFAULT_VOICE, tts_default_voice),
|
(PREF_TTS_DEFAULT_VOICE, tts_default_voice),
|
||||||
(PREF_REMOTE_DOMAIN, remote_domain),
|
(PREF_REMOTE_DOMAIN, remote_domain),
|
||||||
):
|
):
|
||||||
|
@ -112,53 +131,6 @@ class CloudPreferences:
|
||||||
|
|
||||||
await self._save_prefs(prefs)
|
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:
|
async def async_set_username(self, username) -> bool:
|
||||||
"""Set the username that is logged in."""
|
"""Set the username that is logged in."""
|
||||||
# Logging out.
|
# Logging out.
|
||||||
|
@ -186,14 +158,12 @@ class CloudPreferences:
|
||||||
"""Return dictionary version."""
|
"""Return dictionary version."""
|
||||||
return {
|
return {
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE: self.alexa_default_expose,
|
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_ALEXA_REPORT_STATE: self.alexa_report_state,
|
||||||
PREF_CLOUDHOOKS: self.cloudhooks,
|
PREF_CLOUDHOOKS: self.cloudhooks,
|
||||||
PREF_ENABLE_ALEXA: self.alexa_enabled,
|
PREF_ENABLE_ALEXA: self.alexa_enabled,
|
||||||
PREF_ENABLE_GOOGLE: self.google_enabled,
|
PREF_ENABLE_GOOGLE: self.google_enabled,
|
||||||
PREF_ENABLE_REMOTE: self.remote_enabled,
|
PREF_ENABLE_REMOTE: self.remote_enabled,
|
||||||
PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose,
|
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_REPORT_STATE: self.google_report_state,
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
|
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
|
||||||
PREF_TTS_DEFAULT_VOICE: self.tts_default_voice,
|
PREF_TTS_DEFAULT_VOICE: self.tts_default_voice,
|
||||||
|
@ -235,6 +205,11 @@ class CloudPreferences:
|
||||||
"""Return Alexa Entity configurations."""
|
"""Return Alexa Entity configurations."""
|
||||||
return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {})
|
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
|
@property
|
||||||
def google_enabled(self):
|
def google_enabled(self):
|
||||||
"""Return if Google is enabled."""
|
"""Return if Google is enabled."""
|
||||||
|
@ -255,6 +230,11 @@ class CloudPreferences:
|
||||||
"""Return Google Entity configurations."""
|
"""Return Google Entity configurations."""
|
||||||
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
|
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
|
@property
|
||||||
def google_local_webhook_id(self):
|
def google_local_webhook_id(self):
|
||||||
"""Return Google webhook ID to receive local messages."""
|
"""Return Google webhook ID to receive local messages."""
|
||||||
|
@ -319,6 +299,7 @@ class CloudPreferences:
|
||||||
return {
|
return {
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
PREF_ALEXA_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
||||||
PREF_ALEXA_ENTITY_CONFIGS: {},
|
PREF_ALEXA_ENTITY_CONFIGS: {},
|
||||||
|
PREF_ALEXA_SETTINGS_VERSION: ALEXA_SETTINGS_VERSION,
|
||||||
PREF_CLOUD_USER: None,
|
PREF_CLOUD_USER: None,
|
||||||
PREF_CLOUDHOOKS: {},
|
PREF_CLOUDHOOKS: {},
|
||||||
PREF_ENABLE_ALEXA: True,
|
PREF_ENABLE_ALEXA: True,
|
||||||
|
@ -326,6 +307,7 @@ class CloudPreferences:
|
||||||
PREF_ENABLE_REMOTE: False,
|
PREF_ENABLE_REMOTE: False,
|
||||||
PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
||||||
PREF_GOOGLE_ENTITY_CONFIGS: {},
|
PREF_GOOGLE_ENTITY_CONFIGS: {},
|
||||||
|
PREF_GOOGLE_SETTINGS_VERSION: GOOGLE_SETTINGS_VERSION,
|
||||||
PREF_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(),
|
PREF_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(),
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
||||||
PREF_REMOTE_DOMAIN: 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.template import async_load_custom_templates
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DATA_EXPOSED_ENTITIES, DOMAIN
|
||||||
|
from .exposed_entities import ExposedEntities
|
||||||
|
|
||||||
ATTR_ENTRY_ID = "entry_id"
|
ATTR_ENTRY_ID = "entry_id"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DOMAIN = ha.DOMAIN
|
|
||||||
SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
|
SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
|
||||||
SERVICE_RELOAD_CONFIG_ENTRY = "reload_config_entry"
|
SERVICE_RELOAD_CONFIG_ENTRY = "reload_config_entry"
|
||||||
SERVICE_RELOAD_CUSTOM_TEMPLATES = "reload_custom_templates"
|
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
|
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
|
return True
|
||||||
|
|
6
homeassistant/components/homeassistant/const.py
Normal file
6
homeassistant/components/homeassistant/const.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
"""Constants for the Homeassistant integration."""
|
||||||
|
import homeassistant.core as ha
|
||||||
|
|
||||||
|
DOMAIN = ha.DOMAIN
|
||||||
|
|
||||||
|
DATA_EXPOSED_ENTITIES = f"{DOMAIN}.exposed_entites"
|
351
homeassistant/components/homeassistant/exposed_entities.py
Normal file
351
homeassistant/components/homeassistant/exposed_entities.py
Normal file
|
@ -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_return_any = true
|
||||||
warn_unreachable = 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]
|
[mypy-homeassistant.components.homeassistant.triggers.event]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from homeassistant.components import cloud
|
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
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,9 +18,11 @@ async def mock_cloud(hass, config=None):
|
||||||
def mock_cloud_prefs(hass, prefs={}):
|
def mock_cloud_prefs(hass, prefs={}):
|
||||||
"""Fixture for cloud component."""
|
"""Fixture for cloud component."""
|
||||||
prefs_to_set = {
|
prefs_to_set = {
|
||||||
|
const.PREF_ALEXA_SETTINGS_VERSION: cloud_prefs.ALEXA_SETTINGS_VERSION,
|
||||||
const.PREF_ENABLE_ALEXA: True,
|
const.PREF_ENABLE_ALEXA: True,
|
||||||
const.PREF_ENABLE_GOOGLE: True,
|
const.PREF_ENABLE_GOOGLE: True,
|
||||||
const.PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
const.PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
||||||
|
const.PREF_GOOGLE_SETTINGS_VERSION: cloud_prefs.GOOGLE_SETTINGS_VERSION,
|
||||||
}
|
}
|
||||||
prefs_to_set.update(prefs)
|
prefs_to_set.update(prefs)
|
||||||
hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set
|
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.alexa import errors
|
||||||
from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config
|
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.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_fire_time_changed
|
||||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
@ -21,10 +33,23 @@ def cloud_stub():
|
||||||
return Mock(is_logged_in=True, subscription_expired=False)
|
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(
|
async def test_alexa_config_expose_entity_prefs(
|
||||||
hass: HomeAssistant, cloud_prefs, cloud_stub, entity_registry: er.EntityRegistry
|
hass: HomeAssistant, cloud_prefs, cloud_stub, entity_registry: er.EntityRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Alexa config should expose using prefs."""
|
"""Test Alexa config should expose using prefs."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
entity_entry1 = entity_registry.async_get_or_create(
|
entity_entry1 = entity_registry.async_get_or_create(
|
||||||
"light",
|
"light",
|
||||||
"test",
|
"test",
|
||||||
|
@ -53,54 +78,62 @@ async def test_alexa_config_expose_entity_prefs(
|
||||||
suggested_object_id="hidden_user_light",
|
suggested_object_id="hidden_user_light",
|
||||||
hidden_by=er.RegistryEntryHider.USER,
|
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(
|
await cloud_prefs.async_update(
|
||||||
alexa_entity_configs={"light.kitchen": entity_conf},
|
|
||||||
alexa_default_expose=["light"],
|
|
||||||
alexa_enabled=True,
|
alexa_enabled=True,
|
||||||
alexa_report_state=False,
|
alexa_report_state=False,
|
||||||
)
|
)
|
||||||
|
expose_new(hass, True)
|
||||||
|
expose_entity(hass, entity_entry5.entity_id, False)
|
||||||
conf = alexa_config.CloudAlexaConfig(
|
conf = alexa_config.CloudAlexaConfig(
|
||||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
||||||
)
|
)
|
||||||
await conf.async_initialize()
|
await conf.async_initialize()
|
||||||
|
|
||||||
|
# can't expose an entity which is not in the entity registry
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
expose_entity(hass, "light.kitchen", True)
|
||||||
assert not conf.should_expose("light.kitchen")
|
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
|
# categorized and hidden entities should not be exposed
|
||||||
assert not conf.should_expose(entity_entry1.entity_id)
|
assert not conf.should_expose(entity_entry1.entity_id)
|
||||||
assert not conf.should_expose(entity_entry2.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_entry3.entity_id)
|
||||||
assert not conf.should_expose(entity_entry4.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
|
expose_entity(hass, entity_entry5.entity_id, True)
|
||||||
assert conf.should_expose("light.kitchen")
|
assert conf.should_expose(entity_entry5.entity_id)
|
||||||
# categorized and hidden entities should not be exposed
|
|
||||||
assert not conf.should_expose(entity_entry1.entity_id)
|
expose_entity(hass, entity_entry5.entity_id, None)
|
||||||
assert not conf.should_expose(entity_entry2.entity_id)
|
assert not conf.should_expose(entity_entry5.entity_id)
|
||||||
assert not conf.should_expose(entity_entry3.entity_id)
|
|
||||||
assert not conf.should_expose(entity_entry4.entity_id)
|
|
||||||
|
|
||||||
assert "alexa" not in hass.config.components
|
assert "alexa" not in hass.config.components
|
||||||
await cloud_prefs.async_update(
|
|
||||||
alexa_default_expose=["sensor"],
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert "alexa" in hass.config.components
|
assert "alexa" in hass.config.components
|
||||||
assert not conf.should_expose("light.kitchen")
|
assert not conf.should_expose(entity_entry5.entity_id)
|
||||||
|
|
||||||
|
|
||||||
async def test_alexa_config_report_state(
|
async def test_alexa_config_report_state(
|
||||||
hass: HomeAssistant, cloud_prefs, cloud_stub
|
hass: HomeAssistant, cloud_prefs, cloud_stub
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Alexa config should expose using prefs."""
|
"""Test Alexa config should expose using prefs."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
|
|
||||||
await cloud_prefs.async_update(
|
await cloud_prefs.async_update(
|
||||||
alexa_report_state=False,
|
alexa_report_state=False,
|
||||||
)
|
)
|
||||||
|
@ -134,6 +167,8 @@ async def test_alexa_config_invalidate_token(
|
||||||
hass: HomeAssistant, cloud_prefs, aioclient_mock: AiohttpClientMocker
|
hass: HomeAssistant, cloud_prefs, aioclient_mock: AiohttpClientMocker
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Alexa config should expose using prefs."""
|
"""Test Alexa config should expose using prefs."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
|
|
||||||
aioclient_mock.post(
|
aioclient_mock.post(
|
||||||
"https://example/access_token",
|
"https://example/access_token",
|
||||||
json={
|
json={
|
||||||
|
@ -181,10 +216,18 @@ async def test_alexa_config_fail_refresh_token(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cloud_prefs,
|
cloud_prefs,
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
reject_reason,
|
reject_reason,
|
||||||
expected_exception,
|
expected_exception,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Alexa config failing to refresh token."""
|
"""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(
|
aioclient_mock.post(
|
||||||
"https://example/access_token",
|
"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.should_report_state is False
|
||||||
assert conf.is_reporting_states 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
|
# Enable state reporting
|
||||||
await cloud_prefs.async_update(alexa_report_state=True)
|
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
|
assert conf.is_reporting_states is True
|
||||||
|
|
||||||
# Change states to trigger event listener
|
# 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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Invalidate the token and try to fetch another
|
# 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
|
# 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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Check state reporting is still wanted in cloud prefs, but disabled for Alexa
|
# 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(
|
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:
|
) -> None:
|
||||||
"""Test Alexa config responds to updating exposed entities."""
|
"""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(
|
hass.states.async_set(
|
||||||
"sensor.temp",
|
sensor_entry.entity_id,
|
||||||
"23",
|
"23",
|
||||||
{"device_class": "temperature", "unit_of_measurement": "°C"},
|
{"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(
|
await cloud_prefs.async_update(
|
||||||
alexa_enabled=True,
|
alexa_enabled=True,
|
||||||
|
@ -313,34 +370,26 @@ async def test_alexa_update_expose_trigger_sync(
|
||||||
await conf.async_initialize()
|
await conf.async_initialize()
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
await cloud_prefs.async_update_alexa_entity_config(
|
expose_entity(hass, light_entry.entity_id, True)
|
||||||
entity_id="light.kitchen", should_expose=True
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
async_fire_time_changed(hass, fire_all=True)
|
async_fire_time_changed(hass, fire_all=True)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert conf._alexa_sync_unsub is None
|
assert conf._alexa_sync_unsub is None
|
||||||
assert to_update == ["light.kitchen"]
|
assert to_update == [light_entry.entity_id]
|
||||||
assert to_remove == []
|
assert to_remove == []
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
await cloud_prefs.async_update_alexa_entity_config(
|
expose_entity(hass, light_entry.entity_id, False)
|
||||||
entity_id="light.kitchen", should_expose=False
|
expose_entity(hass, binary_sensor_entry.entity_id, True)
|
||||||
)
|
expose_entity(hass, sensor_entry.entity_id, True)
|
||||||
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
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
async_fire_time_changed(hass, fire_all=True)
|
async_fire_time_changed(hass, fire_all=True)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert conf._alexa_sync_unsub is None
|
assert conf._alexa_sync_unsub is None
|
||||||
assert sorted(to_update) == ["binary_sensor.door", "sensor.temp"]
|
assert sorted(to_update) == [binary_sensor_entry.entity_id, sensor_entry.entity_id]
|
||||||
assert to_remove == ["light.kitchen"]
|
assert to_remove == [light_entry.entity_id]
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
await cloud_prefs.async_update(
|
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 conf._alexa_sync_unsub is None
|
||||||
assert to_update == []
|
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(
|
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:
|
) -> None:
|
||||||
"""Test Alexa config responds to entity registry."""
|
"""Test Alexa config responds to entity registry."""
|
||||||
|
# Enable exposing new entities to Alexa
|
||||||
|
expose_new(hass, True)
|
||||||
|
|
||||||
await alexa_config.CloudAlexaConfig(
|
await alexa_config.CloudAlexaConfig(
|
||||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
|
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
|
||||||
).async_initialize()
|
).async_initialize()
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
hass.bus.async_fire(
|
entry = entity_registry.async_get_or_create(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
"light", "test", "unique", suggested_object_id="kitchen"
|
||||||
{"action": "create", "entity_id": "light.kitchen"},
|
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert to_update == ["light.kitchen"]
|
assert to_update == [entry.entity_id]
|
||||||
assert to_remove == []
|
assert to_remove == []
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
{"action": "remove", "entity_id": "light.kitchen"},
|
{"action": "remove", "entity_id": entry.entity_id},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert to_update == []
|
assert to_update == []
|
||||||
assert to_remove == ["light.kitchen"]
|
assert to_remove == [entry.entity_id]
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
{
|
{
|
||||||
"action": "update",
|
"action": "update",
|
||||||
"entity_id": "light.kitchen",
|
"entity_id": entry.entity_id,
|
||||||
"changes": ["entity_id"],
|
"changes": ["entity_id"],
|
||||||
"old_entity_id": "light.living_room",
|
"old_entity_id": "light.living_room",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert to_update == ["light.kitchen"]
|
assert to_update == [entry.entity_id]
|
||||||
assert to_remove == ["light.living_room"]
|
assert to_remove == ["light.living_room"]
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -411,6 +469,7 @@ async def test_alexa_update_report_state(
|
||||||
hass: HomeAssistant, cloud_prefs, cloud_stub
|
hass: HomeAssistant, cloud_prefs, cloud_stub
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Alexa config responds to reporting state."""
|
"""Test Alexa config responds to reporting state."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
await cloud_prefs.async_update(
|
await cloud_prefs.async_update(
|
||||||
alexa_report_state=False,
|
alexa_report_state=False,
|
||||||
)
|
)
|
||||||
|
@ -450,6 +509,7 @@ async def test_alexa_handle_logout(
|
||||||
hass: HomeAssistant, cloud_prefs, cloud_stub
|
hass: HomeAssistant, cloud_prefs, cloud_stub
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Alexa config responds to logging out."""
|
"""Test Alexa config responds to logging out."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
aconf = alexa_config.CloudAlexaConfig(
|
aconf = alexa_config.CloudAlexaConfig(
|
||||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_enable.return_value.mock_calls) == 1
|
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_ALEXA,
|
||||||
PREF_ENABLE_GOOGLE,
|
PREF_ENABLE_GOOGLE,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.homeassistant.exposed_entities import (
|
||||||
|
DATA_EXPOSED_ENTITIES,
|
||||||
|
ExposedEntities,
|
||||||
|
)
|
||||||
from homeassistant.const import CONTENT_TYPE_JSON
|
from homeassistant.const import CONTENT_TYPE_JSON
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
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
|
hass: HomeAssistant, mock_cloud_setup, mock_cloud_login
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Google config exposing entity method uses latest config."""
|
"""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
|
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()
|
gconf = await cloud_client.get_google_config()
|
||||||
|
|
||||||
assert gconf.should_expose(state)
|
assert gconf.should_expose(state)
|
||||||
|
|
||||||
await cloud_client.prefs.async_update_google_entity_config(
|
exposed_entities.async_expose_entity(
|
||||||
entity_id="light.kitchen", should_expose=False
|
"cloud.google_assistant", entity_entry.entity_id, False
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not gconf.should_expose(state)
|
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
|
hass: HomeAssistant, mock_cloud_setup, mock_cloud_login
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Google config disabling 2FA method uses latest config."""
|
"""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
|
cloud_client = hass.data[DOMAIN].client
|
||||||
gconf = await cloud_client.get_google_config()
|
gconf = await cloud_client.get_google_config()
|
||||||
state = State("light.kitchen", "on")
|
state = State(entity_entry.entity_id, "on")
|
||||||
|
|
||||||
assert gconf.should_2fa(state)
|
assert gconf.should_2fa(state)
|
||||||
|
|
||||||
await cloud_client.prefs.async_update_google_entity_config(
|
entity_registry.async_update_entity_options(
|
||||||
entity_id="light.kitchen", disable_2fa=True
|
entity_entry.entity_id, "cloud.google_assistant", {"disable_2fa": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not gconf.should_2fa(state)
|
assert not gconf.should_2fa(state)
|
||||||
|
|
|
@ -6,11 +6,24 @@ from freezegun import freeze_time
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.cloud import GACTIONS_SCHEMA
|
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.google_config import CloudGoogleConfig
|
||||||
|
from homeassistant.components.cloud.prefs import CloudPreferences
|
||||||
from homeassistant.components.google_assistant import helpers as ga_helpers
|
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.const import EVENT_HOMEASSISTANT_STARTED, EntityCategory
|
||||||
from homeassistant.core import CoreState, HomeAssistant, State
|
from homeassistant.core import CoreState, HomeAssistant, State
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
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(
|
async def test_google_update_report_state(
|
||||||
mock_conf, hass: HomeAssistant, cloud_prefs
|
mock_conf, hass: HomeAssistant, cloud_prefs
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Google config responds to updating preference."""
|
"""Test Google config responds to updating preference."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
|
|
||||||
await mock_conf.async_initialize()
|
await mock_conf.async_initialize()
|
||||||
await mock_conf.async_connect_agent_user("mock-user-id")
|
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
|
mock_conf, hass: HomeAssistant, cloud_prefs
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Google config not reporting state when subscription has expired."""
|
"""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_initialize()
|
||||||
await mock_conf.async_connect_agent_user("mock-user-id")
|
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:
|
async def test_sync_entities(mock_conf, hass: HomeAssistant, cloud_prefs) -> None:
|
||||||
"""Test sync devices."""
|
"""Test sync devices."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
|
|
||||||
await mock_conf.async_initialize()
|
await mock_conf.async_initialize()
|
||||||
await mock_conf.async_connect_agent_user("mock-user-id")
|
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
|
hass: HomeAssistant, cloud_prefs
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Google config responds to updating exposed entities."""
|
"""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()):
|
with freeze_time(utcnow()):
|
||||||
config = CloudGoogleConfig(
|
config = CloudGoogleConfig(
|
||||||
hass,
|
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(
|
with patch.object(config, "async_sync_entities") as mock_sync, patch.object(
|
||||||
ga_helpers, "SYNC_DELAY", 0
|
ga_helpers, "SYNC_DELAY", 0
|
||||||
):
|
):
|
||||||
await cloud_prefs.async_update_google_entity_config(
|
expose_entity(hass, light_entry.entity_id, True)
|
||||||
entity_id="light.kitchen", should_expose=True
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
async_fire_time_changed(hass, utcnow())
|
async_fire_time_changed(hass, utcnow())
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -114,15 +161,9 @@ async def test_google_update_expose_trigger_sync(
|
||||||
with patch.object(config, "async_sync_entities") as mock_sync, patch.object(
|
with patch.object(config, "async_sync_entities") as mock_sync, patch.object(
|
||||||
ga_helpers, "SYNC_DELAY", 0
|
ga_helpers, "SYNC_DELAY", 0
|
||||||
):
|
):
|
||||||
await cloud_prefs.async_update_google_entity_config(
|
expose_entity(hass, light_entry.entity_id, False)
|
||||||
entity_id="light.kitchen", should_expose=False
|
expose_entity(hass, binary_sensor_entry.entity_id, True)
|
||||||
)
|
expose_entity(hass, sensor_entry.entity_id, True)
|
||||||
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
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
async_fire_time_changed(hass, utcnow())
|
async_fire_time_changed(hass, utcnow())
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -134,6 +175,11 @@ async def test_google_entity_registry_sync(
|
||||||
hass: HomeAssistant, mock_cloud_login, cloud_prefs
|
hass: HomeAssistant, mock_cloud_login, cloud_prefs
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Google config responds to entity registry."""
|
"""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(
|
config = CloudGoogleConfig(
|
||||||
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
|
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
|
ga_helpers, "SYNC_DELAY", 0
|
||||||
):
|
):
|
||||||
# Created entity
|
# Created entity
|
||||||
hass.bus.async_fire(
|
entry = entity_registry.async_get_or_create(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
"light", "test", "unique", suggested_object_id="kitchen"
|
||||||
{"action": "create", "entity_id": "light.kitchen"},
|
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -157,7 +202,7 @@ async def test_google_entity_registry_sync(
|
||||||
# Removed entity
|
# Removed entity
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
{"action": "remove", "entity_id": "light.kitchen"},
|
{"action": "remove", "entity_id": entry.entity_id},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -168,7 +213,7 @@ async def test_google_entity_registry_sync(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
{
|
{
|
||||||
"action": "update",
|
"action": "update",
|
||||||
"entity_id": "light.kitchen",
|
"entity_id": entry.entity_id,
|
||||||
"changes": ["entity_id"],
|
"changes": ["entity_id"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -179,7 +224,7 @@ async def test_google_entity_registry_sync(
|
||||||
# Entity registry updated with non-relevant changes
|
# Entity registry updated with non-relevant changes
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -189,7 +234,7 @@ async def test_google_entity_registry_sync(
|
||||||
hass.state = CoreState.starting
|
hass.state = CoreState.starting
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
{"action": "create", "entity_id": "light.kitchen"},
|
{"action": "create", "entity_id": entry.entity_id},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
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"]
|
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
|
||||||
)
|
)
|
||||||
ent_reg = er.async_get(hass)
|
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_get_or_create("light", "hue", "1234", device_id="1234")
|
||||||
entity_entry = ent_reg.async_update_entity(entity_entry.entity_id, area_id="ABCD")
|
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
|
hass: HomeAssistant, mock_conf, cloud_prefs, entity_registry: er.EntityRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Google config should expose using prefs."""
|
"""Test Google config should expose using prefs."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
entity_entry1 = entity_registry.async_get_or_create(
|
entity_entry1 = entity_registry.async_get_or_create(
|
||||||
"light",
|
"light",
|
||||||
"test",
|
"test",
|
||||||
|
@ -321,45 +371,49 @@ async def test_google_config_expose_entity_prefs(
|
||||||
suggested_object_id="hidden_user_light",
|
suggested_object_id="hidden_user_light",
|
||||||
hidden_by=er.RegistryEntryHider.USER,
|
hidden_by=er.RegistryEntryHider.USER,
|
||||||
)
|
)
|
||||||
|
entity_entry5 = entity_registry.async_get_or_create(
|
||||||
entity_conf = {"should_expose": False}
|
"light",
|
||||||
await cloud_prefs.async_update(
|
"test",
|
||||||
google_entity_configs={"light.kitchen": entity_conf},
|
"light_basement_id",
|
||||||
google_default_expose=["light"],
|
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 = State("light.kitchen", "on")
|
||||||
state_config = State(entity_entry1.entity_id, "on")
|
state_config = State(entity_entry1.entity_id, "on")
|
||||||
state_diagnostic = State(entity_entry2.entity_id, "on")
|
state_diagnostic = State(entity_entry2.entity_id, "on")
|
||||||
state_hidden_integration = State(entity_entry3.entity_id, "on")
|
state_hidden_integration = State(entity_entry3.entity_id, "on")
|
||||||
state_hidden_user = State(entity_entry4.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)
|
||||||
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
|
# categorized and hidden entities should not be exposed
|
||||||
assert not mock_conf.should_expose(state_config)
|
assert not mock_conf.should_expose(state_config)
|
||||||
assert not mock_conf.should_expose(state_diagnostic)
|
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_integration)
|
||||||
assert not mock_conf.should_expose(state_hidden_user)
|
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
|
expose_entity(hass, entity_entry5.entity_id, True)
|
||||||
assert mock_conf.should_expose(state)
|
assert mock_conf.should_expose(state_not_exposed)
|
||||||
# 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)
|
|
||||||
|
|
||||||
await cloud_prefs.async_update(
|
expose_entity(hass, entity_entry5.entity_id, None)
|
||||||
google_default_expose=["sensor"],
|
assert not mock_conf.should_expose(state_not_exposed)
|
||||||
)
|
|
||||||
assert not mock_conf.should_expose(state)
|
|
||||||
|
|
||||||
|
|
||||||
def test_enabled_requires_valid_sub(
|
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:
|
async def test_setup_integration(hass: HomeAssistant, mock_conf, cloud_prefs) -> None:
|
||||||
"""Test that we set up the integration if used."""
|
"""Test that we set up the integration if used."""
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
mock_conf._cloud.subscription_expired = False
|
mock_conf._cloud.subscription_expired = False
|
||||||
|
|
||||||
assert "google_assistant" not in hass.config.components
|
assert "google_assistant" not in hass.config.components
|
||||||
|
@ -423,3 +478,136 @@ async def test_google_handle_logout(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_enable.return_value.mock_calls) == 1
|
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.cloud.const import DOMAIN
|
||||||
from homeassistant.components.google_assistant.helpers import GoogleEntity
|
from homeassistant.components.google_assistant.helpers import GoogleEntity
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.util.location import LocationInfo
|
from homeassistant.util.location import LocationInfo
|
||||||
|
|
||||||
from . import mock_cloud, mock_cloud_prefs
|
from . import mock_cloud, mock_cloud_prefs
|
||||||
|
@ -399,11 +400,9 @@ async def test_websocket_status(
|
||||||
"alexa_enabled": True,
|
"alexa_enabled": True,
|
||||||
"cloudhooks": {},
|
"cloudhooks": {},
|
||||||
"google_enabled": True,
|
"google_enabled": True,
|
||||||
"google_entity_configs": {},
|
|
||||||
"google_secure_devices_pin": None,
|
"google_secure_devices_pin": None,
|
||||||
"google_default_expose": None,
|
"google_default_expose": None,
|
||||||
"alexa_default_expose": None,
|
"alexa_default_expose": None,
|
||||||
"alexa_entity_configs": {},
|
|
||||||
"alexa_report_state": True,
|
"alexa_report_state": True,
|
||||||
"google_report_state": True,
|
"google_report_state": True,
|
||||||
"remote_enabled": False,
|
"remote_enabled": False,
|
||||||
|
@ -520,8 +519,6 @@ async def test_websocket_update_preferences(
|
||||||
"alexa_enabled": False,
|
"alexa_enabled": False,
|
||||||
"google_enabled": False,
|
"google_enabled": False,
|
||||||
"google_secure_devices_pin": "1234",
|
"google_secure_devices_pin": "1234",
|
||||||
"google_default_expose": ["light", "switch"],
|
|
||||||
"alexa_default_expose": ["sensor", "media_player"],
|
|
||||||
"tts_default_voice": ["en-GB", "male"],
|
"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.google_enabled
|
||||||
assert not setup_api.alexa_enabled
|
assert not setup_api.alexa_enabled
|
||||||
assert setup_api.google_secure_devices_pin == "1234"
|
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")
|
assert setup_api.tts_default_voice == ("en-GB", "male")
|
||||||
|
|
||||||
|
|
||||||
|
@ -683,7 +678,11 @@ async def test_enabling_remote(
|
||||||
|
|
||||||
|
|
||||||
async def test_list_google_entities(
|
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:
|
) -> None:
|
||||||
"""Test that we can list Google entities."""
|
"""Test that we can list Google entities."""
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
@ -699,9 +698,25 @@ async def test_list_google_entities(
|
||||||
"homeassistant.components.google_assistant.helpers.async_get_entities",
|
"homeassistant.components.google_assistant.helpers.async_get_entities",
|
||||||
return_value=[entity, entity2],
|
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()
|
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 response["success"]
|
||||||
assert len(response["result"]) == 2
|
assert len(response["result"]) == 2
|
||||||
assert response["result"][0] == {
|
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(
|
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:
|
) -> None:
|
||||||
"""Test that we can update config of a Google entity."""
|
"""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)
|
client = await hass_ws_client(hass)
|
||||||
await client.send_json(
|
await client.send_json_auto_id(
|
||||||
{
|
{
|
||||||
"id": 5,
|
|
||||||
"type": "cloud/google_assistant/entities/update",
|
"type": "cloud/google_assistant/entities/update",
|
||||||
"entity_id": "light.kitchen",
|
"entity_id": "light.kitchen",
|
||||||
"should_expose": False,
|
|
||||||
"disable_2fa": False,
|
"disable_2fa": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
|
|
||||||
assert response["success"]
|
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": "homeassistant/expose_entity",
|
||||||
"type": "cloud/google_assistant/entities/update",
|
"assistants": ["cloud.google_assistant"],
|
||||||
"entity_id": "light.kitchen",
|
"entity_ids": [entry.entity_id],
|
||||||
"should_expose": None,
|
"should_expose": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
|
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
prefs = hass.data[DOMAIN].client.prefs
|
|
||||||
assert prefs.google_entity_configs["light.kitchen"] == {
|
assert entity_registry.async_get(entry.entity_id).options[
|
||||||
"should_expose": None,
|
"cloud.google_assistant"
|
||||||
"disable_2fa": False,
|
] == {"disable_2fa": False, "should_expose": False}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_list_alexa_entities(
|
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:
|
) -> None:
|
||||||
"""Test that we can list Alexa entities."""
|
"""Test that we can list Alexa entities."""
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
@ -769,9 +853,22 @@ async def test_list_alexa_entities(
|
||||||
"homeassistant.components.alexa.entities.async_get_entities",
|
"homeassistant.components.alexa.entities.async_get_entities",
|
||||||
return_value=[entity],
|
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()
|
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 response["success"]
|
||||||
assert len(response["result"]) == 1
|
assert len(response["result"]) == 1
|
||||||
assert response["result"][0] == {
|
assert response["result"][0] == {
|
||||||
|
@ -782,37 +879,31 @@ async def test_list_alexa_entities(
|
||||||
|
|
||||||
|
|
||||||
async def test_update_alexa_entity(
|
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:
|
) -> None:
|
||||||
"""Test that we can update config of an Alexa entity."""
|
"""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)
|
client = await hass_ws_client(hass)
|
||||||
await client.send_json(
|
await client.send_json_auto_id(
|
||||||
{
|
{
|
||||||
"id": 5,
|
"type": "homeassistant/expose_entity",
|
||||||
"type": "cloud/alexa/entities/update",
|
"assistants": ["cloud.alexa"],
|
||||||
"entity_id": "light.kitchen",
|
"entity_ids": [entry.entity_id],
|
||||||
"should_expose": False,
|
"should_expose": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
|
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
prefs = hass.data[DOMAIN].client.prefs
|
assert entity_registry.async_get(entry.entity_id).options["cloud.alexa"] == {
|
||||||
assert prefs.alexa_entity_configs["light.kitchen"] == {"should_expose": False}
|
"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}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_sync_alexa_entities_timeout(
|
async def test_sync_alexa_entities_timeout(
|
||||||
|
|
348
tests/components/homeassistant/test_exposed_entities.py
Normal file
348
tests/components/homeassistant/test_exposed_entities.py
Normal file
|
@ -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…
Add table
Reference in a new issue