Fully unload wemo config entry (#96620)

* Fully unload wemo config entity

* Test reloading the config entry

* Encapsulate data with dataclasses

* Fix missing test coverage

* Replace if with assert for options that are always set

* Move WemoData/WemoConfigEntryData to models.py

* Use _ to indicate unused argument

* Test that the entry and entity work after reloading

* Nit: Slight test reordering

* Reset the correct mock (get_state)

* from .const import DOMAIN

* Nit: _async_wemo_data -> async_wemo_data; not module private
pull/96537/head^2
Eric Severance 2023-07-20 01:06:16 -07:00 committed by GitHub
parent 0349e47372
commit 5ffffd8dbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 245 additions and 124 deletions

View File

@ -1,9 +1,10 @@
"""Support for WeMo device discovery."""
from __future__ import annotations
from collections.abc import Sequence
from collections.abc import Callable, Coroutine, Sequence
from datetime import datetime
import logging
from typing import Any
import pywemo
import voluptuous as vol
@ -13,13 +14,13 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DISCOVERY, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import CALLBACK_TYPE, Event, 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 homeassistant.helpers.typing import ConfigType
from homeassistant.util.async_ import gather_with_concurrency
from .const import DOMAIN
from .wemo_device import async_register_device
from .models import WemoConfigEntryData, WemoData, async_wemo_data
from .wemo_device import DeviceCoordinator, async_register_device
# Max number of devices to initialize at once. This limit is in place to
# avoid tying up too many executor threads with WeMo device setup.
@ -42,6 +43,7 @@ WEMO_MODEL_DISPATCH = {
_LOGGER = logging.getLogger(__name__)
DispatchCallback = Callable[[DeviceCoordinator], Coroutine[Any, Any, None]]
HostPortTuple = tuple[str, int | None]
@ -81,11 +83,26 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up for WeMo devices."""
hass.data[DOMAIN] = {
"config": config.get(DOMAIN, {}),
"registry": None,
"pending": {},
}
# Keep track of WeMo device subscriptions for push updates
registry = pywemo.SubscriptionRegistry()
await hass.async_add_executor_job(registry.start)
# Respond to discovery requests from WeMo devices.
discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port)
await hass.async_add_executor_job(discovery_responder.start)
async def _on_hass_stop(_: Event) -> None:
await hass.async_add_executor_job(discovery_responder.stop)
await hass.async_add_executor_job(registry.stop)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop)
yaml_config = config.get(DOMAIN, {})
hass.data[DOMAIN] = WemoData(
discovery_enabled=yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY),
static_config=yaml_config.get(CONF_STATIC, []),
registry=registry,
)
if DOMAIN in config:
hass.async_create_task(
@ -99,45 +116,48 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a wemo config entry."""
config = hass.data[DOMAIN].pop("config")
# Keep track of WeMo device subscriptions for push updates
registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry()
await hass.async_add_executor_job(registry.start)
# Respond to discovery requests from WeMo devices.
discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port)
await hass.async_add_executor_job(discovery_responder.start)
static_conf: Sequence[HostPortTuple] = config.get(CONF_STATIC, [])
wemo_dispatcher = WemoDispatcher(entry)
wemo_discovery = WemoDiscovery(hass, wemo_dispatcher, static_conf)
async def async_stop_wemo(_: Event | None = None) -> None:
"""Shutdown Wemo subscriptions and subscription thread on exit."""
_LOGGER.debug("Shutting down WeMo event subscriptions")
await hass.async_add_executor_job(registry.stop)
await hass.async_add_executor_job(discovery_responder.stop)
wemo_discovery.async_stop_discovery()
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo)
wemo_data = async_wemo_data(hass)
dispatcher = WemoDispatcher(entry)
discovery = WemoDiscovery(hass, dispatcher, wemo_data.static_config)
wemo_data.config_entry_data = WemoConfigEntryData(
device_coordinators={},
discovery=discovery,
dispatcher=dispatcher,
)
entry.async_on_unload(async_stop_wemo)
# Need to do this at least once in case statistics are defined and discovery is disabled
await wemo_discovery.discover_statics()
await discovery.discover_statics()
if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY):
await wemo_discovery.async_discover_and_schedule()
if wemo_data.discovery_enabled:
await discovery.async_discover_and_schedule()
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a wemo config entry."""
# This makes sure that `entry.async_on_unload` routines run correctly on unload
return True
_LOGGER.debug("Unloading WeMo")
wemo_data = async_wemo_data(hass)
wemo_data.config_entry_data.discovery.async_stop_discovery()
dispatcher = wemo_data.config_entry_data.dispatcher
if unload_ok := await dispatcher.async_unload_platforms(hass):
assert not wemo_data.config_entry_data.device_coordinators
wemo_data.config_entry_data = None # type: ignore[assignment]
return unload_ok
async def async_wemo_dispatcher_connect(
hass: HomeAssistant,
dispatch: DispatchCallback,
) -> None:
"""Connect a wemo platform with the WemoDispatcher."""
module = dispatch.__module__ # Example: "homeassistant.components.wemo.switch"
platform = Platform(module.rsplit(".", 1)[1])
dispatcher = async_wemo_data(hass).config_entry_data.dispatcher
await dispatcher.async_connect_platform(platform, dispatch)
class WemoDispatcher:
@ -148,7 +168,8 @@ class WemoDispatcher:
self._config_entry = config_entry
self._added_serial_numbers: set[str] = set()
self._failed_serial_numbers: set[str] = set()
self._loaded_platforms: set[Platform] = set()
self._dispatch_backlog: dict[Platform, list[DeviceCoordinator]] = {}
self._dispatch_callbacks: dict[Platform, DispatchCallback] = {}
async def async_add_unique_device(
self, hass: HomeAssistant, wemo: pywemo.WeMoDevice
@ -171,32 +192,47 @@ class WemoDispatcher:
platforms.add(Platform.SENSOR)
for platform in platforms:
# Three cases:
# - First time we see platform, we need to load it and initialize the backlog
# - Platform is loaded, dispatch discovery
# - Platform is being loaded, add to backlog
# - Platform is loaded, backlog is gone, dispatch discovery
# - First time we see platform, we need to load it and initialize the backlog
if platform not in self._loaded_platforms:
hass.data[DOMAIN]["pending"][platform] = [coordinator]
self._loaded_platforms.add(platform)
if platform in self._dispatch_callbacks:
await self._dispatch_callbacks[platform](coordinator)
elif platform in self._dispatch_backlog:
self._dispatch_backlog[platform].append(coordinator)
else:
self._dispatch_backlog[platform] = [coordinator]
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
self._config_entry, platform
)
)
elif platform in hass.data[DOMAIN]["pending"]:
hass.data[DOMAIN]["pending"][platform].append(coordinator)
else:
async_dispatcher_send(
hass,
f"{DOMAIN}.{platform}",
coordinator,
)
self._added_serial_numbers.add(wemo.serial_number)
self._failed_serial_numbers.discard(wemo.serial_number)
async def async_connect_platform(
self, platform: Platform, dispatch: DispatchCallback
) -> None:
"""Consider a platform as loaded and dispatch any backlog of discovered devices."""
self._dispatch_callbacks[platform] = dispatch
await gather_with_concurrency(
MAX_CONCURRENCY,
*(
dispatch(coordinator)
for coordinator in self._dispatch_backlog.pop(platform)
),
)
async def async_unload_platforms(self, hass: HomeAssistant) -> bool:
"""Forward the unloading of an entry to platforms."""
platforms: set[Platform] = set(self._dispatch_backlog.keys())
platforms.update(self._dispatch_callbacks.keys())
return await hass.config_entries.async_unload_platforms(
self._config_entry, platforms
)
class WemoDiscovery:
"""Use SSDP to discover WeMo devices."""

View File

@ -1,22 +1,20 @@
"""Support for WeMo binary sensors."""
import asyncio
from pywemo import Insight, Maker, StandbyState
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN as WEMO_DOMAIN
from . import async_wemo_dispatcher_connect
from .entity import WemoBinaryStateEntity, WemoEntity
from .wemo_device import DeviceCoordinator
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
_config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up WeMo binary sensors."""
@ -30,14 +28,7 @@ async def async_setup_entry(
else:
async_add_entities([WemoBinarySensor(coordinator)])
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.binary_sensor", _discovered_wemo)
await asyncio.gather(
*(
_discovered_wemo(coordinator)
for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor")
)
)
await async_wemo_dispatcher_connect(hass, _discovered_wemo)
class WemoBinarySensor(WemoBinaryStateEntity, BinarySensorEntity):

View File

@ -1,7 +1,6 @@
"""Support for WeMo humidifier."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import math
from typing import Any
@ -13,7 +12,6 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.percentage import (
int_states_in_range,
@ -21,8 +19,8 @@ from homeassistant.util.percentage import (
ranged_value_to_percentage,
)
from . import async_wemo_dispatcher_connect
from .const import (
DOMAIN as WEMO_DOMAIN,
SERVICE_RESET_FILTER_LIFE,
SERVICE_SET_HUMIDITY,
)
@ -50,7 +48,7 @@ SET_HUMIDITY_SCHEMA = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
_config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up WeMo binary sensors."""
@ -59,14 +57,7 @@ async def async_setup_entry(
"""Handle a discovered Wemo device."""
async_add_entities([WemoHumidifier(coordinator)])
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo)
await asyncio.gather(
*(
_discovered_wemo(coordinator)
for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("fan")
)
)
await async_wemo_dispatcher_connect(hass, _discovered_wemo)
platform = entity_platform.async_get_current_platform()

View File

@ -1,7 +1,6 @@
"""Support for Belkin WeMo lights."""
from __future__ import annotations
import asyncio
from typing import Any, cast
from pywemo import Bridge, BridgeLight, Dimmer
@ -18,11 +17,11 @@ from homeassistant.components.light import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.color as color_util
from . import async_wemo_dispatcher_connect
from .const import DOMAIN as WEMO_DOMAIN
from .entity import WemoBinaryStateEntity, WemoEntity
from .wemo_device import DeviceCoordinator
@ -45,14 +44,7 @@ async def async_setup_entry(
else:
async_add_entities([WemoDimmer(coordinator)])
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo)
await asyncio.gather(
*(
_discovered_wemo(coordinator)
for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("light")
)
)
await async_wemo_dispatcher_connect(hass, _discovered_wemo)
@callback

View File

@ -0,0 +1,43 @@
"""Common data structures and helpers for accessing them."""
from collections.abc import Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
import pywemo
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN
if TYPE_CHECKING: # Avoid circular dependencies.
from . import HostPortTuple, WemoDiscovery, WemoDispatcher
from .wemo_device import DeviceCoordinator
@dataclass
class WemoConfigEntryData:
"""Config entry state data."""
device_coordinators: dict[str, "DeviceCoordinator"]
discovery: "WemoDiscovery"
dispatcher: "WemoDispatcher"
@dataclass
class WemoData:
"""Component state data."""
discovery_enabled: bool
static_config: Sequence["HostPortTuple"]
registry: pywemo.SubscriptionRegistry
# config_entry_data is set when the config entry is loaded and unset when it's
# unloaded. It's a programmer error if config_entry_data is accessed when the
# config entry is not loaded
config_entry_data: WemoConfigEntryData = None # type: ignore[assignment]
@callback
def async_wemo_data(hass: HomeAssistant) -> WemoData:
"""Fetch WemoData with proper typing."""
return cast(WemoData, hass.data[DOMAIN])

View File

@ -1,7 +1,6 @@
"""Support for power sensors in WeMo Insight devices."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from dataclasses import dataclass
from typing import cast
@ -15,11 +14,10 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DOMAIN as WEMO_DOMAIN
from . import async_wemo_dispatcher_connect
from .entity import WemoEntity
from .wemo_device import DeviceCoordinator
@ -59,7 +57,7 @@ ATTRIBUTE_SENSORS = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
_config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up WeMo sensors."""
@ -72,14 +70,7 @@ async def async_setup_entry(
if hasattr(coordinator.wemo, description.key)
)
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo)
await asyncio.gather(
*(
_discovered_wemo(coordinator)
for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("sensor")
)
)
await async_wemo_dispatcher_connect(hass, _discovered_wemo)
class AttributeSensor(WemoEntity, SensorEntity):

