diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 9b1c4cd465f..3e4081ae300 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,4 +1,5 @@ """Support for WeMo device discovery.""" +import asyncio import logging import pywemo @@ -6,9 +7,9 @@ import requests import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.discovery import SERVICE_WEMO -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN @@ -26,9 +27,6 @@ WEMO_MODEL_DISPATCH = { "Socket": "switch", } -SUBSCRIPTION_REGISTRY = None -KNOWN_DEVICES = [] - _LOGGER = logging.getLogger(__name__) @@ -70,9 +68,13 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up for WeMo devices.""" - hass.data[DOMAIN] = config + hass.data[DOMAIN] = { + "config": config.get(DOMAIN, {}), + "registry": None, + "pending": {}, + } if DOMAIN in config: hass.async_create_task( @@ -86,106 +88,103 @@ def setup(hass, config): async def async_setup_entry(hass, entry): """Set up a wemo config entry.""" - - config = hass.data[DOMAIN] - - # Keep track of WeMo devices - devices = [] + config = hass.data[DOMAIN].pop("config") # Keep track of WeMo device subscriptions for push updates - global SUBSCRIPTION_REGISTRY - SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() - await hass.async_add_executor_job(SUBSCRIPTION_REGISTRY.start) + registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() + await hass.async_add_executor_job(registry.start) def stop_wemo(event): """Shutdown Wemo subscriptions and subscription thread on exit.""" _LOGGER.debug("Shutting down WeMo event subscriptions") - SUBSCRIPTION_REGISTRY.stop() + registry.stop() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) - def setup_url_for_device(device): - """Determine setup.xml url for given device.""" - return f"http://{device.host}:{device.port}/setup.xml" + devices = {} - def setup_url_for_address(host, port): - """Determine setup.xml url for given host and port pair.""" - if not port: - port = pywemo.ouimeaux_device.probe_wemo(host) - - if not port: - return None - - return f"http://{host}:{port}/setup.xml" - - def discovery_dispatch(service, discovery_info): - """Dispatcher for incoming WeMo discovery events.""" - # name, model, location, mac - model_name = discovery_info.get("model_name") - serial = discovery_info.get("serial") - - # Only register a device once - if serial in KNOWN_DEVICES: - _LOGGER.debug("Ignoring known device %s %s", service, discovery_info) - return - - _LOGGER.debug("Discovered unique WeMo device: %s", serial) - KNOWN_DEVICES.append(serial) - - component = WEMO_MODEL_DISPATCH.get(model_name, "switch") - - discovery.load_platform(hass, component, DOMAIN, discovery_info, config) - - discovery.async_listen(hass, SERVICE_WEMO, discovery_dispatch) - - def discover_wemo_devices(now): - """Run discovery for WeMo devices.""" - _LOGGER.debug("Beginning WeMo device discovery...") + static_conf = config.get(CONF_STATIC, []) + if static_conf: _LOGGER.debug("Adding statically configured WeMo devices...") - for host, port in config.get(DOMAIN, {}).get(CONF_STATIC, []): - url = setup_url_for_address(host, port) - - if not url: - _LOGGER.error( - "Unable to get description url for WeMo at: %s", - f"{host}:{port}" if port else host, - ) + for device in await asyncio.gather( + *[ + hass.async_add_executor_job(validate_static_config, host, port) + for host, port in static_conf + ] + ): + if device is None: continue - try: - device = pywemo.discovery.device_from_description(url, None) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ) as err: - _LOGGER.error("Unable to access WeMo at %s (%s)", url, err) - continue + devices.setdefault(device.serialnumber, device) - if not [d[1] for d in devices if d[1].serialnumber == device.serialnumber]: - devices.append((url, device)) + if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): + _LOGGER.debug("Scanning network for WeMo devices...") + for device in await hass.async_add_executor_job(pywemo.discover_devices): + devices.setdefault( + device.serialnumber, device, + ) - if config.get(DOMAIN, {}).get(CONF_DISCOVERY, DEFAULT_DISCOVERY): - _LOGGER.debug("Scanning network for WeMo devices...") - for device in pywemo.discover_devices(): - if not [ - d[1] for d in devices if d[1].serialnumber == device.serialnumber - ]: - devices.append((setup_url_for_device(device), device)) + loaded_components = set() - for url, device in devices: - _LOGGER.debug("Adding WeMo device at %s:%i", device.host, device.port) + for device in devices.values(): + _LOGGER.debug( + "Adding WeMo device at %s:%i (%s)", + device.host, + device.port, + device.serialnumber, + ) - discovery_info = { - "model_name": device.model_name, - "serial": device.serialnumber, - "mac_address": device.mac, - "ssdp_description": url, - } + component = WEMO_MODEL_DISPATCH.get(device.model_name, "switch") - discovery_dispatch(SERVICE_WEMO, discovery_info) + # Three cases: + # - First time we see component, we need to load it and initialize the backlog + # - Component is being loaded, add to backlog + # - Component is loaded, backlog is gone, dispatch discovery - _LOGGER.debug("WeMo device discovery has finished") + if component not in loaded_components: + hass.data[DOMAIN]["pending"][component] = [device] + loaded_components.add(component) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, discover_wemo_devices) + elif component in hass.data[DOMAIN]["pending"]: + hass.data[DOMAIN]["pending"].append(device) + + else: + async_dispatcher_send( + hass, f"{DOMAIN}.{component}", device, + ) return True + + +def validate_static_config(host, port): + """Handle a static config.""" + url = setup_url_for_address(host, port) + + if not url: + _LOGGER.error( + "Unable to get description url for WeMo at: %s", + f"{host}:{port}" if port else host, + ) + return None + + try: + device = pywemo.discovery.device_from_description(url, None) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,) as err: + _LOGGER.error("Unable to access WeMo at %s (%s)", url, err) + return None + + return device + + +def setup_url_for_address(host, port): + """Determine setup.xml url for given host and port pair.""" + if not port: + port = pywemo.ouimeaux_device.probe_wemo(host) + + if not port: + return None + + return f"http://{host}:{port}/setup.xml" diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 6f7c9e7ee2b..db1ba60364e 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -3,41 +3,36 @@ import asyncio import logging import async_timeout -from pywemo import discovery -import requests from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN as WEMO_DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Register discovered WeMo binary sensors.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo binary sensors.""" - if discovery_info is not None: - location = discovery_info["ssdp_description"] - mac = discovery_info["mac_address"] + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" + async_add_entities([WemoBinarySensor(device)]) - try: - device = discovery.device_from_description(location, mac) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ) as err: - _LOGGER.error("Unable to access %s (%s)", location, err) - raise PlatformNotReady + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.binary_sensor", _discovered_wemo) - if device: - add_entities([WemoBinarySensor(hass, device)]) + await asyncio.gather( + *[ + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor") + ] + ) class WemoBinarySensor(BinarySensorDevice): """Representation a WeMo binary sensor.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize the WeMo sensor.""" self.wemo = device self._state = None @@ -67,7 +62,7 @@ class WemoBinarySensor(BinarySensorDevice): # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() - registry = SUBSCRIPTION_REGISTRY + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) @@ -126,3 +121,13 @@ class WemoBinarySensor(BinarySensorDevice): def available(self): """Return true if sensor is available.""" return self._available + + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.wemo.name, + "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index ac1e202f38d..cec481a2eb4 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -4,8 +4,6 @@ from datetime import timedelta import logging import async_timeout -from pywemo import discovery -import requests import voluptuous as vol from homeassistant.components.fan import ( @@ -17,14 +15,17 @@ from homeassistant.components.fan import ( FanEntity, ) from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import SUBSCRIPTION_REGISTRY -from .const import DOMAIN, SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY +from .const import ( + DOMAIN as WEMO_DOMAIN, + SERVICE_RESET_FILTER_LIFE, + SERVICE_SET_HUMIDITY, +) SCAN_INTERVAL = timedelta(seconds=10) -DATA_KEY = "fan.wemo" +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) @@ -91,36 +92,30 @@ SET_HUMIDITY_SCHEMA = vol.Schema( 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.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo binary sensors.""" + entities = [] - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" + entity = WemoHumidifier(device) + entities.append(entity) + async_add_entities([entity]) - if discovery_info is None: - return + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo) - location = discovery_info["ssdp_description"] - mac = discovery_info["mac_address"] - - try: - device = WemoHumidifier(discovery.device_from_description(location, mac)) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as err: - _LOGGER.error("Unable to access %s (%s)", location, err) - raise PlatformNotReady - - hass.data[DATA_KEY][device.entity_id] = device - add_entities([device]) + await asyncio.gather( + *[ + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("fan") + ] + ) def service_handle(service): """Handle the WeMo humidifier services.""" entity_ids = service.data.get(ATTR_ENTITY_ID) - humidifiers = [ - device - for device in hass.data[DATA_KEY].values() - if device.entity_id in entity_ids - ] + humidifiers = [entity for entity in entities if entity.entity_id in entity_ids] if service.service == SERVICE_SET_HUMIDITY: target_humidity = service.data.get(ATTR_TARGET_HUMIDITY) @@ -132,12 +127,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): humidifier.reset_filter_life() # Register service(s) - hass.services.register( - DOMAIN, SERVICE_SET_HUMIDITY, service_handle, schema=SET_HUMIDITY_SCHEMA + hass.services.async_register( + WEMO_DOMAIN, SERVICE_SET_HUMIDITY, service_handle, schema=SET_HUMIDITY_SCHEMA ) - hass.services.register( - DOMAIN, + hass.services.async_register( + WEMO_DOMAIN, SERVICE_RESET_FILTER_LIFE, service_handle, schema=RESET_FILTER_LIFE_SCHEMA, @@ -199,6 +194,16 @@ class WemoHumidifier(FanEntity): """Return true if switch is available.""" return self._available + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.wemo.name, + "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } + @property def icon(self): """Return the icon of device based on its type.""" @@ -236,7 +241,7 @@ class WemoHumidifier(FanEntity): # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() - registry = SUBSCRIPTION_REGISTRY + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 59b6d9e390e..8e43f47ef00 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -4,8 +4,6 @@ from datetime import timedelta import logging import async_timeout -from pywemo import discovery -import requests from homeassistant import util from homeassistant.components.light import ( @@ -19,10 +17,10 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, Light, ) -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN as WEMO_DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -34,29 +32,29 @@ SUPPORT_WEMO = ( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up discovered WeMo switches.""" - - if discovery_info is not None: - location = discovery_info["ssdp_description"] - mac = discovery_info["mac_address"] - - try: - device = discovery.device_from_description(location, mac) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ) as err: - _LOGGER.error("Unable to access %s (%s)", location, err) - raise PlatformNotReady +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo lights.""" + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" if device.model_name == "Dimmer": - add_entities([WemoDimmer(device)]) + async_add_entities([WemoDimmer(device)]) else: - setup_bridge(device, add_entities) + await hass.async_add_executor_job( + setup_bridge, hass, device, async_add_entities + ) + + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo) + + await asyncio.gather( + *[ + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("light") + ] + ) -def setup_bridge(bridge, add_entities): +def setup_bridge(hass, bridge, async_add_entities): """Set up a WeMo link.""" lights = {} @@ -73,7 +71,7 @@ def setup_bridge(bridge, add_entities): new_lights.append(lights[light_id]) if new_lights: - add_entities(new_lights) + hass.add_job(async_add_entities, new_lights) update_lights() @@ -110,6 +108,16 @@ class WemoLight(Light): """Return the name of the light.""" return self._name + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.wemo.name, + "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -235,7 +243,7 @@ class WemoDimmer(Light): # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() - registry = SUBSCRIPTION_REGISTRY + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 531ac34ce92..ad8ea45ffd6 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -4,18 +4,16 @@ from datetime import datetime, timedelta import logging import async_timeout -from pywemo import discovery -import requests from homeassistant.components.switch import SwitchDevice from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import convert -from . import SUBSCRIPTION_REGISTRY -from .const import DOMAIN +from .const import DOMAIN as WEMO_DOMAIN SCAN_INTERVAL = timedelta(seconds=10) +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) @@ -32,24 +30,21 @@ WEMO_OFF = 0 WEMO_STANDBY = 8 -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up discovered WeMo switches.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo switches.""" - if discovery_info is not None: - location = discovery_info["ssdp_description"] - mac = discovery_info["mac_address"] + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" + async_add_entities([WemoSwitch(device)]) - try: - device = discovery.device_from_description(location, mac) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ) as err: - _LOGGER.error("Unable to access %s (%s)", location, err) - raise PlatformNotReady + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.switch", _discovered_wemo) - if device: - add_entities([WemoSwitch(device)]) + await asyncio.gather( + *[ + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("switch") + ] + ) class WemoSwitch(SwitchDevice): @@ -97,7 +92,12 @@ class WemoSwitch(SwitchDevice): @property def device_info(self): """Return the device info.""" - return {"name": self._name, "identifiers": {(DOMAIN, self._serialnumber)}} + return { + "name": self.wemo.name, + "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } @property def device_state_attributes(self): @@ -200,7 +200,7 @@ class WemoSwitch(SwitchDevice): # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() - registry = SUBSCRIPTION_REGISTRY + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback)