diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index b49d379592f..7b4c8ed8b1b 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -29,7 +29,7 @@ REQUIREMENTS = ['pysensibo==1.0.2'] _LOGGER = logging.getLogger(__name__) -ALL = 'all' +ALL = ['all'] TIMEOUT = 10 SERVICE_ASSUME_STATE = 'sensibo_assume_state' diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index b13ec6d1e45..99075d3d02d 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -16,3 +16,9 @@ MESSAGE_EXPIRATION = """ 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 3220fc372f7..91fbc85df6b 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -10,7 +10,7 @@ from homeassistant.components.google_assistant import smart_home as ga 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 @@ class CloudIoT: 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 @@ class CloudIoT: 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/homeassistant/components/hue.py b/homeassistant/components/hue.py index 36d5a1a56a0..2d64306ca74 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue.py @@ -192,7 +192,7 @@ class HueBridge(object): self.bridge = phue.Bridge( self.host, config_file_path=self.hass.config.path(self.filename)) - except ConnectionRefusedError: # Wrong host was given + except (ConnectionRefusedError, OSError): # Wrong host was given _LOGGER.error("Error connecting to the Hue bridge at %s", self.host) return @@ -201,6 +201,9 @@ class HueBridge(object): self.host) self.request_configuration() return + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error connecting with Hue bridge at %s", + self.host) # If we came here and configuring this host, mark as done if self.config_request_id: diff --git a/homeassistant/const.py b/homeassistant/const.py index 286ea2d2d78..a0e9a44ea5b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 64 -PATCH_VERSION = '2' +PATCH_VERSION = '3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) diff --git a/homeassistant/core.py b/homeassistant/core.py index b1cf9c51efd..6ffc524c3be 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1068,7 +1068,7 @@ class Config(object): """Check if the path is valid for access from outside.""" assert path is not None - parent = pathlib.Path(path).parent + parent = pathlib.Path(path) try: parent = parent.resolve() # pylint: disable=no-member except (FileNotFoundError, RuntimeError, PermissionError): diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 3eec350b2cb..d6a26ee37e0 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -6,7 +6,7 @@ from aiohttp import WSMsgType, client_exceptions 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 diff --git a/tests/test_core.py b/tests/test_core.py index 77a7872526f..261b6385b04 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -809,6 +809,7 @@ class TestConfig(unittest.TestCase): valid = [ test_file, + tmp_dir ] for path in valid: assert self.config.is_allowed_path(path)