View File

@ -1,7 +1,6 @@
"""Support for WeMo switches."""
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
from typing import Any
@ -11,10 +10,9 @@ from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN as WEMO_DOMAIN
from . import async_wemo_dispatcher_connect
from .entity import WemoBinaryStateEntity
from .wemo_device import DeviceCoordinator
@ -36,7 +34,7 @@ MAKER_SWITCH_TOGGLE = "toggle"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
_config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up WeMo switches."""
@ -45,14 +43,7 @@ async def async_setup_entry(
"""Handle a discovered Wemo device."""
async_add_entities([WemoSwitch(coordinator)])
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.switch", _discovered_wemo)
await asyncio.gather(
*(
_discovered_wemo(coordinator)
for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("switch")
)
)
await async_wemo_dispatcher_connect(hass, _discovered_wemo)
class WemoSwitch(WemoBinaryStateEntity, SwitchEntity):

View File

@ -9,7 +9,7 @@ from typing import Literal
from pywemo import Insight, LongPressMixin, WeMoDevice
from pywemo.exceptions import ActionException, PyWeMoException
from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
from pywemo.subscribe import EVENT_TYPE_LONG_PRESS, SubscriptionRegistry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@ -30,6 +30,7 @@ from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, WEMO_SUBSCRIPTION_EVENT
from .models import async_wemo_data
_LOGGER = logging.getLogger(__name__)
@ -124,9 +125,21 @@ class DeviceCoordinator(DataUpdateCoordinator[None]):
updated = self.wemo.subscription_update(event_type, params)
self.hass.create_task(self._async_subscription_callback(updated))
async def async_shutdown(self) -> None:
"""Unregister push subscriptions and remove from coordinators dict."""
await super().async_shutdown()
del _async_coordinators(self.hass)[self.device_id]
assert self.options # Always set by async_register_device.
if self.options.enable_subscription:
await self._async_set_enable_subscription(False)
# Check that the device is available (last_update_success) before disabling long
# press. That avoids long shutdown times for devices that are no longer connected.
if self.options.enable_long_press and self.last_update_success:
await self._async_set_enable_long_press(False)
async def _async_set_enable_subscription(self, enable_subscription: bool) -> None:
"""Turn on/off push updates from the device."""
registry = self.hass.data[DOMAIN]["registry"]
registry = _async_registry(self.hass)
if enable_subscription:
registry.on(self.wemo, None, self.subscription_callback)
await self.hass.async_add_executor_job(registry.register, self.wemo)
@ -199,8 +212,10 @@ class DeviceCoordinator(DataUpdateCoordinator[None]):
# this case so the Sensor entities are properly populated.
return True
registry = self.hass.data[DOMAIN]["registry"]
return not (registry.is_subscribed(self.wemo) and self.last_update_success)
return not (
_async_registry(self.hass).is_subscribed(self.wemo)
and self.last_update_success
)
async def _async_update_data(self) -> None:
"""Update WeMo state."""
@ -258,7 +273,7 @@ async def async_register_device(
)
device = DeviceCoordinator(hass, wemo, entry.id)
hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device
_async_coordinators(hass)[entry.id] = device
config_entry.async_on_unload(
config_entry.add_update_listener(device.async_set_options)
@ -271,5 +286,14 @@ async def async_register_device(
@callback
def async_get_coordinator(hass: HomeAssistant, device_id: str) -> DeviceCoordinator:
"""Return DeviceCoordinator for device_id."""
coordinator: DeviceCoordinator = hass.data[DOMAIN]["devices"][device_id]
return coordinator
return _async_coordinators(hass)[device_id]
@callback
def _async_coordinators(hass: HomeAssistant) -> dict[str, DeviceCoordinator]:
return async_wemo_data(hass).config_entry_data.device_coordinators
@callback
def _async_registry(hass: HomeAssistant) -> SubscriptionRegistry:
return async_wemo_data(hass).registry

View File

@ -1,5 +1,4 @@
"""Fixtures for pywemo."""
import asyncio
import contextlib
from unittest.mock import create_autospec, patch
@ -33,11 +32,9 @@ async def async_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
registry.is_subscribed.return_value = False

View File

@ -1,16 +1,24 @@
"""Tests for the wemo component."""
import asyncio
from datetime import timedelta
from unittest.mock import create_autospec, patch
import pywemo
from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, WemoDiscovery
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.wemo import (
CONF_DISCOVERY,
CONF_STATIC,
WemoDiscovery,
async_wemo_dispatcher_connect,
)
from homeassistant.components.wemo.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from . import entity_test_helpers
from .conftest import (
MOCK_FIRMWARE_VERSION,
MOCK_HOST,
@ -92,6 +100,54 @@ async def test_static_config_without_port(hass: HomeAssistant, pywemo_device) ->
assert len(entity_entries) == 1
async def test_reload_config_entry(
hass: HomeAssistant,
pywemo_device: pywemo.WeMoDevice,
pywemo_registry: pywemo.SubscriptionRegistry,
) -> None:
"""Config entry can be reloaded without errors."""
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: {
CONF_DISCOVERY: False,
CONF_STATIC: [MOCK_HOST],
},
},
)
async def _async_test_entry_and_entity() -> tuple[str, str]:
await hass.async_block_till_done()
pywemo_device.get_state.assert_called()
pywemo_device.get_state.reset_mock()
pywemo_registry.register.assert_called_once_with(pywemo_device)
pywemo_registry.register.reset_mock()
entity_registry = er.async_get(hass)
entity_entries = list(entity_registry.entities.values())
assert len(entity_entries) == 1
await entity_test_helpers.test_turn_off_state(
hass, entity_entries[0], SWITCH_DOMAIN
)
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
return entries[0].entry_id, entity_entries[0].entity_id
entry_id, entity_id = await _async_test_entry_and_entity()
pywemo_registry.unregister.assert_not_called()
assert await hass.config_entries.async_reload(entry_id)
ids = await _async_test_entry_and_entity()
pywemo_registry.unregister.assert_called_once_with(pywemo_device)
assert ids == (entry_id, entity_id)
async def test_static_config_with_invalid_host(hass: HomeAssistant) -> None:
"""Component setup fails if a static host is invalid."""
setup_success = await async_setup_component(
@ -146,17 +202,26 @@ async def test_discovery(hass: HomeAssistant, pywemo_registry) -> None:
device.supports_long_press.return_value = False
return device
semaphore = asyncio.Semaphore(value=0)
async def async_connect(*args):
await async_wemo_dispatcher_connect(*args)
semaphore.release()
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, patch(
"homeassistant.components.wemo.WemoDiscovery.discover_statics"
) as mock_discover_statics:
) as mock_discover_statics, patch(
"homeassistant.components.wemo.binary_sensor.async_wemo_dispatcher_connect",
side_effect=async_connect,
):
assert await async_setup_component(
hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}}
)
await pywemo_registry.semaphore.acquire() # Returns after platform setup.
await semaphore.acquire() # Returns after platform setup.
mock_discovery.assert_called()
mock_discover_statics.assert_called()
pywemo_devices.append(create_device(2))