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