diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 36f15735b8b..a22ebbcd30d 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -42,11 +42,11 @@ class AbstractConfig: self._unsub_proactive_report = self.hass.async_create_task( async_enable_proactive_mode(self.hass, self) ) - resp = await self._unsub_proactive_report - - # Failed to start reporting. - if resp is None: + try: + await self._unsub_proactive_report + except Exception: # pylint: disable=broad-except self._unsub_proactive_report = None + raise async def async_disable_proactive_mode(self): """Disable proactive mode.""" diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 4c11fb8c88c..022b38be59d 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -21,6 +21,9 @@ async def async_enable_proactive_mode(hass, smart_home_config): Proactive mode makes this component report state changes to Alexa. """ + # Validate we can get access token. + await smart_home_config.async_get_access_token() + async def async_entity_state_listener(changed_entity, old_state, new_state): if not new_state: diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 746f01dd04b..aae48df9884 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -103,6 +103,15 @@ class AlexaConfig(alexa_config.AbstractConfig): if resp.status == 400: if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'): + if self.should_report_state: + await self._prefs.async_update(alexa_report_state=False) + self.hass.components.persistent_notification.async_create( + "There was an error reporting state to Alexa ({}). " + "Please re-link your Alexa skill via the Alexa app to " + "continue using it.".format(body['reason']), + "Alexa state reporting disabled", + "cloud_alexa_report", + ) raise RequireRelink raise alexa_errors.NoTokenAvailable @@ -200,6 +209,9 @@ class AlexaConfig(alexa_config.AbstractConfig): if not to_update and not to_remove: return True + # Make sure it's valid. + await self.async_get_access_token() + tasks = [] if to_update: @@ -241,4 +253,7 @@ class AlexaConfig(alexa_config.AbstractConfig): elif action == 'remove' and self.should_expose(entity_id): to_remove.append(entity_id) - await self._sync_helper(to_update, to_remove) + try: + await self._sync_helper(to_update, to_remove) + except alexa_errors.NoTokenAvailable: + pass diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 16a05b0d127..d22e5bf37ba 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -12,7 +12,10 @@ from homeassistant.components.google_assistant import smart_home as ga from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.aiohttp import MockRequest -from homeassistant.components.alexa import smart_home as alexa_sh +from homeassistant.components.alexa import ( + smart_home as alexa_sh, + errors as alexa_errors, +) from . import utils, alexa_config, google_config from .const import DISPATCHER_REMOTE_UPDATE @@ -98,8 +101,14 @@ class CloudClient(Interface): """Initialize the client.""" self.cloud = cloud - if self.alexa_config.should_report_state and self.cloud.is_logged_in: + if (not self.alexa_config.should_report_state or + not self.cloud.is_logged_in): + return + + try: await self.alexa_config.async_enable_proactive_mode() + except alexa_errors.NoTokenAvailable: + pass async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d9c4ddcf1ce..0cd08dd3d5f 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -14,7 +14,10 @@ from homeassistant.components.http.data_validator import ( RequestDataValidator) from homeassistant.components import websocket_api from homeassistant.components.websocket_api import const as ws_const -from homeassistant.components.alexa import entities as alexa_entities +from homeassistant.components.alexa import ( + entities as alexa_entities, + errors as alexa_errors, +) from homeassistant.components.google_assistant import helpers as google_helpers from .const import ( @@ -375,6 +378,24 @@ async def websocket_update_prefs(hass, connection, msg): changes = dict(msg) changes.pop('id') changes.pop('type') + + # If we turn alexa linking on, validate that we can fetch access token + if changes.get(PREF_ALEXA_REPORT_STATE): + try: + with async_timeout.timeout(10): + await cloud.client.alexa_config.async_get_access_token() + except asyncio.TimeoutError: + connection.send_error(msg['id'], 'alexa_timeout', + 'Timeout validating Alexa access token.') + return + except alexa_errors.NoTokenAvailable: + connection.send_error( + msg['id'], 'alexa_relink', + 'Please go to the Alexa app and re-link the Home Assistant ' + 'skill and then try to enable state reporting.' + ) + return + await cloud.client.prefs.async_update(**changes) connection.send_message(websocket_api.result_message(msg['id'])) @@ -575,7 +596,15 @@ async def alexa_sync(hass, connection, msg): cloud = hass.data[DOMAIN] with async_timeout.timeout(10): - success = await cloud.client.alexa_config.async_sync_entities() + try: + success = await cloud.client.alexa_config.async_sync_entities() + except alexa_errors.NoTokenAvailable: + connection.send_error( + msg['id'], 'alexa_relink', + 'Please go to the Alexa app and re-link the Home Assistant ' + 'skill.' + ) + return if success: connection.send_result(msg['id']) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 1aa1efc0eca..b8cce030109 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,4 +1,5 @@ """Connection session.""" +import asyncio import voluptuous as vol from homeassistant.core import callback, Context @@ -101,6 +102,9 @@ class ActiveConnection: elif isinstance(err, vol.Invalid): code = const.ERR_INVALID_FORMAT err_message = vol.humanize.humanize_error(msg, err) + elif isinstance(err, asyncio.TimeoutError): + code = const.ERR_TIMEOUT + err_message = 'Timeout' else: code = const.ERR_UNKNOWN_ERROR err_message = 'Unknown error' diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 9c776e3b949..2f79ced7d99 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -16,6 +16,7 @@ ERR_HOME_ASSISTANT_ERROR = 'home_assistant_error' ERR_UNKNOWN_COMMAND = 'unknown_command' ERR_UNKNOWN_ERROR = 'unknown_error' ERR_UNAUTHORIZED = 'unauthorized' +ERR_TIMEOUT = 'timeout' TYPE_RESULT = 'result' diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 55cd9e9e2e5..bc60568f0d4 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -16,6 +16,7 @@ from homeassistant.components.cloud.const import ( from homeassistant.components.google_assistant.helpers import ( GoogleEntity) from homeassistant.components.alexa.entities import LightCapabilities +from homeassistant.components.alexa import errors as alexa_errors from tests.common import mock_coro from tests.components.google_assistant import MockConfig @@ -847,3 +848,53 @@ async def test_update_alexa_entity( assert prefs.alexa_entity_configs['light.kitchen'] == { 'should_expose': False, } + + +async def test_sync_alexa_entities_timeout( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test that timeout syncing Alexa entities.""" + client = await hass_ws_client(hass) + with patch('homeassistant.components.cloud.alexa_config.AlexaConfig' + '.async_sync_entities', side_effect=asyncio.TimeoutError): + await client.send_json({ + 'id': 5, + 'type': 'cloud/alexa/sync', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 'timeout' + + +async def test_sync_alexa_entities_no_token( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test sync Alexa entities when we have no token.""" + client = await hass_ws_client(hass) + with patch('homeassistant.components.cloud.alexa_config.AlexaConfig' + '.async_sync_entities', + side_effect=alexa_errors.NoTokenAvailable): + await client.send_json({ + 'id': 5, + 'type': 'cloud/alexa/sync', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 'alexa_relink' + + +async def test_enable_alexa_state_report_fail( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test enable Alexa entities state reporting when no token available.""" + client = await hass_ws_client(hass) + with patch('homeassistant.components.cloud.alexa_config.AlexaConfig' + '.async_sync_entities', + side_effect=alexa_errors.NoTokenAvailable): + await client.send_json({ + 'id': 5, + 'type': 'cloud/alexa/sync', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 'alexa_relink'