diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index b13ec6d1e45d..99075d3d02d0 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -16,3 +16,9 @@ It looks like your Home Assistant Cloud subscription has expired. Please check your [account page](/config/cloud/account) to continue using the service. """ + +MESSAGE_AUTH_FAIL = """ +You have been logged out of Home Assistant Cloud because we have been unable +to verify your credentials. Please [log in](/config/cloud) again to continue +using the service. +""" diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 3220fc372f7f..91fbc85df6bf 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -10,7 +10,7 @@ from homeassistant.util.decorator import Registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import auth_api -from .const import MESSAGE_EXPIRATION +from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) @@ -77,9 +77,9 @@ def _handle_hass_stop(event): self.tries += 1 try: - # Sleep 0, 5, 10, 15 ... 30 seconds between retries + # Sleep 2^tries seconds between retries self.retry_task = hass.async_add_job(asyncio.sleep( - min(30, (self.tries - 1) * 5), loop=hass.loop)) + 2**min(9, self.tries), loop=hass.loop)) yield from self.retry_task self.retry_task = None except asyncio.CancelledError: @@ -97,13 +97,23 @@ def _handle_connection(self): try: yield from hass.async_add_job(auth_api.check_token, self.cloud) + except auth_api.Unauthenticated as err: + _LOGGER.error('Unable to refresh token: %s', err) + + hass.components.persistent_notification.async_create( + MESSAGE_AUTH_FAIL, 'Home Assistant Cloud', + 'cloud_subscription_expired') + + # Don't await it because it will cancel this task + hass.async_add_job(self.cloud.logout()) + return except auth_api.CloudError as err: - _LOGGER.warning("Unable to connect: %s", err) + _LOGGER.warning("Unable to refresh token: %s", err) return if self.cloud.subscription_expired: hass.components.persistent_notification.async_create( - MESSAGE_EXPIRATION, 'Subscription expired', + MESSAGE_EXPIRATION, 'Home Assistant Cloud', 'cloud_subscription_expired') self.close_requested = True return diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 3eec350b2cba..d6a26ee37e0e 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -6,7 +6,7 @@ import pytest from homeassistant.setup import async_setup_component -from homeassistant.components.cloud import iot, auth_api +from homeassistant.components.cloud import Cloud, iot, auth_api, MODE_DEV from tests.components.alexa import test_smart_home as test_alexa from tests.common import mock_coro @@ -202,7 +202,7 @@ def test_cloud_check_token_raising(mock_client, caplog, mock_cloud): yield from conn.connect() - assert 'Unable to connect: BLA' in caplog.text + assert 'Unable to refresh token: BLA' in caplog.text @asyncio.coroutine @@ -348,3 +348,17 @@ def test_handler_google_actions(hass): assert device['name']['name'] == 'Config name' assert device['name']['nicknames'] == ['Config alias'] assert device['type'] == 'action.devices.types.LIGHT' + + +async def test_refresh_token_expired(hass): + """Test handling Unauthenticated error raised if refresh token expired.""" + cloud = Cloud(hass, MODE_DEV, None, None) + + with patch('homeassistant.components.cloud.auth_api.check_token', + side_effect=auth_api.Unauthenticated) as mock_check_token, \ + patch.object(hass.components.persistent_notification, + 'async_create') as mock_create: + await cloud.iot.connect() + + assert len(mock_check_token.mock_calls) == 1 + assert len(mock_create.mock_calls) == 1