Improve enabling of Google local fulfillment (#109192)
* Improve enabling of Google local fulfillment * Add test * Improve test coverage
This commit is contained in:
parent
6fc5804818
commit
816c2e9500
8 changed files with 131 additions and 22 deletions
|
@ -29,6 +29,8 @@ from . import alexa_config, google_config
|
||||||
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
|
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
|
||||||
from .prefs import CloudPreferences
|
from .prefs import CloudPreferences
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
VALID_REPAIR_TRANSLATION_KEYS = {
|
VALID_REPAIR_TRANSLATION_KEYS = {
|
||||||
"warn_bad_custom_domain_configuration",
|
"warn_bad_custom_domain_configuration",
|
||||||
"reset_bad_custom_domain_configuration",
|
"reset_bad_custom_domain_configuration",
|
||||||
|
@ -149,6 +151,7 @@ class CloudClient(Interface):
|
||||||
|
|
||||||
async def cloud_connected(self) -> None:
|
async def cloud_connected(self) -> None:
|
||||||
"""When cloud is connected."""
|
"""When cloud is connected."""
|
||||||
|
_LOGGER.debug("cloud_connected")
|
||||||
is_new_user = await self.prefs.async_set_username(self.cloud.username)
|
is_new_user = await self.prefs.async_set_username(self.cloud.username)
|
||||||
|
|
||||||
async def enable_alexa(_: Any) -> None:
|
async def enable_alexa(_: Any) -> None:
|
||||||
|
@ -196,6 +199,9 @@ class CloudClient(Interface):
|
||||||
|
|
||||||
async def cloud_disconnected(self) -> None:
|
async def cloud_disconnected(self) -> None:
|
||||||
"""When cloud disconnected."""
|
"""When cloud disconnected."""
|
||||||
|
_LOGGER.debug("cloud_disconnected")
|
||||||
|
if self._google_config:
|
||||||
|
self._google_config.async_disable_local_sdk()
|
||||||
|
|
||||||
async def cloud_started(self) -> None:
|
async def cloud_started(self) -> None:
|
||||||
"""When cloud is started."""
|
"""When cloud is started."""
|
||||||
|
@ -207,6 +213,8 @@ class CloudClient(Interface):
|
||||||
"""Cleanup some stuff after logout."""
|
"""Cleanup some stuff after logout."""
|
||||||
await self.prefs.async_set_username(None)
|
await self.prefs.async_set_username(None)
|
||||||
|
|
||||||
|
if self._google_config:
|
||||||
|
self._google_config.async_deinitialize()
|
||||||
self._google_config = None
|
self._google_config = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -23,6 +23,7 @@ from homeassistant.components.homeassistant.exposed_entities import (
|
||||||
from homeassistant.components.sensor import SensorDeviceClass
|
from homeassistant.components.sensor import SensorDeviceClass
|
||||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
|
CALLBACK_TYPE,
|
||||||
CoreState,
|
CoreState,
|
||||||
Event,
|
Event,
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
|
@ -144,6 +145,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
self._cloud = cloud
|
self._cloud = cloud
|
||||||
self._sync_entities_lock = asyncio.Lock()
|
self._sync_entities_lock = asyncio.Lock()
|
||||||
|
self._on_deinitialize: list[CALLBACK_TYPE] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
|
@ -209,9 +211,11 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
|
|
||||||
async def async_initialize(self) -> None:
|
async def async_initialize(self) -> None:
|
||||||
"""Perform async initialization of config."""
|
"""Perform async initialization of config."""
|
||||||
|
_LOGGER.debug("async_initialize")
|
||||||
await super().async_initialize()
|
await super().async_initialize()
|
||||||
|
|
||||||
async def on_hass_started(hass: HomeAssistant) -> None:
|
async def on_hass_started(hass: HomeAssistant) -> None:
|
||||||
|
_LOGGER.debug("async_initialize on_hass_started")
|
||||||
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
|
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Start migration of Google Assistant settings from v%s to v%s",
|
"Start migration of Google Assistant settings from v%s to v%s",
|
||||||
|
@ -238,16 +242,19 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
await self._prefs.async_update(
|
await self._prefs.async_update(
|
||||||
google_settings_version=GOOGLE_SETTINGS_VERSION
|
google_settings_version=GOOGLE_SETTINGS_VERSION
|
||||||
)
|
)
|
||||||
async_listen_entity_updates(
|
self._on_deinitialize.append(
|
||||||
self.hass, CLOUD_GOOGLE, self._async_exposed_entities_updated
|
async_listen_entity_updates(
|
||||||
|
self.hass, CLOUD_GOOGLE, self._async_exposed_entities_updated
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_hass_start(hass: HomeAssistant) -> None:
|
async def on_hass_start(hass: HomeAssistant) -> None:
|
||||||
|
_LOGGER.debug("async_initialize on_hass_start")
|
||||||
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, {})
|
||||||
|
|
||||||
start.async_at_start(self.hass, on_hass_start)
|
self._on_deinitialize.append(start.async_at_start(self.hass, on_hass_start))
|
||||||
start.async_at_started(self.hass, on_hass_started)
|
self._on_deinitialize.append(start.async_at_started(self.hass, on_hass_started))
|
||||||
|
|
||||||
# Remove any stored user agent id that is not ours
|
# Remove any stored user agent id that is not ours
|
||||||
remove_agent_user_ids = []
|
remove_agent_user_ids = []
|
||||||
|
@ -255,18 +262,33 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
if agent_user_id != self.agent_user_id:
|
if agent_user_id != self.agent_user_id:
|
||||||
remove_agent_user_ids.append(agent_user_id)
|
remove_agent_user_ids.append(agent_user_id)
|
||||||
|
|
||||||
|
if remove_agent_user_ids:
|
||||||
|
_LOGGER.debug("remove non cloud agent_user_ids: %s", remove_agent_user_ids)
|
||||||
for agent_user_id in remove_agent_user_ids:
|
for agent_user_id in remove_agent_user_ids:
|
||||||
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._on_deinitialize.append(
|
||||||
self.hass.bus.async_listen(
|
self._prefs.async_listen_updates(self._async_prefs_updated)
|
||||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
|
||||||
self._handle_entity_registry_updated,
|
|
||||||
)
|
)
|
||||||
self.hass.bus.async_listen(
|
self._on_deinitialize.append(
|
||||||
dr.EVENT_DEVICE_REGISTRY_UPDATED,
|
self.hass.bus.async_listen(
|
||||||
self._handle_device_registry_updated,
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
|
self._handle_entity_registry_updated,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
self._on_deinitialize.append(
|
||||||
|
self.hass.bus.async_listen(
|
||||||
|
dr.EVENT_DEVICE_REGISTRY_UPDATED,
|
||||||
|
self._handle_device_registry_updated,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_deinitialize(self) -> None:
|
||||||
|
"""Remove listeners."""
|
||||||
|
_LOGGER.debug("async_deinitialize")
|
||||||
|
while self._on_deinitialize:
|
||||||
|
self._on_deinitialize.pop()()
|
||||||
|
|
||||||
def should_expose(self, state: State) -> bool:
|
def should_expose(self, state: State) -> bool:
|
||||||
"""If a state object should be exposed."""
|
"""If a state object should be exposed."""
|
||||||
|
@ -365,6 +387,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
|
|
||||||
async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:
|
async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:
|
||||||
"""Handle updated preferences."""
|
"""Handle updated preferences."""
|
||||||
|
_LOGGER.debug("_async_prefs_updated")
|
||||||
if not self._cloud.is_logged_in:
|
if not self._cloud.is_logged_in:
|
||||||
if self.is_reporting_state:
|
if self.is_reporting_state:
|
||||||
self.async_disable_report_state()
|
self.async_disable_report_state()
|
||||||
|
|
|
@ -316,6 +316,7 @@ class AbstractConfig(ABC):
|
||||||
@callback
|
@callback
|
||||||
def async_enable_local_sdk(self) -> None:
|
def async_enable_local_sdk(self) -> None:
|
||||||
"""Enable the local SDK."""
|
"""Enable the local SDK."""
|
||||||
|
_LOGGER.debug("async_enable_local_sdk")
|
||||||
setup_successful = True
|
setup_successful = True
|
||||||
setup_webhook_ids = []
|
setup_webhook_ids = []
|
||||||
|
|
||||||
|
@ -324,11 +325,16 @@ class AbstractConfig(ABC):
|
||||||
self._local_sdk_active = False
|
self._local_sdk_active = False
|
||||||
return
|
return
|
||||||
|
|
||||||
for user_agent_id, _ in self._store.agent_user_ids.items():
|
for user_agent_id in self._store.agent_user_ids:
|
||||||
if (webhook_id := self.get_local_webhook_id(user_agent_id)) is None:
|
if (webhook_id := self.get_local_webhook_id(user_agent_id)) is None:
|
||||||
setup_successful = False
|
setup_successful = False
|
||||||
break
|
break
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Register webhook handler %s for agent user id %s",
|
||||||
|
webhook_id,
|
||||||
|
user_agent_id,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
webhook.async_register(
|
webhook.async_register(
|
||||||
self.hass,
|
self.hass,
|
||||||
|
@ -360,13 +366,18 @@ class AbstractConfig(ABC):
|
||||||
@callback
|
@callback
|
||||||
def async_disable_local_sdk(self) -> None:
|
def async_disable_local_sdk(self) -> None:
|
||||||
"""Disable the local SDK."""
|
"""Disable the local SDK."""
|
||||||
|
_LOGGER.debug("async_disable_local_sdk")
|
||||||
if not self._local_sdk_active:
|
if not self._local_sdk_active:
|
||||||
return
|
return
|
||||||
|
|
||||||
for agent_user_id in self._store.agent_user_ids:
|
for agent_user_id in self._store.agent_user_ids:
|
||||||
webhook.async_unregister(
|
webhook_id = self.get_local_webhook_id(agent_user_id)
|
||||||
self.hass, self.get_local_webhook_id(agent_user_id)
|
_LOGGER.debug(
|
||||||
|
"Unregister webhook handler %s for agent user id %s",
|
||||||
|
webhook_id,
|
||||||
|
agent_user_id,
|
||||||
)
|
)
|
||||||
|
webhook.async_unregister(self.hass, webhook_id)
|
||||||
|
|
||||||
self._local_sdk_active = False
|
self._local_sdk_active = False
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.components import websocket_api
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
from homeassistant.components.sensor import SensorDeviceClass
|
from homeassistant.components.sensor import SensorDeviceClass
|
||||||
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 CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.entity import get_device_class
|
from homeassistant.helpers.entity import get_device_class
|
||||||
|
@ -129,10 +129,17 @@ class ExposedEntities:
|
||||||
@callback
|
@callback
|
||||||
def async_listen_entity_updates(
|
def async_listen_entity_updates(
|
||||||
self, assistant: str, listener: Callable[[], None]
|
self, assistant: str, listener: Callable[[], None]
|
||||||
) -> None:
|
) -> CALLBACK_TYPE:
|
||||||
"""Listen for updates to entity expose settings."""
|
"""Listen for updates to entity expose settings."""
|
||||||
|
|
||||||
|
def unsubscribe() -> None:
|
||||||
|
"""Stop listening to entity updates."""
|
||||||
|
self._listeners[assistant].remove(listener)
|
||||||
|
|
||||||
self._listeners.setdefault(assistant, []).append(listener)
|
self._listeners.setdefault(assistant, []).append(listener)
|
||||||
|
|
||||||
|
return unsubscribe
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_set_assistant_option(
|
def async_set_assistant_option(
|
||||||
self, assistant: str, entity_id: str, key: str, value: Any
|
self, assistant: str, entity_id: str, key: str, value: Any
|
||||||
|
@ -484,10 +491,10 @@ def ws_expose_new_entities_set(
|
||||||
@callback
|
@callback
|
||||||
def async_listen_entity_updates(
|
def async_listen_entity_updates(
|
||||||
hass: HomeAssistant, assistant: str, listener: Callable[[], None]
|
hass: HomeAssistant, assistant: str, listener: Callable[[], None]
|
||||||
) -> None:
|
) -> CALLBACK_TYPE:
|
||||||
"""Listen for updates to entity expose settings."""
|
"""Listen for updates to entity expose settings."""
|
||||||
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||||
exposed_entities.async_listen_entity_updates(assistant, listener)
|
return exposed_entities.async_listen_entity_updates(assistant, listener)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -115,6 +115,13 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]:
|
||||||
type(mock_cloud).is_connected = is_connected
|
type(mock_cloud).is_connected = is_connected
|
||||||
type(mock_cloud.iot).connected = is_connected
|
type(mock_cloud.iot).connected = is_connected
|
||||||
|
|
||||||
|
def mock_username() -> bool:
|
||||||
|
"""Return the subscription username."""
|
||||||
|
return "abcdefghjkl"
|
||||||
|
|
||||||
|
username = PropertyMock(side_effect=mock_username)
|
||||||
|
type(mock_cloud).username = username
|
||||||
|
|
||||||
# Properties that we mock as attributes.
|
# Properties that we mock as attributes.
|
||||||
mock_cloud.expiration_date = utcnow()
|
mock_cloud.expiration_date = utcnow()
|
||||||
mock_cloud.subscription_expired = False
|
mock_cloud.subscription_expired = False
|
||||||
|
|
|
@ -428,3 +428,47 @@ async def test_async_create_repair_issue_unknown(
|
||||||
)
|
)
|
||||||
issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier)
|
issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier)
|
||||||
assert issue is None
|
assert issue is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_disconnected(hass: HomeAssistant) -> None:
|
||||||
|
"""Test cleanup when disconnected from the cloud."""
|
||||||
|
prefs = MagicMock(
|
||||||
|
alexa_enabled=False,
|
||||||
|
google_enabled=True,
|
||||||
|
async_set_username=AsyncMock(return_value=None),
|
||||||
|
)
|
||||||
|
client = CloudClient(hass, prefs, None, {}, {})
|
||||||
|
client.cloud = MagicMock(is_logged_in=True, username="mock-username")
|
||||||
|
client._google_config = Mock()
|
||||||
|
client._google_config.async_disable_local_sdk.assert_not_called()
|
||||||
|
|
||||||
|
await client.cloud_disconnected()
|
||||||
|
client._google_config.async_disable_local_sdk.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_logged_out(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cloud: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test cleanup when logged out from the cloud."""
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await cloud.login("test-user", "test-pass")
|
||||||
|
|
||||||
|
alexa_config_mock = Mock(async_enable_proactive_mode=AsyncMock())
|
||||||
|
google_config_mock = Mock(async_sync_entities=AsyncMock())
|
||||||
|
cloud.client._alexa_config = alexa_config_mock
|
||||||
|
cloud.client._google_config = google_config_mock
|
||||||
|
|
||||||
|
await cloud.client.cloud_connected()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Simulate logged out
|
||||||
|
await cloud.logout()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Alexa is not cleaned up, Google is
|
||||||
|
assert cloud.client._alexa_config is alexa_config_mock
|
||||||
|
assert cloud.client._google_config is None
|
||||||
|
google_config_mock.async_deinitialize.assert_called_once_with()
|
||||||
|
|
|
@ -441,8 +441,10 @@ def test_enabled_requires_valid_sub(
|
||||||
assert not config.enabled
|
assert not config.enabled
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_integration(hass: HomeAssistant, mock_conf, cloud_prefs) -> None:
|
async def test_setup_google_assistant(
|
||||||
"""Test that we set up the integration if used."""
|
hass: HomeAssistant, mock_conf, cloud_prefs
|
||||||
|
) -> None:
|
||||||
|
"""Test that we set up the google_assistant integration if enabled in cloud."""
|
||||||
assert await async_setup_component(hass, "homeassistant", {})
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
mock_conf._cloud.subscription_expired = False
|
mock_conf._cloud.subscription_expired = False
|
||||||
|
|
||||||
|
@ -473,8 +475,10 @@ async def test_google_handle_logout(
|
||||||
"homeassistant.components.google_assistant.report_state.async_enable_report_state",
|
"homeassistant.components.google_assistant.report_state.async_enable_report_state",
|
||||||
) as mock_enable:
|
) as mock_enable:
|
||||||
gconf.async_enable_report_state()
|
gconf.async_enable_report_state()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_enable.mock_calls) == 1
|
assert len(mock_enable.mock_calls) == 1
|
||||||
|
assert len(gconf._on_deinitialize) == 6
|
||||||
|
|
||||||
# This will trigger a prefs update when we logout.
|
# This will trigger a prefs update when we logout.
|
||||||
await cloud_prefs.get_cloud_user()
|
await cloud_prefs.get_cloud_user()
|
||||||
|
@ -484,8 +488,13 @@ async def test_google_handle_logout(
|
||||||
"async_check_token",
|
"async_check_token",
|
||||||
side_effect=AssertionError("Should not be called"),
|
side_effect=AssertionError("Should not be called"),
|
||||||
):
|
):
|
||||||
|
# Fake logging out; CloudClient.logout_cleanups sets username to None
|
||||||
|
# and deinitializes the Google config.
|
||||||
await cloud_prefs.async_set_username(None)
|
await cloud_prefs.async_set_username(None)
|
||||||
|
gconf.async_deinitialize()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
# Check listeners are removed:
|
||||||
|
assert not gconf._on_deinitialize
|
||||||
|
|
||||||
assert len(mock_enable.return_value.mock_calls) == 1
|
assert len(mock_enable.return_value.mock_calls) == 1
|
||||||
|
|
||||||
|
|
|
@ -103,8 +103,8 @@ async def test_remote_services(
|
||||||
assert mock_disconnect.called is False
|
assert mock_disconnect.called is False
|
||||||
|
|
||||||
|
|
||||||
async def test_startup_shutdown_events(hass: HomeAssistant, mock_cloud_fixture) -> None:
|
async def test_shutdown_event(hass: HomeAssistant, mock_cloud_fixture) -> None:
|
||||||
"""Test if the cloud will start on startup event."""
|
"""Test if the cloud will stop on shutdown event."""
|
||||||
with patch("hass_nabucasa.Cloud.stop") as mock_stop:
|
with patch("hass_nabucasa.Cloud.stop") as mock_stop:
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue