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 privatepull/96537/head^2
parent
0349e47372
commit
5ffffd8dbc
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue