Use DataUpdateCoordinator for wemo (#54866)

* Use DataUpdateCoordinator for wemo

* Rename DeviceWrapper->DeviceCoordinator and make it a subclass of DataUpdateCoordinator

* Rename async_update_data->_async_update_data to override base class

* Rename: device -> coordinator
This commit is contained in:
Eric Severance 2021-08-21 11:14:55 -07:00 committed by GitHub
parent 6cefd558d8
commit 67d04b6082
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 507 additions and 629 deletions

View File

@ -152,7 +152,7 @@ class WemoDispatcher:
if wemo.serialnumber in self._added_serial_numbers: if wemo.serialnumber in self._added_serial_numbers:
return return
device = await async_register_device(hass, self._config_entry, wemo) coordinator = await async_register_device(hass, self._config_entry, wemo)
for component in WEMO_MODEL_DISPATCH.get(wemo.model_name, [SWITCH_DOMAIN]): for component in WEMO_MODEL_DISPATCH.get(wemo.model_name, [SWITCH_DOMAIN]):
# Three cases: # Three cases:
# - First time we see component, we need to load it and initialize the backlog # - First time we see component, we need to load it and initialize the backlog
@ -160,7 +160,7 @@ class WemoDispatcher:
# - Component is loaded, backlog is gone, dispatch discovery # - Component is loaded, backlog is gone, dispatch discovery
if component not in self._loaded_components: if component not in self._loaded_components:
hass.data[DOMAIN]["pending"][component] = [device] hass.data[DOMAIN]["pending"][component] = [coordinator]
self._loaded_components.add(component) self._loaded_components.add(component)
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup( hass.config_entries.async_forward_entry_setup(
@ -169,13 +169,13 @@ class WemoDispatcher:
) )
elif component in hass.data[DOMAIN]["pending"]: elif component in hass.data[DOMAIN]["pending"]:
hass.data[DOMAIN]["pending"][component].append(device) hass.data[DOMAIN]["pending"][component].append(coordinator)
else: else:
async_dispatcher_send( async_dispatcher_send(
hass, hass,
f"{DOMAIN}.{component}", f"{DOMAIN}.{component}",
device, coordinator,
) )
self._added_serial_numbers.add(wemo.serialnumber) self._added_serial_numbers.add(wemo.serialnumber)

View File

@ -6,7 +6,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DOMAIN as WEMO_DOMAIN from .const import DOMAIN as WEMO_DOMAIN
from .entity import WemoSubscriptionEntity from .entity import WemoEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -14,24 +14,24 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up WeMo binary sensors.""" """Set up WeMo binary sensors."""
async def _discovered_wemo(device): async def _discovered_wemo(coordinator):
"""Handle a discovered Wemo device.""" """Handle a discovered Wemo device."""
async_add_entities([WemoBinarySensor(device)]) async_add_entities([WemoBinarySensor(coordinator)])
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.binary_sensor", _discovered_wemo) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.binary_sensor", _discovered_wemo)
await asyncio.gather( await asyncio.gather(
*( *(
_discovered_wemo(device) _discovered_wemo(coordinator)
for device in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor") for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor")
) )
) )
class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity): class WemoBinarySensor(WemoEntity, BinarySensorEntity):
"""Representation a WeMo binary sensor.""" """Representation a WeMo binary sensor."""
def _update(self, force_update=True): @property
"""Update the sensor state.""" def is_on(self) -> bool:
with self._wemo_exception_handler("update status"): """Return true if the state is on. Standby is on."""
self._state = self.wemo.get_state(force_update) return self.wemo.get_state()

View File

@ -3,6 +3,5 @@ DOMAIN = "wemo"
SERVICE_SET_HUMIDITY = "set_humidity" SERVICE_SET_HUMIDITY = "set_humidity"
SERVICE_RESET_FILTER_LIFE = "reset_filter_life" SERVICE_RESET_FILTER_LIFE = "reset_filter_life"
SIGNAL_WEMO_STATE_PUSH = f"{DOMAIN}.state_push"
WEMO_SUBSCRIPTION_EVENT = f"{DOMAIN}_subscription_event" WEMO_SUBSCRIPTION_EVENT = f"{DOMAIN}_subscription_event"

View File

@ -7,7 +7,7 @@ from homeassistant.components.homeassistant.triggers import event as event_trigg
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT
from .wemo_device import async_get_device from .wemo_device import async_get_coordinator
TRIGGER_TYPES = {EVENT_TYPE_LONG_PRESS} TRIGGER_TYPES = {EVENT_TYPE_LONG_PRESS}
@ -28,11 +28,11 @@ async def async_get_triggers(hass, device_id):
CONF_DEVICE_ID: device_id, CONF_DEVICE_ID: device_id,
} }
device = async_get_device(hass, device_id) coordinator = async_get_coordinator(hass, device_id)
triggers = [] triggers = []
# Check for long press support. # Check for long press support.
if device.supports_long_press: if coordinator.supports_long_press:
triggers.append( triggers.append(
{ {
# Required fields of TRIGGER_SCHEMA # Required fields of TRIGGER_SCHEMA

View File

@ -1,49 +1,30 @@
"""Classes shared among Wemo entities.""" """Classes shared among Wemo entities."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Generator from collections.abc import Generator
import contextlib import contextlib
import logging import logging
import async_timeout
from pywemo import WeMoDevice
from pywemo.exceptions import ActionException from pywemo.exceptions import ActionException
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN as WEMO_DOMAIN, SIGNAL_WEMO_STATE_PUSH from .wemo_device import DeviceCoordinator
from .wemo_device import DeviceWrapper
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ExceptionHandlerStatus: class WemoEntity(CoordinatorEntity):
"""Exit status from the _wemo_exception_handler context manager.""" """Common methods for Wemo entities."""
# An exception if one was raised in the _wemo_exception_handler. def __init__(self, coordinator: DeviceCoordinator) -> None:
exception: Exception | None = None
@property
def success(self) -> bool:
"""Return True if the handler completed with no exception."""
return self.exception is None
class WemoEntity(Entity):
"""Common methods for Wemo entities.
Requires that subclasses implement the _update method.
"""
def __init__(self, wemo: WeMoDevice) -> None:
"""Initialize the WeMo device.""" """Initialize the WeMo device."""
self.wemo = wemo super().__init__(coordinator)
self._state = None self.wemo = coordinator.wemo
self._device_info = coordinator.device_info
self._available = True self._available = True
self._update_lock = None
self._has_polled = False
@property @property
def name(self) -> str: def name(self) -> str:
@ -52,81 +33,8 @@ class WemoEntity(Entity):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return true if switch is available.""" """Return true if the device is available."""
return self._available return super().available and self._available
@contextlib.contextmanager
def _wemo_exception_handler(
self, message: str
) -> Generator[ExceptionHandlerStatus, None, None]:
"""Wrap device calls to set `_available` when wemo exceptions happen."""
status = ExceptionHandlerStatus()
try:
yield status
except ActionException as err:
status.exception = err
_LOGGER.warning("Could not %s for %s (%s)", message, self.name, err)
self._available = False
else:
if not self._available:
_LOGGER.info("Reconnected to %s", self.name)
self._available = True
def _update(self, force_update: bool | None = True):
"""Update the device state."""
raise NotImplementedError()
async def async_added_to_hass(self) -> None:
"""Wemo device added to Home Assistant."""
# Define inside async context so we know our event loop
self._update_lock = asyncio.Lock()
async def async_update(self) -> None:
"""Update WeMo state.
Wemo has an aggressive retry logic that sometimes can take over a
minute to return. If we don't get a state within the scan interval,
assume the Wemo switch is unreachable. If update goes through, it will
be made available again.
"""
# If an update is in progress, we don't do anything
if self._update_lock.locked():
return
try:
async with async_timeout.timeout(
self.platform.scan_interval.total_seconds() - 0.1
) as timeout:
await asyncio.shield(self._async_locked_update(True, timeout))
except asyncio.TimeoutError:
_LOGGER.warning("Lost connection to %s", self.name)
self._available = False
async def _async_locked_update(
self, force_update: bool, timeout: async_timeout.timeout | None = None
) -> None:
"""Try updating within an async lock."""
async with self._update_lock:
await self.hass.async_add_executor_job(self._update, force_update)
self._has_polled = True
# When the timeout expires HomeAssistant is no longer waiting for an
# update from the device. Instead, the state needs to be updated
# asynchronously. This also handles the case where an update came
# directly from the device (device push). In that case no polling
# update was involved and the state also needs to be updated
# asynchronously.
if not timeout or timeout.expired:
self.async_write_ha_state()
class WemoSubscriptionEntity(WemoEntity):
"""Common methods for Wemo devices that register for update callbacks."""
def __init__(self, device: DeviceWrapper) -> None:
"""Initialize WemoSubscriptionEntity."""
super().__init__(device.wemo)
self._device_id = device.device_id
self._device_info = device.device_info
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
@ -138,59 +46,17 @@ class WemoSubscriptionEntity(WemoEntity):
"""Return the device info.""" """Return the device info."""
return self._device_info return self._device_info
@property @callback
def is_on(self) -> bool: def _handle_coordinator_update(self) -> None:
"""Return true if the state is on. Standby is on.""" """Handle updated data from the coordinator."""
return self._state self._available = True
super()._handle_coordinator_update()
@property @contextlib.contextmanager
def should_poll(self) -> bool: def _wemo_exception_handler(self, message: str) -> Generator[None, None, None]:
"""Return True if the the device requires local polling, False otherwise. """Wrap device calls to set `_available` when wemo exceptions happen."""
try:
It is desirable to allow devices to enter periods of polling when the yield
callback subscription (device push) is not working. To work with the except ActionException as err:
entity platform polling logic, this entity needs to report True for _LOGGER.warning("Could not %s for %s (%s)", message, self.name, err)
should_poll initially. That is required to cause the entity platform self._available = False
logic to start the polling task (see the discussion in #47182).
Polling can be disabled if three conditions are met:
1. The device has polled to get the initial state (self._has_polled) and
to satisfy the entity platform constraint mentioned above.
2. The polling was successful and the device is in a healthy state
(self.available).
3. The pywemo subscription registry reports that there is an active
subscription and the subscription has been confirmed by receiving an
initial event. This confirms that device push notifications are
working correctly (registry.is_subscribed - this method is async safe).
"""
registry = self.hass.data[WEMO_DOMAIN]["registry"]
return not (
self.available and self._has_polled and registry.is_subscribed(self.wemo)
)
async def async_added_to_hass(self) -> None:
"""Wemo device added to Home Assistant."""
await super().async_added_to_hass()
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_WEMO_STATE_PUSH, self._async_subscription_callback
)
)
async def _async_subscription_callback(
self, device_id: str, event_type: str, params: str
) -> None:
"""Update the state by the Wemo device."""
# Only respond events for this device.
if device_id != self._device_id:
return
# If an update is in progress, we don't do anything
if self._update_lock.locked():
return
_LOGGER.debug("Subscription event (%s) for %s", event_type, self.name)
updated = await self.hass.async_add_executor_job(
self.wemo.subscription_update, event_type, params
)
await self._async_locked_update(not updated)

View File

@ -7,6 +7,7 @@ import math
import voluptuous as vol import voluptuous as vol
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.core import callback
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
@ -20,7 +21,7 @@ from .const import (
SERVICE_RESET_FILTER_LIFE, SERVICE_RESET_FILTER_LIFE,
SERVICE_SET_HUMIDITY, SERVICE_SET_HUMIDITY,
) )
from .entity import WemoSubscriptionEntity from .entity import WemoEntity
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -68,16 +69,16 @@ SET_HUMIDITY_SCHEMA = {
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up WeMo binary sensors.""" """Set up WeMo binary sensors."""
async def _discovered_wemo(device): async def _discovered_wemo(coordinator):
"""Handle a discovered Wemo device.""" """Handle a discovered Wemo device."""
async_add_entities([WemoHumidifier(device)]) async_add_entities([WemoHumidifier(coordinator)])
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo)
await asyncio.gather( await asyncio.gather(
*( *(
_discovered_wemo(device) _discovered_wemo(coordinator)
for device in hass.data[WEMO_DOMAIN]["pending"].pop("fan") for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("fan")
) )
) )
@ -94,19 +95,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) )
class WemoHumidifier(WemoSubscriptionEntity, FanEntity): class WemoHumidifier(WemoEntity, FanEntity):
"""Representation of a WeMo humidifier.""" """Representation of a WeMo humidifier."""
def __init__(self, device): def __init__(self, coordinator):
"""Initialize the WeMo switch.""" """Initialize the WeMo switch."""
super().__init__(device) super().__init__(coordinator)
self._fan_mode = WEMO_FAN_OFF if self.wemo.fan_mode != WEMO_FAN_OFF:
self._fan_mode_string = None self._last_fan_on_mode = self.wemo.fan_mode
self._target_humidity = None else:
self._current_humidity = None
self._water_level = None
self._filter_life = None
self._filter_expired = None
self._last_fan_on_mode = WEMO_FAN_MEDIUM self._last_fan_on_mode = WEMO_FAN_MEDIUM
@property @property
@ -118,18 +115,18 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
return { return {
ATTR_CURRENT_HUMIDITY: self._current_humidity, ATTR_CURRENT_HUMIDITY: self.wemo.current_humidity_percent,
ATTR_TARGET_HUMIDITY: self._target_humidity, ATTR_TARGET_HUMIDITY: self.wemo.desired_humidity_percent,
ATTR_FAN_MODE: self._fan_mode_string, ATTR_FAN_MODE: self.wemo.fan_mode_string,
ATTR_WATER_LEVEL: self._water_level, ATTR_WATER_LEVEL: self.wemo.water_level_string,
ATTR_FILTER_LIFE: self._filter_life, ATTR_FILTER_LIFE: self.wemo.filter_life_percent,
ATTR_FILTER_EXPIRED: self._filter_expired, ATTR_FILTER_EXPIRED: self.wemo.filter_expired,
} }
@property @property
def percentage(self) -> int: def percentage(self) -> int:
"""Return the current speed percentage.""" """Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._fan_mode) return ranged_value_to_percentage(SPEED_RANGE, self.wemo.fan_mode)
@property @property
def speed_count(self) -> int: def speed_count(self) -> int:
@ -141,21 +138,17 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
"""Flag supported features.""" """Flag supported features."""
return SUPPORTED_FEATURES return SUPPORTED_FEATURES
def _update(self, force_update=True): @callback
"""Update the device state.""" def _handle_coordinator_update(self) -> None:
with self._wemo_exception_handler("update status"): """Handle updated data from the coordinator."""
self._state = self.wemo.get_state(force_update)
self._fan_mode = self.wemo.fan_mode
self._fan_mode_string = self.wemo.fan_mode_string
self._target_humidity = self.wemo.desired_humidity_percent
self._current_humidity = self.wemo.current_humidity_percent
self._water_level = self.wemo.water_level_string
self._filter_life = self.wemo.filter_life_percent
self._filter_expired = self.wemo.filter_expired
if self.wemo.fan_mode != WEMO_FAN_OFF: if self.wemo.fan_mode != WEMO_FAN_OFF:
self._last_fan_on_mode = self.wemo.fan_mode self._last_fan_on_mode = self.wemo.fan_mode
super()._handle_coordinator_update()
@property
def is_on(self) -> bool:
"""Return true if the state is on."""
return self.wemo.get_state()
def turn_on( def turn_on(
self, self,

View File

@ -1,9 +1,9 @@
"""Support for Belkin WeMo lights.""" """Support for Belkin WeMo lights."""
import asyncio import asyncio
from datetime import timedelta
import logging import logging
from homeassistant import util from pywemo.ouimeaux_device import bridge
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP, ATTR_COLOR_TEMP,
@ -15,14 +15,13 @@ from homeassistant.components.light import (
SUPPORT_TRANSITION, SUPPORT_TRANSITION,
LightEntity, LightEntity,
) )
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from .const import DOMAIN as WEMO_DOMAIN from .const import DOMAIN as WEMO_DOMAIN
from .entity import WemoEntity, WemoSubscriptionEntity from .entity import WemoEntity
from .wemo_device import DeviceCoordinator
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -31,77 +30,75 @@ SUPPORT_WEMO = (
) )
# The WEMO_ constants below come from pywemo itself # The WEMO_ constants below come from pywemo itself
WEMO_ON = 1
WEMO_OFF = 0 WEMO_OFF = 0
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up WeMo lights.""" """Set up WeMo lights."""
async def _discovered_wemo(device): async def _discovered_wemo(coordinator: DeviceCoordinator):
"""Handle a discovered Wemo device.""" """Handle a discovered Wemo device."""
if device.wemo.model_name == "Dimmer": if isinstance(coordinator.wemo, bridge.Bridge):
async_add_entities([WemoDimmer(device)]) async_setup_bridge(hass, config_entry, async_add_entities, coordinator)
else: else:
await hass.async_add_executor_job( async_add_entities([WemoDimmer(coordinator)])
setup_bridge, hass, device.wemo, async_add_entities
)
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo)
await asyncio.gather( await asyncio.gather(
*( *(
_discovered_wemo(device) _discovered_wemo(coordinator)
for device in hass.data[WEMO_DOMAIN]["pending"].pop("light") for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("light")
) )
) )
def setup_bridge(hass, bridge, async_add_entities): @callback
def async_setup_bridge(hass, config_entry, async_add_entities, coordinator):
"""Set up a WeMo link.""" """Set up a WeMo link."""
lights = {} known_light_ids = set()
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights():
"""Update the WeMo led objects with latest info from the bridge."""
bridge.bridge_update()
@callback
def async_update_lights():
"""Check to see if the bridge has any new lights."""
new_lights = [] new_lights = []
for light_id, device in bridge.Lights.items(): for light_id, light in coordinator.wemo.Lights.items():
if light_id not in lights: if light_id not in known_light_ids:
lights[light_id] = WemoLight(device, update_lights) known_light_ids.add(light_id)
new_lights.append(lights[light_id]) new_lights.append(WemoLight(coordinator, light))
if new_lights: if new_lights:
hass.add_job(async_add_entities, new_lights) async_add_entities(new_lights)
update_lights() async_update_lights()
config_entry.async_on_unload(coordinator.async_add_listener(async_update_lights))
class WemoLight(WemoEntity, LightEntity): class WemoLight(WemoEntity, LightEntity):
"""Representation of a WeMo light.""" """Representation of a WeMo light."""
def __init__(self, device, update_lights): def __init__(self, coordinator: DeviceCoordinator, light: bridge.Light) -> None:
"""Initialize the WeMo light.""" """Initialize the WeMo light."""
super().__init__(device) super().__init__(coordinator)
self._update_lights = update_lights self.light = light
self._brightness = None self._unique_id = self.light.uniqueID
self._hs_color = None self._model_name = type(self.light).__name__
self._color_temp = None
self._is_on = None
self._unique_id = self.wemo.uniqueID
self._model_name = type(self.wemo).__name__
async def async_added_to_hass(self): @property
"""Wemo light added to Home Assistant.""" def name(self) -> str:
# Define inside async context so we know our event loop """Return the name of the device if any."""
self._update_lock = asyncio.Lock() return self.light.name
@property
def available(self) -> bool:
"""Return true if the device is available."""
return super().available and self.light.state.get("available")
@property @property
def unique_id(self): def unique_id(self):
"""Return the ID of this light.""" """Return the ID of this light."""
return self.wemo.uniqueID return self.light.uniqueID
@property @property
def device_info(self): def device_info(self):
@ -116,22 +113,25 @@ class WemoLight(WemoEntity, LightEntity):
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return self._brightness return self.light.state.get("level", 255)
@property @property
def hs_color(self): def hs_color(self):
"""Return the hs color values of this light.""" """Return the hs color values of this light."""
return self._hs_color xy_color = self.light.state.get("color_xy")
if xy_color:
return color_util.color_xy_to_hs(*xy_color)
return None
@property @property
def color_temp(self): def color_temp(self):
"""Return the color temperature of this light in mireds.""" """Return the color temperature of this light in mireds."""
return self._color_temp return self.light.state.get("temperature_mireds")
@property @property
def is_on(self): def is_on(self):
"""Return true if device is on.""" """Return true if device is on."""
return self._is_on return self.light.state.get("onoff") != WEMO_OFF
@property @property
def supported_features(self): def supported_features(self):
@ -158,13 +158,14 @@ class WemoLight(WemoEntity, LightEntity):
with self._wemo_exception_handler("turn on"): with self._wemo_exception_handler("turn on"):
if xy_color is not None: if xy_color is not None:
self.wemo.set_color(xy_color, transition=transition_time) self.light.set_color(xy_color, transition=transition_time)
if color_temp is not None: if color_temp is not None:
self.wemo.set_temperature(mireds=color_temp, transition=transition_time) self.light.set_temperature(
mireds=color_temp, transition=transition_time
)
if self.wemo.turn_on(**turn_on_kwargs): self.light.turn_on(**turn_on_kwargs)
self._state["onoff"] = WEMO_ON
self.schedule_update_ha_state() self.schedule_update_ha_state()
@ -173,37 +174,14 @@ class WemoLight(WemoEntity, LightEntity):
transition_time = int(kwargs.get(ATTR_TRANSITION, 0)) transition_time = int(kwargs.get(ATTR_TRANSITION, 0))
with self._wemo_exception_handler("turn off"): with self._wemo_exception_handler("turn off"):
if self.wemo.turn_off(transition=transition_time): self.light.turn_off(transition=transition_time)
self._state["onoff"] = WEMO_OFF
self.schedule_update_ha_state() self.schedule_update_ha_state()
def _update(self, force_update=True):
"""Synchronize state with bridge."""
with self._wemo_exception_handler("update status") as handler:
self._update_lights(no_throttle=force_update)
self._state = self.wemo.state
if handler.success:
self._is_on = self._state.get("onoff") != WEMO_OFF
self._brightness = self._state.get("level", 255)
self._color_temp = self._state.get("temperature_mireds")
xy_color = self._state.get("color_xy") class WemoDimmer(WemoEntity, LightEntity):
if xy_color:
self._hs_color = color_util.color_xy_to_hs(*xy_color)
else:
self._hs_color = None
class WemoDimmer(WemoSubscriptionEntity, LightEntity):
"""Representation of a WeMo dimmer.""" """Representation of a WeMo dimmer."""
def __init__(self, device):
"""Initialize the WeMo dimmer."""
super().__init__(device)
self._brightness = None
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
@ -212,15 +190,13 @@ class WemoDimmer(WemoSubscriptionEntity, LightEntity):
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 1 and 100.""" """Return the brightness of this light between 1 and 100."""
return self._brightness wemo_brightness = int(self.wemo.get_brightness())
return int((wemo_brightness * 255) / 100)
def _update(self, force_update=True): @property
"""Update the device state.""" def is_on(self) -> bool:
with self._wemo_exception_handler("update status"): """Return true if the state is on."""
self._state = self.wemo.get_state(force_update) return self.wemo.get_state()
wemobrightness = int(self.wemo.get_brightness(force_update))
self._brightness = int((wemobrightness * 255) / 100)
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the dimmer on.""" """Turn the dimmer on."""
@ -231,18 +207,15 @@ class WemoDimmer(WemoSubscriptionEntity, LightEntity):
brightness = int((brightness / 255) * 100) brightness = int((brightness / 255) * 100)
with self._wemo_exception_handler("set brightness"): with self._wemo_exception_handler("set brightness"):
self.wemo.set_brightness(brightness) self.wemo.set_brightness(brightness)
self._state = WEMO_ON
else: else:
with self._wemo_exception_handler("turn on"): with self._wemo_exception_handler("turn on"):
self.wemo.on() self.wemo.on()
self._state = WEMO_ON
self.schedule_update_ha_state() self.schedule_update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the dimmer off.""" """Turn the dimmer off."""
with self._wemo_exception_handler("turn off"): with self._wemo_exception_handler("turn off"):
if self.wemo.off(): self.wemo.off()
self._state = WEMO_OFF
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -1,7 +1,5 @@
"""Support for power sensors in WeMo Insight devices.""" """Support for power sensors in WeMo Insight devices."""
import asyncio import asyncio
from datetime import timedelta
from typing import Callable
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
@ -17,52 +15,35 @@ from homeassistant.const import (
) )
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.util import Throttle, convert from homeassistant.util import convert
from .const import DOMAIN as WEMO_DOMAIN from .const import DOMAIN as WEMO_DOMAIN
from .entity import WemoSubscriptionEntity from .entity import WemoEntity
from .wemo_device import DeviceWrapper from .wemo_device import DeviceCoordinator
SCAN_INTERVAL = timedelta(seconds=10)
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up WeMo sensors.""" """Set up WeMo sensors."""
async def _discovered_wemo(device: DeviceWrapper): async def _discovered_wemo(coordinator: DeviceCoordinator):
"""Handle a discovered Wemo device.""" """Handle a discovered Wemo device."""
@Throttle(SCAN_INTERVAL)
def update_insight_params():
device.wemo.update_insight_params()
async_add_entities( async_add_entities(
[ [InsightCurrentPower(coordinator), InsightTodayEnergy(coordinator)]
InsightCurrentPower(device, update_insight_params),
InsightTodayEnergy(device, update_insight_params),
]
) )
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo)
await asyncio.gather( await asyncio.gather(
*( *(
_discovered_wemo(device) _discovered_wemo(coordinator)
for device in hass.data[WEMO_DOMAIN]["pending"].pop("sensor") for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("sensor")
) )
) )
class InsightSensor(WemoSubscriptionEntity, SensorEntity): class InsightSensor(WemoEntity, SensorEntity):
"""Common base for WeMo Insight power sensors.""" """Common base for WeMo Insight power sensors."""
_name_suffix: str
def __init__(self, device: DeviceWrapper, update_insight_params: Callable) -> None:
"""Initialize the WeMo Insight power sensor."""
super().__init__(device)
self._update_insight_params = update_insight_params
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the entity if any.""" """Return the name of the entity if any."""
@ -81,11 +62,6 @@ class InsightSensor(WemoSubscriptionEntity, SensorEntity):
and super().available and super().available
) )
def _update(self, force_update=True) -> None:
with self._wemo_exception_handler("update status"):
if force_update or not self.wemo.insight_params:
self._update_insight_params()
class InsightCurrentPower(InsightSensor): class InsightCurrentPower(InsightSensor):
"""Current instantaineous power consumption.""" """Current instantaineous power consumption."""

View File

@ -3,13 +3,15 @@ import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from pywemo import CoffeeMaker, Insight, Maker
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import convert from homeassistant.util import convert
from .const import DOMAIN as WEMO_DOMAIN from .const import DOMAIN as WEMO_DOMAIN
from .entity import WemoSubscriptionEntity from .entity import WemoEntity
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -33,63 +35,61 @@ WEMO_STANDBY = 8
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up WeMo switches.""" """Set up WeMo switches."""
async def _discovered_wemo(device): async def _discovered_wemo(coordinator):
"""Handle a discovered Wemo device.""" """Handle a discovered Wemo device."""
async_add_entities([WemoSwitch(device)]) async_add_entities([WemoSwitch(coordinator)])
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.switch", _discovered_wemo) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.switch", _discovered_wemo)
await asyncio.gather( await asyncio.gather(
*( *(
_discovered_wemo(device) _discovered_wemo(coordinator)
for device in hass.data[WEMO_DOMAIN]["pending"].pop("switch") for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("switch")
) )
) )
class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): class WemoSwitch(WemoEntity, SwitchEntity):
"""Representation of a WeMo switch.""" """Representation of a WeMo switch."""
def __init__(self, device):
"""Initialize the WeMo switch."""
super().__init__(device)
self.insight_params = None
self.maker_params = None
self.coffeemaker_mode = None
self._mode_string = None
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attr = {} attr = {}
if self.maker_params: if isinstance(self.wemo, Maker):
# Is the maker sensor on or off. # Is the maker sensor on or off.
if self.maker_params["hassensor"]: if self.wemo.maker_params["hassensor"]:
# Note a state of 1 matches the WeMo app 'not triggered'! # Note a state of 1 matches the WeMo app 'not triggered'!
if self.maker_params["sensorstate"]: if self.wemo.maker_params["sensorstate"]:
attr[ATTR_SENSOR_STATE] = STATE_OFF attr[ATTR_SENSOR_STATE] = STATE_OFF
else: else:
attr[ATTR_SENSOR_STATE] = STATE_ON attr[ATTR_SENSOR_STATE] = STATE_ON
# Is the maker switch configured as toggle(0) or momentary (1). # Is the maker switch configured as toggle(0) or momentary (1).
if self.maker_params["switchmode"]: if self.wemo.maker_params["switchmode"]:
attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_MOMENTARY attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_MOMENTARY
else: else:
attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_TOGGLE attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_TOGGLE
if self.insight_params or (self.coffeemaker_mode is not None): if isinstance(self.wemo, (Insight, CoffeeMaker)):
attr[ATTR_CURRENT_STATE_DETAIL] = self.detail_state attr[ATTR_CURRENT_STATE_DETAIL] = self.detail_state
if self.insight_params: if isinstance(self.wemo, Insight):
attr["on_latest_time"] = WemoSwitch.as_uptime(self.insight_params["onfor"]) attr["on_latest_time"] = WemoSwitch.as_uptime(
attr["on_today_time"] = WemoSwitch.as_uptime(self.insight_params["ontoday"]) self.wemo.insight_params["onfor"]
attr["on_total_time"] = WemoSwitch.as_uptime(self.insight_params["ontotal"]) )
attr["on_today_time"] = WemoSwitch.as_uptime(
self.wemo.insight_params["ontoday"]
)
attr["on_total_time"] = WemoSwitch.as_uptime(
self.wemo.insight_params["ontotal"]
)
attr["power_threshold_w"] = ( attr["power_threshold_w"] = (
convert(self.insight_params["powerthreshold"], float, 0.0) / 1000.0 convert(self.wemo.insight_params["powerthreshold"], float, 0.0) / 1000.0
) )
if self.coffeemaker_mode is not None: if isinstance(self.wemo, CoffeeMaker):
attr[ATTR_COFFEMAKER_MODE] = self.coffeemaker_mode attr[ATTR_COFFEMAKER_MODE] = self.wemo.mode
return attr return attr
@ -104,23 +104,25 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity):
@property @property
def current_power_w(self): def current_power_w(self):
"""Return the current power usage in W.""" """Return the current power usage in W."""
if self.insight_params: if isinstance(self.wemo, Insight):
return convert(self.insight_params["currentpower"], float, 0.0) / 1000.0 return (
convert(self.wemo.insight_params["currentpower"], float, 0.0) / 1000.0
)
@property @property
def today_energy_kwh(self): def today_energy_kwh(self):
"""Return the today total energy usage in kWh.""" """Return the today total energy usage in kWh."""
if self.insight_params: if isinstance(self.wemo, Insight):
miliwatts = convert(self.insight_params["todaymw"], float, 0.0) miliwatts = convert(self.wemo.insight_params["todaymw"], float, 0.0)
return round(miliwatts / (1000.0 * 1000.0 * 60), 2) return round(miliwatts / (1000.0 * 1000.0 * 60), 2)
@property @property
def detail_state(self): def detail_state(self):
"""Return the state of the device.""" """Return the state of the device."""
if self.coffeemaker_mode is not None: if isinstance(self.wemo, CoffeeMaker):
return self._mode_string return self.wemo.mode_string
if self.insight_params: if isinstance(self.wemo, Insight):
standby_state = int(self.insight_params["state"]) standby_state = int(self.wemo.insight_params["state"])
if standby_state == WEMO_ON: if standby_state == WEMO_ON:
return STATE_ON return STATE_ON
if standby_state == WEMO_OFF: if standby_state == WEMO_OFF:
@ -132,36 +134,25 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity):
@property @property
def icon(self): def icon(self):
"""Return the icon of device based on its type.""" """Return the icon of device based on its type."""
if self.wemo.model_name == "CoffeeMaker": if isinstance(self.wemo, CoffeeMaker):
return "mdi:coffee" return "mdi:coffee"
return None return None
@property
def is_on(self) -> bool:
"""Return true if the state is on. Standby is on."""
return self.wemo.get_state()
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""
with self._wemo_exception_handler("turn on"): with self._wemo_exception_handler("turn on"):
if self.wemo.on(): self.wemo.on()
self._state = WEMO_ON
self.schedule_update_ha_state() self.schedule_update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the switch off.""" """Turn the switch off."""
with self._wemo_exception_handler("turn off"): with self._wemo_exception_handler("turn off"):
if self.wemo.off(): self.wemo.off()
self._state = WEMO_OFF
self.schedule_update_ha_state() self.schedule_update_ha_state()
def _update(self, force_update=True):
"""Update the device state."""
with self._wemo_exception_handler("update status"):
self._state = self.wemo.get_state(force_update)
if self.wemo.model_name == "Insight":
self.insight_params = self.wemo.insight_params
self.insight_params["standby_state"] = self.wemo.get_standby_state
elif self.wemo.model_name == "Maker":
self.maker_params = self.wemo.maker_params
elif self.wemo.model_name == "CoffeeMaker":
self.coffeemaker_mode = self.wemo.mode
self._mode_string = self.wemo.mode_string

View File

@ -1,7 +1,10 @@
"""Home Assistant wrapper for a pyWeMo device.""" """Home Assistant wrapper for a pyWeMo device."""
import asyncio
from datetime import timedelta
import logging import logging
from pywemo import WeMoDevice from pywemo import WeMoDevice
from pywemo.exceptions import ActionException
from pywemo.subscribe import EVENT_TYPE_LONG_PRESS from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -14,28 +17,36 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import async_get as async_get_device_registry from homeassistant.helpers.device_registry import async_get as async_get_device_registry
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, SIGNAL_WEMO_STATE_PUSH, WEMO_SUBSCRIPTION_EVENT from .const import DOMAIN, WEMO_SUBSCRIPTION_EVENT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class DeviceWrapper: class DeviceCoordinator(DataUpdateCoordinator):
"""Home Assistant wrapper for a pyWeMo device.""" """Home Assistant wrapper for a pyWeMo device."""
def __init__(self, hass: HomeAssistant, wemo: WeMoDevice, device_id: str) -> None: def __init__(self, hass: HomeAssistant, wemo: WeMoDevice, device_id: str) -> None:
"""Initialize DeviceWrapper.""" """Initialize DeviceCoordinator."""
super().__init__(
hass,
_LOGGER,
name=wemo.name,
update_interval=timedelta(seconds=30),
)
self.hass = hass self.hass = hass
self.wemo = wemo self.wemo = wemo
self.device_id = device_id self.device_id = device_id
self.device_info = _device_info(wemo) self.device_info = _device_info(wemo)
self.supports_long_press = wemo.supports_long_press() self.supports_long_press = wemo.supports_long_press()
self.update_lock = asyncio.Lock()
def subscription_callback( def subscription_callback(
self, _device: WeMoDevice, event_type: str, params: str self, _device: WeMoDevice, event_type: str, params: str
) -> None: ) -> None:
"""Receives push notifications from WeMo devices.""" """Receives push notifications from WeMo devices."""
_LOGGER.debug("Subscription event (%s) for %s", event_type, self.wemo.name)
if event_type == EVENT_TYPE_LONG_PRESS: if event_type == EVENT_TYPE_LONG_PRESS:
self.hass.bus.fire( self.hass.bus.fire(
WEMO_SUBSCRIPTION_EVENT, WEMO_SUBSCRIPTION_EVENT,
@ -48,9 +59,50 @@ class DeviceWrapper:
}, },
) )
else: else:
dispatcher_send( updated = self.wemo.subscription_update(event_type, params)
self.hass, SIGNAL_WEMO_STATE_PUSH, self.device_id, event_type, params self.hass.add_job(self._async_subscription_callback(updated))
async def _async_subscription_callback(self, updated: bool) -> None:
"""Update the state by the Wemo device."""
# If an update is in progress, we don't do anything.
if self.update_lock.locked():
return
try:
await self._async_locked_update(not updated)
except UpdateFailed as err:
self.last_exception = err
if self.last_update_success:
_LOGGER.exception("Subscription callback failed")
self.last_update_success = False
except Exception as err: # pylint: disable=broad-except
self.last_exception = err
self.last_update_success = False
_LOGGER.exception("Unexpected error fetching %s data: %s", self.name, err)
else:
self.async_set_updated_data(None)
async def _async_update_data(self) -> None:
"""Update WeMo state."""
# No need to poll if the device will push updates.
registry = self.hass.data[DOMAIN]["registry"]
if registry.is_subscribed(self.wemo) and self.last_update_success:
return
# If an update is in progress, we don't do anything.
if self.update_lock.locked():
return
await self._async_locked_update(True)
async def _async_locked_update(self, force_update: bool) -> None:
"""Try updating within an async lock."""
async with self.update_lock:
try:
await self.hass.async_add_executor_job(
self.wemo.get_state, force_update
) )
except ActionException as err:
raise UpdateFailed("WeMo update failed") from err
def _device_info(wemo: WeMoDevice): def _device_info(wemo: WeMoDevice):
@ -64,19 +116,21 @@ def _device_info(wemo: WeMoDevice):
async def async_register_device( async def async_register_device(
hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice
) -> DeviceWrapper: ) -> DeviceCoordinator:
"""Register a device with home assistant and enable pywemo event callbacks.""" """Register a device with home assistant and enable pywemo event callbacks."""
# Ensure proper communication with the device and get the initial state.
await hass.async_add_executor_job(wemo.get_state, True)
device_registry = async_get_device_registry(hass) device_registry = async_get_device_registry(hass)
entry = device_registry.async_get_or_create( entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id, **_device_info(wemo) config_entry_id=config_entry.entry_id, **_device_info(wemo)
) )
registry = hass.data[DOMAIN]["registry"] device = DeviceCoordinator(hass, wemo, entry.id)
await hass.async_add_executor_job(registry.register, wemo)
device = DeviceWrapper(hass, wemo, entry.id)
hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device
registry = hass.data[DOMAIN]["registry"]
registry.on(wemo, None, device.subscription_callback) registry.on(wemo, None, device.subscription_callback)
await hass.async_add_executor_job(registry.register, wemo)
if device.supports_long_press: if device.supports_long_press:
try: try:
@ -93,6 +147,6 @@ async def async_register_device(
@callback @callback
def async_get_device(hass: HomeAssistant, device_id: str) -> DeviceWrapper: def async_get_coordinator(hass: HomeAssistant, device_id: str) -> DeviceCoordinator:
"""Return DeviceWrapper for device_id.""" """Return DeviceCoordinator for device_id."""
return hass.data[DOMAIN]["devices"][device_id] return hass.data[DOMAIN]["devices"][device_id]

View File

@ -35,6 +35,7 @@ async def async_pywemo_registry_fixture():
registry.semaphore.release() registry.semaphore.release()
registry.on.side_effect = on_func registry.on.side_effect = on_func
registry.is_subscribed.return_value = False
with patch("pywemo.SubscriptionRegistry", return_value=registry): with patch("pywemo.SubscriptionRegistry", return_value=registry):
yield registry yield registry

View File

@ -4,196 +4,133 @@ This is not a test module. These test methods are used by the platform test modu
""" """
import asyncio import asyncio
import threading import threading
from unittest.mock import patch
import async_timeout from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
import pywemo from homeassistant.components.wemo import wemo_device
from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.const import (
ATTR_ENTITY_ID,
from homeassistant.components.homeassistant import ( SERVICE_TURN_ON,
DOMAIN as HA_DOMAIN, STATE_ON,
SERVICE_UPDATE_ENTITY, STATE_UNAVAILABLE,
) )
from homeassistant.components.wemo.const import SIGNAL_WEMO_STATE_PUSH
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
def _perform_registry_callback(hass, pywemo_registry, pywemo_device): def _perform_registry_callback(coordinator):
"""Return a callable method to trigger a state callback from the device.""" """Return a callable method to trigger a state callback from the device."""
async def async_callback(): async def async_callback():
event = asyncio.Event() await coordinator.hass.async_add_executor_job(
coordinator.subscription_callback, coordinator.wemo, "", ""
async def event_callback(e, *args):
event.set()
stop_dispatcher_listener = async_dispatcher_connect(
hass, SIGNAL_WEMO_STATE_PUSH, event_callback
) )
# Cause a state update callback to be triggered by the device.
await hass.async_add_executor_job(
pywemo_registry.callbacks[pywemo_device.name], pywemo_device, "", ""
)
await event.wait()
stop_dispatcher_listener()
return async_callback return async_callback
def _perform_async_update(hass, wemo_entity): def _perform_async_update(coordinator):
"""Return a callable method to cause hass to update the state of the entity.""" """Return a callable method to cause hass to update the state of the entity."""
@callback async def async_callback():
def async_callback(): await coordinator._async_update_data()
return hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: [wemo_entity.entity_id]},
blocking=True,
)
return async_callback return async_callback
async def _async_multiple_call_helper( async def _async_multiple_call_helper(hass, pywemo_device, call1, call2):
hass,
pywemo_registry,
wemo_entity,
pywemo_device,
call1,
call2,
update_polling_method=None,
):
"""Create two calls (call1 & call2) in parallel; verify only one polls the device. """Create two calls (call1 & call2) in parallel; verify only one polls the device.
The platform entity should only perform one update poll on the device at a time. There should only be one poll on the device at a time. Any parallel updates
Any parallel updates that happen at the same time should be ignored. This is # that happen at the same time should be ignored. This is verified by blocking
verified by blocking in the update polling method. The polling method should in the get_state method. The polling method should only be called once as a
only be called once as a result of calling call1 & call2 simultaneously. result of calling call1 & call2 simultaneously.
""" """
# get_state is called outside the event loop. Use non-async Python Event.
event = threading.Event() event = threading.Event()
waiting = asyncio.Event() waiting = asyncio.Event()
call_count = 0
def get_update(force_update=True): def get_state(force_update=None):
if force_update is None:
return
nonlocal call_count
call_count += 1
hass.add_job(waiting.set) hass.add_job(waiting.set)
event.wait() event.wait()
update_polling_method = update_polling_method or pywemo_device.get_state # Danger! Do not use a Mock side_effect here. The test will deadlock. When
update_polling_method.side_effect = get_update # called though hass.async_add_executor_job, Mock objects !surprisingly!
# run in the same thread as the asyncio event loop.
# https://github.com/home-assistant/core/blob/1ba5c1c9fb1e380549cb655986b5f4d3873d7352/tests/common.py#L179
pywemo_device.get_state = get_state
# One of these two calls will block on `event`. The other will return right # One of these two calls will block on `event`. The other will return right
# away because the `_update_lock` is held. # away because the `_update_lock` is held.
_, pending = await asyncio.wait( done, pending = await asyncio.wait(
[call1(), call2()], return_when=asyncio.FIRST_COMPLETED [call1(), call2()], return_when=asyncio.FIRST_COMPLETED
) )
_ = [d.result() for d in done] # Allow any exceptions to be raised.
# Allow the blocked call to return. # Allow the blocked call to return.
await waiting.wait() await waiting.wait()
event.set() event.set()
if pending: if pending:
await asyncio.wait(pending) done, _ = await asyncio.wait(pending)
_ = [d.result() for d in done] # Allow any exceptions to be raised.
# Make sure the state update only happened once. # Make sure the state update only happened once.
update_polling_method.assert_called_once() assert call_count == 1
async def test_async_update_locked_callback_and_update( async def test_async_update_locked_callback_and_update(
hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs hass, pywemo_device, wemo_entity
): ):
"""Test that a callback and a state update request can't both happen at the same time. """Test that a callback and a state update request can't both happen at the same time.
When a state update is received via a callback from the device at the same time When a state update is received via a callback from the device at the same time
as hass is calling `async_update`, verify that only one of the updates proceeds. as hass is calling `async_update`, verify that only one of the updates proceeds.
""" """
coordinator = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
await async_setup_component(hass, HA_DOMAIN, {}) await async_setup_component(hass, HA_DOMAIN, {})
callback = _perform_registry_callback(hass, pywemo_registry, pywemo_device) callback = _perform_registry_callback(coordinator)
update = _perform_async_update(hass, wemo_entity) update = _perform_async_update(coordinator)
await _async_multiple_call_helper( await _async_multiple_call_helper(hass, pywemo_device, callback, update)
hass, pywemo_registry, wemo_entity, pywemo_device, callback, update, **kwargs
)
async def test_async_update_locked_multiple_updates( async def test_async_update_locked_multiple_updates(hass, pywemo_device, wemo_entity):
hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs
):
"""Test that two hass async_update state updates do not proceed at the same time.""" """Test that two hass async_update state updates do not proceed at the same time."""
coordinator = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
await async_setup_component(hass, HA_DOMAIN, {}) await async_setup_component(hass, HA_DOMAIN, {})
update = _perform_async_update(hass, wemo_entity) update = _perform_async_update(coordinator)
await _async_multiple_call_helper( await _async_multiple_call_helper(hass, pywemo_device, update, update)
hass, pywemo_registry, wemo_entity, pywemo_device, update, update, **kwargs
)
async def test_async_update_locked_multiple_callbacks( async def test_async_update_locked_multiple_callbacks(hass, pywemo_device, wemo_entity):
hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs
):
"""Test that two device callback state updates do not proceed at the same time.""" """Test that two device callback state updates do not proceed at the same time."""
coordinator = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
await async_setup_component(hass, HA_DOMAIN, {}) await async_setup_component(hass, HA_DOMAIN, {})
callback = _perform_registry_callback(hass, pywemo_registry, pywemo_device) callback = _perform_registry_callback(coordinator)
await _async_multiple_call_helper( await _async_multiple_call_helper(hass, pywemo_device, callback, callback)
hass, pywemo_registry, wemo_entity, pywemo_device, callback, callback, **kwargs
)
async def test_async_locked_update_with_exception( async def test_avaliable_after_update(
hass, hass, pywemo_registry, pywemo_device, wemo_entity, domain
wemo_entity,
pywemo_device,
update_polling_method=None,
expected_state=STATE_OFF,
): ):
"""Test that the entity becomes unavailable when communication is lost.""" """Test the avaliability when an On call fails and after an update.
assert hass.states.get(wemo_entity.entity_id).state == expected_state
await async_setup_component(hass, HA_DOMAIN, {}) This test expects that the pywemo_device Mock has been setup to raise an
update_polling_method = update_polling_method or pywemo_device.get_state ActionException when the SERVICE_TURN_ON method is called and that the
update_polling_method.side_effect = ActionException state will be On after the update.
"""
await async_setup_component(hass, domain, {})
await hass.services.async_call( await hass.services.async_call(
HA_DOMAIN, domain,
SERVICE_UPDATE_ENTITY, SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [wemo_entity.entity_id]}, {ATTR_ENTITY_ID: [wemo_entity.entity_id]},
blocking=True, blocking=True,
) )
assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE
pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "")
async def test_async_update_with_timeout_and_recovery(
hass, wemo_entity, pywemo_device, expected_state=STATE_OFF
):
"""Test that the entity becomes unavailable after a timeout, and that it recovers."""
assert hass.states.get(wemo_entity.entity_id).state == expected_state
await async_setup_component(hass, HA_DOMAIN, {})
event = threading.Event()
def get_state(*args):
event.wait()
return 0
if hasattr(pywemo_device, "bridge_update"):
pywemo_device.bridge_update.side_effect = get_state
elif isinstance(pywemo_device, pywemo.Insight):
pywemo_device.update_insight_params.side_effect = get_state
else:
pywemo_device.get_state.side_effect = get_state
timeout = async_timeout.timeout(0)
with patch("async_timeout.timeout", return_value=timeout):
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: [wemo_entity.entity_id]},
blocking=True,
)
assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE
# Check that the entity recovers and is available after the update succeeds.
event.set()
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get(wemo_entity.entity_id).state == expected_state assert hass.states.get(wemo_entity.entity_id).state == STATE_ON

View File

@ -30,12 +30,6 @@ test_async_update_locked_multiple_callbacks = (
test_async_update_locked_callback_and_update = ( test_async_update_locked_callback_and_update = (
entity_test_helpers.test_async_update_locked_callback_and_update entity_test_helpers.test_async_update_locked_callback_and_update
) )
test_async_locked_update_with_exception = (
entity_test_helpers.test_async_locked_update_with_exception
)
test_async_update_with_timeout_and_recovery = (
entity_test_helpers.test_async_update_with_timeout_and_recovery
)
async def test_binary_sensor_registry_state_callback( async def test_binary_sensor_registry_state_callback(

View File

@ -1,7 +1,9 @@
"""Tests for the Wemo fan entity.""" """Tests for the Wemo fan entity."""
import pytest import pytest
from pywemo.exceptions import ActionException
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
from homeassistant.components.homeassistant import ( from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN, DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY, SERVICE_UPDATE_ENTITY,
@ -32,12 +34,6 @@ test_async_update_locked_multiple_callbacks = (
test_async_update_locked_callback_and_update = ( test_async_update_locked_callback_and_update = (
entity_test_helpers.test_async_update_locked_callback_and_update entity_test_helpers.test_async_update_locked_callback_and_update
) )
test_async_locked_update_with_exception = (
entity_test_helpers.test_async_locked_update_with_exception
)
test_async_update_with_timeout_and_recovery = (
entity_test_helpers.test_async_update_with_timeout_and_recovery
)
async def test_fan_registry_state_callback( async def test_fan_registry_state_callback(
@ -82,6 +78,17 @@ async def test_fan_update_entity(hass, pywemo_registry, pywemo_device, wemo_enti
assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF
async def test_available_after_update(
hass, pywemo_registry, pywemo_device, wemo_entity
):
"""Test the avaliability when an On call fails and after an update."""
pywemo_device.set_state.side_effect = ActionException
pywemo_device.get_state.return_value = 1
await entity_test_helpers.test_avaliable_after_update(
hass, pywemo_registry, pywemo_device, wemo_entity, FAN_DOMAIN
)
async def test_fan_reset_filter_service(hass, pywemo_device, wemo_entity): async def test_fan_reset_filter_service(hass, pywemo_device, wemo_entity):
"""Verify that SERVICE_RESET_FILTER_LIFE is registered and works.""" """Verify that SERVICE_RESET_FILTER_LIFE is registered and works."""
assert await hass.services.async_call( assert await hass.services.async_call(

View File

@ -1,5 +1,5 @@
"""Tests for the Wemo light entity via the bridge.""" """Tests for the Wemo light entity via the bridge."""
from unittest.mock import create_autospec, patch from unittest.mock import create_autospec
import pytest import pytest
import pywemo import pywemo
@ -8,10 +8,9 @@ from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN, DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY, SERVICE_UPDATE_ENTITY,
) )
from homeassistant.components.wemo.light import MIN_TIME_BETWEEN_SCANS from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from . import entity_test_helpers from . import entity_test_helpers
@ -32,59 +31,52 @@ def pywemo_bridge_light_fixture(pywemo_device):
light.uniqueID = pywemo_device.serialnumber light.uniqueID = pywemo_device.serialnumber
light.name = pywemo_device.name light.name = pywemo_device.name
light.bridge = pywemo_device light.bridge = pywemo_device
light.state = {"onoff": 0} light.state = {"onoff": 0, "available": True}
pywemo_device.Lights = {pywemo_device.serialnumber: light} pywemo_device.Lights = {pywemo_device.serialnumber: light}
return light return light
def _bypass_throttling(): async def test_async_update_locked_callback_and_update(
"""Bypass the util.Throttle on the update_lights method.""" hass, pywemo_bridge_light, wemo_entity, pywemo_device
utcnow = dt_util.utcnow() ):
"""Test that a callback and a state update request can't both happen at the same time."""
def increment_and_return_time(): await entity_test_helpers.test_async_update_locked_callback_and_update(
nonlocal utcnow hass,
utcnow += MIN_TIME_BETWEEN_SCANS pywemo_device,
return utcnow wemo_entity,
)
return patch("homeassistant.util.utcnow", side_effect=increment_and_return_time)
async def test_async_update_locked_multiple_updates( async def test_async_update_locked_multiple_updates(
hass, pywemo_registry, pywemo_bridge_light, wemo_entity, pywemo_device hass, pywemo_bridge_light, wemo_entity, pywemo_device
): ):
"""Test that two state updates do not proceed at the same time.""" """Test that two state updates do not proceed at the same time."""
pywemo_device.bridge_update.reset_mock()
with _bypass_throttling():
await entity_test_helpers.test_async_update_locked_multiple_updates( await entity_test_helpers.test_async_update_locked_multiple_updates(
hass, hass,
pywemo_registry,
wemo_entity,
pywemo_device, pywemo_device,
update_polling_method=pywemo_device.bridge_update, wemo_entity,
) )
async def test_async_update_with_timeout_and_recovery( async def test_async_update_locked_multiple_callbacks(
hass, pywemo_bridge_light, wemo_entity, pywemo_device hass, pywemo_bridge_light, wemo_entity, pywemo_device
): ):
"""Test that the entity becomes unavailable after a timeout, and that it recovers.""" """Test that two device callback state updates do not proceed at the same time."""
with _bypass_throttling(): await entity_test_helpers.test_async_update_locked_multiple_callbacks(
await entity_test_helpers.test_async_update_with_timeout_and_recovery(
hass, wemo_entity, pywemo_device
)
async def test_async_locked_update_with_exception(
hass, pywemo_bridge_light, wemo_entity, pywemo_device
):
"""Test that the entity becomes unavailable when communication is lost."""
with _bypass_throttling():
await entity_test_helpers.test_async_locked_update_with_exception(
hass, hass,
wemo_entity,
pywemo_device, pywemo_device,
update_polling_method=pywemo_device.bridge_update, wemo_entity,
)
async def test_available_after_update(
hass, pywemo_registry, pywemo_device, pywemo_bridge_light, wemo_entity
):
"""Test the avaliability when an On call fails and after an update."""
pywemo_bridge_light.turn_on.side_effect = pywemo.exceptions.ActionException
pywemo_bridge_light.state["onoff"] = 1
await entity_test_helpers.test_avaliable_after_update(
hass, pywemo_registry, pywemo_device, wemo_entity, LIGHT_DOMAIN
) )
@ -95,7 +87,7 @@ async def test_light_update_entity(
await async_setup_component(hass, HA_DOMAIN, {}) await async_setup_component(hass, HA_DOMAIN, {})
# On state. # On state.
pywemo_bridge_light.state = {"onoff": 1} pywemo_bridge_light.state["onoff"] = 1
await hass.services.async_call( await hass.services.async_call(
HA_DOMAIN, HA_DOMAIN,
SERVICE_UPDATE_ENTITY, SERVICE_UPDATE_ENTITY,
@ -105,7 +97,7 @@ async def test_light_update_entity(
assert hass.states.get(wemo_entity.entity_id).state == STATE_ON assert hass.states.get(wemo_entity.entity_id).state == STATE_ON
# Off state. # Off state.
pywemo_bridge_light.state = {"onoff": 0} pywemo_bridge_light.state["onoff"] = 0
await hass.services.async_call( await hass.services.async_call(
HA_DOMAIN, HA_DOMAIN,
SERVICE_UPDATE_ENTITY, SERVICE_UPDATE_ENTITY,

View File

@ -1,11 +1,13 @@
"""Tests for the Wemo standalone/non-bridge light entity.""" """Tests for the Wemo standalone/non-bridge light entity."""
import pytest import pytest
from pywemo.exceptions import ActionException
from homeassistant.components.homeassistant import ( from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN, DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY, SERVICE_UPDATE_ENTITY,
) )
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -30,12 +32,17 @@ test_async_update_locked_multiple_callbacks = (
test_async_update_locked_callback_and_update = ( test_async_update_locked_callback_and_update = (
entity_test_helpers.test_async_update_locked_callback_and_update entity_test_helpers.test_async_update_locked_callback_and_update
) )
test_async_locked_update_with_exception = (
entity_test_helpers.test_async_locked_update_with_exception
) async def test_available_after_update(
test_async_update_with_timeout_and_recovery = ( hass, pywemo_registry, pywemo_device, wemo_entity
entity_test_helpers.test_async_update_with_timeout_and_recovery ):
) """Test the avaliability when an On call fails and after an update."""
pywemo_device.on.side_effect = ActionException
pywemo_device.get_state.return_value = 1
await entity_test_helpers.test_avaliable_after_update(
hass, pywemo_registry, pywemo_device, wemo_entity, LIGHT_DOMAIN
)
async def test_light_registry_state_callback( async def test_light_registry_state_callback(

View File

@ -78,62 +78,33 @@ class InsightTestTemplate:
# in the scope of this test module. They will run using the pywemo_model from # in the scope of this test module. They will run using the pywemo_model from
# this test module (Insight). # this test module (Insight).
async def test_async_update_locked_multiple_updates( async def test_async_update_locked_multiple_updates(
self, hass, pywemo_registry, wemo_entity, pywemo_device self, hass, pywemo_device, wemo_entity
): ):
"""Test that two hass async_update state updates do not proceed at the same time.""" """Test that two hass async_update state updates do not proceed at the same time."""
pywemo_device.subscription_update.return_value = False
await entity_test_helpers.test_async_update_locked_multiple_updates( await entity_test_helpers.test_async_update_locked_multiple_updates(
hass, hass,
pywemo_registry,
wemo_entity,
pywemo_device, pywemo_device,
update_polling_method=pywemo_device.update_insight_params, wemo_entity,
) )
async def test_async_update_locked_multiple_callbacks( async def test_async_update_locked_multiple_callbacks(
self, hass, pywemo_registry, wemo_entity, pywemo_device self, hass, pywemo_device, wemo_entity
): ):
"""Test that two device callback state updates do not proceed at the same time.""" """Test that two device callback state updates do not proceed at the same time."""
pywemo_device.subscription_update.return_value = False
await entity_test_helpers.test_async_update_locked_multiple_callbacks( await entity_test_helpers.test_async_update_locked_multiple_callbacks(
hass, hass,
pywemo_registry,
wemo_entity,
pywemo_device, pywemo_device,
update_polling_method=pywemo_device.update_insight_params, wemo_entity,
) )
async def test_async_update_locked_callback_and_update( async def test_async_update_locked_callback_and_update(
self, hass, pywemo_registry, wemo_entity, pywemo_device self, hass, pywemo_device, wemo_entity
): ):
"""Test that a callback and a state update request can't both happen at the same time.""" """Test that a callback and a state update request can't both happen at the same time."""
pywemo_device.subscription_update.return_value = False
await entity_test_helpers.test_async_update_locked_callback_and_update( await entity_test_helpers.test_async_update_locked_callback_and_update(
hass, hass,
pywemo_registry,
wemo_entity,
pywemo_device, pywemo_device,
update_polling_method=pywemo_device.update_insight_params,
)
async def test_async_locked_update_with_exception(
self, hass, wemo_entity, pywemo_device
):
"""Test that the entity becomes unavailable when communication is lost."""
await entity_test_helpers.test_async_locked_update_with_exception(
hass,
wemo_entity, wemo_entity,
pywemo_device,
update_polling_method=pywemo_device.update_insight_params,
expected_state=self.EXPECTED_STATE_VALUE,
)
async def test_async_update_with_timeout_and_recovery(
self, hass, wemo_entity, pywemo_device
):
"""Test that the entity becomes unavailable after a timeout, and that it recovers."""
await entity_test_helpers.test_async_update_with_timeout_and_recovery(
hass, wemo_entity, pywemo_device, expected_state=self.EXPECTED_STATE_VALUE
) )
async def test_state_unavailable(self, hass, wemo_entity, pywemo_device): async def test_state_unavailable(self, hass, wemo_entity, pywemo_device):

View File

@ -1,11 +1,13 @@
"""Tests for the Wemo switch entity.""" """Tests for the Wemo switch entity."""
import pytest import pytest
from pywemo.exceptions import ActionException
from homeassistant.components.homeassistant import ( from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN, DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY, SERVICE_UPDATE_ENTITY,
) )
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -30,12 +32,6 @@ test_async_update_locked_multiple_callbacks = (
test_async_update_locked_callback_and_update = ( test_async_update_locked_callback_and_update = (
entity_test_helpers.test_async_update_locked_callback_and_update entity_test_helpers.test_async_update_locked_callback_and_update
) )
test_async_locked_update_with_exception = (
entity_test_helpers.test_async_locked_update_with_exception
)
test_async_update_with_timeout_and_recovery = (
entity_test_helpers.test_async_update_with_timeout_and_recovery
)
async def test_switch_registry_state_callback( async def test_switch_registry_state_callback(
@ -78,3 +74,14 @@ async def test_switch_update_entity(hass, pywemo_registry, pywemo_device, wemo_e
blocking=True, blocking=True,
) )
assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF
async def test_available_after_update(
hass, pywemo_registry, pywemo_device, wemo_entity
):
"""Test the avaliability when an On call fails and after an update."""
pywemo_device.on.side_effect = ActionException
pywemo_device.get_state.return_value = 1
await entity_test_helpers.test_avaliable_after_update(
hass, pywemo_registry, pywemo_device, wemo_entity, SWITCH_DOMAIN
)

View File

@ -1,16 +1,24 @@
"""Tests for wemo_device.py.""" """Tests for wemo_device.py."""
import asyncio
from unittest.mock import patch from unittest.mock import patch
import async_timeout
import pytest import pytest
from pywemo import PyWeMoException from pywemo.exceptions import ActionException, PyWeMoException
from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
from homeassistant import runner
from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, wemo_device from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, wemo_device
from homeassistant.components.wemo.const import DOMAIN from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT
from homeassistant.core import callback
from homeassistant.helpers import device_registry from homeassistant.helpers import device_registry
from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .conftest import MOCK_HOST from .conftest import MOCK_HOST
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(True))
@pytest.fixture @pytest.fixture
def pywemo_model(): def pywemo_model():
@ -36,5 +44,107 @@ async def test_async_register_device_longpress_fails(hass, pywemo_device):
dr = device_registry.async_get(hass) dr = device_registry.async_get(hass)
device_entries = list(dr.devices.values()) device_entries = list(dr.devices.values())
assert len(device_entries) == 1 assert len(device_entries) == 1
device_wrapper = wemo_device.async_get_device(hass, device_entries[0].id) device = wemo_device.async_get_coordinator(hass, device_entries[0].id)
assert device_wrapper.supports_long_press is False assert device.supports_long_press is False
async def test_long_press_event(hass, pywemo_registry, wemo_entity):
"""Device fires a long press event."""
device = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
got_event = asyncio.Event()
event_data = {}
@callback
def async_event_received(event):
nonlocal event_data
event_data = event.data
got_event.set()
hass.bus.async_listen_once(WEMO_SUBSCRIPTION_EVENT, async_event_received)
await hass.async_add_executor_job(
pywemo_registry.callbacks[device.wemo.name],
device.wemo,
EVENT_TYPE_LONG_PRESS,
"testing_params",
)
async with async_timeout.timeout(8):
await got_event.wait()
assert event_data == {
"device_id": wemo_entity.device_id,
"name": device.wemo.name,
"params": "testing_params",
"type": EVENT_TYPE_LONG_PRESS,
"unique_id": device.wemo.serialnumber,
}
async def test_subscription_callback(hass, pywemo_registry, wemo_entity):
"""Device processes a registry subscription callback."""
device = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
device.last_update_success = False
got_callback = asyncio.Event()
@callback
def async_received_callback():
got_callback.set()
device.async_add_listener(async_received_callback)
await hass.async_add_executor_job(
pywemo_registry.callbacks[device.wemo.name], device.wemo, "", ""
)
async with async_timeout.timeout(8):
await got_callback.wait()
assert device.last_update_success
async def test_subscription_update_action_exception(hass, pywemo_device, wemo_entity):
"""Device handles ActionException on get_state properly."""
device = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
device.last_update_success = True
pywemo_device.subscription_update.return_value = False
pywemo_device.get_state.reset_mock()
pywemo_device.get_state.side_effect = ActionException
await hass.async_add_executor_job(
device.subscription_callback, pywemo_device, "", ""
)
await hass.async_block_till_done()
pywemo_device.get_state.assert_called_once_with(True)
assert device.last_update_success is False
assert isinstance(device.last_exception, UpdateFailed)
async def test_subscription_update_exception(hass, pywemo_device, wemo_entity):
"""Device handles Exception on get_state properly."""
device = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
device.last_update_success = True
pywemo_device.subscription_update.return_value = False
pywemo_device.get_state.reset_mock()
pywemo_device.get_state.side_effect = Exception
await hass.async_add_executor_job(
device.subscription_callback, pywemo_device, "", ""
)
await hass.async_block_till_done()
pywemo_device.get_state.assert_called_once_with(True)
assert device.last_update_success is False
assert isinstance(device.last_exception, Exception)
async def test_async_update_data_subscribed(
hass, pywemo_registry, pywemo_device, wemo_entity
):
"""No update happens when the device is subscribed."""
device = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
pywemo_registry.is_subscribed.return_value = True
pywemo_device.get_state.reset_mock()
await device._async_update_data()
pywemo_device.get_state.assert_not_called()