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.light import DOMAIN as LIGHT_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.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@ -90,7 +93,7 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up a wemo config entry."""
|
||||
config = hass.data[DOMAIN].pop("config")
|
||||
|
||||
@ -98,14 +101,16 @@ async def async_setup_entry(hass, entry):
|
||||
registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry()
|
||||
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."""
|
||||
_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)
|
||||
|
||||
devices = {}
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo)
|
||||
|
||||
static_conf = config.get(CONF_STATIC, [])
|
||||
if static_conf:
|
||||
@ -116,28 +121,31 @@ async def async_setup_entry(hass, entry):
|
||||
for host, port in static_conf
|
||||
]
|
||||
):
|
||||
if device is None:
|
||||
continue
|
||||
|
||||
devices.setdefault(device.serialnumber, device)
|
||||
if device:
|
||||
wemo_dispatcher.async_add_unique_device(hass, 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,
|
||||
)
|
||||
await wemo_discovery.async_discover_and_schedule()
|
||||
|
||||
loaded_components = set()
|
||||
return True
|
||||
|
||||
for device in devices.values():
|
||||
_LOGGER.debug(
|
||||
"Adding WeMo device at %s:%i (%s)",
|
||||
device.host,
|
||||
device.port,
|
||||
device.serialnumber,
|
||||
)
|
||||
|
||||
class WemoDispatcher:
|
||||
"""Dispatch WeMo devices to the correct platform."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry):
|
||||
"""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)
|
||||
|
||||
@ -146,11 +154,13 @@ async def async_setup_entry(hass, entry):
|
||||
# - Component is being loaded, add to backlog
|
||||
# - 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]
|
||||
loaded_components.add(component)
|
||||
self._loaded_components.add(component)
|
||||
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"]:
|
||||
@ -163,7 +173,48 @@ async def async_setup_entry(hass, entry):
|
||||
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):
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Fixtures for pywemo."""
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
import pywemo
|
||||
|
||||
@ -26,9 +28,11 @@ def pywemo_registry_fixture():
|
||||
registry = create_autospec(pywemo.SubscriptionRegistry, instance=True)
|
||||
|
||||
registry.callbacks = {}
|
||||
registry.semaphore = asyncio.Semaphore(value=0)
|
||||
|
||||
def on_func(device, type_filter, callback):
|
||||
registry.callbacks[device.name] = callback
|
||||
registry.semaphore.release()
|
||||
|
||||
registry.on.side_effect = on_func
|
||||
|
||||
|
@ -1,9 +1,17 @@
|
||||
"""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.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):
|
||||
@ -87,3 +95,47 @@ async def test_static_config_with_invalid_host(hass):
|
||||
},
|
||||
)
|
||||
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