From 3f97944f6b5125a08fbd7e43b3acda33762c0833 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Sep 2018 13:44:56 +0200 Subject: [PATCH 01/30] Bumped version to 0.78.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3bb468c1b1e..ba7011b388a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 78 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 7a5e828f6b45a7d22197e1261f5920a1b7de9e33 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Sep 2018 14:28:21 +0200 Subject: [PATCH 02/30] Updates documentation repo URL in PR template (#16537) --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c2f65f9a8be..1e37cf86fc3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ **Related issue (if applicable):** fixes # -**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io# +**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io# ## Example entry for `configuration.yaml` (if applicable): ```yaml @@ -15,7 +15,7 @@ - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** If user exposed functionality or configuration variables are added/changed: - - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) + - [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) If the code communicates with devices, web services, or third-party tools: - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). From 00d8907c5ec811c2de6a3a72af643a490b17e3ca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Sep 2018 21:36:54 +0200 Subject: [PATCH 03/30] Update translations --- .../components/auth/.translations/sv.json | 4 ++++ .../components/hangouts/.translations/sv.json | 8 ++++++++ .../homematicip_cloud/.translations/sv.json | 1 + .../components/openuv/.translations/ar.json | 5 +++++ .../components/openuv/.translations/de.json | 19 ++++++++++++++++++ .../components/openuv/.translations/fa.json | 5 +++++ .../components/openuv/.translations/ko.json | 20 +++++++++++++++++++ .../components/openuv/.translations/nl.json | 19 ++++++++++++++++++ .../components/openuv/.translations/no.json | 20 +++++++++++++++++++ .../components/openuv/.translations/pl.json | 4 +++- .../components/openuv/.translations/sv.json | 4 +++- 11 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/openuv/.translations/ar.json create mode 100644 homeassistant/components/openuv/.translations/de.json create mode 100644 homeassistant/components/openuv/.translations/fa.json create mode 100644 homeassistant/components/openuv/.translations/ko.json create mode 100644 homeassistant/components/openuv/.translations/nl.json create mode 100644 homeassistant/components/openuv/.translations/no.json diff --git a/homeassistant/components/auth/.translations/sv.json b/homeassistant/components/auth/.translations/sv.json index e148766e2a1..cf8227c09a3 100644 --- a/homeassistant/components/auth/.translations/sv.json +++ b/homeassistant/components/auth/.translations/sv.json @@ -1,8 +1,12 @@ { "mfa_setup": { "totp": { + "error": { + "invalid_code": "Ogiltig kod, f\u00f6rs\u00f6k igen. Om du flera g\u00e5nger i rad f\u00e5r detta fel, se till att klockan i din Home Assistant \u00e4r korrekt inst\u00e4lld." + }, "step": { "init": { + "description": "F\u00f6r att aktivera tv\u00e5faktorsautentisering som anv\u00e4nder tidsbaserade eng\u00e5ngsl\u00f6senord, skanna QR-koden med din autentiseringsapp. Om du inte har en, rekommenderar vi antingen [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n{qr_code} \n\nN\u00e4r du har skannat koden anger du den sexsiffriga koden fr\u00e5n din app f\u00f6r att verifiera inst\u00e4llningen. Om du har problem med att skanna QR-koden, g\u00f6r en manuell inst\u00e4llning med kod ** ` {code} ` **.", "title": "St\u00e4ll in tv\u00e5faktorsautentisering med TOTP" } }, diff --git a/homeassistant/components/hangouts/.translations/sv.json b/homeassistant/components/hangouts/.translations/sv.json index d926097dfc2..90bf4e97712 100644 --- a/homeassistant/components/hangouts/.translations/sv.json +++ b/homeassistant/components/hangouts/.translations/sv.json @@ -4,8 +4,16 @@ "already_configured": "Google Hangouts \u00e4r redan inst\u00e4llt", "unknown": "Ett ok\u00e4nt fel intr\u00e4ffade" }, + "error": { + "invalid_2fa": "Ogiltig 2FA autentisering, f\u00f6rs\u00f6k igen.", + "invalid_2fa_method": "Ogiltig 2FA-metod (Verifiera med telefon).", + "invalid_login": "Ogiltig inloggning, f\u00f6rs\u00f6k igen." + }, "step": { "2fa": { + "data": { + "2fa": "2FA Pinkod" + }, "title": "Tv\u00e5faktorsautentisering" }, "user": { diff --git a/homeassistant/components/homematicip_cloud/.translations/sv.json b/homeassistant/components/homematicip_cloud/.translations/sv.json index 945dca8a277..4e8aac999de 100644 --- a/homeassistant/components/homematicip_cloud/.translations/sv.json +++ b/homeassistant/components/homematicip_cloud/.translations/sv.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Accesspunkten \u00e4r redan konfigurerad", "conection_aborted": "Kunde inte ansluta till HMIP server", + "connection_aborted": "Det gick inte att ansluta till HMIP-servern", "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" }, "error": { diff --git a/homeassistant/components/openuv/.translations/ar.json b/homeassistant/components/openuv/.translations/ar.json new file mode 100644 index 00000000000..288fae919dc --- /dev/null +++ b/homeassistant/components/openuv/.translations/ar.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/de.json b/homeassistant/components/openuv/.translations/de.json new file mode 100644 index 00000000000..1f81ac30f53 --- /dev/null +++ b/homeassistant/components/openuv/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinaten existieren bereits", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-Schl\u00fcssel", + "elevation": "H\u00f6he", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + } + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/fa.json b/homeassistant/components/openuv/.translations/fa.json new file mode 100644 index 00000000000..288fae919dc --- /dev/null +++ b/homeassistant/components/openuv/.translations/fa.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ko.json b/homeassistant/components/openuv/.translations/ko.json new file mode 100644 index 00000000000..bb054f0b3a6 --- /dev/null +++ b/homeassistant/components/openuv/.translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "\uc798\ubabb\ub41c API \ud0a4" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API \ud0a4", + "elevation": "\uace0\ub3c4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4" + }, + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/nl.json b/homeassistant/components/openuv/.translations/nl.json new file mode 100644 index 00000000000..2c5086b3365 --- /dev/null +++ b/homeassistant/components/openuv/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_api_key": "Ongeldige API-sleutel" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-Sleutel", + "elevation": "Hoogte", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + }, + "title": "Vul uw gegevens in" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/no.json b/homeassistant/components/openuv/.translations/no.json new file mode 100644 index 00000000000..2ffd5e7fb41 --- /dev/null +++ b/homeassistant/components/openuv/.translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinatene er allerede registrert", + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-n\u00f8kkel", + "elevation": "Elevasjon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + }, + "title": "Fyll ut informasjonen din" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json index fdb94b293b8..f6c52ffd04e 100644 --- a/homeassistant/components/openuv/.translations/pl.json +++ b/homeassistant/components/openuv/.translations/pl.json @@ -8,9 +8,11 @@ "user": { "data": { "api_key": "Klucz API OpenUV", + "elevation": "Wysoko\u015b\u0107", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" - } + }, + "title": "Wpisz swoje informacje" } }, "title": "OpenUV" diff --git a/homeassistant/components/openuv/.translations/sv.json b/homeassistant/components/openuv/.translations/sv.json index 0df9a0434ab..d9de0f7c0a6 100644 --- a/homeassistant/components/openuv/.translations/sv.json +++ b/homeassistant/components/openuv/.translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "identifier_exists": "Koordinater \u00e4r redan registrerade", "invalid_api_key": "Ogiltigt API-l\u00f6senord" }, "step": { @@ -13,6 +14,7 @@ }, "title": "Fyll i dina uppgifter" } - } + }, + "title": "OpenUV" } } \ No newline at end of file From b9b8bc678c51615677294078b2ac8061a902ad45 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Sep 2018 21:37:38 +0200 Subject: [PATCH 04/30] Update frontend to 20180911.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index add323c4c28..5fea70ce846 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180910.0'] +REQUIREMENTS = ['home-assistant-frontend==20180911.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 358f5ac2244..ab83a023381 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -452,7 +452,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180910.0 +home-assistant-frontend==20180911.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f875118b2e..7708aeb6650 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.4 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180910.0 +home-assistant-frontend==20180911.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 968809c99130baedf34c91569f5b208459b252c2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 11 Sep 2018 10:30:20 +0100 Subject: [PATCH 05/30] Replace api_password in Camera.Push (#16339) * Use access_token and user provided token instead of api_password * address comments by @awarecan * new tests * add extra checks and test * lint * add comment --- homeassistant/components/camera/push.py | 32 ++++++++++-- tests/components/camera/test_push.py | 67 ++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index 305e29d62d3..c9deca1309d 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -13,8 +13,10 @@ import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ STATE_IDLE, STATE_RECORDING from homeassistant.core import callback -from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST +from homeassistant.components.http.view import KEY_AUTHENTICATED,\ + HomeAssistantView +from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\ + HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util @@ -25,11 +27,13 @@ DEPENDENCIES = ['http'] CONF_BUFFER_SIZE = 'buffer' CONF_IMAGE_FIELD = 'field' +CONF_TOKEN = 'token' DEFAULT_NAME = "Push Camera" ATTR_FILENAME = 'filename' ATTR_LAST_TRIP = 'last_trip' +ATTR_TOKEN = 'token' PUSH_CAMERA_DATA = 'push_camera' @@ -39,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All( cv.time_period, cv.positive_timedelta), vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string, + vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)), }) @@ -50,7 +55,8 @@ async def async_setup_platform(hass, config, async_add_entities, cameras = [PushCamera(config[CONF_NAME], config[CONF_BUFFER_SIZE], - config[CONF_TIMEOUT])] + config[CONF_TIMEOUT], + config.get(CONF_TOKEN))] hass.http.register_view(CameraPushReceiver(hass, config[CONF_IMAGE_FIELD])) @@ -63,6 +69,7 @@ class CameraPushReceiver(HomeAssistantView): url = "/api/camera_push/{entity_id}" name = 'api:camera_push:camera_entity' + requires_auth = False def __init__(self, hass, image_field): """Initialize CameraPushReceiver with camera entity.""" @@ -75,8 +82,21 @@ class CameraPushReceiver(HomeAssistantView): if _camera is None: _LOGGER.error("Unknown %s", entity_id) + status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\ + else HTTP_UNAUTHORIZED return self.json_message('Unknown {}'.format(entity_id), - HTTP_BAD_REQUEST) + status) + + # Supports HA authentication and token based + # when token has been configured + authenticated = (request[KEY_AUTHENTICATED] or + (_camera.token is not None and + request.query.get('token') == _camera.token)) + + if not authenticated: + return self.json_message( + 'Invalid authorization credentials for {}'.format(entity_id), + HTTP_UNAUTHORIZED) try: data = await request.post() @@ -95,7 +115,7 @@ class CameraPushReceiver(HomeAssistantView): class PushCamera(Camera): """The representation of a Push camera.""" - def __init__(self, name, buffer_size, timeout): + def __init__(self, name, buffer_size, timeout, token): """Initialize push camera component.""" super().__init__() self._name = name @@ -106,6 +126,7 @@ class PushCamera(Camera): self._timeout = timeout self.queue = deque([], buffer_size) self._current_image = None + self.token = token async def async_added_to_hass(self): """Call when entity is added to hass.""" @@ -168,5 +189,6 @@ class PushCamera(Camera): name: value for name, value in ( (ATTR_LAST_TRIP, self._last_trip), (ATTR_FILENAME, self._filename), + (ATTR_TOKEN, self.token), ) if value is not None } diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index f9a3c62aa4a..6d9688c10e6 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -6,7 +6,7 @@ from datetime import timedelta from homeassistant import core as ha from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.components.auth import async_setup_auth +from homeassistant.components.http.auth import setup_auth async def test_bad_posting(aioclient_mock, hass, aiohttp_client): @@ -15,19 +15,69 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): 'camera': { 'platform': 'push', 'name': 'config_test', + 'token': '12345678' }}) - - client = await async_setup_auth(hass, aiohttp_client) + client = await aiohttp_client(hass.http.app) # missing file resp = await client.post('/api/camera_push/camera.config_test') assert resp.status == 400 - files = {'image': io.BytesIO(b'fake')} - # wrong entity + files = {'image': io.BytesIO(b'fake')} resp = await client.post('/api/camera_push/camera.wrong', data=files) - assert resp.status == 400 + assert resp.status == 404 + + +async def test_cases_with_no_auth(aioclient_mock, hass, aiohttp_client): + """Test cases where aiohttp_client is not auth.""" + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'push', + 'name': 'config_test', + 'token': '12345678' + }}) + + setup_auth(hass.http.app, [], True, api_password=None) + client = await aiohttp_client(hass.http.app) + + # wrong token + files = {'image': io.BytesIO(b'fake')} + resp = await client.post('/api/camera_push/camera.config_test?token=1234', + data=files) + assert resp.status == 401 + + # right token + files = {'image': io.BytesIO(b'fake')} + resp = await client.post( + '/api/camera_push/camera.config_test?token=12345678', + data=files) + assert resp.status == 200 + + +async def test_no_auth_no_token(aioclient_mock, hass, aiohttp_client): + """Test cases where aiohttp_client is not auth.""" + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'push', + 'name': 'config_test', + }}) + + setup_auth(hass.http.app, [], True, api_password=None) + client = await aiohttp_client(hass.http.app) + + # no token + files = {'image': io.BytesIO(b'fake')} + resp = await client.post('/api/camera_push/camera.config_test', + data=files) + assert resp.status == 401 + + # fake token + files = {'image': io.BytesIO(b'fake')} + resp = await client.post( + '/api/camera_push/camera.config_test?token=12345678', + data=files) + assert resp.status == 401 async def test_posting_url(hass, aiohttp_client): @@ -36,6 +86,7 @@ async def test_posting_url(hass, aiohttp_client): 'camera': { 'platform': 'push', 'name': 'config_test', + 'token': '12345678' }}) client = await aiohttp_client(hass.http.app) @@ -46,7 +97,9 @@ async def test_posting_url(hass, aiohttp_client): assert camera_state.state == 'idle' # post image - resp = await client.post('/api/camera_push/camera.config_test', data=files) + resp = await client.post( + '/api/camera_push/camera.config_test?token=12345678', + data=files) assert resp.status == 200 # state recording From 44d210698ed5ad5c23b28c5ee6b966b7dd7fdbc7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Sep 2018 23:51:40 +0200 Subject: [PATCH 06/30] Fail fetch auth providers if onboarding required (#16454) --- homeassistant/components/auth/login_flow.py | 15 ++++++++++++--- tests/components/auth/test_login_flow.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index a518bdde415..73a739c2960 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -66,7 +66,7 @@ associate with an credential if "type" set to "link_user" in "version": 1 } """ -import aiohttp.web +from aiohttp import web import voluptuous as vol from homeassistant import data_entry_flow @@ -95,11 +95,20 @@ class AuthProvidersView(HomeAssistantView): async def get(self, request): """Get available auth providers.""" + hass = request.app['hass'] + + if not hass.components.onboarding.async_is_onboarded(): + return self.json_message( + message='Onboarding not finished', + status_code=400, + message_code='onboarding_required' + ) + return self.json([{ 'name': provider.name, 'id': provider.id, 'type': provider.type, - } for provider in request.app['hass'].auth.auth_providers]) + } for provider in hass.auth.auth_providers]) def _prepare_result_json(result): @@ -139,7 +148,7 @@ class LoginFlowIndexView(HomeAssistantView): async def get(self, request): """Do not allow index of flows in progress.""" - return aiohttp.web.Response(status=405) + return web.Response(status=405) @RequestDataValidator(vol.Schema({ vol.Required('client_id'): str, diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index 8b6108067c5..d759bac74b7 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -1,4 +1,6 @@ """Tests for the login flow.""" +from unittest.mock import patch + from . import async_setup_auth from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI @@ -16,6 +18,19 @@ async def test_fetch_auth_providers(hass, aiohttp_client): }] +async def test_fetch_auth_providers_onboarding(hass, aiohttp_client): + """Test fetching auth providers.""" + client = await async_setup_auth(hass, aiohttp_client) + with patch('homeassistant.components.onboarding.async_is_onboarded', + return_value=False): + resp = await client.get('/auth/providers') + assert resp.status == 400 + assert await resp.json() == { + 'message': 'Onboarding not finished', + 'code': 'onboarding_required', + } + + async def test_cannot_get_flows_in_progress(hass, aiohttp_client): """Test we cannot get flows in progress.""" client = await async_setup_auth(hass, aiohttp_client, []) From 1ea8c1ece3cca8ea5d191d6e2ba04605f3bb68a1 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 10 Sep 2018 10:54:17 -0400 Subject: [PATCH 07/30] Fix insteon Hub v1 support (#16472) * Fix support for Hub version 1 (i.e. pre-2014 Hub model 2242) * Bump insteonplm to 0.14.1 * Code review changes * Clean up and better document set_default_port * Simplify set_default_port based on code review * Remove Callable type import * Simplify port setup --- homeassistant/components/insteon/__init__.py | 40 +++++++++++++------- requirements_all.txt | 2 +- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index d79640b77ab..749d167e6de 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -7,6 +7,8 @@ https://home-assistant.io/components/insteon/ import asyncio import collections import logging +from typing import Dict + import voluptuous as vol from homeassistant.core import callback @@ -18,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.13.1'] +REQUIREMENTS = ['insteonplm==0.14.2'] _LOGGER = logging.getLogger(__name__) @@ -27,9 +29,9 @@ DOMAIN = 'insteon' CONF_IP_PORT = 'ip_port' CONF_HUB_USERNAME = 'username' CONF_HUB_PASSWORD = 'password' +CONF_HUB_VERSION = 'hub_version' CONF_OVERRIDE = 'device_override' -CONF_PLM_HUB_MSG = ('Must configure either a PLM port or a Hub host, username ' - 'and password') +CONF_PLM_HUB_MSG = 'Must configure either a PLM port or a Hub host' CONF_ADDRESS = 'address' CONF_CAT = 'cat' CONF_SUBCAT = 'subcat' @@ -66,6 +68,22 @@ EVENT_BUTTON_ON = 'insteon.button_on' EVENT_BUTTON_OFF = 'insteon.button_off' EVENT_CONF_BUTTON = 'button' + +def set_default_port(schema: Dict) -> Dict: + """Set the default port based on the Hub version.""" + # If the ip_port is found do nothing + # If it is not found the set the default + ip_port = schema.get(CONF_IP_PORT) + if not ip_port: + hub_version = schema.get(CONF_HUB_VERSION) + # Found hub_version but not ip_port + if hub_version == 1: + schema[CONF_IP_PORT] = 9761 + else: + schema[CONF_IP_PORT] = 25105 + return schema + + CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ vol.Required(CONF_ADDRESS): cv.string, @@ -88,12 +106,13 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All( vol.Schema( {vol.Exclusive(CONF_PORT, 'plm_or_hub', - msg=CONF_PLM_HUB_MSG): cv.isdevice, + msg=CONF_PLM_HUB_MSG): cv.string, vol.Exclusive(CONF_HOST, 'plm_or_hub', msg=CONF_PLM_HUB_MSG): cv.string, - vol.Optional(CONF_IP_PORT, default=25105): int, + vol.Optional(CONF_IP_PORT): cv.port, vol.Optional(CONF_HUB_USERNAME): cv.string, vol.Optional(CONF_HUB_PASSWORD): cv.string, + vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]), vol.Optional(CONF_OVERRIDE): vol.All( cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]), vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), @@ -103,14 +122,7 @@ CONFIG_SCHEMA = vol.Schema({ [CONF_X10_SCHEMA]) }, extra=vol.ALLOW_EXTRA, required=True), cv.has_at_least_one_key(CONF_PORT, CONF_HOST), - vol.Schema( - {vol.Inclusive(CONF_HOST, 'hub', - msg=CONF_PLM_HUB_MSG): cv.string, - vol.Inclusive(CONF_HUB_USERNAME, 'hub', - msg=CONF_PLM_HUB_MSG): cv.string, - vol.Inclusive(CONF_HUB_PASSWORD, 'hub', - msg=CONF_PLM_HUB_MSG): cv.string, - }, extra=vol.ALLOW_EXTRA, required=True)) + set_default_port) }, extra=vol.ALLOW_EXTRA) @@ -151,6 +163,7 @@ def async_setup(hass, config): ip_port = conf.get(CONF_IP_PORT) username = conf.get(CONF_HUB_USERNAME) password = conf.get(CONF_HUB_PASSWORD) + hub_version = conf.get(CONF_HUB_VERSION) overrides = conf.get(CONF_OVERRIDE, []) x10_devices = conf.get(CONF_X10, []) x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF) @@ -284,6 +297,7 @@ def async_setup(hass, config): port=ip_port, username=username, password=password, + hub_version=hub_version, loop=hass.loop, workdir=hass.config.config_dir) else: diff --git a/requirements_all.txt b/requirements_all.txt index ab83a023381..f11be0e1a89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -486,7 +486,7 @@ ihcsdk==2.2.0 influxdb==5.0.0 # homeassistant.components.insteon -insteonplm==0.13.1 +insteonplm==0.14.2 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From 60b45d4ba5f3e1f683fe2404f4d1028bb42c6642 Mon Sep 17 00:00:00 2001 From: vikramgorla Date: Mon, 10 Sep 2018 16:13:05 +0200 Subject: [PATCH 08/30] bugfix - incorrect camera type and missing sensors when multiple netatmo cameras (#16490) fixed get_camera_type as it was originally not consuming any input, was looping with all cameras and the first camera type was retutned, modified to call cameraType using provided camera name. --- homeassistant/components/netatmo.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index c25b57fbd62..59b0a64f6e9 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -101,10 +101,10 @@ class CameraData: return self.module_names def get_camera_type(self, camera=None, home=None, cid=None): - """Return all module available on the API as a list.""" - for camera_name in self.camera_names: - self.camera_type = self.camera_data.cameraType(camera_name) - return self.camera_type + """Return camera type for a camera, cid has preference over camera.""" + self.camera_type = self.camera_data.cameraType(camera=camera, + home=home, cid=cid) + return self.camera_type @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): From a4cec0b871abdba21d6fc061fb10c4eda7674b60 Mon Sep 17 00:00:00 2001 From: Zellux Wang Date: Tue, 11 Sep 2018 03:25:38 -0600 Subject: [PATCH 09/30] Fix arlo intilization when no base station available (#16529) * Fix arlo intilization when no base station * Fix pylint for empty camera check * Fix typo * Minor change to trigger CI again --- homeassistant/components/arlo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index c6a414b9d91..015e1e0d1fc 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -61,10 +61,12 @@ def setup(hass, config): arlo_base_station = next(( station for station in arlo.base_stations), None) - if arlo_base_station is None: + if arlo_base_station is not None: + arlo_base_station.refresh_rate = scan_interval.total_seconds() + elif not arlo.cameras: + _LOGGER.error("No Arlo camera or base station available.") return False - arlo_base_station.refresh_rate = scan_interval.total_seconds() hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: From 20e562b81610531bcc0ae82d4f7712063685c7c1 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 11 Sep 2018 03:05:15 -0700 Subject: [PATCH 10/30] Long-lived access token (#16453) * Allow create refresh_token with specific access_token_expiration * Add token_type, client_name and client_icon * Add unit test * Add websocket API to create long-lived access token * Allow URL use as client_id for long-lived access token * Remove mutate_refresh_token method * Use client name as id for long_lived_access_token type refresh token * Minor change * Do not allow duplicate client name * Update docstring * Remove unnecessary `list` --- homeassistant/auth/__init__.py | 45 ++++++- homeassistant/auth/auth_store.py | 34 +++++- homeassistant/auth/models.py | 16 ++- homeassistant/components/auth/__init__.py | 104 +++++++++++++++- tests/auth/test_init.py | 141 +++++++++++++++++++++- tests/components/auth/test_init.py | 59 ++++++++- tests/components/conftest.py | 2 + 7 files changed, 385 insertions(+), 16 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 4ef8440de62..b0cebb5fd6c 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -2,11 +2,13 @@ import asyncio import logging from collections import OrderedDict +from datetime import timedelta from typing import Any, Dict, List, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow +from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import callback, HomeAssistant from homeassistant.util import dt as dt_util @@ -242,8 +244,12 @@ class AuthManager: modules[module_id] = module.name return modules - async def async_create_refresh_token(self, user: models.User, - client_id: Optional[str] = None) \ + async def async_create_refresh_token( + self, user: models.User, client_id: Optional[str] = None, + client_name: Optional[str] = None, + client_icon: Optional[str] = None, + token_type: Optional[str] = None, + access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \ -> models.RefreshToken: """Create a new refresh token for a user.""" if not user.is_active: @@ -254,10 +260,36 @@ class AuthManager: 'System generated users cannot have refresh tokens connected ' 'to a client.') - if not user.system_generated and client_id is None: + if token_type is None: + if user.system_generated: + token_type = models.TOKEN_TYPE_SYSTEM + else: + token_type = models.TOKEN_TYPE_NORMAL + + if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM): + raise ValueError( + 'System generated users can only have system type ' + 'refresh tokens') + + if token_type == models.TOKEN_TYPE_NORMAL and client_id is None: raise ValueError('Client is required to generate a refresh token.') - return await self._store.async_create_refresh_token(user, client_id) + if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and + client_name is None): + raise ValueError('Client_name is required for long-lived access ' + 'token') + + if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN: + for token in user.refresh_tokens.values(): + if (token.client_name == client_name and token.token_type == + models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN): + # Each client_name can only have one + # long_lived_access_token type of refresh token + raise ValueError('{} already exists'.format(client_name)) + + return await self._store.async_create_refresh_token( + user, client_id, client_name, client_icon, + token_type, access_token_expiration) async def async_get_refresh_token( self, token_id: str) -> Optional[models.RefreshToken]: @@ -280,10 +312,11 @@ class AuthManager: refresh_token: models.RefreshToken) -> str: """Create a new access token.""" # pylint: disable=no-self-use + now = dt_util.utcnow() return jwt.encode({ 'iss': refresh_token.id, - 'iat': dt_util.utcnow(), - 'exp': dt_util.utcnow() + refresh_token.access_token_expiration, + 'iat': now, + 'exp': now + refresh_token.access_token_expiration, }, refresh_token.jwt_key, algorithm='HS256').decode() async def async_validate_access_token( diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 0f12d69211c..d78a1f4225e 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -5,6 +5,7 @@ from logging import getLogger from typing import Any, Dict, List, Optional # noqa: F401 import hmac +from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util @@ -128,11 +129,27 @@ class AuthStore: self._async_schedule_save() async def async_create_refresh_token( - self, user: models.User, client_id: Optional[str] = None) \ + self, user: models.User, client_id: Optional[str] = None, + client_name: Optional[str] = None, + client_icon: Optional[str] = None, + token_type: str = models.TOKEN_TYPE_NORMAL, + access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \ -> models.RefreshToken: """Create a new token for a user.""" - refresh_token = models.RefreshToken(user=user, client_id=client_id) + kwargs = { + 'user': user, + 'client_id': client_id, + 'token_type': token_type, + 'access_token_expiration': access_token_expiration + } # type: Dict[str, Any] + if client_name: + kwargs['client_name'] = client_name + if client_icon: + kwargs['client_icon'] = client_icon + + refresh_token = models.RefreshToken(**kwargs) user.refresh_tokens[refresh_token.id] = refresh_token + self._async_schedule_save() return refresh_token @@ -216,10 +233,20 @@ class AuthStore: 'Ignoring refresh token %(id)s with invalid created_at ' '%(created_at)s for user_id %(user_id)s', rt_dict) continue + token_type = rt_dict.get('token_type') + if token_type is None: + if rt_dict['clinet_id'] is None: + token_type = models.TOKEN_TYPE_SYSTEM + else: + token_type = models.TOKEN_TYPE_NORMAL token = models.RefreshToken( id=rt_dict['id'], user=users[rt_dict['user_id']], client_id=rt_dict['client_id'], + # use dict.get to keep backward compatibility + client_name=rt_dict.get('client_name'), + client_icon=rt_dict.get('client_icon'), + token_type=token_type, created_at=created_at, access_token_expiration=timedelta( seconds=rt_dict['access_token_expiration']), @@ -271,6 +298,9 @@ class AuthStore: 'id': refresh_token.id, 'user_id': user.id, 'client_id': refresh_token.client_id, + 'client_name': refresh_token.client_name, + 'client_icon': refresh_token.client_icon, + 'token_type': refresh_token.token_type, 'created_at': refresh_token.created_at.isoformat(), 'access_token_expiration': refresh_token.access_token_expiration.total_seconds(), diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index a6500510e0d..c5273d7fa1d 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -7,9 +7,12 @@ import attr from homeassistant.util import dt as dt_util -from .const import ACCESS_TOKEN_EXPIRATION from .util import generate_secret +TOKEN_TYPE_NORMAL = 'normal' +TOKEN_TYPE_SYSTEM = 'system' +TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token' + @attr.s(slots=True) class User: @@ -37,11 +40,16 @@ class RefreshToken: """RefreshToken for a user to grant new access tokens.""" user = attr.ib(type=User) - client_id = attr.ib(type=str) # type: Optional[str] + client_id = attr.ib(type=Optional[str]) + access_token_expiration = attr.ib(type=timedelta) + client_name = attr.ib(type=Optional[str], default=None) + client_icon = attr.ib(type=Optional[str], default=None) + token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL, + validator=attr.validators.in_(( + TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, + TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN))) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) - access_token_expiration = attr.ib(type=timedelta, - default=ACCESS_TOKEN_EXPIRATION) token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64))) jwt_key = attr.ib(type=str, diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index a87e646761c..5839b7ec403 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -12,6 +12,7 @@ be in JSON as it's more readable. Exchange the authorization code retrieved from the login flow for tokens. { + "client_id": "https://hassbian.local:8123/", "grant_type": "authorization_code", "code": "411ee2f916e648d691e937ae9344681e" } @@ -32,6 +33,7 @@ token. Request a new access token using a refresh token. { + "client_id": "https://hassbian.local:8123/", "grant_type": "refresh_token", "refresh_token": "IJKLMNOPQRST" } @@ -55,6 +57,67 @@ ever been granted by that refresh token. Response code will ALWAYS be 200. "action": "revoke" } +# Websocket API + +## Get current user + +Send websocket command `auth/current_user` will return current user of the +active websocket connection. + +{ + "id": 10, + "type": "auth/current_user", +} + +The result payload likes + +{ + "id": 10, + "type": "result", + "success": true, + "result": { + "id": "USER_ID", + "name": "John Doe", + "is_owner': true, + "credentials": [ + { + "auth_provider_type": "homeassistant", + "auth_provider_id": null + } + ], + "mfa_modules": [ + { + "id": "totp", + "name": "TOTP", + "enabled": true, + } + ] + } +} + +## Create a long-lived access token + +Send websocket command `auth/long_lived_access_token` will create +a long-lived access token for current user. Access token will not be saved in +Home Assistant. User need to record the token in secure place. + +{ + "id": 11, + "type": "auth/long_lived_access_token", + "client_name": "GPS Logger", + "client_icon": null, + "lifespan": 365 +} + +Result will be a long-lived access token: + +{ + "id": 11, + "type": "result", + "success": true, + "result": "ABCDEFGH" +} + """ import logging import uuid @@ -63,7 +126,8 @@ from datetime import timedelta from aiohttp import web import voluptuous as vol -from homeassistant.auth.models import User, Credentials +from homeassistant.auth.models import User, Credentials, \ + TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN from homeassistant.components import websocket_api from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator @@ -83,6 +147,15 @@ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_CURRENT_USER, }) +WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token' +SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + vol.Required('lifespan'): int, # days + vol.Required('client_name'): str, + vol.Optional('client_icon'): str, + }) + RESULT_TYPE_CREDENTIALS = 'credentials' RESULT_TYPE_USER = 'user' @@ -100,6 +173,11 @@ async def async_setup(hass, config): WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER ) + hass.components.websocket_api.async_register_command( + WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + websocket_create_long_lived_access_token, + SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN + ) await login_flow.async_setup(hass, store_result) await mfa_setup_flow.async_setup(hass) @@ -343,3 +421,27 @@ def websocket_current_user( })) hass.async_create_task(async_get_current_user(connection.user)) + + +@websocket_api.ws_require_user() +@callback +def websocket_create_long_lived_access_token( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Create or a long-lived access token.""" + async def async_create_long_lived_access_token(user): + """Create or a long-lived access token.""" + refresh_token = await hass.auth.async_create_refresh_token( + user, + client_name=msg['client_name'], + client_icon=msg.get('client_icon'), + token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + access_token_expiration=timedelta(days=msg['lifespan'])) + + access_token = hass.auth.async_create_access_token( + refresh_token) + + connection.send_message_outside( + websocket_api.result_message(msg['id'], access_token)) + + hass.async_create_task( + async_create_long_lived_access_token(connection.user)) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 63b2b4408dd..765199b256c 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import Mock, patch +import jwt import pytest import voluptuous as vol @@ -323,7 +324,7 @@ async def test_generating_system_user(hass): async def test_refresh_token_requires_client_for_user(hass): - """Test that we can add a system user.""" + """Test create refresh token for a user with client_id.""" manager = await auth.auth_manager_from_config(hass, [], []) user = MockUser().add_to_auth_manager(manager) assert user.system_generated is False @@ -334,10 +335,14 @@ async def test_refresh_token_requires_client_for_user(hass): token = await manager.async_create_refresh_token(user, CLIENT_ID) assert token is not None assert token.client_id == CLIENT_ID + assert token.token_type == auth_models.TOKEN_TYPE_NORMAL + # default access token expiration + assert token.access_token_expiration == \ + auth_const.ACCESS_TOKEN_EXPIRATION async def test_refresh_token_not_requires_client_for_system_user(hass): - """Test that we can add a system user.""" + """Test create refresh token for a system user w/o client_id.""" manager = await auth.auth_manager_from_config(hass, [], []) user = await manager.async_create_system_user('Hass.io') assert user.system_generated is True @@ -348,6 +353,56 @@ async def test_refresh_token_not_requires_client_for_system_user(hass): token = await manager.async_create_refresh_token(user) assert token is not None assert token.client_id is None + assert token.token_type == auth_models.TOKEN_TYPE_SYSTEM + + +async def test_refresh_token_with_specific_access_token_expiration(hass): + """Test create a refresh token with specific access token expiration.""" + manager = await auth.auth_manager_from_config(hass, [], []) + user = MockUser().add_to_auth_manager(manager) + + token = await manager.async_create_refresh_token( + user, CLIENT_ID, + access_token_expiration=timedelta(days=100)) + assert token is not None + assert token.client_id == CLIENT_ID + assert token.access_token_expiration == timedelta(days=100) + + +async def test_refresh_token_type(hass): + """Test create a refresh token with token type.""" + manager = await auth.auth_manager_from_config(hass, [], []) + user = MockUser().add_to_auth_manager(manager) + + with pytest.raises(ValueError): + await manager.async_create_refresh_token( + user, CLIENT_ID, token_type=auth_models.TOKEN_TYPE_SYSTEM) + + token = await manager.async_create_refresh_token( + user, CLIENT_ID, + token_type=auth_models.TOKEN_TYPE_NORMAL) + assert token is not None + assert token.client_id == CLIENT_ID + assert token.token_type == auth_models.TOKEN_TYPE_NORMAL + + +async def test_refresh_token_type_long_lived_access_token(hass): + """Test create a refresh token has long-lived access token type.""" + manager = await auth.auth_manager_from_config(hass, [], []) + user = MockUser().add_to_auth_manager(manager) + + with pytest.raises(ValueError): + await manager.async_create_refresh_token( + user, token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN) + + token = await manager.async_create_refresh_token( + user, client_name='GPS LOGGER', client_icon='mdi:home', + token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN) + assert token is not None + assert token.client_id is None + assert token.client_name == 'GPS LOGGER' + assert token.client_icon == 'mdi:home' + assert token.token_type == auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN async def test_cannot_deactive_owner(mock_hass): @@ -378,6 +433,88 @@ async def test_remove_refresh_token(mock_hass): ) +async def test_create_access_token(mock_hass): + """Test normal refresh_token's jwt_key keep same after used.""" + manager = await auth.auth_manager_from_config(mock_hass, [], []) + user = MockUser().add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) + assert refresh_token.token_type == auth_models.TOKEN_TYPE_NORMAL + jwt_key = refresh_token.jwt_key + access_token = manager.async_create_access_token(refresh_token) + assert access_token is not None + assert refresh_token.jwt_key == jwt_key + jwt_payload = jwt.decode(access_token, jwt_key, algorithm=['HS256']) + assert jwt_payload['iss'] == refresh_token.id + assert jwt_payload['exp'] - jwt_payload['iat'] == \ + timedelta(minutes=30).total_seconds() + + +async def test_create_long_lived_access_token(mock_hass): + """Test refresh_token's jwt_key changed for long-lived access token.""" + manager = await auth.auth_manager_from_config(mock_hass, [], []) + user = MockUser().add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token( + user, client_name='GPS Logger', + token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + access_token_expiration=timedelta(days=300)) + assert refresh_token.token_type == \ + auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN + access_token = manager.async_create_access_token(refresh_token) + jwt_payload = jwt.decode( + access_token, refresh_token.jwt_key, algorithm=['HS256']) + assert jwt_payload['iss'] == refresh_token.id + assert jwt_payload['exp'] - jwt_payload['iat'] == \ + timedelta(days=300).total_seconds() + + +async def test_one_long_lived_access_token_per_refresh_token(mock_hass): + """Test one refresh_token can only have one long-lived access token.""" + manager = await auth.auth_manager_from_config(mock_hass, [], []) + user = MockUser().add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token( + user, client_name='GPS Logger', + token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + access_token_expiration=timedelta(days=3000)) + assert refresh_token.token_type == \ + auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN + access_token = manager.async_create_access_token(refresh_token) + jwt_key = refresh_token.jwt_key + + rt = await manager.async_validate_access_token(access_token) + assert rt.id == refresh_token.id + + with pytest.raises(ValueError): + await manager.async_create_refresh_token( + user, client_name='GPS Logger', + token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + access_token_expiration=timedelta(days=3000)) + + await manager.async_remove_refresh_token(refresh_token) + assert refresh_token.id not in user.refresh_tokens + rt = await manager.async_validate_access_token(access_token) + assert rt is None, 'Previous issued access token has been invoked' + + refresh_token_2 = await manager.async_create_refresh_token( + user, client_name='GPS Logger', + token_type=auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + access_token_expiration=timedelta(days=3000)) + assert refresh_token_2.id != refresh_token.id + assert refresh_token_2.token_type == \ + auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN + access_token_2 = manager.async_create_access_token(refresh_token_2) + jwt_key_2 = refresh_token_2.jwt_key + + assert access_token != access_token_2 + assert jwt_key != jwt_key_2 + + rt = await manager.async_validate_access_token(access_token_2) + jwt_payload = jwt.decode( + access_token_2, rt.jwt_key, algorithm=['HS256']) + assert jwt_payload['iss'] == refresh_token_2.id + assert jwt_payload['exp'] - jwt_payload['iat'] == \ + timedelta(days=3000).total_seconds() + + async def test_login_with_auth_module(mock_hass): """Test login as existing user with auth module.""" manager = await auth.auth_manager_from_config(mock_hass, [{ diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 7b9dda6acb3..e0fe00bd9d8 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -2,6 +2,8 @@ from datetime import timedelta from unittest.mock import patch +from homeassistant import const +from homeassistant.auth import auth_manager_from_config from homeassistant.auth.models import Credentials from homeassistant.components.auth import RESULT_TYPE_USER from homeassistant.setup import async_setup_component @@ -10,7 +12,8 @@ from homeassistant.components import auth from . import async_setup_auth -from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser +from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser, \ + ensure_auth_manager_loaded async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): @@ -267,3 +270,57 @@ async def test_revoking_refresh_token(hass, aiohttp_client): }) assert resp.status == 400 + + +async def test_ws_long_lived_access_token(hass, hass_ws_client): + """Test generate long-lived access token.""" + hass.auth = await auth_manager_from_config( + hass, provider_configs=[{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name', + }] + }], module_configs=[]) + ensure_auth_manager_loaded(hass.auth) + assert await async_setup_component(hass, 'auth', {'http': {}}) + assert await async_setup_component(hass, 'api', {'http': {}}) + + user = MockUser(id='mock-user').add_to_hass(hass) + cred = await hass.auth.auth_providers[0].async_get_or_create_credentials( + {'username': 'test-user'}) + await hass.auth.async_link_user(user, cred) + + ws_client = await hass_ws_client(hass, hass.auth.async_create_access_token( + await hass.auth.async_create_refresh_token(user, CLIENT_ID))) + + # verify create long-lived access token + await ws_client.send_json({ + 'id': 5, + 'type': auth.WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + 'client_name': 'GPS Logger', + 'lifespan': 365, + }) + + result = await ws_client.receive_json() + assert result['success'], result + + long_lived_access_token = result['result'] + assert long_lived_access_token is not None + + refresh_token = await hass.auth.async_validate_access_token( + long_lived_access_token) + assert refresh_token.client_id is None + assert refresh_token.client_name == 'GPS Logger' + assert refresh_token.client_icon is None + + # verify long-lived access token can be used as bearer token + api_client = ws_client.client + resp = await api_client.get(const.URL_API) + assert resp.status == 401 + + resp = await api_client.get(const.URL_API, headers={ + 'Authorization': 'Bearer {}'.format(long_lived_access_token) + }) + assert resp.status == 200 diff --git a/tests/components/conftest.py b/tests/components/conftest.py index bb9b643296e..7cec790c847 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -34,6 +34,8 @@ def hass_ws_client(aiohttp_client): auth_ok = await websocket.receive_json() assert auth_ok['type'] == wapi.TYPE_AUTH_OK + # wrap in client + websocket.client = client return websocket return create_client From 37c8aac76f54a49dcd05db5d11e90a6e4a725f6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Sep 2018 12:55:05 +0200 Subject: [PATCH 11/30] Fix typo (#16556) --- homeassistant/auth/auth_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index d78a1f4225e..8e8d03253e5 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -235,7 +235,7 @@ class AuthStore: continue token_type = rt_dict.get('token_type') if token_type is None: - if rt_dict['clinet_id'] is None: + if rt_dict['client_id'] is None: token_type = models.TOKEN_TYPE_SYSTEM else: token_type = models.TOKEN_TYPE_NORMAL From 20f06f4eb9fe6ab97e79851784851abd8552f335 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Sep 2018 21:40:35 +0200 Subject: [PATCH 12/30] Fix invalid state (#16558) * Fix invalid state * Make slightly more efficient in unsubscribing * Use uuid4" --- homeassistant/const.py | 3 +++ homeassistant/core.py | 20 +++++++++++++++----- tests/test_core.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ba7011b388a..e8349f2f0b8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -225,6 +225,9 @@ ATTR_ID = 'id' # Name ATTR_NAME = 'name' +# Data for a SERVICE_EXECUTED event +ATTR_SERVICE_CALL_ID = 'service_call_id' + # Contains one string or a list of strings, each being an entity id ATTR_ENTITY_ID = 'entity_id' diff --git a/homeassistant/core.py b/homeassistant/core.py index 2b7a2479471..b4c824fe44b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -30,7 +30,7 @@ from voluptuous.humanize import humanize_error from homeassistant.const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE, - ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + ATTR_SERVICE_CALL_ID, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_SERVICE_EXECUTED, EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, EVENT_HOMEASSISTANT_CLOSE, @@ -1043,10 +1043,12 @@ class ServiceRegistry: This method is a coroutine. """ context = context or Context() + call_id = uuid.uuid4().hex event_data = { ATTR_DOMAIN: domain.lower(), ATTR_SERVICE: service.lower(), ATTR_SERVICE_DATA: service_data, + ATTR_SERVICE_CALL_ID: call_id, } if not blocking: @@ -1059,8 +1061,9 @@ class ServiceRegistry: @callback def service_executed(event: Event) -> None: """Handle an executed service.""" - if event.context == context: + if event.data[ATTR_SERVICE_CALL_ID] == call_id: fut.set_result(True) + unsub() unsub = self._hass.bus.async_listen( EVENT_SERVICE_EXECUTED, service_executed) @@ -1070,7 +1073,8 @@ class ServiceRegistry: done, _ = await asyncio.wait([fut], timeout=SERVICE_CALL_LIMIT) success = bool(done) - unsub() + if not success: + unsub() return success async def _event_to_service_call(self, event: Event) -> None: @@ -1078,6 +1082,7 @@ class ServiceRegistry: service_data = event.data.get(ATTR_SERVICE_DATA) or {} domain = event.data.get(ATTR_DOMAIN).lower() # type: ignore service = event.data.get(ATTR_SERVICE).lower() # type: ignore + call_id = event.data.get(ATTR_SERVICE_CALL_ID) if not self.has_service(domain, service): if event.origin == EventOrigin.local: @@ -1089,12 +1094,17 @@ class ServiceRegistry: def fire_service_executed() -> None: """Fire service executed event.""" + if not call_id: + return + + data = {ATTR_SERVICE_CALL_ID: call_id} + if (service_handler.is_coroutinefunction or service_handler.is_callback): - self._hass.bus.async_fire(EVENT_SERVICE_EXECUTED, {}, + self._hass.bus.async_fire(EVENT_SERVICE_EXECUTED, data, EventOrigin.local, event.context) else: - self._hass.bus.fire(EVENT_SERVICE_EXECUTED, {}, + self._hass.bus.fire(EVENT_SERVICE_EXECUTED, data, EventOrigin.local, event.context) try: diff --git a/tests/test_core.py b/tests/test_core.py index ce066135709..7e6d57136e4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -20,9 +20,10 @@ from homeassistant.util.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( __version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM, ATTR_NOW, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED) + EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, + EVENT_SERVICE_EXECUTED) -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, async_mock_service PST = pytz.timezone('America/Los_Angeles') @@ -969,3 +970,27 @@ def test_track_task_functions(loop): assert hass._track_task finally: yield from hass.async_stop() + + +async def test_service_executed_with_subservices(hass): + """Test we block correctly till all services done.""" + calls = async_mock_service(hass, 'test', 'inner') + + async def handle_outer(call): + """Handle outer service call.""" + calls.append(call) + call1 = hass.services.async_call('test', 'inner', blocking=True, + context=call.context) + call2 = hass.services.async_call('test', 'inner', blocking=True, + context=call.context) + await asyncio.wait([call1, call2]) + calls.append(call) + + hass.services.async_register('test', 'outer', handle_outer) + + await hass.services.async_call('test', 'outer', blocking=True) + + assert len(calls) == 4 + assert [call.service for call in calls] == [ + 'outer', 'inner', 'inner', 'outer'] + assert len(hass.bus.async_listeners().get(EVENT_SERVICE_EXECUTED, [])) == 0 From e50fc69c1e367ad22aa684c184eb605107d614b7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Sep 2018 18:08:03 +0200 Subject: [PATCH 13/30] Add websocket commands for refresh tokens (#16559) * Add websocket commands for refresh tokens * Comment --- homeassistant/components/auth/__init__.py | 60 +++++++++++++++++ tests/components/auth/test_init.py | 82 ++++++++++++++--------- tests/components/conftest.py | 42 ++++++++---- 3 files changed, 140 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 5839b7ec403..5fac423f27a 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -156,6 +156,19 @@ SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \ vol.Optional('client_icon'): str, }) +WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens' +SCHEMA_WS_REFRESH_TOKENS = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_REFRESH_TOKENS, + }) + +WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token' +SCHEMA_WS_DELETE_REFRESH_TOKEN = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN, + vol.Required('refresh_token_id'): str, + }) + RESULT_TYPE_CREDENTIALS = 'credentials' RESULT_TYPE_USER = 'user' @@ -178,6 +191,16 @@ async def async_setup(hass, config): websocket_create_long_lived_access_token, SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN ) + hass.components.websocket_api.async_register_command( + WS_TYPE_REFRESH_TOKENS, + websocket_refresh_tokens, + SCHEMA_WS_REFRESH_TOKENS + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_DELETE_REFRESH_TOKEN, + websocket_delete_refresh_token, + SCHEMA_WS_DELETE_REFRESH_TOKEN + ) await login_flow.async_setup(hass, store_result) await mfa_setup_flow.async_setup(hass) @@ -445,3 +468,40 @@ def websocket_create_long_lived_access_token( hass.async_create_task( async_create_long_lived_access_token(connection.user)) + + +@websocket_api.ws_require_user() +@callback +def websocket_refresh_tokens( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Return metadata of users refresh tokens.""" + connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{ + 'id': refresh.id, + 'client_id': refresh.client_id, + 'client_name': refresh.client_name, + 'client_icon': refresh.client_icon, + 'type': refresh.token_type, + 'created_at': refresh.created_at, + } for refresh in connection.user.refresh_tokens.values()])) + + +@websocket_api.ws_require_user() +@callback +def websocket_delete_refresh_token( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Handle a delete refresh token request.""" + async def async_delete_refresh_token(user, refresh_token_id): + """Delete a refresh token.""" + refresh_token = connection.user.refresh_tokens.get(refresh_token_id) + + if refresh_token is None: + return websocket_api.error_message( + msg['id'], 'invalid_token_id', 'Received invalid token') + + await hass.auth.async_remove_refresh_token(refresh_token) + + connection.send_message_outside( + websocket_api.result_message(msg['id'], {})) + + hass.async_create_task( + async_delete_refresh_token(connection.user, msg['refresh_token_id'])) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index e0fe00bd9d8..a8e95c73a36 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -2,18 +2,15 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant import const -from homeassistant.auth import auth_manager_from_config from homeassistant.auth.models import Credentials from homeassistant.components.auth import RESULT_TYPE_USER from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from homeassistant.components import auth -from . import async_setup_auth +from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser -from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser, \ - ensure_auth_manager_loaded +from . import async_setup_auth async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): @@ -272,28 +269,12 @@ async def test_revoking_refresh_token(hass, aiohttp_client): assert resp.status == 400 -async def test_ws_long_lived_access_token(hass, hass_ws_client): +async def test_ws_long_lived_access_token(hass, hass_ws_client, + hass_access_token): """Test generate long-lived access token.""" - hass.auth = await auth_manager_from_config( - hass, provider_configs=[{ - 'type': 'insecure_example', - 'users': [{ - 'username': 'test-user', - 'password': 'test-pass', - 'name': 'Test Name', - }] - }], module_configs=[]) - ensure_auth_manager_loaded(hass.auth) assert await async_setup_component(hass, 'auth', {'http': {}}) - assert await async_setup_component(hass, 'api', {'http': {}}) - user = MockUser(id='mock-user').add_to_hass(hass) - cred = await hass.auth.auth_providers[0].async_get_or_create_credentials( - {'username': 'test-user'}) - await hass.auth.async_link_user(user, cred) - - ws_client = await hass_ws_client(hass, hass.auth.async_create_access_token( - await hass.auth.async_create_refresh_token(user, CLIENT_ID))) + ws_client = await hass_ws_client(hass, hass_access_token) # verify create long-lived access token await ws_client.send_json({ @@ -315,12 +296,51 @@ async def test_ws_long_lived_access_token(hass, hass_ws_client): assert refresh_token.client_name == 'GPS Logger' assert refresh_token.client_icon is None - # verify long-lived access token can be used as bearer token - api_client = ws_client.client - resp = await api_client.get(const.URL_API) - assert resp.status == 401 - resp = await api_client.get(const.URL_API, headers={ - 'Authorization': 'Bearer {}'.format(long_lived_access_token) +async def test_ws_refresh_tokens(hass, hass_ws_client, hass_access_token): + """Test fetching refresh token metadata.""" + assert await async_setup_component(hass, 'auth', {'http': {}}) + + ws_client = await hass_ws_client(hass, hass_access_token) + + await ws_client.send_json({ + 'id': 5, + 'type': auth.WS_TYPE_REFRESH_TOKENS, }) - assert resp.status == 200 + + result = await ws_client.receive_json() + assert result['success'], result + assert len(result['result']) == 1 + token = result['result'][0] + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + assert token['id'] == refresh_token.id + assert token['type'] == refresh_token.token_type + assert token['client_id'] == refresh_token.client_id + assert token['client_name'] == refresh_token.client_name + assert token['client_icon'] == refresh_token.client_icon + assert token['created_at'] == refresh_token.created_at.isoformat() + + +async def test_ws_delete_refresh_token(hass, hass_ws_client, + hass_access_token): + """Test deleting a refresh token.""" + assert await async_setup_component(hass, 'auth', {'http': {}}) + + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + + ws_client = await hass_ws_client(hass, hass_access_token) + + # verify create long-lived access token + await ws_client.send_json({ + 'id': 5, + 'type': auth.WS_TYPE_DELETE_REFRESH_TOKEN, + 'refresh_token_id': refresh_token.id + }) + + result = await ws_client.receive_json() + assert result['success'], result + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + assert refresh_token is None diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 7cec790c847..232405a632c 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,4 +1,6 @@ """Fixtures for component testing.""" +from unittest.mock import patch + import pytest from homeassistant.setup import async_setup_component @@ -16,23 +18,37 @@ def hass_ws_client(aiohttp_client): assert await async_setup_component(hass, 'websocket_api') client = await aiohttp_client(hass.http.app) - websocket = await client.ws_connect(wapi.URL) - auth_resp = await websocket.receive_json() - if auth_resp['type'] == wapi.TYPE_AUTH_OK: - assert access_token is None, \ - 'Access token given but no auth required' - return websocket + patching = None - assert access_token is not None, 'Access token required for fixture' + if access_token is not None: + patching = patch('homeassistant.auth.AuthManager.active', + return_value=True) + patching.start() - await websocket.send_json({ - 'type': websocket_api.TYPE_AUTH, - 'access_token': access_token - }) + try: + websocket = await client.ws_connect(wapi.URL) + auth_resp = await websocket.receive_json() - auth_ok = await websocket.receive_json() - assert auth_ok['type'] == wapi.TYPE_AUTH_OK + if auth_resp['type'] == wapi.TYPE_AUTH_OK: + assert access_token is None, \ + 'Access token given but no auth required' + return websocket + + assert access_token is not None, \ + 'Access token required for fixture' + + await websocket.send_json({ + 'type': websocket_api.TYPE_AUTH, + 'access_token': access_token + }) + + auth_ok = await websocket.receive_json() + assert auth_ok['type'] == wapi.TYPE_AUTH_OK + + finally: + if patching is not None: + patching.stop() # wrap in client websocket.client = client From 6d33fb2dc8825e05514cac66600e6edaa93b8ef2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Sep 2018 21:45:42 +0200 Subject: [PATCH 14/30] Bumped version to 0.78.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e8349f2f0b8..03434439702 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 78 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 68c21530ca74baa27adfa1f6943384f123ce3c09 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Sep 2018 13:23:50 +0200 Subject: [PATCH 15/30] Update frontend to 20180912.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5fea70ce846..a7a6187c939 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180911.0'] +REQUIREMENTS = ['home-assistant-frontend==20180912.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index f11be0e1a89..5d9518b3c13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -452,7 +452,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180911.0 +home-assistant-frontend==20180912.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7708aeb6650..c333b3d6ca5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.4 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180911.0 +home-assistant-frontend==20180912.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 1983361373028b0bcb6a33224f7ab0ace9d15eab Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 12 Sep 2018 00:49:44 -0700 Subject: [PATCH 16/30] Return if refresh token is current used one in WS API (#16575) --- homeassistant/components/auth/__init__.py | 2 ++ homeassistant/components/websocket_api.py | 1 + tests/components/auth/test_init.py | 1 + 3 files changed, 4 insertions(+) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 5fac423f27a..01cfe4724bf 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -475,6 +475,7 @@ def websocket_create_long_lived_access_token( def websocket_refresh_tokens( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): """Return metadata of users refresh tokens.""" + current_id = connection.request.get('refresh_token_id') connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{ 'id': refresh.id, 'client_id': refresh.client_id, @@ -482,6 +483,7 @@ def websocket_refresh_tokens( 'client_icon': refresh.client_icon, 'type': refresh.token_type, 'created_at': refresh.created_at, + 'is_current': refresh.id == current_id, } for refresh in connection.user.refresh_tokens.values()])) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 0c9ab366534..e9db666c032 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -360,6 +360,7 @@ class ActiveConnection: authenticated = refresh_token is not None if authenticated: request['hass_user'] = refresh_token.user + request['refresh_token_id'] = refresh_token.id elif ((not self.hass.auth.active or self.hass.auth.support_legacy) and diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index a8e95c73a36..ad2aa01737b 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -320,6 +320,7 @@ async def test_ws_refresh_tokens(hass, hass_ws_client, hass_access_token): assert token['client_name'] == refresh_token.client_name assert token['client_icon'] == refresh_token.client_icon assert token['created_at'] == refresh_token.created_at.isoformat() + assert token['is_current'] is True async def test_ws_delete_refresh_token(hass, hass_ws_client, From 4c1b816bb8c12a04428347c7b211f22eb54bbdc1 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 12 Sep 2018 04:24:16 -0700 Subject: [PATCH 17/30] Track refresh token last usage information (#16408) * Extend refresh_token to support last_used_at and last_used_by * Address code review comment * Remove unused code * Add it to websocket response * Fix typing --- homeassistant/auth/__init__.py | 5 ++++- homeassistant/auth/auth_store.py | 26 ++++++++++++++++++++++- homeassistant/auth/models.py | 5 ++++- homeassistant/components/auth/__init__.py | 19 +++++++++++------ tests/auth/test_init.py | 18 +++++++++++++++- tests/components/auth/test_init.py | 2 ++ 6 files changed, 65 insertions(+), 10 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index b0cebb5fd6c..c6f978640f6 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -309,8 +309,11 @@ class AuthManager: @callback def async_create_access_token(self, - refresh_token: models.RefreshToken) -> str: + refresh_token: models.RefreshToken, + remote_ip: Optional[str] = None) -> str: """Create a new access token.""" + self._store.async_log_refresh_token_usage(refresh_token, remote_ip) + # pylint: disable=no-self-use now = dt_util.utcnow() return jwt.encode({ diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 8e8d03253e5..fb4700c806f 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -195,6 +195,15 @@ class AuthStore: return found + @callback + def async_log_refresh_token_usage( + self, refresh_token: models.RefreshToken, + remote_ip: Optional[str] = None) -> None: + """Update refresh token last used information.""" + refresh_token.last_used_at = dt_util.utcnow() + refresh_token.last_used_ip = remote_ip + self._async_schedule_save() + async def _async_load(self) -> None: """Load the users.""" data = await self._store.async_load() @@ -233,12 +242,21 @@ class AuthStore: 'Ignoring refresh token %(id)s with invalid created_at ' '%(created_at)s for user_id %(user_id)s', rt_dict) continue + token_type = rt_dict.get('token_type') if token_type is None: if rt_dict['client_id'] is None: token_type = models.TOKEN_TYPE_SYSTEM else: token_type = models.TOKEN_TYPE_NORMAL + + # old refresh_token don't have last_used_at (pre-0.78) + last_used_at_str = rt_dict.get('last_used_at') + if last_used_at_str: + last_used_at = dt_util.parse_datetime(last_used_at_str) + else: + last_used_at = None + token = models.RefreshToken( id=rt_dict['id'], user=users[rt_dict['user_id']], @@ -251,7 +269,9 @@ class AuthStore: access_token_expiration=timedelta( seconds=rt_dict['access_token_expiration']), token=rt_dict['token'], - jwt_key=rt_dict['jwt_key'] + jwt_key=rt_dict['jwt_key'], + last_used_at=last_used_at, + last_used_ip=rt_dict.get('last_used_ip'), ) users[rt_dict['user_id']].refresh_tokens[token.id] = token @@ -306,6 +326,10 @@ class AuthStore: refresh_token.access_token_expiration.total_seconds(), 'token': refresh_token.token, 'jwt_key': refresh_token.jwt_key, + 'last_used_at': + refresh_token.last_used_at.isoformat() + if refresh_token.last_used_at else None, + 'last_used_ip': refresh_token.last_used_ip, } for user in self._users.values() for refresh_token in user.refresh_tokens.values() diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index c5273d7fa1d..b0f4024c3ab 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -55,13 +55,16 @@ class RefreshToken: jwt_key = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64))) + last_used_at = attr.ib(type=Optional[datetime], default=None) + last_used_ip = attr.ib(type=Optional[str], default=None) + @attr.s(slots=True) class Credentials: """Credentials for a user on an auth provider.""" auth_provider_type = attr.ib(type=str) - auth_provider_id = attr.ib(type=str) # type: Optional[str] + auth_provider_id = attr.ib(type=Optional[str]) # Allow the auth provider to store data to represent their auth. data = attr.ib(type=dict) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 01cfe4724bf..bee72d8e4fc 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -129,6 +129,7 @@ import voluptuous as vol from homeassistant.auth.models import User, Credentials, \ TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN from homeassistant.components import websocket_api +from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView @@ -236,10 +237,12 @@ class TokenView(HomeAssistantView): return await self._async_handle_revoke_token(hass, data) if grant_type == 'authorization_code': - return await self._async_handle_auth_code(hass, data) + return await self._async_handle_auth_code( + hass, data, str(request[KEY_REAL_IP])) if grant_type == 'refresh_token': - return await self._async_handle_refresh_token(hass, data) + return await self._async_handle_refresh_token( + hass, data, str(request[KEY_REAL_IP])) return self.json({ 'error': 'unsupported_grant_type', @@ -264,7 +267,7 @@ class TokenView(HomeAssistantView): await hass.auth.async_remove_refresh_token(refresh_token) return web.Response(status=200) - async def _async_handle_auth_code(self, hass, data): + async def _async_handle_auth_code(self, hass, data, remote_addr): """Handle authorization code request.""" client_id = data.get('client_id') if client_id is None or not indieauth.verify_client_id(client_id): @@ -300,7 +303,8 @@ class TokenView(HomeAssistantView): refresh_token = await hass.auth.async_create_refresh_token(user, client_id) - access_token = hass.auth.async_create_access_token(refresh_token) + access_token = hass.auth.async_create_access_token( + refresh_token, remote_addr) return self.json({ 'access_token': access_token, @@ -310,7 +314,7 @@ class TokenView(HomeAssistantView): int(refresh_token.access_token_expiration.total_seconds()), }) - async def _async_handle_refresh_token(self, hass, data): + async def _async_handle_refresh_token(self, hass, data, remote_addr): """Handle authorization code request.""" client_id = data.get('client_id') if client_id is not None and not indieauth.verify_client_id(client_id): @@ -338,7 +342,8 @@ class TokenView(HomeAssistantView): 'error': 'invalid_request', }, status_code=400) - access_token = hass.auth.async_create_access_token(refresh_token) + access_token = hass.auth.async_create_access_token( + refresh_token, remote_addr) return self.json({ 'access_token': access_token, @@ -484,6 +489,8 @@ def websocket_refresh_tokens( 'type': refresh.token_type, 'created_at': refresh.created_at, 'is_current': refresh.id == current_id, + 'last_used_at': refresh.last_used_at, + 'last_used_ip': refresh.last_used_ip, } for refresh in connection.user.refresh_tokens.values()])) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 765199b256c..8325bd2551a 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -278,7 +278,11 @@ async def test_saving_loading(hass, hass_storage): }) user = step['result'] await manager.async_activate_user(user) - await manager.async_create_refresh_token(user, CLIENT_ID) + # the first refresh token will be used to create access token + refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) + manager.async_create_access_token(refresh_token, '192.168.0.1') + # the second refresh token will not be used + await manager.async_create_refresh_token(user, 'dummy-client') await flush_store(manager._store._store) @@ -286,6 +290,18 @@ async def test_saving_loading(hass, hass_storage): users = await store2.async_get_users() assert len(users) == 1 assert users[0] == user + assert len(users[0].refresh_tokens) == 2 + for r_token in users[0].refresh_tokens.values(): + if r_token.client_id == CLIENT_ID: + # verify the first refresh token + assert r_token.last_used_at is not None + assert r_token.last_used_ip == '192.168.0.1' + elif r_token.client_id == 'dummy-client': + # verify the second refresh token + assert r_token.last_used_at is None + assert r_token.last_used_ip is None + else: + assert False, 'Unknown client_id: %s' % r_token.client_id async def test_cannot_retrieve_expired_access_token(hass): diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index ad2aa01737b..a3974553661 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -321,6 +321,8 @@ async def test_ws_refresh_tokens(hass, hass_ws_client, hass_access_token): assert token['client_icon'] == refresh_token.client_icon assert token['created_at'] == refresh_token.created_at.isoformat() assert token['is_current'] is True + assert token['last_used_at'] == refresh_token.last_used_at.isoformat() + assert token['last_used_ip'] == refresh_token.last_used_ip async def test_ws_delete_refresh_token(hass, hass_ws_client, From 08100a485a7c3bd46d44f5dd1a5bc07e45980210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=A9=20B=C5=91le?= Date: Wed, 12 Sep 2018 12:44:14 +0200 Subject: [PATCH 18/30] Increasing python-websockets' version number (#16578) * increasing python-websockets' version number so now it works with python 3.7 * required version for websockets increased to work with Python 3.7 * script/gen_requirements_all.py is done --- homeassistant/components/media_player/webostv.py | 2 +- homeassistant/components/spc.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index b3cd07b9d35..d78619a8279 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -26,7 +26,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2'] +REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==6.0'] _CONFIGURING = {} # type: Dict[str, str] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index bf7db87f06b..5b357efcabd 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['websockets==3.2'] +REQUIREMENTS = ['websockets==6.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5d9518b3c13..7dba008becb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1480,7 +1480,7 @@ websocket-client==0.37.0 # homeassistant.components.spc # homeassistant.components.media_player.webostv -websockets==3.2 +websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.3.0 From a88cda44d977aa43abe73ac0a62f27508aeb6d81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Sep 2018 13:30:14 +0200 Subject: [PATCH 19/30] Bumped version to 0.78.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 03434439702..6bdb431d1ff 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 78 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 336289d7e7528294b4266a87f57db9686b0cdc0b Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 12 Sep 2018 04:42:54 -0700 Subject: [PATCH 20/30] Add retry limit for chromecast connection (#16471) --- homeassistant/components/media_player/cast.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 83b84f5c3bb..088aef82373 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -62,6 +62,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(cv.ensure_list, [cv.string]), }) +CONNECTION_RETRY = 3 +CONNECTION_RETRY_WAIT = 2 +CONNECTION_TIMEOUT = 10 + @attr.s(slots=True, frozen=True) class ChromecastInfo: @@ -369,15 +373,13 @@ class CastDevice(MediaPlayerDevice): return await self._async_disconnect() - # Failed connection will unfortunately never raise an exception, it - # will instead just try connecting indefinitely. # pylint: disable=protected-access _LOGGER.debug("Connecting to cast device %s", cast_info) chromecast = await self.hass.async_add_job( pychromecast._get_chromecast_from_host, ( cast_info.host, cast_info.port, cast_info.uuid, cast_info.model_name, cast_info.friendly_name - )) + ), CONNECTION_RETRY, CONNECTION_RETRY_WAIT, CONNECTION_TIMEOUT) self._chromecast = chromecast self._status_listener = CastStatusListener(self, chromecast) # Initialise connection status as connected because we can only From b231fa26163f54ee9460c34ebb40305c1065bb49 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 12 Sep 2018 22:52:31 -0700 Subject: [PATCH 21/30] Fix broken bluetooth tracker (#16589) --- homeassistant/components/device_tracker/bluetooth_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 217df0aacd4..d22a1ba7c1f 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -80,7 +80,7 @@ def setup_scanner(hass, config, see, discovery_info=None): request_rssi = config.get(CONF_REQUEST_RSSI, False) - def update_bluetooth(): + def update_bluetooth(_): """Update Bluetooth and set timer for the next update.""" update_bluetooth_once() track_point_in_utc_time( @@ -111,7 +111,7 @@ def setup_scanner(hass, config, see, discovery_info=None): """Update bluetooth devices on demand.""" update_bluetooth_once() - update_bluetooth() + update_bluetooth(dt_util.utcnow()) hass.services.register( DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) From abe61c55296596ec569f40dfbb82002de9509cc2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 14 Sep 2018 13:49:20 +0200 Subject: [PATCH 22/30] Rewrite bluetooth le (#16592) * Rewrite bluetooth le * Update requirements_all.txt * Update gen_requirements_all.py * Update bluetooth_le_tracker.py * Update bluetooth_le_tracker.py * Update bluetooth_le_tracker.py * Update bluetooth_le_tracker.py * Update bluetooth_le_tracker.py * Update bluetooth_le_tracker.py --- .../device_tracker/bluetooth_le_tracker.py | 35 +++++++------------ requirements_all.txt | 4 +-- script/gen_requirements_all.py | 1 - 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index d9cda24b699..47b86ab9ab2 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -6,35 +6,25 @@ https://home-assistant.io/components/device_tracker.bluetooth_le_tracker/ """ import logging -import voluptuous as vol from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import ( YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, - PLATFORM_SCHEMA, load_config, SOURCE_TYPE_BLUETOOTH_LE + load_config, SOURCE_TYPE_BLUETOOTH_LE ) import homeassistant.util.dt as dt_util -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['gattlib==0.20150805'] +REQUIREMENTS = ['pygatt==3.2.0'] BLE_PREFIX = 'BLE_' MIN_SEEN_NEW = 5 -CONF_SCAN_DURATION = 'scan_duration' -CONF_BLUETOOTH_DEVICE = 'device_id' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SCAN_DURATION, default=10): cv.positive_int, - vol.Optional(CONF_BLUETOOTH_DEVICE, default='hci0'): cv.string -}) def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" # pylint: disable=import-error - from gattlib import DiscoveryService - + import pygatt new_devices = {} def see_device(address, name, new_device=False): @@ -61,17 +51,17 @@ def setup_scanner(hass, config, see, discovery_info=None): """Discover Bluetooth LE devices.""" _LOGGER.debug("Discovering Bluetooth LE devices") try: - service = DiscoveryService(ble_dev_id) - devices = service.discover(duration) + adapter = pygatt.GATTToolBackend() + devs = adapter.scan() + + devices = {x['address']: x['name'] for x in devs} _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) except RuntimeError as error: _LOGGER.error("Error during Bluetooth LE scan: %s", error) - devices = [] + return {} return devices yaml_path = hass.config.path(YAML_DEVICES) - duration = config.get(CONF_SCAN_DURATION) - ble_dev_id = config.get(CONF_BLUETOOTH_DEVICE) devs_to_track = [] devs_donot_track = [] @@ -102,11 +92,11 @@ def setup_scanner(hass, config, see, discovery_info=None): """Lookup Bluetooth LE devices and update status.""" devs = discover_ble_devices() for mac in devs_to_track: - _LOGGER.debug("Checking %s", mac) - result = mac in devs - if not result: - # Could not lookup device name + if mac not in devs: continue + + if devs[mac] is None: + devs[mac] = mac see_device(mac, devs[mac]) if track_new: @@ -119,5 +109,4 @@ def setup_scanner(hass, config, see, discovery_info=None): track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval) update_ble(dt_util.utcnow()) - return True diff --git a/requirements_all.txt b/requirements_all.txt index 7dba008becb..318b92bba1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -385,9 +385,6 @@ fritzhome==1.0.4 # homeassistant.components.tts.google gTTS-token==1.1.1 -# homeassistant.components.device_tracker.bluetooth_le_tracker -# gattlib==0.20150805 - # homeassistant.components.sensor.gearbest gearbest_parser==1.0.7 @@ -874,6 +871,7 @@ pyfritzhome==0.3.7 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.device_tracker.bluetooth_le_tracker # homeassistant.components.sensor.skybeacon pygatt==3.2.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a307ec9ee15..ec024bef614 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -19,7 +19,6 @@ COMMENT_REQUIREMENTS = ( 'bluepy', 'opencv-python', 'python-lirc', - 'gattlib', 'pyuserinput', 'evdev', 'pycups', From 7f7372198ab1e414fc74b1d3cda9aab6b285df37 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 15 Sep 2018 12:45:45 +0200 Subject: [PATCH 23/30] Update translations --- .../components/auth/.translations/fr.json | 2 +- .../components/cast/.translations/ca.json | 2 +- .../components/cast/.translations/fr.json | 2 +- .../components/hangouts/.translations/fr.json | 5 ++++ .../homematicip_cloud/.translations/fr.json | 15 ++++++++---- .../components/hue/.translations/fr.json | 2 +- .../components/ios/.translations/ca.json | 14 +++++++++++ .../components/ios/.translations/cs.json | 10 ++++++++ .../components/ios/.translations/fr.json | 14 +++++++++++ .../components/ios/.translations/ko.json | 14 +++++++++++ .../components/ios/.translations/nl.json | 14 +++++++++++ .../components/ios/.translations/pl.json | 14 +++++++++++ .../components/ios/.translations/zh-Hans.json | 14 +++++++++++ .../components/mqtt/.translations/ca.json | 23 +++++++++++++++++++ .../components/mqtt/.translations/fr.json | 23 +++++++++++++++++++ .../components/mqtt/.translations/ko.json | 23 +++++++++++++++++++ .../components/nest/.translations/fr.json | 6 ++++- .../components/openuv/.translations/nl.json | 1 + .../components/sonos/.translations/ca.json | 2 +- .../components/sonos/.translations/fr.json | 2 +- 20 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/ios/.translations/ca.json create mode 100644 homeassistant/components/ios/.translations/cs.json create mode 100644 homeassistant/components/ios/.translations/fr.json create mode 100644 homeassistant/components/ios/.translations/ko.json create mode 100644 homeassistant/components/ios/.translations/nl.json create mode 100644 homeassistant/components/ios/.translations/pl.json create mode 100644 homeassistant/components/ios/.translations/zh-Hans.json create mode 100644 homeassistant/components/mqtt/.translations/ca.json create mode 100644 homeassistant/components/mqtt/.translations/fr.json create mode 100644 homeassistant/components/mqtt/.translations/ko.json diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json index e8a8037c39a..b8d10dc89d0 100644 --- a/homeassistant/components/auth/.translations/fr.json +++ b/homeassistant/components/auth/.translations/fr.json @@ -2,7 +2,7 @@ "mfa_setup": { "totp": { "error": { - "invalid_code": "Code invalide. S'il vous pla\u00eet essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte." + "invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte." }, "step": { "init": { diff --git a/homeassistant/components/cast/.translations/ca.json b/homeassistant/components/cast/.translations/ca.json index e65e00f8624..570cc7fdc00 100644 --- a/homeassistant/components/cast/.translations/ca.json +++ b/homeassistant/components/cast/.translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.", - "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Google Cast." + "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Google Cast." }, "step": { "confirm": { diff --git a/homeassistant/components/cast/.translations/fr.json b/homeassistant/components/cast/.translations/fr.json index d3b95121de6..99feeb3c898 100644 --- a/homeassistant/components/cast/.translations/fr.json +++ b/homeassistant/components/cast/.translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.", - "single_instance_allowed": "Seulement une seule configuration de Google Cast est n\u00e9cessaire." + "single_instance_allowed": "Une seule configuration de Google Cast est n\u00e9cessaire." }, "step": { "confirm": { diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json index 53759f9b534..00a7d5fd80d 100644 --- a/homeassistant/components/hangouts/.translations/fr.json +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -5,10 +5,15 @@ "unknown": "Une erreur inconnue s'est produite" }, "error": { + "invalid_2fa": "Authentification \u00e0 2 facteurs invalide, veuillez r\u00e9essayer.", + "invalid_2fa_method": "M\u00e9thode 2FA non valide (v\u00e9rifiez sur le t\u00e9l\u00e9phone).", "invalid_login": "Login invalide, veuillez r\u00e9essayer." }, "step": { "2fa": { + "data": { + "2fa": "Code PIN d'authentification \u00e0 2 facteurs" + }, "title": "Authentification \u00e0 2 facteurs" }, "user": { diff --git a/homeassistant/components/homematicip_cloud/.translations/fr.json b/homeassistant/components/homematicip_cloud/.translations/fr.json index 8405946daa2..6cab0993c01 100644 --- a/homeassistant/components/homematicip_cloud/.translations/fr.json +++ b/homeassistant/components/homematicip_cloud/.translations/fr.json @@ -4,12 +4,13 @@ "already_configured": "Le point d'acc\u00e8s est d\u00e9j\u00e0 configur\u00e9", "conection_aborted": "Impossible de se connecter au serveur HMIP", "connection_aborted": "Impossible de se connecter au serveur HMIP", - "unknown": "Une erreur inconnue s'est produite" + "unknown": "Une erreur inconnue s'est produite." }, "error": { "invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.", "press_the_button": "Veuillez appuyer sur le bouton bleu.", - "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer." + "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer.", + "timeout_button": "D\u00e9lai d'attente expir\u00e9, veuillez r\u00e9\u00e9ssayer." }, "step": { "init": { @@ -17,8 +18,14 @@ "hapid": "ID du point d'acc\u00e8s (SGTIN)", "name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les p\u00e9riph\u00e9riques)", "pin": "Code PIN (facultatif)" - } + }, + "title": "Choisissez le point d'acc\u00e8s HomematicIP" + }, + "link": { + "description": "Appuyez sur le bouton bleu du point d'acc\u00e8s et sur le bouton Envoyer pour enregistrer HomematicIP avec Home Assistant. \n\n ![Emplacement du bouton sur le pont](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Lier le point d'acc\u00e8s" } - } + }, + "title": "HomematicIP Cloud" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json index 73613f237da..5414bf01ea7 100644 --- a/homeassistant/components/hue/.translations/fr.json +++ b/homeassistant/components/hue/.translations/fr.json @@ -24,6 +24,6 @@ "title": "Hub de liaison" } }, - "title": "Pont Philips Hue" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/ca.json b/homeassistant/components/ios/.translations/ca.json new file mode 100644 index 00000000000..1b1ed732ab3 --- /dev/null +++ b/homeassistant/components/ios/.translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Voleu configurar el component Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/cs.json b/homeassistant/components/ios/.translations/cs.json new file mode 100644 index 00000000000..95d675076da --- /dev/null +++ b/homeassistant/components/ios/.translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/fr.json b/homeassistant/components/ios/.translations/fr.json new file mode 100644 index 00000000000..934849549e7 --- /dev/null +++ b/homeassistant/components/ios/.translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Seule une configuration de Home Assistant iOS est n\u00e9cessaire." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer le composant Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/ko.json b/homeassistant/components/ios/.translations/ko.json new file mode 100644 index 00000000000..6d69ea3126c --- /dev/null +++ b/homeassistant/components/ios/.translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 Home Assistant iOS \uad6c\uc131\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc124\uc815\uc744 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/nl.json b/homeassistant/components/ios/.translations/nl.json new file mode 100644 index 00000000000..8e5c46692a0 --- /dev/null +++ b/homeassistant/components/ios/.translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Home Assistant iOS nodig." + }, + "step": { + "confirm": { + "description": "Wilt u het Home Assistant iOS component instellen?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/pl.json b/homeassistant/components/ios/.translations/pl.json new file mode 100644 index 00000000000..6240f074cfc --- /dev/null +++ b/homeassistant/components/ios/.translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/zh-Hans.json b/homeassistant/components/ios/.translations/zh-Hans.json new file mode 100644 index 00000000000..0de30f0f3da --- /dev/null +++ b/homeassistant/components/ios/.translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Home Assistant iOS \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8bbe\u7f6e Home Assistant iOS \u7ec4\u4ef6\uff1f", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json new file mode 100644 index 00000000000..57e9a83d201 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 de MQTT." + }, + "error": { + "cannot_connect": "No es pot connectar amb el broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "description": "Introdu\u00efu la informaci\u00f3 de connexi\u00f3 del vostre broker MQTT.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/fr.json b/homeassistant/components/mqtt/.translations/fr.json new file mode 100644 index 00000000000..1870c598e3b --- /dev/null +++ b/homeassistant/components/mqtt/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Une seule configuration de MQTT est autoris\u00e9e." + }, + "error": { + "cannot_connect": "Impossible de se connecter au broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json new file mode 100644 index 00000000000..a38b00fd68d --- /dev/null +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 MQTT \ube0c\ub85c\ucee4\ub9cc \uad6c\uc131\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc74c" + }, + "step": { + "broker": { + "data": { + "broker": "\ube0c\ub85c\ucee4", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud558\uc138\uc694.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/fr.json b/homeassistant/components/nest/.translations/fr.json index fd2cb435eb2..822ec6ae836 100644 --- a/homeassistant/components/nest/.translations/fr.json +++ b/homeassistant/components/nest/.translations/fr.json @@ -2,18 +2,22 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Nest.", + "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "no_flows": "Vous devez configurer Nest avant de pouvoir vous authentifier avec celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Erreur interne lors de la validation du code", - "invalid_code": "Code invalide" + "invalid_code": "Code invalide", + "timeout": "D\u00e9lai de la validation du code expir\u00e9", + "unknown": "Erreur inconnue lors de la validation du code" }, "step": { "init": { "data": { "flow_impl": "Fournisseur" }, + "description": "S\u00e9lectionnez via quel fournisseur d'authentification vous souhaitez vous authentifier avec Nest.", "title": "Fournisseur d'authentification" }, "link": { diff --git a/homeassistant/components/openuv/.translations/nl.json b/homeassistant/components/openuv/.translations/nl.json index 2c5086b3365..e2b264182d0 100644 --- a/homeassistant/components/openuv/.translations/nl.json +++ b/homeassistant/components/openuv/.translations/nl.json @@ -1,6 +1,7 @@ { "config": { "error": { + "identifier_exists": "Co\u00f6rdinaten al geregistreerd", "invalid_api_key": "Ongeldige API-sleutel" }, "step": { diff --git a/homeassistant/components/sonos/.translations/ca.json b/homeassistant/components/sonos/.translations/ca.json index 9a745784b25..a6f1f99a379 100644 --- a/homeassistant/components/sonos/.translations/ca.json +++ b/homeassistant/components/sonos/.translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "No s'han trobat dispositius Sonos a la xarxa.", - "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Sonos." + "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Sonos." }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/.translations/fr.json b/homeassistant/components/sonos/.translations/fr.json index 809591dee7c..fd2a77bd129 100644 --- a/homeassistant/components/sonos/.translations/fr.json +++ b/homeassistant/components/sonos/.translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Aucun p\u00e9riph\u00e9rique Sonos trouv\u00e9 sur le r\u00e9seau.", - "single_instance_allowed": "Seulement une seule configuration de Sonos est n\u00e9cessaire." + "single_instance_allowed": "Une seule configuration de Sonos est n\u00e9cessaire." }, "step": { "confirm": { From 18ce5092b4c8cf42ce40a6002b5bd11d2c87054c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 15 Sep 2018 12:46:02 +0200 Subject: [PATCH 24/30] Bumped version to 0.78.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6bdb431d1ff..128fb8dbce6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 78 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From f918d625719444f7bdf4eb8df6548371293237cc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Sep 2018 10:48:09 +0200 Subject: [PATCH 25/30] version bump to 0.78.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 128fb8dbce6..546f47a62b3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 78 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 366e270e94c33d4428f0f06a8c05190a48ddb3ef Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Sep 2018 18:33:41 +0200 Subject: [PATCH 26/30] Bump frontend to 20180916.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index a7a6187c939..6dd4be7ecec 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180912.0'] +REQUIREMENTS = ['home-assistant-frontend==20180916.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 318b92bba1e..de30bc379d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180912.0 +home-assistant-frontend==20180916.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c333b3d6ca5..e1276da644c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.4 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180912.0 +home-assistant-frontend==20180916.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 83795676364f2d6f3f1bc9bdc2b0719e74d07d7d Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Wed, 19 Sep 2018 22:01:54 +0200 Subject: [PATCH 27/30] SnmpSensor: Fix async_update (#16679) (#16716) Bugfix provided by awarecan. --- homeassistant/components/sensor/snmp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index d66a24524fb..718e4f6fb0d 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -182,7 +182,7 @@ class SnmpSensor(Entity): if value is None: value = STATE_UNKNOWN elif self._value_template is not None: - value = self._value_template.render_with_possible_json_value( + value = self._value_template.async_render_with_possible_json_value( value, STATE_UNKNOWN) self._state = value From 7a7a164cb8925216da68012174bcc26da64ed85c Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 20 Sep 2018 01:48:45 -0700 Subject: [PATCH 28/30] Handle chromecast CONNECTION_STATUS_DISCONNECTED event (#16732) --- homeassistant/components/media_player/cast.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 088aef82373..e14244793cf 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -369,7 +369,8 @@ class CastDevice(MediaPlayerDevice): if self._chromecast is not None: if old_cast_info.host_port == cast_info.host_port: - # Nothing connection-related updated + _LOGGER.debug("No connection related update: %s", + cast_info.host_port) return await self._async_disconnect() @@ -403,7 +404,12 @@ class CastDevice(MediaPlayerDevice): await self.hass.async_add_job(self._chromecast.disconnect) - # Invalidate some attributes + self._invalidate() + + self.async_schedule_update_ha_state() + + def _invalidate(self): + """Invalidate some attributes.""" self._chromecast = None self.cast_status = None self.media_status = None @@ -412,8 +418,6 @@ class CastDevice(MediaPlayerDevice): self._status_listener.invalidate() self._status_listener = None - self.async_schedule_update_ha_state() - # ========== Callbacks ========== def new_cast_status(self, cast_status): """Handle updates of the cast status.""" @@ -428,7 +432,16 @@ class CastDevice(MediaPlayerDevice): def new_connection_status(self, connection_status): """Handle updates of connection status.""" - from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED + from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED + + _LOGGER.debug("Received cast device connection status: %s", + connection_status.status) + if connection_status.status == CONNECTION_STATUS_DISCONNECTED: + self._available = False + self._invalidate() + self.schedule_update_ha_state() + return new_available = connection_status.status == CONNECTION_STATUS_CONNECTED if new_available != self._available: From 9bbd61cbe4ca9a56b3b7c751ec7f1d00d3551d65 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 20 Sep 2018 02:11:39 -0700 Subject: [PATCH 29/30] Upgrade netdisco to 2.1.0 (#16735) --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 41cf3791256..7784f3771de 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==2.0.0'] +REQUIREMENTS = ['netdisco==2.1.0'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index de30bc379d9..1297d421b67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -611,7 +611,7 @@ ndms2_client==0.0.4 netdata==0.1.2 # homeassistant.components.discovery -netdisco==2.0.0 +netdisco==2.1.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From c7d5f7698e1191f68d4ca4e6bfd2b2536feecb06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 20 Sep 2018 11:32:26 +0200 Subject: [PATCH 30/30] Bumped version to 0.78.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 546f47a62b3..4719e934109 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 78 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)