From eb1ca20cfc0509d8b591bc9ed7cece0f0838cd8a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 14:14:36 -0700 Subject: [PATCH 01/11] Version bump to 0.65.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 203a9c63d95..12d988c552e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 65 -PATCH_VERSION = '3' +PATCH_VERSION = '4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 101a6ab07c96e770b004059837ec39fec08431c7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 13:54:56 -0700 Subject: [PATCH 02/11] Fix unavailable property for wemo switch (#13106) * Fix unavailable property for wemo switch * Have subscriptions respect the lock * Move subscription callback to added to hass section --- homeassistant/components/switch/wemo.py | 91 +++++++++++++++++-------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 4339c92bb60..4f06f941558 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -4,16 +4,19 @@ Support for WeMo switches. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.wemo/ """ +import asyncio import logging from datetime import datetime, timedelta +import async_timeout + from homeassistant.components.switch import SwitchDevice from homeassistant.util import convert from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN) -from homeassistant.loader import get_component DEPENDENCIES = ['wemo'] +SCAN_INTERVAL = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) @@ -54,29 +57,35 @@ class WemoSwitch(SwitchDevice): self.maker_params = None self.coffeemaker_mode = None self._state = None + self._available = True + self._update_lock = None # look up model name once as it incurs network traffic self._model_name = self.wemo.model_name - wemo = get_component('wemo') - wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) - wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) - - def _update_callback(self, _device, _type, _params): + def _subscription_callback(self, _device, _type, _params): """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", _device) + _LOGGER.info("Subscription update for %s", self.name) updated = self.wemo.subscription_update(_type, _params) - self._update(force_update=(not updated)) + self.hass.add_job( + self._async_locked_subscription_callback(not updated)) - if not hasattr(self, 'hass'): + async def _async_locked_subscription_callback(self, force_update): + """Helper to handle an update from a subscription.""" + # If an update is in progress, we don't do anything + if self._update_lock.locked(): return - self.schedule_update_ha_state() + + await self._async_locked_update(force_update) + self.async_schedule_update_ha_state() @property def should_poll(self): - """No polling needed with subscriptions.""" - if self._model_name == 'Insight': - return True - return False + """Device should poll. + + Subscriptions push the state, however it won't detect if a device + is no longer available. Use polling to detect if a device is available. + """ + return True @property def unique_id(self): @@ -172,13 +181,7 @@ class WemoSwitch(SwitchDevice): @property def available(self): """Return true if switch is available.""" - if self._model_name == 'Insight' and self.insight_params is None: - return False - if self._model_name == 'Maker' and self.maker_params is None: - return False - if self._model_name == 'CoffeeMaker' and self.coffeemaker_mode is None: - return False - return True + return self._available @property def icon(self): @@ -189,21 +192,46 @@ class WemoSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - self._state = WEMO_ON self.wemo.on() - self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the switch off.""" - self._state = WEMO_OFF self.wemo.off() - self.schedule_update_ha_state() - def update(self): - """Update WeMo state.""" - self._update(force_update=True) + async def async_added_to_hass(self): + """Wemo switch added to HASS.""" + # Define inside async context so we know our event loop + self._update_lock = asyncio.Lock() - def _update(self, force_update=True): + registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY + await self.hass.async_add_job(registry.register, self.wemo) + registry.on(self.wemo, None, self._subscription_callback) + + async def async_update(self): + """Update WeMo state. + + Wemo has an aggressive retry logic that sometimes can take over a + minute to return. If we don't get a state after 5 seconds, assume the + Wemo switch is unreachable. If update goes through, it will be made + available again. + """ + # If an update is in progress, we don't do anything + if self._update_lock.locked(): + return + + try: + with async_timeout.timeout(5): + await asyncio.shield(self._async_locked_update(True)) + except asyncio.TimeoutError: + _LOGGER.warning('Lost connection to %s', self.name) + self._available = False + + async def _async_locked_update(self, force_update): + """Try updating within an async lock.""" + async with self._update_lock: + await self.hass.async_add_job(self._update, force_update) + + def _update(self, force_update): """Update the device state.""" try: self._state = self.wemo.get_state(force_update) @@ -215,6 +243,11 @@ class WemoSwitch(SwitchDevice): self.maker_params = self.wemo.maker_params elif self._model_name == 'CoffeeMaker': self.coffeemaker_mode = self.wemo.mode + + if not self._available: + _LOGGER.info('Reconnected to %s', self.name) + self._available = True except AttributeError as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) + self._available = False From 3560fa754c8c410b7c0f0d1e0d1d1d9f00de62aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 13:55:22 -0700 Subject: [PATCH 03/11] Catch if bridge goes unavailable (#13109) --- homeassistant/components/hue.py | 1 + homeassistant/components/light/hue.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue.py index d3870f0a3a1..f6e654ab44b 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue.py @@ -181,6 +181,7 @@ class HueBridge(object): self.allow_in_emulated_hue = allow_in_emulated_hue self.allow_hue_groups = allow_hue_groups + self.available = True self.bridge = None self.lights = {} self.lightgroups = {} diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 75825683928..661b7c2b3a1 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -123,15 +123,20 @@ def unthrottled_update_lights(hass, bridge, add_devices): api = bridge.get_api() except phue.PhueRequestTimeout: _LOGGER.warning("Timeout trying to reach the bridge") + bridge.available = False return except ConnectionRefusedError: _LOGGER.error("The bridge refused the connection") + bridge.available = False return except socket.error: # socket.error when we cannot reach Hue _LOGGER.exception("Cannot reach the bridge") + bridge.available = False return + bridge.available = True + new_lights = process_lights( hass, api, bridge, lambda **kw: update_lights(hass, bridge, add_devices, **kw)) @@ -266,8 +271,9 @@ class HueLight(Light): @property def available(self): """Return if light is available.""" - return (self.is_group or self.allow_unreachable or - self.info['state']['reachable']) + return self.bridge.available and (self.is_group or + self.allow_unreachable or + self.info['state']['reachable']) @property def supported_features(self): From c384fd96533955fded181c1fd8e301d072a43cbf Mon Sep 17 00:00:00 2001 From: Jeroen ter Heerdt Date: Mon, 12 Mar 2018 22:03:05 +0100 Subject: [PATCH 04/11] Adding check for empty discovery info in alarm control panel Egardia. (#13114) --- homeassistant/components/alarm_control_panel/egardia.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 64e165f6b16..845eb81bbe0 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -33,6 +33,8 @@ STATES = { def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Egardia platform.""" + if discovery_info is None: + return device = EgardiaAlarm( discovery_info['name'], hass.data[EGARDIA_DEVICE], From e54394e90698e95388ed1466ecab26de3dab8f2a Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 12 Mar 2018 16:56:33 -0400 Subject: [PATCH 05/11] Throttle Arlo api calls (#13143) --- homeassistant/components/arlo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 7e51ec8c045..77201e5ead9 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -5,11 +5,13 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/arlo/ """ import logging +from datetime import timedelta import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['pyarlo==0.1.2'] @@ -45,6 +47,7 @@ def setup(hass, config): arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: return False + arlo.update = Throttle(timedelta(seconds=10))(arlo.update) hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) From 874cccd5302477b072fd428230e8693dfe6a45d8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 14 Mar 2018 13:22:38 +0100 Subject: [PATCH 06/11] Bugfix HomeKit: Error string values for temperature (#13162) --- homeassistant/components/homekit/thermostats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/thermostats.py index 766a7e3585d..6d342273e8d 100644 --- a/homeassistant/components/homekit/thermostats.py +++ b/homeassistant/components/homekit/thermostats.py @@ -157,12 +157,12 @@ class Thermostat(HomeAccessory): # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) - if current_temp is not None: + if isinstance(current_temp, (int, float)): self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) - if target_temp is not None: + if isinstance(target_temp, (int, float)): if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) From 8a6370f7c91fec6c220bc2e438a236816c636341 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 13 Mar 2018 17:12:28 -0400 Subject: [PATCH 07/11] Revert throttle Arlo api calls (#13174) --- homeassistant/components/arlo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 77201e5ead9..7e51ec8c045 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -5,13 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/arlo/ """ import logging -from datetime import timedelta import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv -from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['pyarlo==0.1.2'] @@ -47,7 +45,6 @@ def setup(hass, config): arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: return False - arlo.update = Throttle(timedelta(seconds=10))(arlo.update) hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) From 4e569ac0c3f1a24c3cad2a66e6f57581abc587f3 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Mar 2018 20:08:41 +0100 Subject: [PATCH 08/11] Ignore unsupported Sonos favorites (#13195) --- homeassistant/components/media_player/sonos.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 9ea33b4c396..ec9bdf34d56 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -426,7 +426,17 @@ class SonosDevice(MediaPlayerDevice): self._play_mode = self.soco.play_mode self._night_sound = self.soco.night_mode self._speech_enhance = self.soco.dialog_mode - self._favorites = self.soco.music_library.get_sonos_favorites() + + self._favorites = [] + for fav in self.soco.music_library.get_sonos_favorites(): + # SoCo 0.14 raises a generic Exception on invalid xml in favorites. + # Filter those out now so our list is safe to use. + try: + if fav.reference.get_uri(): + self._favorites.append(fav) + # pylint: disable=broad-except + except Exception: + _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) def _subscribe_to_player_events(self): """Add event subscriptions.""" From 30a1fedce8e8a736bae88b2186ef531fc9a5d99b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Mar 2018 20:07:50 +0100 Subject: [PATCH 09/11] Avoid Sonos error when joining with self (#13196) --- homeassistant/components/media_player/sonos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index ec9bdf34d56..2a12b59e7c7 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -896,7 +896,8 @@ class SonosDevice(MediaPlayerDevice): self.soco.unjoin() for slave in slaves: - slave.soco.join(self.soco) + if slave.unique_id != self.unique_id: + slave.soco.join(self.soco) @soco_error() def unjoin(self): From 25fe6ec53672d493c4ace565fa60220ecb6c51a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 15:07:37 -0700 Subject: [PATCH 10/11] Fix input_boolean Google Assistant serialize error (#13220) --- .../components/google_assistant/smart_home.py | 22 ++++++++++++++----- .../google_assistant/test_smart_home.py | 15 +++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 48d24c00b97..834d40c367c 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -17,7 +17,16 @@ from homeassistant.core import callback from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES) from homeassistant.components import ( - switch, light, cover, media_player, group, fan, scene, script, climate, + climate, + cover, + fan, + group, + input_boolean, + light, + media_player, + scene, + script, + switch, ) from . import trait @@ -33,15 +42,16 @@ HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) DOMAIN_TO_GOOGLE_TYPES = { + climate.DOMAIN: TYPE_THERMOSTAT, + cover.DOMAIN: TYPE_SWITCH, + fan.DOMAIN: TYPE_SWITCH, group.DOMAIN: TYPE_SWITCH, + input_boolean.DOMAIN: TYPE_SWITCH, + light.DOMAIN: TYPE_LIGHT, + media_player.DOMAIN: TYPE_SWITCH, scene.DOMAIN: TYPE_SCENE, script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, - fan.DOMAIN: TYPE_SWITCH, - light.DOMAIN: TYPE_LIGHT, - cover.DOMAIN: TYPE_SWITCH, - media_player.DOMAIN: TYPE_SWITCH, - climate.DOMAIN: TYPE_THERMOSTAT, } diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 8d139fa8211..24d74afa6da 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,4 +1,5 @@ """Test Google Smart Home.""" +from homeassistant.core import State from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from homeassistant.setup import async_setup_component @@ -244,3 +245,17 @@ async def test_raising_error_trait(hass): }] } } + + +def test_serialize_input_boolean(): + """Test serializing an input boolean entity.""" + state = State('input_boolean.bla', 'on') + entity = sh._GoogleEntity(None, BASIC_CONFIG, state) + assert entity.sync_serialize() == { + 'id': 'input_boolean.bla', + 'attributes': {}, + 'name': {'name': 'bla'}, + 'traits': ['action.devices.traits.OnOff'], + 'type': 'action.devices.types.SWITCH', + 'willReportState': False, + } From 8e05a5c12bd99484d7d8100263113e58ea30d766 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 15:08:34 -0700 Subject: [PATCH 11/11] Version bump to 0.65.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 12d988c552e..7e2b9f3061a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 65 -PATCH_VERSION = '4' +PATCH_VERSION = '5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)