From 8623294fcdcd1dec9b75a5756bd75a4dfd53ece0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 19 Jun 2019 16:37:53 -0700 Subject: [PATCH 01/38] Bumped version to 0.95.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 972e9e25f72..3466918dbd3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -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 8f928982e0f4848c478758313a59844fdd35bd05 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 20 Jun 2019 23:52:45 -0700 Subject: [PATCH 02/38] Updated frontend to 20190620.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 7d84e0a492e..355a26931fe 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190619.0" + "home-assistant-frontend==20190620.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 506e9788e02..31a2e79e06d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2018.04.16 cryptography==2.6.1 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190619.0 +home-assistant-frontend==20190620.0 importlib-metadata==0.15 jinja2>=2.10 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1b305a82236..052babd0670 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -595,7 +595,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190619.0 +home-assistant-frontend==20190620.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 264e43dd93e..ff8b87f594e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -160,7 +160,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190619.0 +home-assistant-frontend==20190620.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From b899dd59c5f6f1cf1027a48d40a16dadaf461ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20P=C3=A9rez?= Date: Fri, 21 Jun 2019 06:13:47 -0300 Subject: [PATCH 03/38] Vlc telnet (#24290) * Vlc telnet first commit First functional version, remains to add more functionality. * New functions added and bugfixes * Compliance with dev checklist * Compliance with dev checklist * Compliance with pydocstyle * Removed unused import * Fixed wrong reference for exception * Module renamed * Fixed module rename in other * Fixed wrong reference for exception Module renamed Fixed module rename in other * Update homeassistant/components/vlc_telnet/media_player.py Accepted suggestion by @OttoWinter Co-Authored-By: Otto Winter * Update homeassistant/components/vlc_telnet/media_player.py Accepted suggestion by @OttoWinter Co-Authored-By: Otto Winter * Update homeassistant/components/vlc_telnet/media_player.py Accepted suggestion by @OttoWinter Co-Authored-By: Otto Winter * Update homeassistant/components/vlc_telnet/media_player.py Accepted suggestion by @OttoWinter Co-Authored-By: Otto Winter * Suggestions by @OttoWinter +Manage error when the VLC dissapears to show status unavailable. * Removed error log, instead set unavailable state * Changes suggested by @pvizeli -Import location -Use of constants * Implemented available method * Improved available method --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/vlc_telnet/__init__.py | 1 + .../components/vlc_telnet/manifest.json | 10 + .../components/vlc_telnet/media_player.py | 233 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 249 insertions(+) create mode 100644 homeassistant/components/vlc_telnet/__init__.py create mode 100644 homeassistant/components/vlc_telnet/manifest.json create mode 100644 homeassistant/components/vlc_telnet/media_player.py diff --git a/.coveragerc b/.coveragerc index 8bf5509c126..397db5394d6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -665,6 +665,7 @@ omit = homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py + homeassistant/components/vlc_telnet/media_player.py homeassistant/components/volkszaehler/sensor.py homeassistant/components/volumio/media_player.py homeassistant/components/volvooncall/* diff --git a/CODEOWNERS b/CODEOWNERS index 60703b8cf42..86e731264ec 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -275,6 +275,7 @@ homeassistant/components/utility_meter/* @dgomes homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vizio/* @raman325 +homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py new file mode 100644 index 00000000000..91a3eb35444 --- /dev/null +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -0,0 +1 @@ +"""The vlc component.""" diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json new file mode 100644 index 00000000000..1e0f1c71df5 --- /dev/null +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "vlc_telnet", + "name": "VLC telnet", + "documentation": "https://www.home-assistant.io/components/vlc-telnet", + "requirements": [ + "python-telnet-vlc==1.0.4" + ], + "dependencies": [], + "codeowners": ["@rodripf"] +} diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py new file mode 100644 index 00000000000..096afcc1044 --- /dev/null +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -0,0 +1,233 @@ +"""Provide functionality to interact with the vlc telnet interface.""" +import logging +import voluptuous as vol + +from python_telnet_vlc import VLCTelnet, ConnectionError as ConnErr + +from homeassistant.components.media_player import ( + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_NEXT_TRACK, SUPPORT_CLEAR_PLAYLIST, SUPPORT_SHUFFLE_SET) +from homeassistant.const import ( + CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, + CONF_HOST, CONF_PORT, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'vlc_telnet' + +DEFAULT_NAME = 'VLC-TELNET' +DEFAULT_PORT = 4212 + +SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_SEEK | SUPPORT_VOLUME_SET \ + | SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK \ + | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP \ + | SUPPORT_CLEAR_PLAYLIST | SUPPORT_PLAY \ + | SUPPORT_SHUFFLE_SET +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the vlc platform.""" + add_entities([VlcDevice(config.get(CONF_NAME), + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_PASSWORD))], True) + + +class VlcDevice(MediaPlayerDevice): + """Representation of a vlc player.""" + + def __init__(self, name, host, port, passwd): + """Initialize the vlc device.""" + self._instance = None + self._name = name + self._volume = None + self._muted = None + self._state = STATE_UNAVAILABLE + self._media_position_updated_at = None + self._media_position = None + self._media_duration = None + self._host = host + self._port = port + self._password = passwd + self._vlc = None + self._available = False + self._volume_bkp = 0 + self._media_artist = "" + self._media_title = "" + + def update(self): + """Get the latest details from the device.""" + if self._vlc is None: + try: + self._vlc = VLCTelnet(self._host, self._password, self._port) + self._state = STATE_IDLE + self._available = True + except (ConnErr, EOFError): + self._available = False + self._vlc = None + else: + try: + status = self._vlc.status() + if status: + if 'volume' in status: + self._volume = int(status['volume']) / 500.0 + else: + self._volume = None + if 'state' in status: + state = status["state"] + if state == "playing": + self._state = STATE_PLAYING + elif state == "paused": + self._state = STATE_PAUSED + else: + self._state = STATE_IDLE + else: + self._state = STATE_IDLE + + self._media_duration = self._vlc.get_length() + self._media_position = self._vlc.get_time() + + info = self._vlc.info() + if info: + self._media_artist = info[0].get('artist') + self._media_title = info[0].get('title') + + except (ConnErr, EOFError): + self._available = False + self._vlc = None + + return True + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_VLC + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._media_duration + + @property + def media_position(self): + """Position of current playing media in seconds.""" + return self._media_position + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid.""" + return self._media_position_updated_at + + @property + def media_title(self): + """Title of current playing media.""" + return self._media_title + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self._media_artist + + def media_seek(self, position): + """Seek the media to a specific location.""" + track_length = self._vlc.get_length() / 1000 + self._vlc.seek(position / track_length) + + def mute_volume(self, mute): + """Mute the volume.""" + if mute: + self._volume_bkp = self._volume + self._volume = 0 + self._vlc.set_volume("0") + else: + self._vlc.set_volume(str(self._volume_bkp)) + self._volume = self._volume_bkp + + self._muted = mute + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._vlc.set_volume(str(volume * 500)) + self._volume = volume + + def media_play(self): + """Send play command.""" + self._vlc.play() + self._state = STATE_PLAYING + + def media_pause(self): + """Send pause command.""" + self._vlc.pause() + self._state = STATE_PAUSED + + def media_stop(self): + """Send stop command.""" + self._vlc.stop() + self._state = STATE_IDLE + + def play_media(self, media_type, media_id, **kwargs): + """Play media from a URL or file.""" + if media_type != MEDIA_TYPE_MUSIC: + _LOGGER.error( + "Invalid media type %s. Only %s is supported", + media_type, MEDIA_TYPE_MUSIC) + return + self._vlc.add(media_id) + self._state = STATE_PLAYING + + def media_previous_track(self): + """Send previous track command.""" + self._vlc.prev() + + def media_next_track(self): + """Send next track command.""" + self._vlc.next() + + def clear_playlist(self): + """Clear players playlist.""" + self._vlc.clear() + + def set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + self._vlc.random(shuffle) diff --git a/requirements_all.txt b/requirements_all.txt index 052babd0670..3dfc9029e60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1473,6 +1473,9 @@ python-tado==0.2.9 # homeassistant.components.telegram_bot python-telegram-bot==11.1.0 +# homeassistant.components.vlc_telnet +python-telnet-vlc==1.0.4 + # homeassistant.components.twitch python-twitch-client==0.6.0 From d527e2c926d9eed9677c69878460cde6c98a840f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 20 Jun 2019 03:22:33 +0200 Subject: [PATCH 04/38] Fix device tracker see for entity registry entities (#24633) * Add a test for see service gaurd * Guard from seeing devices part of entity registry * Await registry task early * Lint * Correct comment * Clean up wait for registry * Fix spelling Co-Authored-By: Paulus Schoutsen * Fix spelling Co-Authored-By: Paulus Schoutsen --- .../components/device_tracker/legacy.py | 10 ++++++ tests/components/device_tracker/test_init.py | 32 +++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 1fdd8077728..1a2e7c854e5 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -14,6 +14,7 @@ from homeassistant.components.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import GPSType, HomeAssistantType from homeassistant import util @@ -115,6 +116,7 @@ class DeviceTracker: This method is a coroutine. """ + registry = await async_get_registry(self.hass) if mac is None and dev_id is None: raise HomeAssistantError('Neither mac or device id passed in') if mac is not None: @@ -134,6 +136,14 @@ class DeviceTracker: await device.async_update_ha_state() return + # Guard from calling see on entity registry entities. + entity_id = ENTITY_ID_FORMAT.format(dev_id) + if registry.async_is_registered(entity_id): + LOGGER.error( + "The see service is not supported for this entity %s", + entity_id) + return + # If no device can be found, create it dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) device = Device( diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 9a59855e8c1..cd518770c5b 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import json import logging import os -from unittest.mock import call +from unittest.mock import Mock, call from asynctest import patch import pytest @@ -12,9 +12,9 @@ from homeassistant.components import zone import homeassistant.components.device_tracker as device_tracker from homeassistant.components.device_tracker import const, legacy from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, - ATTR_ICON, CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, - ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY) + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_GPS_ACCURACY, + ATTR_HIDDEN, ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_PLATFORM, + STATE_HOME, STATE_NOT_HOME) from homeassistant.core import State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery @@ -23,8 +23,8 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( - assert_setup_component, async_fire_time_changed, mock_restore_cache, - patch_yaml_files) + assert_setup_component, async_fire_time_changed, mock_registry, + mock_restore_cache, patch_yaml_files) from tests.components.device_tracker import common TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -321,6 +321,26 @@ async def test_see_service(mock_see, hass): assert mock_see.call_args == call(**params) +async def test_see_service_guard_config_entry(hass, mock_device_tracker_conf): + """Test the guard if the device is registered in the entity registry.""" + mock_entry = Mock() + dev_id = 'test' + entity_id = const.ENTITY_ID_FORMAT.format(dev_id) + mock_registry(hass, {entity_id: mock_entry}) + devices = mock_device_tracker_conf + assert await async_setup_component( + hass, device_tracker.DOMAIN, TEST_PLATFORM) + params = { + 'dev_id': dev_id, + 'gps': [.3, .8], + } + + common.async_see(hass, **params) + await hass.async_block_till_done() + + assert not devices + + async def test_new_device_event_fired(hass, mock_device_tracker_conf): """Test that the device tracker will fire an event.""" with assert_setup_component(1, device_tracker.DOMAIN): From d5edbb424a60f59f06ade6b9204ab91bb0cbf016 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 19 Jun 2019 22:32:31 -0400 Subject: [PATCH 05/38] Bump ZHA dependencies. (#24637) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4e327381902..9734b10fab2 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,9 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ - "bellows-homeassistant==0.8.0", + "bellows-homeassistant==0.8.1", "zha-quirks==0.0.14", - "zigpy-deconz==0.1.4", + "zigpy-deconz==0.1.6", "zigpy-homeassistant==0.5.0", "zigpy-xbee-homeassistant==0.3.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 3dfc9029e60..25d4963e5ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -241,7 +241,7 @@ batinfo==0.4.2 beautifulsoup4==4.7.1 # homeassistant.components.zha -bellows-homeassistant==0.8.0 +bellows-homeassistant==0.8.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.3 @@ -1926,7 +1926,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.1.4 +zigpy-deconz==0.1.6 # homeassistant.components.zha zigpy-homeassistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff8b87f594e..f0338a877f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -79,7 +79,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.8.0 +bellows-homeassistant==0.8.1 # homeassistant.components.caldav caldav==0.6.1 From 79b10612aaae79ead11abfd07471e4e8785fbef9 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 20 Jun 2019 22:24:45 +0200 Subject: [PATCH 06/38] Update LIFX brightness during long transitions (#24653) --- homeassistant/components/lifx/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 5f462941062..42d9ecd8c9f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -484,7 +484,8 @@ class LIFXLight(Light): @property def brightness(self): """Return the brightness of this light between 0..255.""" - return convert_16_to_8(self.bulb.color[2]) + fade = self.bulb.power_level / 65535 + return convert_16_to_8(int(fade * self.bulb.color[2])) @property def color_temp(self): From da12ceae5b367655504ed40ad25c183c976ec6bd Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Thu, 20 Jun 2019 16:24:03 -0400 Subject: [PATCH 07/38] Upgrade blinkpy==0.14.1 for startup bugfix (#24656) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index abce8a4a0d1..98c609731c6 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -3,7 +3,7 @@ "name": "Blink", "documentation": "https://www.home-assistant.io/components/blink", "requirements": [ - "blinkpy==0.14.0" + "blinkpy==0.14.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 25d4963e5ee..d713b1e7664 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -250,7 +250,7 @@ bimmer_connected==0.5.3 bizkaibus==0.1.1 # homeassistant.components.blink -blinkpy==0.14.0 +blinkpy==0.14.1 # homeassistant.components.blinksticklight blinkstick==1.1.8 From d4cab60343557a57fc506feac0ddbd40e2af4bee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 20 Jun 2019 13:22:12 -0700 Subject: [PATCH 08/38] Warn when user tries run custom config flow (#24657) --- homeassistant/config_entries.py | 8 ++++++++ homeassistant/loader.py | 5 +++++ tests/test_config_entries.py | 16 +++++++++++++++- tests/test_loader.py | 10 ++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a018713dee7..bfd8c0f2df7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -553,6 +553,14 @@ class ConfigEntries: _LOGGER.error('Cannot find integration %s', handler_key) raise data_entry_flow.UnknownHandler + # Our config flow list is based on built-in integrations. If overriden, + # we should not load it's config flow. + if not integration.is_built_in: + _LOGGER.error( + 'Config flow is not supported for custom integration %s', + handler_key) + raise data_entry_flow.UnknownHandler + # Make sure requirements and dependencies of component are resolved await async_process_deps_reqs( self.hass, self._hass_config, integration) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index fb2c1bae894..70fbc371027 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -123,6 +123,11 @@ class Integration: self.requirements = manifest['requirements'] # type: List[str] _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) + @property + def is_built_in(self) -> bool: + """Test if package is a built-in integration.""" + return self.pkg_path.startswith(PACKAGE_BUILTIN) + def get_component(self) -> ModuleType: """Return the component.""" cache = self.hass.data.setdefault(DATA_COMPONENTS, {}) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 752cb5eb277..9de92f88557 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries, data_entry_flow, loader from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component @@ -934,3 +934,17 @@ async def test_entry_reload_error(hass, manager, state): assert len(async_setup_entry.mock_calls) == 0 assert entry.state == state + + +async def test_init_custom_integration(hass): + """Test initializing flow for custom integration.""" + integration = loader.Integration(hass, 'custom_components.hue', None, { + 'name': 'Hue', + 'dependencies': [], + 'requirements': [], + 'domain': 'hue', + }) + with pytest.raises(data_entry_flow.UnknownHandler): + with patch('homeassistant.loader.async_get_integration', + return_value=mock_coro(integration)): + await hass.config_entries.flow.async_init('bla') diff --git a/tests/test_loader.py b/tests/test_loader.py index 8af000c5d05..cd0cb692702 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -152,6 +152,16 @@ def test_integration_properties(hass): assert integration.domain == 'hue' assert integration.dependencies == ['test-dep'] assert integration.requirements == ['test-req==1.0.0'] + assert integration.is_built_in is True + + integration = loader.Integration( + hass, 'custom_components.hue', None, { + 'name': 'Philips Hue', + 'domain': 'hue', + 'dependencies': ['test-dep'], + 'requirements': ['test-req==1.0.0'], + }) + assert integration.is_built_in is False async def test_integrations_only_once(hass): From a868685ac900e5cf53e64f7f752e53db53b406cc Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Thu, 20 Jun 2019 15:25:32 -0500 Subject: [PATCH 09/38] Bump pysmartthings (#24659) --- homeassistant/components/smartthings/manifest.json | 2 +- homeassistant/components/smartthings/smartapp.py | 12 ++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 75b113354ff..621da91f4f8 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/components/smartthings", "requirements": [ "pysmartapp==0.3.2", - "pysmartthings==0.6.8" + "pysmartthings==0.6.9" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 9aa44d26f2d..68999914d71 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -282,9 +282,9 @@ async def smartapp_sync_subscriptions( await api.create_subscription(sub) _LOGGER.debug("Created subscription for '%s' under app '%s'", target, installed_app_id) - except Exception: # pylint:disable=broad-except - _LOGGER.exception("Failed to create subscription for '%s' under " - "app '%s'", target, installed_app_id) + except Exception as error: # pylint:disable=broad-except + _LOGGER.error("Failed to create subscription for '%s' under app " + "'%s': %s", target, installed_app_id, error) async def delete_subscription(sub: SubscriptionEntity): try: @@ -293,9 +293,9 @@ async def smartapp_sync_subscriptions( _LOGGER.debug("Removed subscription for '%s' under app '%s' " "because it was no longer needed", sub.capability, installed_app_id) - except Exception: # pylint:disable=broad-except - _LOGGER.exception("Failed to remove subscription for '%s' under " - "app '%s'", sub.capability, installed_app_id) + except Exception as error: # pylint:disable=broad-except + _LOGGER.error("Failed to remove subscription for '%s' under app " + "'%s': %s", sub.capability, installed_app_id, error) # Build set of capabilities and prune unsupported ones capabilities = set() diff --git a/requirements_all.txt b/requirements_all.txt index d713b1e7664..1986d325ae8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1351,7 +1351,7 @@ pysma==0.3.1 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.6.8 +pysmartthings==0.6.9 # homeassistant.components.smarty pysmarty==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0338a877f7..96d13c97e9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -290,7 +290,7 @@ pyqwikswitch==0.93 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.6.8 +pysmartthings==0.6.9 # homeassistant.components.sonos pysonos==0.0.16 From 198432f2220af9dfb61ceaa3421c0aff27b0af0e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 21 Jun 2019 17:47:56 +0200 Subject: [PATCH 10/38] Prefere binary with wheels (#24669) --- homeassistant/util/package.py | 2 +- tests/util/test_package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 6f6d03d67b6..bc2245fd208 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -66,7 +66,7 @@ def install_package(package: str, upgrade: bool = True, if constraints is not None: args += ['--constraint', constraints] if find_links is not None: - args += ['--find-links', find_links] + args += ['--find-links', find_links, '--prefer-binary'] if target: assert not is_virtual_env() # This only works if not running in venv diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 3751c056907..623d79ddfe0 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -178,7 +178,7 @@ def test_install_find_links(mock_sys, mock_popen, mock_env_copy, mock_venv): mock_popen.call_args == call([ mock_sys.executable, '-m', 'pip', 'install', '--quiet', - TEST_NEW_REQ, '--find-links', link + TEST_NEW_REQ, '--find-links', link, '--prefer-binary' ], stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) ) assert mock_popen.return_value.communicate.call_count == 1 From 1761a7133812ca7fa5285f3e2f880a03c31d1ced Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 21 Jun 2019 09:27:58 -0700 Subject: [PATCH 11/38] Bumped version to 0.95.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3466918dbd3..2e58571add3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -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 7f169e97ca213ddd79b76526b9cece7661897d63 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 21 Jun 2019 20:08:19 +0200 Subject: [PATCH 12/38] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 8f250f16ce3..d6395dad5ac 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -8,7 +8,7 @@ trigger: pr: none variables: - name: versionBuilder - value: '3.2' + value: '4.2' - group: docker - group: github - group: twine From 9b096322e1124404e18157a6f68b084dc124d07a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2019 08:26:50 -0700 Subject: [PATCH 13/38] Updated frontend to 20190624.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 355a26931fe..c0d9c95849b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190620.0" + "home-assistant-frontend==20190624.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 31a2e79e06d..b73b4ba784b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2018.04.16 cryptography==2.6.1 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190620.0 +home-assistant-frontend==20190624.0 importlib-metadata==0.15 jinja2>=2.10 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1986d325ae8..cb267d08ff7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -595,7 +595,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190620.0 +home-assistant-frontend==20190624.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96d13c97e9b..feb08c3d226 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -160,7 +160,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190620.0 +home-assistant-frontend==20190624.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 23722dc291028681bc8d9f17386af0fd543fdc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Fri, 21 Jun 2019 22:16:28 +0200 Subject: [PATCH 14/38] Allow extra js modules to be included in frontend (#24675) * Add extra_module_url and extra_module_url_es5 to frontend options * Address review comments --- homeassistant/components/frontend/__init__.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index a18ed6eb3d1..b295c94ec31 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,6 +24,8 @@ DOMAIN = 'frontend' CONF_THEMES = 'themes' CONF_EXTRA_HTML_URL = 'extra_html_url' CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5' +CONF_EXTRA_MODULE_URL = 'extra_module_url' +CONF_EXTRA_JS_URL_ES5 = 'extra_js_url_es5' CONF_FRONTEND_REPO = 'development_repo' CONF_JS_VERSION = 'javascript_version' EVENT_PANELS_UPDATED = 'panels_updated' @@ -55,6 +57,8 @@ DATA_PANELS = 'frontend_panels' DATA_JS_VERSION = 'frontend_js_version' DATA_EXTRA_HTML_URL = 'frontend_extra_html_url' DATA_EXTRA_HTML_URL_ES5 = 'frontend_extra_html_url_es5' +DATA_EXTRA_MODULE_URL = 'frontend_extra_module_url' +DATA_EXTRA_JS_URL_ES5 = 'frontend_extra_js_url_es5' DATA_THEMES = 'frontend_themes' DATA_DEFAULT_THEME = 'frontend_default_theme' DEFAULT_THEME = 'default' @@ -71,6 +75,10 @@ CONFIG_SCHEMA = vol.Schema({ }), vol.Optional(CONF_EXTRA_HTML_URL): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXTRA_MODULE_URL): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXTRA_JS_URL_ES5): + vol.All(cv.ensure_list, [cv.string]), # We no longer use these options. vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, vol.Optional(CONF_JS_VERSION): cv.match_all, @@ -184,6 +192,15 @@ def add_extra_html_url(hass, url, es5=False): url_set.add(url) +def add_extra_js_url(hass, url, es5=False): + """Register extra js or module url to load.""" + key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL + url_set = hass.data.get(key) + if url_set is None: + url_set = hass.data[key] = set() + url_set.add(url) + + def add_manifest_json_key(key, val): """Add a keyval to the manifest.json.""" MANIFEST_JSON[key] = val @@ -249,6 +266,18 @@ async def async_setup(hass, config): for url in conf.get(CONF_EXTRA_HTML_URL, []): add_extra_html_url(hass, url, False) + if DATA_EXTRA_MODULE_URL not in hass.data: + hass.data[DATA_EXTRA_MODULE_URL] = set() + + for url in conf.get(CONF_EXTRA_MODULE_URL, []): + add_extra_js_url(hass, url) + + if DATA_EXTRA_JS_URL_ES5 not in hass.data: + hass.data[DATA_EXTRA_JS_URL_ES5] = set() + + for url in conf.get(CONF_EXTRA_JS_URL_ES5, []): + add_extra_js_url(hass, url, True) + _async_setup_themes(hass, conf.get(CONF_THEMES)) return True @@ -396,6 +425,8 @@ class IndexView(web_urldispatcher.AbstractResource): text=template.render( theme_color=MANIFEST_JSON['theme_color'], extra_urls=hass.data[DATA_EXTRA_HTML_URL], + extra_modules=hass.data[DATA_EXTRA_MODULE_URL], + extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5], ), content_type='text/html' ) From fb0cb43261b0c0908389bf8014b5a082ed089bf7 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 22 Jun 2019 13:39:33 +0200 Subject: [PATCH 15/38] Fix time expression parsing (#24696) --- homeassistant/util/dt.py | 4 ++-- tests/util/test_dt.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index b3f7cdd434c..b0c80399064 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -221,7 +221,7 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) \ if parameter is None or parameter == MATCH_ALL: res = [x for x in range(min_value, max_value + 1)] elif isinstance(parameter, str) and parameter.startswith('/'): - parameter = float(parameter[1:]) + parameter = int(parameter[1:]) res = [x for x in range(min_value, max_value + 1) if x % parameter == 0] elif not hasattr(parameter, '__iter__'): @@ -302,7 +302,7 @@ def find_next_time_expression_time(now: dt.datetime, next_hour = _lower_bound(hours, result.hour) if next_hour != result.hour: # We're in the next hour. Seconds+minutes needs to be reset. - result.replace(second=seconds[0], minute=minutes[0]) + result = result.replace(second=seconds[0], minute=minutes[0]) if next_hour is None: # No minute to match in this day. Roll-over to next day. diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 61f10ab1bf6..19d96227a44 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -213,7 +213,7 @@ def test_find_next_time_expression_time_basic(): assert datetime(2018, 10, 7, 10, 30, 0) == \ find(datetime(2018, 10, 7, 10, 30, 0), '*', '/30', 0) - assert datetime(2018, 10, 7, 12, 30, 30) == \ + assert datetime(2018, 10, 7, 12, 0, 30) == \ find(datetime(2018, 10, 7, 10, 30, 0), '/3', '/30', [30, 45]) assert datetime(2018, 10, 8, 5, 0, 0) == \ From 9c85ba5b669aafefe2a6ecf639c49b634e0b2bd1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 22 Jun 2019 15:05:36 -0400 Subject: [PATCH 16/38] ZHA fix device type mappings (#24699) --- homeassistant/components/zha/core/registries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index a7b89362de9..8a6832caed6 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -131,8 +131,6 @@ def establish_device_mappings(): zha.DeviceType.DIMMABLE_LIGHT: LIGHT, zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, - zha.DeviceType.DIMMER_SWITCH: LIGHT, - zha.DeviceType.COLOR_DIMMER_SWITCH: LIGHT, zha.DeviceType.ON_OFF_BALLAST: SWITCH, zha.DeviceType.DIMMABLE_BALLAST: LIGHT, zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, @@ -202,6 +200,8 @@ def establish_device_mappings(): REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_CONTROLLER) REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.REMOTE_CONTROL) REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.SCENE_SELECTOR) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH) zllp = zll.PROFILE_ID REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_CONTROLLER) From 4a8149627e7218f6b2fc5d500bc7022832f2bd88 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 23 Jun 2019 07:50:04 +0200 Subject: [PATCH 17/38] Bump version pyatmo to 2.0.1 (#24703) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index dd72dab5763..d057dcd6e80 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/components/netatmo", "requirements": [ - "pyatmo==2.0.0" + "pyatmo==2.0.1" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index cb267d08ff7..4d18f6c730c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1018,7 +1018,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.0.0 +pyatmo==2.0.1 # homeassistant.components.apple_tv pyatv==0.3.12 From 14b62120fdc12b2ae8acdce97e0618819ec7703b Mon Sep 17 00:00:00 2001 From: Oleg Kurapov Date: Sun, 23 Jun 2019 21:11:25 +0200 Subject: [PATCH 18/38] Extend websocket method usage to port 8002 in Samsung TV media player (#24716) --- homeassistant/components/samsungtv/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 6b2235fe7e6..6f928e830dc 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -120,7 +120,7 @@ class SamsungTVDevice(MediaPlayerDevice): 'timeout': timeout, } - if self._config['port'] == 8001: + if self._config['port'] in (8001, 8002): self._config['method'] = 'websocket' else: self._config['method'] = 'legacy' From 48e97426582fd10796262e1be3d509d0551162e7 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 23 Jun 2019 13:43:19 -0400 Subject: [PATCH 19/38] Update ZHA dependencies (#24718) * update deps and remove legacy constants bridge * run deps script and fix test import --- homeassistant/components/zha/const.py | 4 ---- homeassistant/components/zha/device_entity.py | 2 +- homeassistant/components/zha/light.py | 2 +- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- tests/components/zha/test_config_flow.py | 2 +- 7 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 homeassistant/components/zha/const.py diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py deleted file mode 100644 index 1ccc3e0ea25..00000000000 --- a/homeassistant/components/zha/const.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Backwards compatible constants bridge.""" -# pylint: disable=W0614,W0401 -from .core.const import * # noqa: F401,F403 -from .core.registries import * # noqa: F401,F403 diff --git a/homeassistant/components/zha/device_entity.py b/homeassistant/components/zha/device_entity.py index b3cb19f2c5a..c61c0347704 100644 --- a/homeassistant/components/zha/device_entity.py +++ b/homeassistant/components/zha/device_entity.py @@ -7,7 +7,7 @@ import time from homeassistant.core import callback from homeassistant.util import slugify from .entity import ZhaEntity -from .const import POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR +from .core.const import POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 64c515b06b0..9e0f2739290 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -9,7 +9,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util -from .const import ( +from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, COLOR_CHANNEL, ON_OFF_CHANNEL, LEVEL_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL ) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9734b10fab2..e8f417b8eb0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,9 +5,9 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.8.1", - "zha-quirks==0.0.14", + "zha-quirks==0.0.15", "zigpy-deconz==0.1.6", - "zigpy-homeassistant==0.5.0", + "zigpy-homeassistant==0.6.1", "zigpy-xbee-homeassistant==0.3.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 4d18f6c730c..10b756da8cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1917,7 +1917,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.14 +zha-quirks==0.0.15 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -1929,7 +1929,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.1.6 # homeassistant.components.zha -zigpy-homeassistant==0.5.0 +zigpy-homeassistant==0.6.1 # homeassistant.components.zha zigpy-xbee-homeassistant==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index feb08c3d226..03ff023866d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -371,4 +371,4 @@ wakeonlan==1.1.6 zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.5.0 +zigpy-homeassistant==0.6.1 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index e46f1849fa1..a05de08f804 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for ZHA config flow.""" from asynctest import patch from homeassistant.components.zha import config_flow -from homeassistant.components.zha.const import DOMAIN +from homeassistant.components.zha.core.const import DOMAIN from tests.common import MockConfigEntry From 2c5080e382cf2cddc7e341d64624913be3708710 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Mon, 24 Jun 2019 10:05:34 -0500 Subject: [PATCH 20/38] Add show_as_state options to Life360 (#24725) --- homeassistant/components/life360/__init__.py | 7 +++++- homeassistant/components/life360/const.py | 4 ++++ .../components/life360/device_tracker.py | 22 ++++++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index a42dcf9b72c..b59ace1d1ff 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -16,7 +16,8 @@ import homeassistant.helpers.config_validation as cv from .const import ( CONF_AUTHORIZATION, CONF_CIRCLES, CONF_DRIVING_SPEED, CONF_ERROR_THRESHOLD, CONF_MAX_GPS_ACCURACY, CONF_MAX_UPDATE_WAIT, CONF_MEMBERS, - CONF_WARNING_THRESHOLD, DOMAIN) + CONF_SHOW_AS_STATE, CONF_WARNING_THRESHOLD, DOMAIN, SHOW_DRIVING, + SHOW_MOVING) from .helpers import get_api _LOGGER = logging.getLogger(__name__) @@ -25,6 +26,8 @@ DEFAULT_PREFIX = DOMAIN CONF_ACCOUNTS = 'accounts' +SHOW_AS_STATE_OPTS = [SHOW_DRIVING, SHOW_MOVING] + def _excl_incl_list_to_filter_dict(value): return { @@ -108,6 +111,8 @@ LIFE360_SCHEMA = vol.All( vol.All(vol.Any(None, cv.string), _prefix), vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_SHOW_AS_STATE, default=[]): vol.All( + cv.ensure_list, [vol.In(SHOW_AS_STATE_OPTS)]), vol.Optional(CONF_WARNING_THRESHOLD): _THRESHOLD, }), _thresholds diff --git a/homeassistant/components/life360/const.py b/homeassistant/components/life360/const.py index 4c4016c6b40..602c5ee4846 100644 --- a/homeassistant/components/life360/const.py +++ b/homeassistant/components/life360/const.py @@ -8,4 +8,8 @@ CONF_ERROR_THRESHOLD = 'error_threshold' CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' CONF_MAX_UPDATE_WAIT = 'max_update_wait' CONF_MEMBERS = 'members' +CONF_SHOW_AS_STATE = 'show_as_state' CONF_WARNING_THRESHOLD = 'warning_threshold' + +SHOW_DRIVING = 'driving' +SHOW_MOVING = 'moving' diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 00201f1aa0d..cf69d8b656a 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -8,18 +8,21 @@ import voluptuous as vol from homeassistant.components.device_tracker import CONF_SCAN_INTERVAL from homeassistant.components.device_tracker.const import ( ENTITY_ID_FORMAT as DT_ENTITY_ID_FORMAT) +from homeassistant.components.zone import async_active_zone from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_ENTITY_ID, CONF_PREFIX, LENGTH_FEET, LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.distance import convert import homeassistant.util.dt as dt_util from .const import ( CONF_CIRCLES, CONF_DRIVING_SPEED, CONF_ERROR_THRESHOLD, CONF_MAX_GPS_ACCURACY, CONF_MAX_UPDATE_WAIT, CONF_MEMBERS, - CONF_WARNING_THRESHOLD, DOMAIN) + CONF_SHOW_AS_STATE, CONF_WARNING_THRESHOLD, DOMAIN, SHOW_DRIVING, + SHOW_MOVING) _LOGGER = logging.getLogger(__name__) @@ -107,6 +110,7 @@ class Life360Scanner: self._circles_filter = config.get(CONF_CIRCLES) self._members_filter = config.get(CONF_MEMBERS) self._driving_speed = config.get(CONF_DRIVING_SPEED) + self._show_as_state = config[CONF_SHOW_AS_STATE] self._apis = apis self._errs = {} self._error_threshold = config[CONF_ERROR_THRESHOLD] @@ -266,8 +270,20 @@ class Life360Scanner: ATTR_WIFI_ON: _bool_attr_from_int(loc.get('wifiState')), } - self._see(dev_id=dev_id, gps=(lat, lon), gps_accuracy=gps_accuracy, - battery=battery, attributes=attrs, + # If user wants driving or moving to be shown as state, and current + # location is not in a HA zone, then set location name accordingly. + loc_name = None + active_zone = run_callback_threadsafe( + self._hass.loop, async_active_zone, self._hass, lat, lon, + gps_accuracy).result() + if not active_zone: + if SHOW_DRIVING in self._show_as_state and driving is True: + loc_name = SHOW_DRIVING + elif SHOW_MOVING in self._show_as_state and moving is True: + loc_name = SHOW_MOVING + + self._see(dev_id=dev_id, location_name=loc_name, gps=(lat, lon), + gps_accuracy=gps_accuracy, battery=battery, attributes=attrs, picture=member.get('avatar')) def _update_members(self, members, members_updated): From 75ec8558226cb86a79e73e71989f09cbe2435805 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2019 08:33:21 -0700 Subject: [PATCH 21/38] Bumped version to 0.95.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2e58571add3..a9a3e9daa4c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -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 34231383ec5a12734d57f430873ddce4b3f8365f Mon Sep 17 00:00:00 2001 From: Evan Bruhn Date: Tue, 25 Jun 2019 02:36:39 +1000 Subject: [PATCH 22/38] Save cached logi_circle tokens in config folder (#24726) Instead of the working directory, which it's doing currently. Matches pattern observed on Abode, Ring, Skybell integrations. --- homeassistant/components/logi_circle/__init__.py | 2 +- homeassistant/components/logi_circle/config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 4e5ad0c5aeb..2f34366aafa 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass, entry): client_secret=entry.data[CONF_CLIENT_SECRET], api_key=entry.data[CONF_API_KEY], redirect_uri=entry.data[CONF_REDIRECT_URI], - cache_file=DEFAULT_CACHEDB + cache_file=hass.config.path(DEFAULT_CACHEDB) ) if not logi_circle.authorized: diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index 728ca27ba51..7f1f085bbac 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -157,7 +157,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow): client_secret=client_secret, api_key=api_key, redirect_uri=redirect_uri, - cache_file=DEFAULT_CACHEDB) + cache_file=self.hass.config.path(DEFAULT_CACHEDB)) try: with async_timeout.timeout(_TIMEOUT): From 82cad58b8dd06683d5be54fe30c60905983ff69c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 24 Jun 2019 16:57:07 -0400 Subject: [PATCH 23/38] Update ZHA dependencies. (#24736) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e8f417b8eb0..15fcf38100f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ - "bellows-homeassistant==0.8.1", + "bellows-homeassistant==0.8.2", "zha-quirks==0.0.15", "zigpy-deconz==0.1.6", "zigpy-homeassistant==0.6.1", diff --git a/requirements_all.txt b/requirements_all.txt index 10b756da8cc..f16b59d2afe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -241,7 +241,7 @@ batinfo==0.4.2 beautifulsoup4==4.7.1 # homeassistant.components.zha -bellows-homeassistant==0.8.1 +bellows-homeassistant==0.8.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03ff023866d..c59e04ef8e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -79,7 +79,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.8.1 +bellows-homeassistant==0.8.2 # homeassistant.components.caldav caldav==0.6.1 From ec777a802c769232e184f0bfca7a5207cdf82001 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2019 14:46:32 -0700 Subject: [PATCH 24/38] AdGuard to update entry (#24737) --- .../components/adguard/config_flow.py | 27 +++++- homeassistant/components/adguard/strings.json | 5 +- tests/components/adguard/test_config_flow.py | 87 +++++++++++++++++-- 3 files changed, 108 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 7e144a76e22..9ef789f83a8 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -104,12 +104,33 @@ class AdGuardHomeFlowHandler(ConfigFlow): This flow is triggered by the discovery component. """ - if self._async_current_entries(): + entries = self._async_current_entries() + + if not entries: + self._hassio_discovery = user_input + return await self.async_step_hassio_confirm() + + cur_entry = entries[0] + + if (cur_entry.data[CONF_HOST] == user_input[CONF_HOST] and + cur_entry.data[CONF_PORT] == user_input[CONF_PORT]): return self.async_abort(reason='single_instance_allowed') - self._hassio_discovery = user_input + is_loaded = cur_entry.state == config_entries.ENTRY_STATE_LOADED - return await self.async_step_hassio_confirm() + if is_loaded: + await self.hass.config_entries.async_unload(cur_entry.entry_id) + + self.hass.config_entries.async_update_entry(cur_entry, data={ + **cur_entry.data, + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + }) + + if is_loaded: + await self.hass.config_entries.async_setup(cur_entry.entry_id) + + return self.async_abort(reason='existing_instance_updated') async def async_step_hassio_confirm(self, user_input=None): """Confirm Hass.io discovery.""" diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index c88f7085e34..b3966bca820 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -23,7 +23,8 @@ "connection_error": "Failed to connect." }, "abort": { - "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." + "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed.", + "existing_instance_updated": "Updated existing configuration." } } -} \ No newline at end of file +} diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 451fd1436d4..41af02345a9 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -1,14 +1,16 @@ """Tests for the AdGuard Home config flow.""" +from unittest.mock import patch + import aiohttp -from homeassistant import data_entry_flow +from homeassistant import data_entry_flow, config_entries from homeassistant.components.adguard import config_flow from homeassistant.components.adguard.const import DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro FIXTURE_USER_INPUT = { CONF_HOST: '127.0.0.1', @@ -94,17 +96,90 @@ async def test_integration_already_exists(hass): async def test_hassio_single_instance(hass): """Test we only allow a single config flow.""" - MockConfigEntry(domain='adguard', data={'host': '1.2.3.4'}).add_to_hass( - hass - ) + MockConfigEntry(domain='adguard', data={ + 'host': 'mock-adguard', + 'port': '3000' + }).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - 'adguard', context={'source': 'hassio'} + 'adguard', + data={ + 'addon': 'AdGuard Home Addon', + 'host': 'mock-adguard', + 'port': '3000', + }, + context={'source': 'hassio'} ) assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT assert result['reason'] == 'single_instance_allowed' +async def test_hassio_update_instance_not_running(hass): + """Test we only allow a single config flow.""" + entry = MockConfigEntry(domain='adguard', data={ + 'host': 'mock-adguard', + 'port': '3000' + }) + entry.add_to_hass(hass) + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + + result = await hass.config_entries.flow.async_init( + 'adguard', + data={ + 'addon': 'AdGuard Home Addon', + 'host': 'mock-adguard-updated', + 'port': '3000', + }, + context={'source': 'hassio'} + ) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'existing_instance_updated' + + +async def test_hassio_update_instance_running(hass): + """Test we only allow a single config flow.""" + entry = MockConfigEntry(domain='adguard', data={ + 'host': 'mock-adguard', + 'port': '3000', + 'verify_ssl': False, + 'username': None, + 'password': None, + 'ssl': False, + }) + entry.add_to_hass(hass) + + with patch.object( + hass.config_entries, 'async_forward_entry_setup', + side_effect=lambda *_: mock_coro(True) + ) as mock_load: + assert await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(mock_load.mock_calls) == 2 + + with patch.object( + hass.config_entries, 'async_forward_entry_unload', + side_effect=lambda *_: mock_coro(True) + ) as mock_unload, patch.object( + hass.config_entries, 'async_forward_entry_setup', + side_effect=lambda *_: mock_coro(True) + ) as mock_load: + result = await hass.config_entries.flow.async_init( + 'adguard', + data={ + 'addon': 'AdGuard Home Addon', + 'host': 'mock-adguard-updated', + 'port': '3000', + }, + context={'source': 'hassio'} + ) + assert len(mock_unload.mock_calls) == 2 + assert len(mock_load.mock_calls) == 2 + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'existing_instance_updated' + assert entry.data['host'] == 'mock-adguard-updated' + + async def test_hassio_confirm(hass, aioclient_mock): """Test we can finish a config flow.""" aioclient_mock.get( From f71d4312e2bb73f7d9ac6ac58c087ea4ebc639c8 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 24 Jun 2019 23:59:15 +0200 Subject: [PATCH 25/38] Update pysonos to 0.0.17 (#24740) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 0aee135652d..98f5784a028 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/sonos", "requirements": [ - "pysonos==0.0.16" + "pysonos==0.0.17" ], "dependencies": [], "ssdp": { diff --git a/requirements_all.txt b/requirements_all.txt index f16b59d2afe..f35cfa95536 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1360,7 +1360,7 @@ pysmarty==0.8 pysnmp==4.4.9 # homeassistant.components.sonos -pysonos==0.0.16 +pysonos==0.0.17 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c59e04ef8e6..376c0d03ae5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -293,7 +293,7 @@ pysmartapp==0.3.2 pysmartthings==0.6.9 # homeassistant.components.sonos -pysonos==0.0.16 +pysonos==0.0.17 # homeassistant.components.spc pyspcwebgw==0.4.0 From d699a550c858b4b4b6244dcaa78f11f3d5a7c28d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2019 15:01:17 -0700 Subject: [PATCH 26/38] Bumped version to 0.95.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a9a3e9daa4c..84444e9d580 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -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 0f5c9b4af323cff8444b8528a8a24ab812662c72 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2019 22:07:39 -0700 Subject: [PATCH 27/38] Updated frontend to 20190624.1 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c0d9c95849b..2dae7aaa1ec 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190624.0" + "home-assistant-frontend==20190624.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b73b4ba784b..c704336ddaf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2018.04.16 cryptography==2.6.1 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190624.0 +home-assistant-frontend==20190624.1 importlib-metadata==0.15 jinja2>=2.10 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index f35cfa95536..8f511a2a64c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -595,7 +595,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190624.0 +home-assistant-frontend==20190624.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 376c0d03ae5..36bb3c0ae84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -160,7 +160,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190624.0 +home-assistant-frontend==20190624.1 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 327fe63047ce7fb7b25f3393cab1b27f40b0cfd0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 21 Jun 2019 02:17:21 -0700 Subject: [PATCH 28/38] Clean up Google Config (#24663) * Clean up Google Config * Lint * pylint * pylint2 --- .../components/cloud/alexa_config.py | 244 +++++++++++++++ homeassistant/components/cloud/client.py | 293 +----------------- .../components/cloud/google_config.py | 52 ++++ .../components/google_assistant/helpers.py | 32 +- .../components/google_assistant/http.py | 59 ++-- tests/components/cloud/__init__.py | 20 +- tests/components/cloud/conftest.py | 2 +- tests/components/cloud/test_client.py | 80 +++-- tests/components/cloud/test_http_api.py | 14 +- tests/components/google_assistant/__init__.py | 31 +- .../google_assistant/test_smart_home.py | 11 +- .../components/google_assistant/test_trait.py | 9 +- 12 files changed, 460 insertions(+), 387 deletions(-) create mode 100644 homeassistant/components/cloud/alexa_config.py create mode 100644 homeassistant/components/cloud/google_config.py diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py new file mode 100644 index 00000000000..746f01dd04b --- /dev/null +++ b/homeassistant/components/cloud/alexa_config.py @@ -0,0 +1,244 @@ +"""Alexa configuration for Home Assistant Cloud.""" +import asyncio +from datetime import timedelta +import logging + +import aiohttp +import async_timeout +from hass_nabucasa import cloud_api + +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.helpers import entity_registry +from homeassistant.helpers.event import async_call_later +from homeassistant.util.dt import utcnow +from homeassistant.components.alexa import ( + config as alexa_config, + errors as alexa_errors, + entities as alexa_entities, + state_report as alexa_state_report, +) + + +from .const import ( + CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, + RequireRelink +) + +_LOGGER = logging.getLogger(__name__) + +# Time to wait when entity preferences have changed before syncing it to +# the cloud. +SYNC_DELAY = 1 + + +class AlexaConfig(alexa_config.AbstractConfig): + """Alexa Configuration.""" + + def __init__(self, hass, config, prefs, cloud): + """Initialize the Alexa config.""" + super().__init__(hass) + self._config = config + self._prefs = prefs + self._cloud = cloud + self._token = None + self._token_valid = None + self._cur_entity_prefs = prefs.alexa_entity_configs + self._alexa_sync_unsub = None + self._endpoint = None + + prefs.async_listen_updates(self._async_prefs_updated) + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._handle_entity_registry_updated + ) + + @property + def enabled(self): + """Return if Alexa is enabled.""" + return self._prefs.alexa_enabled + + @property + def supports_auth(self): + """Return if config supports auth.""" + return True + + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + return self._prefs.alexa_report_state + + @property + def endpoint(self): + """Endpoint for report state.""" + if self._endpoint is None: + raise ValueError("No endpoint available. Fetch access token first") + + return self._endpoint + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG, {}) + + def should_expose(self, entity_id): + """If an entity should be exposed.""" + if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + return False + + if not self._config[CONF_FILTER].empty_filter: + return self._config[CONF_FILTER](entity_id) + + entity_configs = self._prefs.alexa_entity_configs + entity_config = entity_configs.get(entity_id, {}) + return entity_config.get( + PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + + async def async_get_access_token(self): + """Get an access token.""" + if self._token_valid is not None and self._token_valid < utcnow(): + return self._token + + resp = await cloud_api.async_alexa_access_token(self._cloud) + body = await resp.json() + + if resp.status == 400: + if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'): + raise RequireRelink + + raise alexa_errors.NoTokenAvailable + + self._token = body['access_token'] + self._endpoint = body['event_endpoint'] + self._token_valid = utcnow() + timedelta(seconds=body['expires_in']) + return self._token + + async def _async_prefs_updated(self, prefs): + """Handle updated preferences.""" + if self.should_report_state != self.is_reporting_states: + if self.should_report_state: + await self.async_enable_proactive_mode() + else: + await self.async_disable_proactive_mode() + + # If entity prefs are the same or we have filter in config.yaml, + # don't sync. + if (self._cur_entity_prefs is prefs.alexa_entity_configs or + not self._config[CONF_FILTER].empty_filter): + return + + if self._alexa_sync_unsub: + self._alexa_sync_unsub() + + self._alexa_sync_unsub = async_call_later( + self.hass, SYNC_DELAY, self._sync_prefs) + + async def _sync_prefs(self, _now): + """Sync the updated preferences to Alexa.""" + self._alexa_sync_unsub = None + old_prefs = self._cur_entity_prefs + new_prefs = self._prefs.alexa_entity_configs + + seen = set() + to_update = [] + to_remove = [] + + for entity_id, info in old_prefs.items(): + seen.add(entity_id) + old_expose = info.get(PREF_SHOULD_EXPOSE) + + if entity_id in new_prefs: + new_expose = new_prefs[entity_id].get(PREF_SHOULD_EXPOSE) + else: + new_expose = None + + if old_expose == new_expose: + continue + + if new_expose: + to_update.append(entity_id) + else: + to_remove.append(entity_id) + + # Now all the ones that are in new prefs but never were in old prefs + for entity_id, info in new_prefs.items(): + if entity_id in seen: + continue + + new_expose = info.get(PREF_SHOULD_EXPOSE) + + if new_expose is None: + continue + + # Only test if we should expose. It can never be a remove action, + # as it didn't exist in old prefs object. + if new_expose: + to_update.append(entity_id) + + # We only set the prefs when update is successful, that way we will + # retry when next change comes in. + if await self._sync_helper(to_update, to_remove): + self._cur_entity_prefs = new_prefs + + async def async_sync_entities(self): + """Sync all entities to Alexa.""" + to_update = [] + to_remove = [] + + for entity in alexa_entities.async_get_entities(self.hass, self): + if self.should_expose(entity.entity_id): + to_update.append(entity.entity_id) + else: + to_remove.append(entity.entity_id) + + return await self._sync_helper(to_update, to_remove) + + async def _sync_helper(self, to_update, to_remove) -> bool: + """Sync entities to Alexa. + + Return boolean if it was successful. + """ + if not to_update and not to_remove: + return True + + tasks = [] + + if to_update: + tasks.append(alexa_state_report.async_send_add_or_update_message( + self.hass, self, to_update + )) + + if to_remove: + tasks.append(alexa_state_report.async_send_delete_message( + self.hass, self, to_remove + )) + + try: + with async_timeout.timeout(10): + await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) + + return True + + except asyncio.TimeoutError: + _LOGGER.warning("Timeout trying to sync entitites to Alexa") + return False + + except aiohttp.ClientError as err: + _LOGGER.warning("Error trying to sync entities to Alexa: %s", err) + return False + + async def _handle_entity_registry_updated(self, event): + """Handle when entity registry updated.""" + if not self.enabled or not self._cloud.is_logged_in: + return + + action = event.data['action'] + entity_id = event.data['entity_id'] + to_update = [] + to_remove = [] + + if action == 'create' and self.should_expose(entity_id): + to_update.append(entity_id) + elif action == 'remove' and self.should_expose(entity_id): + to_remove.append(entity_id) + + await self._sync_helper(to_update, to_remove) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index f8cfc255aa4..16a05b0d127 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -2,257 +2,24 @@ import asyncio from pathlib import Path from typing import Any, Dict -from datetime import timedelta import logging import aiohttp -import async_timeout -from hass_nabucasa import cloud_api from hass_nabucasa.client import CloudClient as Interface from homeassistant.core import callback -from homeassistant.components.alexa import ( - config as alexa_config, - errors as alexa_errors, - smart_home as alexa_sh, - entities as alexa_entities, - state_report as alexa_state_report, -) -from homeassistant.components.google_assistant import ( - helpers as ga_h, smart_home as ga) -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES -from homeassistant.helpers.event import async_call_later +from homeassistant.components.google_assistant import smart_home as ga from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers import entity_registry from homeassistant.util.aiohttp import MockRequest -from homeassistant.util.dt import utcnow +from homeassistant.components.alexa import smart_home as alexa_sh -from . import utils -from .const import ( - CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE, - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA, RequireRelink) +from . import utils, alexa_config, google_config +from .const import DISPATCHER_REMOTE_UPDATE from .prefs import CloudPreferences _LOGGER = logging.getLogger(__name__) -# Time to wait when entity preferences have changed before syncing it to -# the cloud. -SYNC_DELAY = 1 - - -class AlexaConfig(alexa_config.AbstractConfig): - """Alexa Configuration.""" - - def __init__(self, hass, config, prefs, cloud): - """Initialize the Alexa config.""" - super().__init__(hass) - self._config = config - self._prefs = prefs - self._cloud = cloud - self._token = None - self._token_valid = None - self._cur_entity_prefs = prefs.alexa_entity_configs - self._alexa_sync_unsub = None - self._endpoint = None - - prefs.async_listen_updates(self._async_prefs_updated) - hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, - self._handle_entity_registry_updated - ) - - @property - def enabled(self): - """Return if Alexa is enabled.""" - return self._prefs.alexa_enabled - - @property - def supports_auth(self): - """Return if config supports auth.""" - return True - - @property - def should_report_state(self): - """Return if states should be proactively reported.""" - return self._prefs.alexa_report_state - - @property - def endpoint(self): - """Endpoint for report state.""" - if self._endpoint is None: - raise ValueError("No endpoint available. Fetch access token first") - - return self._endpoint - - @property - def entity_config(self): - """Return entity config.""" - return self._config.get(CONF_ENTITY_CONFIG, {}) - - def should_expose(self, entity_id): - """If an entity should be exposed.""" - if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: - return False - - if not self._config[CONF_FILTER].empty_filter: - return self._config[CONF_FILTER](entity_id) - - entity_configs = self._prefs.alexa_entity_configs - entity_config = entity_configs.get(entity_id, {}) - return entity_config.get( - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) - - async def async_get_access_token(self): - """Get an access token.""" - if self._token_valid is not None and self._token_valid < utcnow(): - return self._token - - resp = await cloud_api.async_alexa_access_token(self._cloud) - body = await resp.json() - - if resp.status == 400: - if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'): - raise RequireRelink - - raise alexa_errors.NoTokenAvailable - - self._token = body['access_token'] - self._endpoint = body['event_endpoint'] - self._token_valid = utcnow() + timedelta(seconds=body['expires_in']) - return self._token - - async def _async_prefs_updated(self, prefs): - """Handle updated preferences.""" - if self.should_report_state != self.is_reporting_states: - if self.should_report_state: - await self.async_enable_proactive_mode() - else: - await self.async_disable_proactive_mode() - - # If entity prefs are the same or we have filter in config.yaml, - # don't sync. - if (self._cur_entity_prefs is prefs.alexa_entity_configs or - not self._config[CONF_FILTER].empty_filter): - return - - if self._alexa_sync_unsub: - self._alexa_sync_unsub() - - self._alexa_sync_unsub = async_call_later( - self.hass, SYNC_DELAY, self._sync_prefs) - - async def _sync_prefs(self, _now): - """Sync the updated preferences to Alexa.""" - self._alexa_sync_unsub = None - old_prefs = self._cur_entity_prefs - new_prefs = self._prefs.alexa_entity_configs - - seen = set() - to_update = [] - to_remove = [] - - for entity_id, info in old_prefs.items(): - seen.add(entity_id) - old_expose = info.get(PREF_SHOULD_EXPOSE) - - if entity_id in new_prefs: - new_expose = new_prefs[entity_id].get(PREF_SHOULD_EXPOSE) - else: - new_expose = None - - if old_expose == new_expose: - continue - - if new_expose: - to_update.append(entity_id) - else: - to_remove.append(entity_id) - - # Now all the ones that are in new prefs but never were in old prefs - for entity_id, info in new_prefs.items(): - if entity_id in seen: - continue - - new_expose = info.get(PREF_SHOULD_EXPOSE) - - if new_expose is None: - continue - - # Only test if we should expose. It can never be a remove action, - # as it didn't exist in old prefs object. - if new_expose: - to_update.append(entity_id) - - # We only set the prefs when update is successful, that way we will - # retry when next change comes in. - if await self._sync_helper(to_update, to_remove): - self._cur_entity_prefs = new_prefs - - async def async_sync_entities(self): - """Sync all entities to Alexa.""" - to_update = [] - to_remove = [] - - for entity in alexa_entities.async_get_entities(self.hass, self): - if self.should_expose(entity.entity_id): - to_update.append(entity.entity_id) - else: - to_remove.append(entity.entity_id) - - return await self._sync_helper(to_update, to_remove) - - async def _sync_helper(self, to_update, to_remove) -> bool: - """Sync entities to Alexa. - - Return boolean if it was successful. - """ - if not to_update and not to_remove: - return True - - tasks = [] - - if to_update: - tasks.append(alexa_state_report.async_send_add_or_update_message( - self.hass, self, to_update - )) - - if to_remove: - tasks.append(alexa_state_report.async_send_delete_message( - self.hass, self, to_remove - )) - - try: - with async_timeout.timeout(10): - await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) - - return True - - except asyncio.TimeoutError: - _LOGGER.warning("Timeout trying to sync entitites to Alexa") - return False - - except aiohttp.ClientError as err: - _LOGGER.warning("Error trying to sync entities to Alexa: %s", err) - return False - - async def _handle_entity_registry_updated(self, event): - """Handle when entity registry updated.""" - if not self.enabled or not self._cloud.is_logged_in: - return - - action = event.data['action'] - entity_id = event.data['entity_id'] - to_update = [] - to_remove = [] - - if action == 'create' and self.should_expose(entity_id): - to_update.append(entity_id) - elif action == 'remove' and self.should_expose(entity_id): - to_remove.append(entity_id) - - await self._sync_helper(to_update, to_remove) class CloudClient(Interface): @@ -260,13 +27,14 @@ class CloudClient(Interface): def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences, websession: aiohttp.ClientSession, - alexa_cfg: Dict[str, Any], google_config: Dict[str, Any]): + alexa_user_config: Dict[str, Any], + google_user_config: Dict[str, Any]): """Initialize client interface to Cloud.""" self._hass = hass self._prefs = prefs self._websession = websession - self.google_user_config = google_config - self.alexa_user_config = alexa_cfg + self.google_user_config = google_user_config + self.alexa_user_config = alexa_user_config self._alexa_config = None self._google_config = None self.cloud = None @@ -307,53 +75,22 @@ class CloudClient(Interface): return self._prefs.remote_enabled @property - def alexa_config(self) -> AlexaConfig: + def alexa_config(self) -> alexa_config.AlexaConfig: """Return Alexa config.""" if self._alexa_config is None: - self._alexa_config = AlexaConfig( + assert self.cloud is not None + self._alexa_config = alexa_config.AlexaConfig( self._hass, self.alexa_user_config, self._prefs, self.cloud) return self._alexa_config @property - def google_config(self) -> ga_h.Config: + def google_config(self) -> google_config.CloudGoogleConfig: """Return Google config.""" if not self._google_config: - google_conf = self.google_user_config - - def should_expose(entity): - """If an entity should be exposed.""" - if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: - return False - - if not google_conf['filter'].empty_filter: - return google_conf['filter'](entity.entity_id) - - entity_configs = self.prefs.google_entity_configs - entity_config = entity_configs.get(entity.entity_id, {}) - return entity_config.get( - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) - - def should_2fa(entity): - """If an entity should be checked for 2FA.""" - entity_configs = self.prefs.google_entity_configs - entity_config = entity_configs.get(entity.entity_id, {}) - return not entity_config.get( - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) - - username = self._hass.data[DOMAIN].claims["cognito:username"] - - self._google_config = ga_h.Config( - should_expose=should_expose, - should_2fa=should_2fa, - secure_devices_pin=self._prefs.google_secure_devices_pin, - entity_config=google_conf.get(CONF_ENTITY_CONFIG), - agent_user_id=username, - ) - - # Set it to the latest. - self._google_config.secure_devices_pin = \ - self._prefs.google_secure_devices_pin + assert self.cloud is not None + self._google_config = google_config.CloudGoogleConfig( + self.google_user_config, self._prefs, self.cloud) return self._google_config diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py new file mode 100644 index 00000000000..b047d25ee49 --- /dev/null +++ b/homeassistant/components/cloud/google_config.py @@ -0,0 +1,52 @@ +"""Google config for Cloud.""" +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.components.google_assistant.helpers import AbstractConfig + +from .const import ( + PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, CONF_ENTITY_CONFIG, + PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) + + +class CloudGoogleConfig(AbstractConfig): + """HA Cloud Configuration for Google Assistant.""" + + def __init__(self, config, prefs, cloud): + """Initialize the Alexa config.""" + self._config = config + self._prefs = prefs + self._cloud = cloud + + @property + def agent_user_id(self): + """Return Agent User Id to use for query responses.""" + return self._cloud.claims["cognito:username"] + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG) + + @property + def secure_devices_pin(self): + """Return entity config.""" + return self._prefs.google_secure_devices_pin + + def should_expose(self, state): + """If an entity should be exposed.""" + if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + return False + + if not self._config['filter'].empty_filter: + return self._config['filter'](state.entity_id) + + entity_configs = self._prefs.google_entity_configs + entity_config = entity_configs.get(state.entity_id, {}) + return entity_config.get( + PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + + def should_2fa(self, state): + """If an entity should be checked for 2FA.""" + entity_configs = self._prefs.google_entity_configs + entity_config = entity_configs.get(state.entity_id, {}) + return not entity_config.get( + PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 770a502ad5d..87c4fb78f3a 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -17,24 +17,32 @@ from .const import ( from .error import SmartHomeError -class Config: +class AbstractConfig: """Hold the configuration for Google Assistant.""" - def __init__(self, should_expose, - entity_config=None, secure_devices_pin=None, - agent_user_id=None, should_2fa=None): - """Initialize the configuration.""" - self.should_expose = should_expose - self.entity_config = entity_config or {} - self.secure_devices_pin = secure_devices_pin - self._should_2fa = should_2fa + @property + def agent_user_id(self): + """Return Agent User Id to use for query responses.""" + return None - # Agent User Id to use for query responses - self.agent_user_id = agent_user_id + @property + def entity_config(self): + """Return entity config.""" + return {} + + @property + def secure_devices_pin(self): + """Return entity config.""" + return None + + def should_expose(self, state) -> bool: + """Return if entity should be exposed.""" + raise NotImplementedError def should_2fa(self, state): """If an entity should have 2FA checked.""" - return self._should_2fa is None or self._should_2fa(state) + # pylint: disable=no-self-use + return True class RequestData: diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index d385d742c7d..95528eea3ca 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -17,33 +17,50 @@ from .const import ( CONF_SECURE_DEVICES_PIN, ) from .smart_home import async_handle_message -from .helpers import Config +from .helpers import AbstractConfig _LOGGER = logging.getLogger(__name__) -@callback -def async_register_http(hass, cfg): - """Register HTTP views for Google Assistant.""" - expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT) - exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) - entity_config = cfg.get(CONF_ENTITY_CONFIG) or {} - secure_devices_pin = cfg.get(CONF_SECURE_DEVICES_PIN) +class GoogleConfig(AbstractConfig): + """Config for manual setup of Google.""" - def is_exposed(entity) -> bool: - """Determine if an entity should be exposed to Google Assistant.""" - if entity.attributes.get('view') is not None: + def __init__(self, config): + """Initialize the config.""" + self._config = config + + @property + def agent_user_id(self): + """Return Agent User Id to use for query responses.""" + return None + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG, {}) + + @property + def secure_devices_pin(self): + """Return entity config.""" + return self._config.get(CONF_SECURE_DEVICES_PIN) + + def should_expose(self, state) -> bool: + """Return if entity should be exposed.""" + expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) + exposed_domains = self._config.get(CONF_EXPOSED_DOMAINS) + + if state.attributes.get('view') is not None: # Ignore entities that are views return False - if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False explicit_expose = \ - entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE) + self.entity_config.get(state.entity_id, {}).get(CONF_EXPOSE) domain_exposed_by_default = \ - expose_by_default and entity.domain in exposed_domains + expose_by_default and state.domain in exposed_domains # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being @@ -53,13 +70,15 @@ def async_register_http(hass, cfg): return is_default_exposed or explicit_expose - config = Config( - should_expose=is_exposed, - entity_config=entity_config, - secure_devices_pin=secure_devices_pin - ) + def should_2fa(self, state): + """If an entity should have 2FA checked.""" + return True - hass.http.register_view(GoogleAssistantView(config)) + +@callback +def async_register_http(hass, cfg): + """Register HTTP views for Google Assistant.""" + hass.http.register_view(GoogleAssistantView(GoogleConfig(cfg))) class GoogleAssistantView(HomeAssistantView): diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 08ab5324b97..3f2b8f034cd 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,24 +1,22 @@ """Tests for the cloud component.""" from unittest.mock import patch + from homeassistant.setup import async_setup_component from homeassistant.components import cloud from homeassistant.components.cloud import const -from jose import jwt - from tests.common import mock_coro -def mock_cloud(hass, config={}): +async def mock_cloud(hass, config=None): """Mock cloud.""" - with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): - assert hass.loop.run_until_complete(async_setup_component( - hass, cloud.DOMAIN, { - 'cloud': config - })) - - hass.data[cloud.DOMAIN]._decode_claims = \ - lambda token: jwt.get_unverified_claims(token) + assert await async_setup_component( + hass, cloud.DOMAIN, { + 'cloud': config or {} + }) + cloud_inst = hass.data['cloud'] + with patch('hass_nabucasa.Cloud.run_executor', return_value=mock_coro()): + await cloud_inst.start() def mock_cloud_prefs(hass, prefs={}): diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index c9fd6360929..87ef6809fdd 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -18,7 +18,7 @@ def mock_user_data(): @pytest.fixture def mock_cloud_fixture(hass): """Fixture for cloud component.""" - mock_cloud(hass) + hass.loop.run_until_complete(mock_cloud(hass)) return mock_cloud_prefs(hass) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 7d1afda7e6a..fa42bda32db 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -9,7 +9,7 @@ import pytest from homeassistant.core import State from homeassistant.setup import async_setup_component from homeassistant.components.cloud import ( - DOMAIN, ALEXA_SCHEMA, client) + DOMAIN, ALEXA_SCHEMA, alexa_config) from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) from homeassistant.util.dt import utcnow @@ -17,11 +17,11 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from tests.components.alexa import test_smart_home as test_alexa from tests.common import mock_coro, async_fire_time_changed -from . import mock_cloud_prefs +from . import mock_cloud_prefs, mock_cloud @pytest.fixture -def mock_cloud(): +def mock_cloud_inst(): """Mock cloud class.""" return MagicMock(subscription_expired=False) @@ -29,10 +29,7 @@ def mock_cloud(): @pytest.fixture async def mock_cloud_setup(hass): """Set up the cloud.""" - with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): - assert await async_setup_component(hass, 'cloud', { - 'cloud': {} - }) + await mock_cloud(hass) @pytest.fixture @@ -52,24 +49,20 @@ async def test_handler_alexa(hass): hass.states.async_set( 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) - with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): - setup = await async_setup_component(hass, 'cloud', { - 'cloud': { - 'alexa': { - 'filter': { - 'exclude_entities': 'switch.test2' - }, - 'entity_config': { - 'switch.test': { - 'name': 'Config name', - 'description': 'Config description', - 'display_categories': 'LIGHT' - } - } + await mock_cloud(hass, { + 'alexa': { + 'filter': { + 'exclude_entities': 'switch.test2' + }, + 'entity_config': { + 'switch.test': { + 'name': 'Config name', + 'description': 'Config description', + 'display_categories': 'LIGHT' } } - }) - assert setup + } + }) mock_cloud_prefs(hass) cloud = hass.data['cloud'] @@ -110,24 +103,20 @@ async def test_handler_google_actions(hass): hass.states.async_set( 'group.all_locks', 'on', {'friendly_name': "Evil locks"}) - with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): - setup = await async_setup_component(hass, 'cloud', { - 'cloud': { - 'google_actions': { - 'filter': { - 'exclude_entities': 'switch.test2' - }, - 'entity_config': { - 'switch.test': { - 'name': 'Config name', - 'aliases': 'Config alias', - 'room': 'living room' - } - } + await mock_cloud(hass, { + 'google_actions': { + 'filter': { + 'exclude_entities': 'switch.test2' + }, + 'entity_config': { + 'switch.test': { + 'name': 'Config name', + 'aliases': 'Config alias', + 'room': 'living room' } } - }) - assert setup + } + }) mock_cloud_prefs(hass) cloud = hass.data['cloud'] @@ -265,7 +254,7 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): await cloud_prefs.async_update(alexa_entity_configs={ 'light.kitchen': entity_conf }) - conf = client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) assert not conf.should_expose('light.kitchen') entity_conf['should_expose'] = True @@ -274,7 +263,7 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): async def test_alexa_config_report_state(hass, cloud_prefs): """Test Alexa config should expose using prefs.""" - conf = client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) assert cloud_prefs.alexa_report_state is False assert conf.should_report_state is False @@ -307,9 +296,9 @@ def patch_sync_helper(): to_remove = [] with patch( - 'homeassistant.components.cloud.client.SYNC_DELAY', 0 + 'homeassistant.components.cloud.alexa_config.SYNC_DELAY', 0 ), patch( - 'homeassistant.components.cloud.client.AlexaConfig._sync_helper', + 'homeassistant.components.cloud.alexa_config.AlexaConfig._sync_helper', side_effect=mock_coro ) as mock_helper: yield to_update, to_remove @@ -321,7 +310,7 @@ def patch_sync_helper(): async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): """Test Alexa config responds to updating exposed entities.""" - client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) with patch_sync_helper() as (to_update, to_remove): await cloud_prefs.async_update_alexa_entity_config( @@ -354,7 +343,8 @@ async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Alexa config responds to entity registry.""" - client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data['cloud']) + alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data['cloud']) with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 60346dc6ea1..55cd9e9e2e5 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -14,10 +14,11 @@ from homeassistant.components.cloud.const import ( PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN, DOMAIN) from homeassistant.components.google_assistant.helpers import ( - GoogleEntity, Config) + GoogleEntity) from homeassistant.components.alexa.entities import LightCapabilities from tests.common import mock_coro +from tests.components.google_assistant import MockConfig from . import mock_cloud, mock_cloud_prefs @@ -45,7 +46,7 @@ def mock_cloud_login(hass, setup_api): @pytest.fixture(autouse=True) def setup_api(hass, aioclient_mock): """Initialize HTTP API.""" - mock_cloud(hass, { + hass.loop.run_until_complete(mock_cloud(hass, { 'mode': 'development', 'cognito_client_id': 'cognito_client_id', 'user_pool_id': 'user_pool_id', @@ -63,7 +64,7 @@ def setup_api(hass, aioclient_mock): 'include_entities': ['light.kitchen', 'switch.ac'] } } - }) + })) return mock_cloud_prefs(hass) @@ -709,9 +710,10 @@ async def test_list_google_entities( hass, hass_ws_client, setup_api, mock_cloud_login): """Test that we can list Google entities.""" client = await hass_ws_client(hass) - entity = GoogleEntity(hass, Config(lambda *_: False), State( - 'light.kitchen', 'on' - )) + entity = GoogleEntity( + hass, MockConfig(should_expose=lambda *_: False), State( + 'light.kitchen', 'on' + )) with patch('homeassistant.components.google_assistant.helpers' '.async_get_entities', return_value=[entity]): await client.send_json({ diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index f3732c12213..c7930f3c62f 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,6 +1,33 @@ - - """Tests for the Google Assistant integration.""" +from homeassistant.components.google_assistant import helpers + + +class MockConfig(helpers.AbstractConfig): + """Fake config that always exposes everything.""" + + def __init__(self, *, secure_devices_pin=None, should_expose=None, + entity_config=None): + """Initialize config.""" + self._should_expose = should_expose + self._secure_devices_pin = secure_devices_pin + self._entity_config = entity_config or {} + + @property + def secure_devices_pin(self): + """Return secure devices pin.""" + return self._secure_devices_pin + + @property + def entity_config(self): + """Return secure devices pin.""" + return self._entity_config + + def should_expose(self, state): + """Expose it all.""" + return self._should_expose is None or self._should_expose(state) + + +BASIC_CONFIG = MockConfig() DEMO_DEVICES = [{ 'id': diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index a65387d48a2..cfe7b946611 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -11,7 +11,7 @@ from homeassistant.components.climate.const import ( ATTR_MIN_TEMP, ATTR_MAX_TEMP, STATE_HEAT, SUPPORT_OPERATION_MODE ) from homeassistant.components.google_assistant import ( - const, trait, helpers, smart_home as sh, + const, trait, smart_home as sh, EVENT_COMMAND_RECEIVED, EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED) from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover @@ -23,9 +23,8 @@ from homeassistant.helpers import device_registry from tests.common import (mock_device_registry, mock_registry, mock_area_registry, mock_coro) -BASIC_CONFIG = helpers.Config( - should_expose=lambda state: True, -) +from . import BASIC_CONFIG, MockConfig + REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' @@ -57,7 +56,7 @@ async def test_sync_message(hass): # Excluded via config hass.states.async_set('light.not_expose', 'on') - config = helpers.Config( + config = MockConfig( should_expose=lambda state: state.entity_id != 'light.not_expose', entity_config={ 'light.demo_light': { @@ -145,7 +144,7 @@ async def test_sync_in_area(hass, registries): light.entity_id = entity.entity_id await light.async_update_ha_state() - config = helpers.Config( + config = MockConfig( should_expose=lambda _: True, entity_config={} ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 6b1b6a7c9f4..d2d216a9fc5 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -29,10 +29,8 @@ from homeassistant.const import ( from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE from homeassistant.util import color from tests.common import async_mock_service, mock_coro +from . import BASIC_CONFIG, MockConfig -BASIC_CONFIG = helpers.Config( - should_expose=lambda state: True, -) REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' @@ -42,8 +40,7 @@ BASIC_DATA = helpers.RequestData( REQ_ID, ) -PIN_CONFIG = helpers.Config( - should_expose=lambda state: True, +PIN_CONFIG = MockConfig( secure_devices_pin='1234' ) @@ -927,7 +924,7 @@ async def test_lock_unlock_unlock(hass): # Test with 2FA override with patch('homeassistant.components.google_assistant.helpers' - '.Config.should_2fa', return_value=False): + '.AbstractConfig.should_2fa', return_value=False): await trt.execute( trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}, {}) assert len(calls) == 2 From 8830054fad14e32b918bdc84392c6be3b3c43656 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 25 Jun 2019 05:00:28 +0200 Subject: [PATCH 29/38] Fix locative device update (#24744) * Add a test for two devices * Fix locative updating all devices * Add a guard clause that checks if correct device is passed. --- .../components/locative/device_tracker.py | 2 + tests/components/locative/test_init.py | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index 6f86519c47c..38efab7e8c0 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -85,6 +85,8 @@ class LocativeEntity(DeviceTrackerEntity): @callback def _async_receive_data(self, device, location, location_name): """Update device data.""" + if device != self._name: + return self._location_name = location_name self._location = location self.async_write_ha_state() diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 81248764971..ba96789007b 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -242,6 +242,43 @@ async def test_exit_first(hass, locative_client, webhook_id): assert state.state == 'not_home' +async def test_two_devices(hass, locative_client, webhook_id): + """Test updating two different devices.""" + url = '/api/webhook/{}'.format(webhook_id) + + data_device_1 = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': 'device_1', + 'id': 'Home', + 'trigger': 'exit' + } + + # Exit Home + req = await locative_client.post(url, data=data_device_1) + await hass.async_block_till_done() + assert req.status == HTTP_OK + + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data_device_1['device'])) + assert state.state == 'not_home' + + # Enter Home + data_device_2 = dict(data_device_1) + data_device_2['device'] = 'device_2' + data_device_2['trigger'] = 'enter' + req = await locative_client.post(url, data=data_device_2) + await hass.async_block_till_done() + assert req.status == HTTP_OK + + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data_device_2['device'])) + assert state.state == 'home' + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data_device_1['device'])) + assert state.state == 'not_home' + + @pytest.mark.xfail( reason='The device_tracker component does not support unloading yet.' ) From 510d6d78745760810ece48ca2ad9f0e6f6d2299a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2019 22:04:31 -0700 Subject: [PATCH 30/38] Improve Alexa error handling (#24745) --- homeassistant/components/alexa/config.py | 8 +-- .../components/alexa/state_report.py | 3 ++ .../components/cloud/alexa_config.py | 17 ++++++- homeassistant/components/cloud/client.py | 13 ++++- homeassistant/components/cloud/http_api.py | 33 +++++++++++- .../components/websocket_api/connection.py | 4 ++ .../components/websocket_api/const.py | 1 + tests/components/cloud/test_http_api.py | 51 +++++++++++++++++++ 8 files changed, 121 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 36f15735b8b..a22ebbcd30d 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -42,11 +42,11 @@ class AbstractConfig: self._unsub_proactive_report = self.hass.async_create_task( async_enable_proactive_mode(self.hass, self) ) - resp = await self._unsub_proactive_report - - # Failed to start reporting. - if resp is None: + try: + await self._unsub_proactive_report + except Exception: # pylint: disable=broad-except self._unsub_proactive_report = None + raise async def async_disable_proactive_mode(self): """Disable proactive mode.""" diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 4c11fb8c88c..022b38be59d 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -21,6 +21,9 @@ async def async_enable_proactive_mode(hass, smart_home_config): Proactive mode makes this component report state changes to Alexa. """ + # Validate we can get access token. + await smart_home_config.async_get_access_token() + async def async_entity_state_listener(changed_entity, old_state, new_state): if not new_state: diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 746f01dd04b..aae48df9884 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -103,6 +103,15 @@ class AlexaConfig(alexa_config.AbstractConfig): if resp.status == 400: if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'): + if self.should_report_state: + await self._prefs.async_update(alexa_report_state=False) + self.hass.components.persistent_notification.async_create( + "There was an error reporting state to Alexa ({}). " + "Please re-link your Alexa skill via the Alexa app to " + "continue using it.".format(body['reason']), + "Alexa state reporting disabled", + "cloud_alexa_report", + ) raise RequireRelink raise alexa_errors.NoTokenAvailable @@ -200,6 +209,9 @@ class AlexaConfig(alexa_config.AbstractConfig): if not to_update and not to_remove: return True + # Make sure it's valid. + await self.async_get_access_token() + tasks = [] if to_update: @@ -241,4 +253,7 @@ class AlexaConfig(alexa_config.AbstractConfig): elif action == 'remove' and self.should_expose(entity_id): to_remove.append(entity_id) - await self._sync_helper(to_update, to_remove) + try: + await self._sync_helper(to_update, to_remove) + except alexa_errors.NoTokenAvailable: + pass diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 16a05b0d127..d22e5bf37ba 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -12,7 +12,10 @@ from homeassistant.components.google_assistant import smart_home as ga from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.aiohttp import MockRequest -from homeassistant.components.alexa import smart_home as alexa_sh +from homeassistant.components.alexa import ( + smart_home as alexa_sh, + errors as alexa_errors, +) from . import utils, alexa_config, google_config from .const import DISPATCHER_REMOTE_UPDATE @@ -98,8 +101,14 @@ class CloudClient(Interface): """Initialize the client.""" self.cloud = cloud - if self.alexa_config.should_report_state and self.cloud.is_logged_in: + if (not self.alexa_config.should_report_state or + not self.cloud.is_logged_in): + return + + try: await self.alexa_config.async_enable_proactive_mode() + except alexa_errors.NoTokenAvailable: + pass async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d9c4ddcf1ce..0cd08dd3d5f 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -14,7 +14,10 @@ from homeassistant.components.http.data_validator import ( RequestDataValidator) from homeassistant.components import websocket_api from homeassistant.components.websocket_api import const as ws_const -from homeassistant.components.alexa import entities as alexa_entities +from homeassistant.components.alexa import ( + entities as alexa_entities, + errors as alexa_errors, +) from homeassistant.components.google_assistant import helpers as google_helpers from .const import ( @@ -375,6 +378,24 @@ async def websocket_update_prefs(hass, connection, msg): changes = dict(msg) changes.pop('id') changes.pop('type') + + # If we turn alexa linking on, validate that we can fetch access token + if changes.get(PREF_ALEXA_REPORT_STATE): + try: + with async_timeout.timeout(10): + await cloud.client.alexa_config.async_get_access_token() + except asyncio.TimeoutError: + connection.send_error(msg['id'], 'alexa_timeout', + 'Timeout validating Alexa access token.') + return + except alexa_errors.NoTokenAvailable: + connection.send_error( + msg['id'], 'alexa_relink', + 'Please go to the Alexa app and re-link the Home Assistant ' + 'skill and then try to enable state reporting.' + ) + return + await cloud.client.prefs.async_update(**changes) connection.send_message(websocket_api.result_message(msg['id'])) @@ -575,7 +596,15 @@ async def alexa_sync(hass, connection, msg): cloud = hass.data[DOMAIN] with async_timeout.timeout(10): - success = await cloud.client.alexa_config.async_sync_entities() + try: + success = await cloud.client.alexa_config.async_sync_entities() + except alexa_errors.NoTokenAvailable: + connection.send_error( + msg['id'], 'alexa_relink', + 'Please go to the Alexa app and re-link the Home Assistant ' + 'skill.' + ) + return if success: connection.send_result(msg['id']) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 1aa1efc0eca..b8cce030109 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,4 +1,5 @@ """Connection session.""" +import asyncio import voluptuous as vol from homeassistant.core import callback, Context @@ -101,6 +102,9 @@ class ActiveConnection: elif isinstance(err, vol.Invalid): code = const.ERR_INVALID_FORMAT err_message = vol.humanize.humanize_error(msg, err) + elif isinstance(err, asyncio.TimeoutError): + code = const.ERR_TIMEOUT + err_message = 'Timeout' else: code = const.ERR_UNKNOWN_ERROR err_message = 'Unknown error' diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 9c776e3b949..2f79ced7d99 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -16,6 +16,7 @@ ERR_HOME_ASSISTANT_ERROR = 'home_assistant_error' ERR_UNKNOWN_COMMAND = 'unknown_command' ERR_UNKNOWN_ERROR = 'unknown_error' ERR_UNAUTHORIZED = 'unauthorized' +ERR_TIMEOUT = 'timeout' TYPE_RESULT = 'result' diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 55cd9e9e2e5..bc60568f0d4 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -16,6 +16,7 @@ from homeassistant.components.cloud.const import ( from homeassistant.components.google_assistant.helpers import ( GoogleEntity) from homeassistant.components.alexa.entities import LightCapabilities +from homeassistant.components.alexa import errors as alexa_errors from tests.common import mock_coro from tests.components.google_assistant import MockConfig @@ -847,3 +848,53 @@ async def test_update_alexa_entity( assert prefs.alexa_entity_configs['light.kitchen'] == { 'should_expose': False, } + + +async def test_sync_alexa_entities_timeout( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test that timeout syncing Alexa entities.""" + client = await hass_ws_client(hass) + with patch('homeassistant.components.cloud.alexa_config.AlexaConfig' + '.async_sync_entities', side_effect=asyncio.TimeoutError): + await client.send_json({ + 'id': 5, + 'type': 'cloud/alexa/sync', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 'timeout' + + +async def test_sync_alexa_entities_no_token( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test sync Alexa entities when we have no token.""" + client = await hass_ws_client(hass) + with patch('homeassistant.components.cloud.alexa_config.AlexaConfig' + '.async_sync_entities', + side_effect=alexa_errors.NoTokenAvailable): + await client.send_json({ + 'id': 5, + 'type': 'cloud/alexa/sync', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 'alexa_relink' + + +async def test_enable_alexa_state_report_fail( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test enable Alexa entities state reporting when no token available.""" + client = await hass_ws_client(hass) + with patch('homeassistant.components.cloud.alexa_config.AlexaConfig' + '.async_sync_entities', + side_effect=alexa_errors.NoTokenAvailable): + await client.send_json({ + 'id': 5, + 'type': 'cloud/alexa/sync', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 'alexa_relink' From 87712b9fa5ded8bf12b4d8f09330643069f92f0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2019 22:23:41 -0700 Subject: [PATCH 31/38] Bumped version to 0.95.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 84444e9d580..a3bfbf3b2c7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 5d2f97de747caa6ca3a33221536c8a8fd0fc67a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Jun 2019 09:15:54 -0700 Subject: [PATCH 32/38] Updated frontend to 20190626.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2dae7aaa1ec..d4bd24f8ab7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190624.1" + "home-assistant-frontend==20190626.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c704336ddaf..1f36e9f8fdd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2018.04.16 cryptography==2.6.1 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190624.1 +home-assistant-frontend==20190626.0 importlib-metadata==0.15 jinja2>=2.10 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8f511a2a64c..37b53f6365d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -595,7 +595,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190624.1 +home-assistant-frontend==20190626.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36bb3c0ae84..413d239690a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -160,7 +160,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190624.1 +home-assistant-frontend==20190626.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From b47b555c4f66bc0435c7e0800c8ed374fdfa5774 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 24 Jun 2019 07:43:49 +0200 Subject: [PATCH 33/38] Bump pyatmo to v2.1.0 (#24724) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index d057dcd6e80..a8a8c28f237 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/components/netatmo", "requirements": [ - "pyatmo==2.0.1" + "pyatmo==2.1.0" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index 37b53f6365d..a0445ff1b5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1018,7 +1018,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.0.1 +pyatmo==2.1.0 # homeassistant.components.apple_tv pyatv==0.3.12 From ca4c6ffe8d0898bafca8ff549470830c2e27a1ff Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 25 Jun 2019 17:57:43 +0200 Subject: [PATCH 34/38] Handle timeouts gracefully (#24752) --- homeassistant/components/netatmo/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index a49c83d2dd9..ec8d8275b1b 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -371,6 +372,9 @@ class ThermostatData: except TypeError: _LOGGER.error("Error when getting homestatus.") return + except requests.exceptions.Timeout: + _LOGGER.warning("Timed out when connecting to Netatmo server.") + return _LOGGER.debug("Following is the debugging output for homestatus:") _LOGGER.debug(self.homestatus.rawData) for room in self.homestatus.rooms: From 92053342359584a662cba024ad0a988c3530c43e Mon Sep 17 00:00:00 2001 From: John Dyer Date: Tue, 25 Jun 2019 18:25:53 -0400 Subject: [PATCH 35/38] Update Waze route dependency to 0.10 (#24754) * Update manifest.json Update waze calculator to 0.10, this was supposed to have been done in #22428 but was missed. See discussion [here](https://community.home-assistant.io/t/waze-travel-time-update/50955/201) * Update requirements_all.txt --- homeassistant/components/waze_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 64b384356ce..09ae4f812d7 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -3,7 +3,7 @@ "name": "Waze travel time", "documentation": "https://www.home-assistant.io/components/waze_travel_time", "requirements": [ - "WazeRouteCalculator==0.9" + "WazeRouteCalculator==0.10" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index a0445ff1b5b..fd6af461a0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -93,7 +93,7 @@ TwitterAPI==2.5.9 # VL53L1X2==0.1.5 # homeassistant.components.waze_travel_time -WazeRouteCalculator==0.9 +WazeRouteCalculator==0.10 # homeassistant.components.yessssms YesssSMS==0.2.3 From 760b62e06816d38ae8c15112ad6a6262b506ec84 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Jun 2019 09:54:40 -0700 Subject: [PATCH 36/38] Ignore duplicate tradfri discovery (#24759) * Ignore duplicate tradfri discovery * Update name --- .../components/tradfri/config_flow.py | 13 +++++++++++-- homeassistant/components/tradfri/strings.json | 3 ++- tests/components/tradfri/test_config_flow.py | 19 ++++++++++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index bfabf4fd12a..7cdf4b9de6c 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -78,13 +78,22 @@ class FlowHandler(config_entries.ConfigFlow): async def async_step_zeroconf(self, user_input): """Handle zeroconf discovery.""" + host = user_input['host'] + + # pylint: disable=unsupported-assignment-operation + self.context['host'] = host + + if any(host == flow['context']['host'] + for flow in self._async_in_progress()): + return self.async_abort(reason='already_in_progress') + for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == user_input['host']: + if entry.data[CONF_HOST] == host: return self.async_abort( reason='already_configured' ) - self._host = user_input['host'] + self._host = host return await self.async_step_auth() async_step_homekit = async_step_zeroconf diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json index 38c58486a6a..868fbbed550 100644 --- a/homeassistant/components/tradfri/strings.json +++ b/homeassistant/components/tradfri/strings.json @@ -17,7 +17,8 @@ "timeout": "Timeout validating the code." }, "abort": { - "already_configured": "Bridge is already configured" + "already_configured": "Bridge is already configured.", + "already_in_progress": "Bridge configuration is already in progress." } } } diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 8fcc72dd4a5..490f8484bbf 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -258,7 +258,7 @@ async def test_discovery_duplicate_aborted(hass): async def test_import_duplicate_aborted(hass): - """Test a duplicate discovery host is ignored.""" + """Test a duplicate import host is ignored.""" MockConfigEntry( domain='tradfri', data={'host': 'some-host'} @@ -271,3 +271,20 @@ async def test_import_duplicate_aborted(hass): assert flow['type'] == data_entry_flow.RESULT_TYPE_ABORT assert flow['reason'] == 'already_configured' + + +async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): + """Test a duplicate discovery in progress is ignored.""" + result = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'zeroconf'}, data={ + 'host': '123.123.123.123' + }) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'zeroconf'}, data={ + 'host': '123.123.123.123' + }) + + assert result2['type'] == data_entry_flow.RESULT_TYPE_ABORT From 5fe8a43e36a83c40e42e28a9a2528f6bd40870dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 25 Jun 2019 22:09:04 +0200 Subject: [PATCH 37/38] Return correct name for met.no (#24763) --- homeassistant/components/met/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index c9d0912e623..e97918ceba1 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -166,7 +166,7 @@ class MetWeather(WeatherEntity): name = self._config.get(CONF_NAME) if name is not None: - return CONF_NAME + return name if self.track_home: return self.hass.config.location_name From 5f37852695aaedd48d927a6e05b9f15081b2b2ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Jun 2019 09:17:45 -0700 Subject: [PATCH 38/38] Bumped version to 0.95.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a3bfbf3b2c7..6cf77275f6e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)