Skip to content

Commit

Permalink
Various enhancements for WeMo component/platforms (home-assistant#19419)
Browse files Browse the repository at this point in the history
* WeMo - Various fixes and improvements

Various fixes & improvements to the WeMo components, including:
-- Fixes to rediscovery
-- New reset filter service for the WeMo Humidifier
-- Switched the remainder of the WeMo components to async IO
-- Removed any remaining IO in entity properties and moved them to the polling/subscription update process

* WeMo - Fix pywemo version and remove test code from WeMo fan component

* WeMo Humidifier - Add services.yaml entry for reset filter life service

* WeMo - Update binary_sensor component to use asyncio

* WeMo - Add available property to binary_sensor component

* WeMo - Fixed line length issue

* WeMo - Fix issue with discovering the same device multiple times

* WeMo - Fix for the fix for discovering devices multiple times

* WeMo - Fix long lines

* WeMo - Fixes from code review

* WeMo - Breaking Change - entity_ids is now required on wemo_set_humidity

* WeMo - Code review fixes

* WeMo - Code review fixes

* WeMo - Code review fixes
  • Loading branch information
sqldiablo authored and dshokouhi committed Dec 25, 2018
1 parent 148408d commit 1636cfa
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 100 deletions.
99 changes: 72 additions & 27 deletions homeassistant/components/binary_sensor/wemo.py
Expand Up @@ -4,7 +4,10 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.wemo/
"""
import asyncio
import logging

import async_timeout
import requests

from homeassistant.components.binary_sensor import BinarySensorDevice
Expand Down Expand Up @@ -41,48 +44,90 @@ def __init__(self, hass, device):
"""Initialize the WeMo sensor."""
self.wemo = device
self._state = None
self._available = True
self._update_lock = None
self._model_name = self.wemo.model_name
self._name = self.wemo.name
self._serialnumber = self.wemo.serialnumber

def _subscription_callback(self, _device, _type, _params):
"""Update the state by the Wemo sensor."""
_LOGGER.debug("Subscription update for %s", self.name)
updated = self.wemo.subscription_update(_type, _params)
self.hass.add_job(
self._async_locked_subscription_callback(not updated))

async def _async_locked_subscription_callback(self, force_update):
"""Handle an update from a subscription."""
# If an update is in progress, we don't do anything
if self._update_lock.locked():
return

wemo = hass.components.wemo
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
await self._async_locked_update(force_update)
self.async_schedule_update_ha_state()

def _update_callback(self, _device, _type, _params):
"""Handle state changes."""
_LOGGER.info("Subscription update for %s", _device)
updated = self.wemo.subscription_update(_type, _params)
self._update(force_update=(not updated))
async def async_added_to_hass(self):
"""Wemo sensor added to HASS."""
# Define inside async context so we know our event loop
self._update_lock = asyncio.Lock()

registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY
await self.hass.async_add_executor_job(registry.register, self.wemo)
registry.on(self.wemo, None, self._subscription_callback)

if not hasattr(self, 'hass'):
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 sensor 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
self.schedule_update_ha_state()

@property
def should_poll(self):
"""No polling needed with subscriptions."""
return False
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_executor_job(self._update, force_update)

def _update(self, force_update=True):
"""Update the sensor state."""
try:
self._state = self.wemo.get_state(force_update)

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

@property
def unique_id(self):
"""Return the id of this WeMo device."""
return self.wemo.serialnumber
"""Return the id of this WeMo sensor."""
return self._serialnumber

@property
def name(self):
"""Return the name of the service if any."""
return self.wemo.name
return self._name

@property
def is_on(self):
"""Return true if sensor is on."""
return self._state

def update(self):
"""Update WeMo state."""
self._update(force_update=True)

def _update(self, force_update=True):
try:
self._state = self.wemo.get_state(force_update)
except AttributeError as err:
_LOGGER.warning(
"Could not update status for %s (%s)", self.name, err)
@property
def available(self):
"""Return true if sensor is available."""
return self._available
9 changes: 8 additions & 1 deletion homeassistant/components/fan/services.yaml
Expand Up @@ -209,8 +209,15 @@ wemo_set_humidity:
description: Set the target humidity of WeMo humidifier devices.
fields:
entity_id:
description: Names of the WeMo humidifier entities (0 or more entities, if no entity_id is provided, all WeMo humidifiers will have the target humidity set).
description: Names of the WeMo humidifier entities (1 or more entity_ids are required).
example: 'fan.wemo_humidifier'
target_humidity:
description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value.
example: 56.5

wemo_reset_filter_life:
description: Reset the WeMo Humidifier's filter life to 100%.
fields:
entity_id:
description: Names of the WeMo humidifier entities (1 or more entity_ids are required).
example: 'fan.wemo_humidifier'
51 changes: 32 additions & 19 deletions homeassistant/components/fan/wemo.py
Expand Up @@ -78,11 +78,17 @@
SERVICE_SET_HUMIDITY = 'wemo_set_humidity'

SET_HUMIDITY_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_TARGET_HUMIDITY):
vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
})

SERVICE_RESET_FILTER_LIFE = 'wemo_reset_filter_life'

RESET_FILTER_LIFE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids
})


def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up discovered WeMo humidifiers."""
Expand Down Expand Up @@ -111,22 +117,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def service_handle(service):
"""Handle the WeMo humidifier services."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
target_humidity = service.data.get(ATTR_TARGET_HUMIDITY)

if entity_ids:
humidifiers = [device for device in hass.data[DATA_KEY].values() if
device.entity_id in entity_ids]
else:
humidifiers = hass.data[DATA_KEY].values()
humidifiers = [device for device in
hass.data[DATA_KEY].values() if
device.entity_id in entity_ids]

if service.service == SERVICE_SET_HUMIDITY:
target_humidity = service.data.get(ATTR_TARGET_HUMIDITY)

for humidifier in humidifiers:
humidifier.set_humidity(target_humidity)
for humidifier in humidifiers:
humidifier.set_humidity(target_humidity)
elif service.service == SERVICE_RESET_FILTER_LIFE:
for humidifier in humidifiers:
humidifier.reset_filter_life()

# Register service(s)
hass.services.register(
DOMAIN, SERVICE_SET_HUMIDITY, service_handle,
schema=SET_HUMIDITY_SCHEMA)

hass.services.register(
DOMAIN, SERVICE_RESET_FILTER_LIFE, service_handle,
schema=RESET_FILTER_LIFE_SCHEMA)


class WemoHumidifier(FanEntity):
"""Representation of a WeMo humidifier."""
Expand All @@ -137,17 +150,13 @@ def __init__(self, device):
self._state = None
self._available = True
self._update_lock = None

self._fan_mode = None
self._target_humidity = None
self._current_humidity = None
self._water_level = None
self._filter_life = None
self._filter_expired = None
self._last_fan_on_mode = WEMO_FAN_MEDIUM

# look up model name, name, and serial number
# once as it incurs network traffic
self._model_name = self.wemo.model_name
self._name = self.wemo.name
self._serialnumber = self.wemo.serialnumber
Expand Down Expand Up @@ -211,12 +220,12 @@ def speed(self) -> str:
return WEMO_FAN_SPEED_TO_HASS.get(self._fan_mode)

@property
def speed_list(self: FanEntity) -> list:
def speed_list(self) -> list:
"""Get the list of available speeds."""
return SUPPORTED_SPEEDS

@property
def supported_features(self: FanEntity) -> int:
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORTED_FEATURES

Expand Down Expand Up @@ -276,22 +285,22 @@ def _update(self, force_update=True):
self.name, err)
self._available = False

def turn_on(self: FanEntity, speed: str = None, **kwargs) -> None:
def turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the switch on."""
if speed is None:
self.wemo.set_state(self._last_fan_on_mode)
else:
self.set_speed(speed)

def turn_off(self: FanEntity, **kwargs) -> None:
def turn_off(self, **kwargs) -> None:
"""Turn the switch off."""
self.wemo.set_state(WEMO_FAN_OFF)

def set_speed(self: FanEntity, speed: str) -> None:
def set_speed(self, speed: str) -> None:
"""Set the fan_mode of the Humidifier."""
self.wemo.set_state(HASS_FAN_SPEED_TO_WEMO.get(speed))

def set_humidity(self: FanEntity, humidity: float) -> None:
def set_humidity(self, humidity: float) -> None:
"""Set the target humidity level for the Humidifier."""
if humidity < 50:
self.wemo.set_humidity(WEMO_HUMIDITY_45)
Expand All @@ -303,3 +312,7 @@ def set_humidity(self: FanEntity, humidity: float) -> None:
self.wemo.set_humidity(WEMO_HUMIDITY_60)
elif humidity >= 100:
self.wemo.set_humidity(WEMO_HUMIDITY_100)

def reset_filter_life(self) -> None:
"""Reset the filter life to 100%."""
self.wemo.reset_filter_life()

0 comments on commit 1636cfa

Please sign in to comment.