From ab1939f56f4dbcfc3c954059410f2f854c5fe9ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <balloob@gmail.com> Date: Mon, 25 Jun 2018 16:04:17 -0400 Subject: [PATCH 1/7] Bump frontend to 20180625.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 3d2231ab43b..54a77af5cfb 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180622.1'] +REQUIREMENTS = ['home-assistant-frontend==20180625.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 52a5e052560..65f6485f176 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.1 +home-assistant-frontend==20180625.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a38c7f259b4..e1fdc2cb3d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.1 +home-assistant-frontend==20180625.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 1c8b52f63073ca2f955e4acf1ebba40ee3193262 Mon Sep 17 00:00:00 2001 From: Jason Hu <awarecan@users.noreply.github.com> Date: Mon, 25 Jun 2018 13:06:00 -0700 Subject: [PATCH 2/7] Prevent Nest component setup crash due insufficient permission. (#14966) * Prevent Nest component setup crash due insufficient permission. * Trigger CI * Better error handle and address code review comments * Lint * Tiny wording adjust * Notify user if async_setup_entry failed * Return False if exception occurred in NestDevice.initialize --- homeassistant/components/nest/__init__.py | 85 +++++++++++++---------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index bd74897371a..bf99fadc1d7 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -122,7 +122,8 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("proceeding with setup") conf = hass.data.get(DATA_NEST_CONFIG, {}) hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - await hass.async_add_job(hass.data[DATA_NEST].initialize) + if not await hass.async_add_job(hass.data[DATA_NEST].initialize): + return False for component in 'climate', 'camera', 'sensor', 'binary_sensor': hass.async_add_job(hass.config_entries.async_forward_entry_setup( @@ -192,63 +193,73 @@ class NestDevice(object): def initialize(self): """Initialize Nest.""" - if self.local_structure is None: - self.local_structure = [s.name for s in self.nest.structures] + from nest.nest import AuthorizationError, APIError + try: + # Do not optimize next statement, it is here for initialize + # persistence Nest API connection. + structure_names = [s.name for s in self.nest.structures] + if self.local_structure is None: + self.local_structure = structure_names + + except (AuthorizationError, APIError, socket.error) as err: + _LOGGER.error( + "Connection error while access Nest web service: %s", err) + return False + return True def structures(self): """Generate a list of structures.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - yield structure - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + yield structure + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) def thermostats(self): - """Generate a list of thermostats and their location.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.thermostats: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, self.local_structure) - except socket.error: - _LOGGER.error( - "Connection error logging into the nest web service.") + """Generate a list of thermostats.""" + return self._devices('thermostats') def smoke_co_alarms(self): """Generate a list of smoke co alarms.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.smoke_co_alarms: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, self.local_structure) - except socket.error: - _LOGGER.error( - "Connection error logging into the nest web service.") + return self._devices('smoke_co_alarms') def cameras(self): """Generate a list of cameras.""" + return self._devices('cameras') + + def _devices(self, device_type): + """Generate a list of Nest devices.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.cameras: - yield (structure, device) - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + + for device in getattr(structure, device_type, []): + try: + # Do not optimize next statement, + # it is here for verify Nest API permission. + device.name_long + except KeyError: + _LOGGER.warning("Cannot retrieve device name for [%s]" + ", please check your Nest developer " + "account permission settings.", + device.serial) + continue + yield (structure, device) + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) class NestSensorDevice(Entity): From 893e0f8db630f2c6dbc35b6ec9daa6a4118ed526 Mon Sep 17 00:00:00 2001 From: Aaron Bach <bachya1208@gmail.com> Date: Sat, 23 Jun 2018 13:22:48 -0600 Subject: [PATCH 3/7] Fix socket bug with Yi in 0.72 (#15109) * Fixes BrokenPipeError exceptions with Yi (#15108) * Make sure to close the socket --- homeassistant/components/camera/yi.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 868c5afb447..93f526c2b96 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -53,7 +53,6 @@ class YiCamera(Camera): """Initialize.""" super().__init__() self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) - self._ftp = None self._last_image = None self._last_url = None self._manager = hass.data[DATA_FFMPEG] @@ -64,8 +63,6 @@ class YiCamera(Camera): self.user = config[CONF_USERNAME] self.passwd = config[CONF_PASSWORD] - hass.async_add_job(self._connect_to_client) - @property def brand(self): """Camera brand.""" @@ -76,38 +73,35 @@ class YiCamera(Camera): """Return the name of this camera.""" return self._name - async def _connect_to_client(self): - """Attempt to establish a connection via FTP.""" + async def _get_latest_video_url(self): + """Retrieve the latest video file from the customized Yi FTP server.""" from aioftp import Client, StatusCodeError ftp = Client() try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) - self._ftp = ftp except StatusCodeError as err: raise PlatformNotReady(err) - async def _get_latest_video_url(self): - """Retrieve the latest video file from the customized Yi FTP server.""" - from aioftp import StatusCodeError - try: - await self._ftp.change_directory(self.path) + await ftp.change_directory(self.path) dirs = [] - for path, attrs in await self._ftp.list(): + for path, attrs in await ftp.list(): if attrs['type'] == 'dir' and '.' not in str(path): dirs.append(path) latest_dir = dirs[-1] - await self._ftp.change_directory(latest_dir) + await ftp.change_directory(latest_dir) videos = [] - for path, _ in await self._ftp.list(): + for path, _ in await ftp.list(): videos.append(path) if not videos: _LOGGER.info('Video folder "%s" empty; delaying', latest_dir) return None + await ftp.quit() + return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( self.user, self.passwd, self.host, self.port, self.path, latest_dir, videos[-1]) From 69502163bd298ed2eff13d76893e2129d477ec07 Mon Sep 17 00:00:00 2001 From: Jason Hu <awarecan@users.noreply.github.com> Date: Mon, 25 Jun 2018 10:13:41 -0700 Subject: [PATCH 4/7] Skip nest security state sensor if no Nest Cam exists (#15112) --- homeassistant/components/sensor/nest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index bf1b3f65c4a..75c25f25baa 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -24,10 +24,14 @@ PROTECT_SENSOR_TYPES = ['co_status', # color_status: "gray", "green", "yellow", "red" 'color_status'] -STRUCTURE_SENSOR_TYPES = ['eta', 'security_state'] +STRUCTURE_SENSOR_TYPES = ['eta'] + +# security_state is structure level sensor, but only meaningful when +# Nest Cam exist +STRUCTURE_CAMERA_SENSOR_TYPES = ['security_state'] _VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ - + STRUCTURE_SENSOR_TYPES + + STRUCTURE_SENSOR_TYPES + STRUCTURE_CAMERA_SENSOR_TYPES SENSOR_UNITS = {'humidity': '%'} @@ -105,6 +109,14 @@ async def async_setup_entry(hass, entry, async_add_devices): for variable in conditions if variable in PROTECT_SENSOR_TYPES] + structures_has_camera = {} + for structure, device in nest.cameras(): + structures_has_camera[structure] = True + for structure in structures_has_camera: + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_CAMERA_SENSOR_TYPES] + return all_sensors async_add_devices(await hass.async_add_job(get_sensors), True) From 3f21966ec92446571f095f158c848b56c9350e10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <paulus@paulusschoutsen.nl> Date: Mon, 25 Jun 2018 15:59:05 -0400 Subject: [PATCH 5/7] Fix cast config (#15143) --- homeassistant/components/media_player/cast.py | 12 ++-- tests/components/media_player/test_cast.py | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index eced0dbbe25..4e24d5f2f71 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -4,7 +4,7 @@ Provide functionality to interact with Cast devices on the network. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.cast/ """ -# pylint: disable=import-error +import asyncio import logging import threading from typing import Optional, Tuple @@ -200,9 +200,13 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up Cast from a config entry.""" - await _async_setup_platform( - hass, hass.data[CAST_DOMAIN].get('media_player', {}), - async_add_devices, None) + config = hass.data[CAST_DOMAIN].get('media_player', {}) + if not isinstance(config, list): + config = [config] + + await asyncio.wait([ + _async_setup_platform(hass, cfg, async_add_devices, None) + for cfg in config]) async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 41cf6749b71..47be39c68e5 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -17,6 +17,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ from homeassistant.components.media_player import cast from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry, mock_coro + @pytest.fixture(autouse=True) def cast_mock(): @@ -359,3 +361,56 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert chromecast.disconnect.call_count == 1 + + +async def test_entry_setup_no_config(hass: HomeAssistantType): + """Test setting up entry with no config..""" + await async_setup_component(hass, 'cast', {}) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {} + + +async def test_entry_setup_single_config(hass: HomeAssistantType): + """Test setting up entry and having a single config option.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': { + 'host': 'bla' + } + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + + +async def test_entry_setup_list_config(hass: HomeAssistantType): + """Test setting up entry and having multiple config options.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': [ + {'host': 'bla'}, + {'host': 'blu'}, + ] + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 2 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + assert mock_setup.mock_calls[1][1][1] == {'host': 'blu'} From 2520fddbdf5ee8680eaa4fbb2d56075746c47c7e Mon Sep 17 00:00:00 2001 From: Jason Hu <awarecan@users.noreply.github.com> Date: Mon, 25 Jun 2018 10:04:32 -0700 Subject: [PATCH 6/7] Bump python-nest to 4.0.3 (#15098) Resolve network reconnect issue --- homeassistant/components/nest/__init__.py | 3 ++- homeassistant/components/sensor/nest.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index bf99fadc1d7..58fa1953ef0 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN from . import local_auth -REQUIREMENTS = ['python-nest==4.0.2'] +REQUIREMENTS = ['python-nest==4.0.3'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -86,6 +86,7 @@ async def async_nest_update_event_broker(hass, nest): _LOGGER.debug("dispatching nest data update") async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) else: + _LOGGER.debug("stop listening nest.update_event") return diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 75c25f25baa..d2e1501ad7e 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -145,7 +145,8 @@ class NestBasicSensor(NestSensorDevice): elif self.variable in PROTECT_SENSOR_TYPES \ and self.variable != 'color_status': # keep backward compatibility - self._state = getattr(self.device, self.variable).capitalize() + state = getattr(self.device, self.variable) + self._state = state.capitalize() if state is not None else None else: self._state = getattr(self.device, self.variable) diff --git a/requirements_all.txt b/requirements_all.txt index 65f6485f176..cd9dfb5194a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1060,7 +1060,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.4 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1fdc2cb3d4..5f7967761de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -156,7 +156,7 @@ pyqwikswitch==0.8 python-forecastio==1.4.0 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.sensor.whois pythonwhois==2.4.3 From 9b950f51928d66293eb942bdf6a7fd4a8eaad12b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <balloob@gmail.com> Date: Mon, 25 Jun 2018 16:59:14 -0400 Subject: [PATCH 7/7] Bumped version to 0.72.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a22605c37f4..f1a4e55d662 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)