mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Periodically attempt to discover new wemo devices (#44361)
* Periodically attempt to discover new wemo devices * Set self._stop=None after stopping the periodic discovery task * Use the async_fire_time_changed test helper to simplify test_discovery * Stop the pywemo registry outside of the async loop * Add a comment describing why async_fire_time_changed is used
This commit is contained in:
parent
677fc6e2bb
commit
d912e91e81
@ -11,9 +11,12 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI
|
|||||||
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
@ -90,7 +93,7 @@ async def async_setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, entry):
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Set up a wemo config entry."""
|
"""Set up a wemo config entry."""
|
||||||
config = hass.data[DOMAIN].pop("config")
|
config = hass.data[DOMAIN].pop("config")
|
||||||
|
|
||||||
@ -98,14 +101,16 @@ async def async_setup_entry(hass, entry):
|
|||||||
registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry()
|
registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry()
|
||||||
await hass.async_add_executor_job(registry.start)
|
await hass.async_add_executor_job(registry.start)
|
||||||
|
|
||||||
def stop_wemo(event):
|
wemo_dispatcher = WemoDispatcher(entry)
|
||||||
|
wemo_discovery = WemoDiscovery(hass, wemo_dispatcher)
|
||||||
|
|
||||||
|
async def async_stop_wemo(event):
|
||||||
"""Shutdown Wemo subscriptions and subscription thread on exit."""
|
"""Shutdown Wemo subscriptions and subscription thread on exit."""
|
||||||
_LOGGER.debug("Shutting down WeMo event subscriptions")
|
_LOGGER.debug("Shutting down WeMo event subscriptions")
|
||||||
registry.stop()
|
await hass.async_add_executor_job(registry.stop)
|
||||||
|
wemo_discovery.async_stop_discovery()
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo)
|
||||||
|
|
||||||
devices = {}
|
|
||||||
|
|
||||||
static_conf = config.get(CONF_STATIC, [])
|
static_conf = config.get(CONF_STATIC, [])
|
||||||
if static_conf:
|
if static_conf:
|
||||||
@ -116,28 +121,31 @@ async def async_setup_entry(hass, entry):
|
|||||||
for host, port in static_conf
|
for host, port in static_conf
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
if device is None:
|
if device:
|
||||||
continue
|
wemo_dispatcher.async_add_unique_device(hass, device)
|
||||||
|
|
||||||
devices.setdefault(device.serialnumber, device)
|
|
||||||
|
|
||||||
if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY):
|
if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY):
|
||||||
_LOGGER.debug("Scanning network for WeMo devices...")
|
await wemo_discovery.async_discover_and_schedule()
|
||||||
for device in await hass.async_add_executor_job(pywemo.discover_devices):
|
|
||||||
devices.setdefault(
|
|
||||||
device.serialnumber,
|
|
||||||
device,
|
|
||||||
)
|
|
||||||
|
|
||||||
loaded_components = set()
|
return True
|
||||||
|
|
||||||
for device in devices.values():
|
|
||||||
_LOGGER.debug(
|
class WemoDispatcher:
|
||||||
"Adding WeMo device at %s:%i (%s)",
|
"""Dispatch WeMo devices to the correct platform."""
|
||||||
device.host,
|
|
||||||
device.port,
|
def __init__(self, config_entry: ConfigEntry):
|
||||||
device.serialnumber,
|
"""Initialize the WemoDispatcher."""
|
||||||
)
|
self._config_entry = config_entry
|
||||||
|
self._added_serial_numbers = set()
|
||||||
|
self._loaded_components = set()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_unique_device(
|
||||||
|
self, hass: HomeAssistant, device: pywemo.WeMoDevice
|
||||||
|
) -> None:
|
||||||
|
"""Add a WeMo device to hass if it has not already been added."""
|
||||||
|
if device.serialnumber in self._added_serial_numbers:
|
||||||
|
return
|
||||||
|
|
||||||
component = WEMO_MODEL_DISPATCH.get(device.model_name, SWITCH_DOMAIN)
|
component = WEMO_MODEL_DISPATCH.get(device.model_name, SWITCH_DOMAIN)
|
||||||
|
|
||||||
@ -146,11 +154,13 @@ async def async_setup_entry(hass, entry):
|
|||||||
# - Component is being loaded, add to backlog
|
# - Component is being loaded, add to backlog
|
||||||
# - Component is loaded, backlog is gone, dispatch discovery
|
# - Component is loaded, backlog is gone, dispatch discovery
|
||||||
|
|
||||||
if component not in loaded_components:
|
if component not in self._loaded_components:
|
||||||
hass.data[DOMAIN]["pending"][component] = [device]
|
hass.data[DOMAIN]["pending"][component] = [device]
|
||||||
loaded_components.add(component)
|
self._loaded_components.add(component)
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
hass.config_entries.async_forward_entry_setup(
|
||||||
|
self._config_entry, component
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif component in hass.data[DOMAIN]["pending"]:
|
elif component in hass.data[DOMAIN]["pending"]:
|
||||||
@ -163,7 +173,48 @@ async def async_setup_entry(hass, entry):
|
|||||||
device,
|
device,
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
self._added_serial_numbers.add(device.serialnumber)
|
||||||
|
|
||||||
|
|
||||||
|
class WemoDiscovery:
|
||||||
|
"""Use SSDP to discover WeMo devices."""
|
||||||
|
|
||||||
|
ADDITIONAL_SECONDS_BETWEEN_SCANS = 10
|
||||||
|
MAX_SECONDS_BETWEEN_SCANS = 300
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, wemo_dispatcher: WemoDispatcher) -> None:
|
||||||
|
"""Initialize the WemoDiscovery."""
|
||||||
|
self._hass = hass
|
||||||
|
self._wemo_dispatcher = wemo_dispatcher
|
||||||
|
self._stop = None
|
||||||
|
self._scan_delay = 0
|
||||||
|
|
||||||
|
async def async_discover_and_schedule(self, *_) -> None:
|
||||||
|
"""Periodically scan the network looking for WeMo devices."""
|
||||||
|
_LOGGER.debug("Scanning network for WeMo devices...")
|
||||||
|
try:
|
||||||
|
for device in await self._hass.async_add_executor_job(
|
||||||
|
pywemo.discover_devices
|
||||||
|
):
|
||||||
|
self._wemo_dispatcher.async_add_unique_device(self._hass, device)
|
||||||
|
finally:
|
||||||
|
# Run discovery more frequently after hass has just started.
|
||||||
|
self._scan_delay = min(
|
||||||
|
self._scan_delay + self.ADDITIONAL_SECONDS_BETWEEN_SCANS,
|
||||||
|
self.MAX_SECONDS_BETWEEN_SCANS,
|
||||||
|
)
|
||||||
|
self._stop = async_call_later(
|
||||||
|
self._hass,
|
||||||
|
self._scan_delay,
|
||||||
|
self.async_discover_and_schedule,
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_stop_discovery(self) -> None:
|
||||||
|
"""Stop the periodic background scanning."""
|
||||||
|
if self._stop:
|
||||||
|
self._stop()
|
||||||
|
self._stop = None
|
||||||
|
|
||||||
|
|
||||||
def validate_static_config(host, port):
|
def validate_static_config(host, port):
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Fixtures for pywemo."""
|
"""Fixtures for pywemo."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pywemo
|
import pywemo
|
||||||
|
|
||||||
@ -26,9 +28,11 @@ def pywemo_registry_fixture():
|
|||||||
registry = create_autospec(pywemo.SubscriptionRegistry, instance=True)
|
registry = create_autospec(pywemo.SubscriptionRegistry, instance=True)
|
||||||
|
|
||||||
registry.callbacks = {}
|
registry.callbacks = {}
|
||||||
|
registry.semaphore = asyncio.Semaphore(value=0)
|
||||||
|
|
||||||
def on_func(device, type_filter, callback):
|
def on_func(device, type_filter, callback):
|
||||||
registry.callbacks[device.name] = callback
|
registry.callbacks[device.name] = callback
|
||||||
|
registry.semaphore.release()
|
||||||
|
|
||||||
registry.on.side_effect = on_func
|
registry.on.side_effect = on_func
|
||||||
|
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
"""Tests for the wemo component."""
|
"""Tests for the wemo component."""
|
||||||
from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import pywemo
|
||||||
|
|
||||||
|
from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, WemoDiscovery
|
||||||
from homeassistant.components.wemo.const import DOMAIN
|
from homeassistant.components.wemo.const import DOMAIN
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt
|
||||||
|
|
||||||
from .conftest import MOCK_HOST, MOCK_PORT
|
from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_SERIAL_NUMBER
|
||||||
|
|
||||||
|
from tests.async_mock import create_autospec, patch
|
||||||
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
async def test_config_no_config(hass):
|
async def test_config_no_config(hass):
|
||||||
@ -87,3 +95,47 @@ async def test_static_config_with_invalid_host(hass):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert not setup_success
|
assert not setup_success
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery(hass, pywemo_registry):
|
||||||
|
"""Verify that discovery dispatches devices to the platform for setup."""
|
||||||
|
|
||||||
|
def create_device(counter):
|
||||||
|
"""Create a unique mock Motion detector device for each counter value."""
|
||||||
|
device = create_autospec(pywemo.Motion, instance=True)
|
||||||
|
device.host = f"{MOCK_HOST}_{counter}"
|
||||||
|
device.port = MOCK_PORT + counter
|
||||||
|
device.name = f"{MOCK_NAME}_{counter}"
|
||||||
|
device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{counter}"
|
||||||
|
device.model_name = "Motion"
|
||||||
|
device.get_state.return_value = 0 # Default to Off
|
||||||
|
return device
|
||||||
|
|
||||||
|
pywemo_devices = [create_device(0), create_device(1)]
|
||||||
|
# Setup the component and start discovery.
|
||||||
|
with patch(
|
||||||
|
"pywemo.discover_devices", return_value=pywemo_devices
|
||||||
|
) as mock_discovery:
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}}
|
||||||
|
)
|
||||||
|
await pywemo_registry.semaphore.acquire() # Returns after platform setup.
|
||||||
|
mock_discovery.assert_called()
|
||||||
|
pywemo_devices.append(create_device(2))
|
||||||
|
|
||||||
|
# Test that discovery runs periodically and the async_dispatcher_send code works.
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass,
|
||||||
|
dt.utcnow()
|
||||||
|
+ timedelta(seconds=WemoDiscovery.ADDITIONAL_SECONDS_BETWEEN_SCANS + 1),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Verify that the expected number of devices were setup.
|
||||||
|
entity_reg = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
entity_entries = list(entity_reg.entities.values())
|
||||||
|
assert len(entity_entries) == 3
|
||||||
|
|
||||||
|
# Verify that hass stops cleanly.
|
||||||
|
await hass.async_stop()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user