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:
parent
6cefd558d8
commit
67d04b6082
19 changed files with 507 additions and 629 deletions
|
@ -152,7 +152,7 @@ class WemoDispatcher:
|
|||
if wemo.serialnumber in self._added_serial_numbers:
|
||||
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]):
|
||||
# Three cases:
|
||||
# - 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
|
||||
|
||||
if component not in self._loaded_components:
|
||||
hass.data[DOMAIN]["pending"][component] = [device]
|
||||
hass.data[DOMAIN]["pending"][component] = [coordinator]
|
||||
self._loaded_components.add(component)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(
|
||||
|
@ -169,13 +169,13 @@ class WemoDispatcher:
|
|||
)
|
||||
|
||||
elif component in hass.data[DOMAIN]["pending"]:
|
||||
hass.data[DOMAIN]["pending"][component].append(device)
|
||||
hass.data[DOMAIN]["pending"][component].append(coordinator)
|
||||
|
||||
else:
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
f"{DOMAIN}.{component}",
|
||||
device,
|
||||
coordinator,
|
||||
)
|
||||
|
||||
self._added_serial_numbers.add(wemo.serialnumber)
|
||||
|
|
|
@ -6,7 +6,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import DOMAIN as WEMO_DOMAIN
|
||||
from .entity import WemoSubscriptionEntity
|
||||
from .entity import WemoEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -14,24 +14,24 @@ _LOGGER = logging.getLogger(__name__)
|
|||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up WeMo binary sensors."""
|
||||
|
||||
async def _discovered_wemo(device):
|
||||
async def _discovered_wemo(coordinator):
|
||||
"""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)
|
||||
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_discovered_wemo(device)
|
||||
for device in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor")
|
||||
_discovered_wemo(coordinator)
|
||||
for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity):
|
||||
class WemoBinarySensor(WemoEntity, BinarySensorEntity):
|
||||
"""Representation a WeMo binary sensor."""
|
||||
|
||||
def _update(self, force_update=True):
|
||||
"""Update the sensor state."""
|
||||
with self._wemo_exception_handler("update status"):
|
||||
self._state = self.wemo.get_state(force_update)
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the state is on. Standby is on."""
|
||||
return self.wemo.get_state()
|
||||
|
|
|
@ -3,6 +3,5 @@ DOMAIN = "wemo"
|
|||
|
||||
SERVICE_SET_HUMIDITY = "set_humidity"
|
||||
SERVICE_RESET_FILTER_LIFE = "reset_filter_life"
|
||||
SIGNAL_WEMO_STATE_PUSH = f"{DOMAIN}.state_push"
|
||||
|
||||
WEMO_SUBSCRIPTION_EVENT = f"{DOMAIN}_subscription_event"
|
||||
|
|
|
@ -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 .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}
|
||||
|
||||
|
@ -28,11 +28,11 @@ async def async_get_triggers(hass, device_id):
|
|||
CONF_DEVICE_ID: device_id,
|
||||
}
|
||||
|
||||
device = async_get_device(hass, device_id)
|
||||
coordinator = async_get_coordinator(hass, device_id)
|
||||
triggers = []
|
||||
|
||||
# Check for long press support.
|
||||
if device.supports_long_press:
|
||||
if coordinator.supports_long_press:
|
||||
triggers.append(
|
||||
{
|
||||
# Required fields of TRIGGER_SCHEMA
|
||||
|
|
|
@ -1,49 +1,30 @@
|
|||
"""Classes shared among Wemo entities."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from pywemo import WeMoDevice
|
||||
from pywemo.exceptions import ActionException
|
||||
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.core import callback
|
||||
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 DeviceWrapper
|
||||
from .wemo_device import DeviceCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExceptionHandlerStatus:
|
||||
"""Exit status from the _wemo_exception_handler context manager."""
|
||||
class WemoEntity(CoordinatorEntity):
|
||||
"""Common methods for Wemo entities."""
|
||||
|
||||
# An exception if one was raised in the _wemo_exception_handler.
|
||||
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:
|
||||
def __init__(self, coordinator: DeviceCoordinator) -> None:
|
||||
"""Initialize the WeMo device."""
|
||||
self.wemo = wemo
|
||||
self._state = None
|
||||
super().__init__(coordinator)
|
||||
self.wemo = coordinator.wemo
|
||||
self._device_info = coordinator.device_info
|
||||
self._available = True
|
||||
self._update_lock = None
|
||||
self._has_polled = False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -52,81 +33,8 @@ class WemoEntity(Entity):
|
|||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return true if switch is available."""
|
||||
return 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
|
||||
"""Return true if the device is available."""
|
||||
return super().available and self._available
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
|
@ -138,59 +46,17 @@ class WemoSubscriptionEntity(WemoEntity):
|
|||
"""Return the device info."""
|
||||
return self._device_info
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the state is on. Standby is on."""
|
||||
return self._state
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._available = True
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if the the device requires local polling, False otherwise.
|
||||
|
||||
It is desirable to allow devices to enter periods of polling when the
|
||||
callback subscription (device push) is not working. To work with the
|
||||
entity platform polling logic, this entity needs to report True for
|
||||
should_poll initially. That is required to cause the entity platform
|
||||
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)
|
||||
@contextlib.contextmanager
|
||||
def _wemo_exception_handler(self, message: str) -> Generator[None, None, None]:
|
||||
"""Wrap device calls to set `_available` when wemo exceptions happen."""
|
||||
try:
|
||||
yield
|
||||
except ActionException as err:
|
||||
_LOGGER.warning("Could not %s for %s (%s)", message, self.name, err)
|
||||
self._available = False
|
||||
|
|
|
@ -7,6 +7,7 @@ import math
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util.percentage import (
|
||||
|
@ -20,7 +21,7 @@ from .const import (
|
|||
SERVICE_RESET_FILTER_LIFE,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
)
|
||||
from .entity import WemoSubscriptionEntity
|
||||
from .entity import WemoEntity
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
@ -68,16 +69,16 @@ SET_HUMIDITY_SCHEMA = {
|
|||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up WeMo binary sensors."""
|
||||
|
||||
async def _discovered_wemo(device):
|
||||
async def _discovered_wemo(coordinator):
|
||||
"""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)
|
||||
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_discovered_wemo(device)
|
||||
for device in hass.data[WEMO_DOMAIN]["pending"].pop("fan")
|
||||
_discovered_wemo(coordinator)
|
||||
for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("fan")
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -94,20 +95,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
)
|
||||
|
||||
|
||||
class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
|
||||
class WemoHumidifier(WemoEntity, FanEntity):
|
||||
"""Representation of a WeMo humidifier."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, coordinator):
|
||||
"""Initialize the WeMo switch."""
|
||||
super().__init__(device)
|
||||
self._fan_mode = WEMO_FAN_OFF
|
||||
self._fan_mode_string = None
|
||||
self._target_humidity = None
|
||||
self._current_humidity = None
|
||||
self._water_level = None
|
||||
self._filter_life = None
|
||||
self._filter_expired = None
|
||||
self._last_fan_on_mode = WEMO_FAN_MEDIUM
|
||||
super().__init__(coordinator)
|
||||
if self.wemo.fan_mode != WEMO_FAN_OFF:
|
||||
self._last_fan_on_mode = self.wemo.fan_mode
|
||||
else:
|
||||
self._last_fan_on_mode = WEMO_FAN_MEDIUM
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
|
@ -118,18 +115,18 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
|
|||
def extra_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return {
|
||||
ATTR_CURRENT_HUMIDITY: self._current_humidity,
|
||||
ATTR_TARGET_HUMIDITY: self._target_humidity,
|
||||
ATTR_FAN_MODE: self._fan_mode_string,
|
||||
ATTR_WATER_LEVEL: self._water_level,
|
||||
ATTR_FILTER_LIFE: self._filter_life,
|
||||
ATTR_FILTER_EXPIRED: self._filter_expired,
|
||||
ATTR_CURRENT_HUMIDITY: self.wemo.current_humidity_percent,
|
||||
ATTR_TARGET_HUMIDITY: self.wemo.desired_humidity_percent,
|
||||
ATTR_FAN_MODE: self.wemo.fan_mode_string,
|
||||
ATTR_WATER_LEVEL: self.wemo.water_level_string,
|
||||
ATTR_FILTER_LIFE: self.wemo.filter_life_percent,
|
||||
ATTR_FILTER_EXPIRED: self.wemo.filter_expired,
|
||||
}
|
||||
|
||||
@property
|
||||
def percentage(self) -> int:
|
||||
"""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
|
||||
def speed_count(self) -> int:
|
||||
|
@ -141,21 +138,17 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
|
|||
"""Flag supported features."""
|
||||
return SUPPORTED_FEATURES
|
||||
|
||||
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)
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if self.wemo.fan_mode != WEMO_FAN_OFF:
|
||||
self._last_fan_on_mode = self.wemo.fan_mode
|
||||
super()._handle_coordinator_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:
|
||||
self._last_fan_on_mode = self.wemo.fan_mode
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the state is on."""
|
||||
return self.wemo.get_state()
|
||||
|
||||
def turn_on(
|
||||
self,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"""Support for Belkin WeMo lights."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant import util
|
||||
from pywemo.ouimeaux_device import bridge
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
|
@ -15,14 +15,13 @@ from homeassistant.components.light import (
|
|||
SUPPORT_TRANSITION,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .const import DOMAIN as WEMO_DOMAIN
|
||||
from .entity import WemoEntity, WemoSubscriptionEntity
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||
from .entity import WemoEntity
|
||||
from .wemo_device import DeviceCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -31,77 +30,75 @@ SUPPORT_WEMO = (
|
|||
)
|
||||
|
||||
# The WEMO_ constants below come from pywemo itself
|
||||
WEMO_ON = 1
|
||||
WEMO_OFF = 0
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up WeMo lights."""
|
||||
|
||||
async def _discovered_wemo(device):
|
||||
async def _discovered_wemo(coordinator: DeviceCoordinator):
|
||||
"""Handle a discovered Wemo device."""
|
||||
if device.wemo.model_name == "Dimmer":
|
||||
async_add_entities([WemoDimmer(device)])
|
||||
if isinstance(coordinator.wemo, bridge.Bridge):
|
||||
async_setup_bridge(hass, config_entry, async_add_entities, coordinator)
|
||||
else:
|
||||
await hass.async_add_executor_job(
|
||||
setup_bridge, hass, device.wemo, async_add_entities
|
||||
)
|
||||
async_add_entities([WemoDimmer(coordinator)])
|
||||
|
||||
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo)
|
||||
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_discovered_wemo(device)
|
||||
for device in hass.data[WEMO_DOMAIN]["pending"].pop("light")
|
||||
_discovered_wemo(coordinator)
|
||||
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."""
|
||||
lights = {}
|
||||
|
||||
@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()
|
||||
known_light_ids = set()
|
||||
|
||||
@callback
|
||||
def async_update_lights():
|
||||
"""Check to see if the bridge has any new lights."""
|
||||
new_lights = []
|
||||
|
||||
for light_id, device in bridge.Lights.items():
|
||||
if light_id not in lights:
|
||||
lights[light_id] = WemoLight(device, update_lights)
|
||||
new_lights.append(lights[light_id])
|
||||
for light_id, light in coordinator.wemo.Lights.items():
|
||||
if light_id not in known_light_ids:
|
||||
known_light_ids.add(light_id)
|
||||
new_lights.append(WemoLight(coordinator, light))
|
||||
|
||||
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):
|
||||
"""Representation of a WeMo light."""
|
||||
|
||||
def __init__(self, device, update_lights):
|
||||
def __init__(self, coordinator: DeviceCoordinator, light: bridge.Light) -> None:
|
||||
"""Initialize the WeMo light."""
|
||||
super().__init__(device)
|
||||
self._update_lights = update_lights
|
||||
self._brightness = None
|
||||
self._hs_color = None
|
||||
self._color_temp = None
|
||||
self._is_on = None
|
||||
self._unique_id = self.wemo.uniqueID
|
||||
self._model_name = type(self.wemo).__name__
|
||||
super().__init__(coordinator)
|
||||
self.light = light
|
||||
self._unique_id = self.light.uniqueID
|
||||
self._model_name = type(self.light).__name__
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Wemo light added to Home Assistant."""
|
||||
# Define inside async context so we know our event loop
|
||||
self._update_lock = asyncio.Lock()
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device if any."""
|
||||
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
|
||||
def unique_id(self):
|
||||
"""Return the ID of this light."""
|
||||
return self.wemo.uniqueID
|
||||
return self.light.uniqueID
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
|
@ -116,22 +113,25 @@ class WemoLight(WemoEntity, LightEntity):
|
|||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
return self.light.state.get("level", 255)
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""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
|
||||
def color_temp(self):
|
||||
"""Return the color temperature of this light in mireds."""
|
||||
return self._color_temp
|
||||
return self.light.state.get("temperature_mireds")
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._is_on
|
||||
return self.light.state.get("onoff") != WEMO_OFF
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -158,13 +158,14 @@ class WemoLight(WemoEntity, LightEntity):
|
|||
|
||||
with self._wemo_exception_handler("turn on"):
|
||||
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:
|
||||
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._state["onoff"] = WEMO_ON
|
||||
self.light.turn_on(**turn_on_kwargs)
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
@ -173,37 +174,14 @@ class WemoLight(WemoEntity, LightEntity):
|
|||
transition_time = int(kwargs.get(ATTR_TRANSITION, 0))
|
||||
|
||||
with self._wemo_exception_handler("turn off"):
|
||||
if self.wemo.turn_off(transition=transition_time):
|
||||
self._state["onoff"] = WEMO_OFF
|
||||
self.light.turn_off(transition=transition_time)
|
||||
|
||||
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")
|
||||
|
||||
if xy_color:
|
||||
self._hs_color = color_util.color_xy_to_hs(*xy_color)
|
||||
else:
|
||||
self._hs_color = None
|
||||
|
||||
|
||||
class WemoDimmer(WemoSubscriptionEntity, LightEntity):
|
||||
class WemoDimmer(WemoEntity, LightEntity):
|
||||
"""Representation of a WeMo dimmer."""
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the WeMo dimmer."""
|
||||
super().__init__(device)
|
||||
self._brightness = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
|
@ -212,15 +190,13 @@ class WemoDimmer(WemoSubscriptionEntity, LightEntity):
|
|||
@property
|
||||
def brightness(self):
|
||||
"""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):
|
||||
"""Update the device state."""
|
||||
with self._wemo_exception_handler("update status"):
|
||||
self._state = self.wemo.get_state(force_update)
|
||||
|
||||
wemobrightness = int(self.wemo.get_brightness(force_update))
|
||||
self._brightness = int((wemobrightness * 255) / 100)
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the state is on."""
|
||||
return self.wemo.get_state()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the dimmer on."""
|
||||
|
@ -231,18 +207,15 @@ class WemoDimmer(WemoSubscriptionEntity, LightEntity):
|
|||
brightness = int((brightness / 255) * 100)
|
||||
with self._wemo_exception_handler("set brightness"):
|
||||
self.wemo.set_brightness(brightness)
|
||||
self._state = WEMO_ON
|
||||
else:
|
||||
with self._wemo_exception_handler("turn on"):
|
||||
self.wemo.on()
|
||||
self._state = WEMO_ON
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the dimmer off."""
|
||||
with self._wemo_exception_handler("turn off"):
|
||||
if self.wemo.off():
|
||||
self._state = WEMO_OFF
|
||||
self.wemo.off()
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
"""Support for power sensors in WeMo Insight devices."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
|
@ -17,52 +15,35 @@ from homeassistant.const import (
|
|||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
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 .entity import WemoSubscriptionEntity
|
||||
from .wemo_device import DeviceWrapper
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
from .entity import WemoEntity
|
||||
from .wemo_device import DeviceCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up WeMo sensors."""
|
||||
|
||||
async def _discovered_wemo(device: DeviceWrapper):
|
||||
async def _discovered_wemo(coordinator: DeviceCoordinator):
|
||||
"""Handle a discovered Wemo device."""
|
||||
|
||||
@Throttle(SCAN_INTERVAL)
|
||||
def update_insight_params():
|
||||
device.wemo.update_insight_params()
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
InsightCurrentPower(device, update_insight_params),
|
||||
InsightTodayEnergy(device, update_insight_params),
|
||||
]
|
||||
[InsightCurrentPower(coordinator), InsightTodayEnergy(coordinator)]
|
||||
)
|
||||
|
||||
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo)
|
||||
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_discovered_wemo(device)
|
||||
for device in hass.data[WEMO_DOMAIN]["pending"].pop("sensor")
|
||||
_discovered_wemo(coordinator)
|
||||
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."""
|
||||
|
||||
_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
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity if any."""
|
||||
|
@ -81,11 +62,6 @@ class InsightSensor(WemoSubscriptionEntity, SensorEntity):
|
|||
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):
|
||||
"""Current instantaineous power consumption."""
|
||||
|
|
|
@ -3,13 +3,15 @@ import asyncio
|
|||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from pywemo import CoffeeMaker, Insight, Maker
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util import convert
|
||||
|
||||
from .const import DOMAIN as WEMO_DOMAIN
|
||||
from .entity import WemoSubscriptionEntity
|
||||
from .entity import WemoEntity
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
@ -33,63 +35,61 @@ WEMO_STANDBY = 8
|
|||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up WeMo switches."""
|
||||
|
||||
async def _discovered_wemo(device):
|
||||
async def _discovered_wemo(coordinator):
|
||||
"""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)
|
||||
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_discovered_wemo(device)
|
||||
for device in hass.data[WEMO_DOMAIN]["pending"].pop("switch")
|
||||
_discovered_wemo(coordinator)
|
||||
for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("switch")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class WemoSwitch(WemoSubscriptionEntity, SwitchEntity):
|
||||
class WemoSwitch(WemoEntity, SwitchEntity):
|
||||
"""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
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
attr = {}
|
||||
if self.maker_params:
|
||||
if isinstance(self.wemo, Maker):
|
||||
# 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'!
|
||||
if self.maker_params["sensorstate"]:
|
||||
if self.wemo.maker_params["sensorstate"]:
|
||||
attr[ATTR_SENSOR_STATE] = STATE_OFF
|
||||
else:
|
||||
attr[ATTR_SENSOR_STATE] = STATE_ON
|
||||
|
||||
# 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
|
||||
else:
|
||||
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
|
||||
|
||||
if self.insight_params:
|
||||
attr["on_latest_time"] = WemoSwitch.as_uptime(self.insight_params["onfor"])
|
||||
attr["on_today_time"] = WemoSwitch.as_uptime(self.insight_params["ontoday"])
|
||||
attr["on_total_time"] = WemoSwitch.as_uptime(self.insight_params["ontotal"])
|
||||
if isinstance(self.wemo, Insight):
|
||||
attr["on_latest_time"] = WemoSwitch.as_uptime(
|
||||
self.wemo.insight_params["onfor"]
|
||||
)
|
||||
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"] = (
|
||||
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:
|
||||
attr[ATTR_COFFEMAKER_MODE] = self.coffeemaker_mode
|
||||
if isinstance(self.wemo, CoffeeMaker):
|
||||
attr[ATTR_COFFEMAKER_MODE] = self.wemo.mode
|
||||
|
||||
return attr
|
||||
|
||||
|
@ -104,23 +104,25 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity):
|
|||
@property
|
||||
def current_power_w(self):
|
||||
"""Return the current power usage in W."""
|
||||
if self.insight_params:
|
||||
return convert(self.insight_params["currentpower"], float, 0.0) / 1000.0
|
||||
if isinstance(self.wemo, Insight):
|
||||
return (
|
||||
convert(self.wemo.insight_params["currentpower"], float, 0.0) / 1000.0
|
||||
)
|
||||
|
||||
@property
|
||||
def today_energy_kwh(self):
|
||||
"""Return the today total energy usage in kWh."""
|
||||
if self.insight_params:
|
||||
miliwatts = convert(self.insight_params["todaymw"], float, 0.0)
|
||||
if isinstance(self.wemo, Insight):
|
||||
miliwatts = convert(self.wemo.insight_params["todaymw"], float, 0.0)
|
||||
return round(miliwatts / (1000.0 * 1000.0 * 60), 2)
|
||||
|
||||
@property
|
||||
def detail_state(self):
|
||||
"""Return the state of the device."""
|
||||
if self.coffeemaker_mode is not None:
|
||||
return self._mode_string
|
||||
if self.insight_params:
|
||||
standby_state = int(self.insight_params["state"])
|
||||
if isinstance(self.wemo, CoffeeMaker):
|
||||
return self.wemo.mode_string
|
||||
if isinstance(self.wemo, Insight):
|
||||
standby_state = int(self.wemo.insight_params["state"])
|
||||
if standby_state == WEMO_ON:
|
||||
return STATE_ON
|
||||
if standby_state == WEMO_OFF:
|
||||
|
@ -132,36 +134,25 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity):
|
|||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of device based on its type."""
|
||||
if self.wemo.model_name == "CoffeeMaker":
|
||||
if isinstance(self.wemo, CoffeeMaker):
|
||||
return "mdi:coffee"
|
||||
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):
|
||||
"""Turn the switch on."""
|
||||
with self._wemo_exception_handler("turn on"):
|
||||
if self.wemo.on():
|
||||
self._state = WEMO_ON
|
||||
self.wemo.on()
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the switch off."""
|
||||
with self._wemo_exception_handler("turn off"):
|
||||
if self.wemo.off():
|
||||
self._state = WEMO_OFF
|
||||
self.wemo.off()
|
||||
|
||||
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
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
"""Home Assistant wrapper for a pyWeMo device."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pywemo import WeMoDevice
|
||||
from pywemo.exceptions import ActionException
|
||||
from pywemo.subscribe import EVENT_TYPE_LONG_PRESS
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@ -14,28 +17,36 @@ from homeassistant.const import (
|
|||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
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__)
|
||||
|
||||
|
||||
class DeviceWrapper:
|
||||
class DeviceCoordinator(DataUpdateCoordinator):
|
||||
"""Home Assistant wrapper for a pyWeMo device."""
|
||||
|
||||
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.wemo = wemo
|
||||
self.device_id = device_id
|
||||
self.device_info = _device_info(wemo)
|
||||
self.supports_long_press = wemo.supports_long_press()
|
||||
self.update_lock = asyncio.Lock()
|
||||
|
||||
def subscription_callback(
|
||||
self, _device: WeMoDevice, event_type: str, params: str
|
||||
) -> None:
|
||||
"""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:
|
||||
self.hass.bus.fire(
|
||||
WEMO_SUBSCRIPTION_EVENT,
|
||||
|
@ -48,9 +59,50 @@ class DeviceWrapper:
|
|||
},
|
||||
)
|
||||
else:
|
||||
dispatcher_send(
|
||||
self.hass, SIGNAL_WEMO_STATE_PUSH, self.device_id, event_type, params
|
||||
)
|
||||
updated = self.wemo.subscription_update(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):
|
||||
|
@ -64,19 +116,21 @@ def _device_info(wemo: WeMoDevice):
|
|||
|
||||
async def async_register_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice
|
||||
) -> DeviceWrapper:
|
||||
) -> DeviceCoordinator:
|
||||
"""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)
|
||||
entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id, **_device_info(wemo)
|
||||
)
|
||||
|
||||
registry = hass.data[DOMAIN]["registry"]
|
||||
await hass.async_add_executor_job(registry.register, wemo)
|
||||
|
||||
device = DeviceWrapper(hass, wemo, entry.id)
|
||||
device = DeviceCoordinator(hass, wemo, entry.id)
|
||||
hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device
|
||||
registry = hass.data[DOMAIN]["registry"]
|
||||
registry.on(wemo, None, device.subscription_callback)
|
||||
await hass.async_add_executor_job(registry.register, wemo)
|
||||
|
||||
if device.supports_long_press:
|
||||
try:
|
||||
|
@ -93,6 +147,6 @@ async def async_register_device(
|
|||
|
||||
|
||||
@callback
|
||||
def async_get_device(hass: HomeAssistant, device_id: str) -> DeviceWrapper:
|
||||
"""Return DeviceWrapper for device_id."""
|
||||
def async_get_coordinator(hass: HomeAssistant, device_id: str) -> DeviceCoordinator:
|
||||
"""Return DeviceCoordinator for device_id."""
|
||||
return hass.data[DOMAIN]["devices"][device_id]
|
||||
|
|
|
@ -35,6 +35,7 @@ async def async_pywemo_registry_fixture():
|
|||
registry.semaphore.release()
|
||||
|
||||
registry.on.side_effect = on_func
|
||||
registry.is_subscribed.return_value = False
|
||||
|
||||
with patch("pywemo.SubscriptionRegistry", return_value=registry):
|
||||
yield registry
|
||||
|
|
|
@ -4,196 +4,133 @@ This is not a test module. These test methods are used by the platform test modu
|
|||
"""
|
||||
import asyncio
|
||||
import threading
|
||||
from unittest.mock import patch
|
||||
|
||||
import async_timeout
|
||||
import pywemo
|
||||
from pywemo.ouimeaux_device.api.service import ActionException
|
||||
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.components.wemo import wemo_device
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
async def async_callback():
|
||||
event = asyncio.Event()
|
||||
|
||||
async def event_callback(e, *args):
|
||||
event.set()
|
||||
|
||||
stop_dispatcher_listener = async_dispatcher_connect(
|
||||
hass, SIGNAL_WEMO_STATE_PUSH, event_callback
|
||||
await coordinator.hass.async_add_executor_job(
|
||||
coordinator.subscription_callback, coordinator.wemo, "", ""
|
||||
)
|
||||
# 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
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
@callback
|
||||
def async_callback():
|
||||
return hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{ATTR_ENTITY_ID: [wemo_entity.entity_id]},
|
||||
blocking=True,
|
||||
)
|
||||
async def async_callback():
|
||||
await coordinator._async_update_data()
|
||||
|
||||
return async_callback
|
||||
|
||||
|
||||
async def _async_multiple_call_helper(
|
||||
hass,
|
||||
pywemo_registry,
|
||||
wemo_entity,
|
||||
pywemo_device,
|
||||
call1,
|
||||
call2,
|
||||
update_polling_method=None,
|
||||
):
|
||||
async def _async_multiple_call_helper(hass, pywemo_device, call1, call2):
|
||||
"""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.
|
||||
Any parallel updates that happen at the same time should be ignored. This is
|
||||
verified by blocking in the update polling method. The polling method should
|
||||
only be called once as a result of calling call1 & call2 simultaneously.
|
||||
There should only be one poll on the device at a time. Any parallel updates
|
||||
# that happen at the same time should be ignored. This is verified by blocking
|
||||
in the get_state method. The polling method should only be called once as a
|
||||
result of calling call1 & call2 simultaneously.
|
||||
"""
|
||||
# get_state is called outside the event loop. Use non-async Python Event.
|
||||
event = threading.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)
|
||||
event.wait()
|
||||
|
||||
update_polling_method = update_polling_method or pywemo_device.get_state
|
||||
update_polling_method.side_effect = get_update
|
||||
# Danger! Do not use a Mock side_effect here. The test will deadlock. When
|
||||
# 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
|
||||
# away because the `_update_lock` is held.
|
||||
_, pending = await asyncio.wait(
|
||||
done, pending = await asyncio.wait(
|
||||
[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.
|
||||
await waiting.wait()
|
||||
event.set()
|
||||
|
||||
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.
|
||||
update_polling_method.assert_called_once()
|
||||
assert call_count == 1
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
coordinator = wemo_device.async_get_coordinator(hass, wemo_entity.device_id)
|
||||
await async_setup_component(hass, HA_DOMAIN, {})
|
||||
callback = _perform_registry_callback(hass, pywemo_registry, pywemo_device)
|
||||
update = _perform_async_update(hass, wemo_entity)
|
||||
await _async_multiple_call_helper(
|
||||
hass, pywemo_registry, wemo_entity, pywemo_device, callback, update, **kwargs
|
||||
)
|
||||
callback = _perform_registry_callback(coordinator)
|
||||
update = _perform_async_update(coordinator)
|
||||
await _async_multiple_call_helper(hass, pywemo_device, callback, update)
|
||||
|
||||
|
||||
async def test_async_update_locked_multiple_updates(
|
||||
hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs
|
||||
):
|
||||
async def test_async_update_locked_multiple_updates(hass, pywemo_device, wemo_entity):
|
||||
"""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, {})
|
||||
update = _perform_async_update(hass, wemo_entity)
|
||||
await _async_multiple_call_helper(
|
||||
hass, pywemo_registry, wemo_entity, pywemo_device, update, update, **kwargs
|
||||
)
|
||||
update = _perform_async_update(coordinator)
|
||||
await _async_multiple_call_helper(hass, pywemo_device, update, update)
|
||||
|
||||
|
||||
async def test_async_update_locked_multiple_callbacks(
|
||||
hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs
|
||||
):
|
||||
async def test_async_update_locked_multiple_callbacks(hass, pywemo_device, wemo_entity):
|
||||
"""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, {})
|
||||
callback = _perform_registry_callback(hass, pywemo_registry, pywemo_device)
|
||||
await _async_multiple_call_helper(
|
||||
hass, pywemo_registry, wemo_entity, pywemo_device, callback, callback, **kwargs
|
||||
)
|
||||
callback = _perform_registry_callback(coordinator)
|
||||
await _async_multiple_call_helper(hass, pywemo_device, callback, callback)
|
||||
|
||||
|
||||
async def test_async_locked_update_with_exception(
|
||||
hass,
|
||||
wemo_entity,
|
||||
pywemo_device,
|
||||
update_polling_method=None,
|
||||
expected_state=STATE_OFF,
|
||||
async def test_avaliable_after_update(
|
||||
hass, pywemo_registry, pywemo_device, wemo_entity, domain
|
||||
):
|
||||
"""Test that the entity becomes unavailable when communication is lost."""
|
||||
assert hass.states.get(wemo_entity.entity_id).state == expected_state
|
||||
await async_setup_component(hass, HA_DOMAIN, {})
|
||||
update_polling_method = update_polling_method or pywemo_device.get_state
|
||||
update_polling_method.side_effect = ActionException
|
||||
"""Test the avaliability when an On call fails and after an update.
|
||||
|
||||
This test expects that the pywemo_device Mock has been setup to raise an
|
||||
ActionException when the SERVICE_TURN_ON method is called and that the
|
||||
state will be On after the update.
|
||||
"""
|
||||
await async_setup_component(hass, domain, {})
|
||||
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
domain,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: [wemo_entity.entity_id]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
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()
|
||||
pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "")
|
||||
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
|
||||
|
|
|
@ -30,12 +30,6 @@ test_async_update_locked_multiple_callbacks = (
|
|||
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(
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""Tests for the Wemo fan entity."""
|
||||
|
||||
import pytest
|
||||
from pywemo.exceptions import ActionException
|
||||
|
||||
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
|
@ -32,12 +34,6 @@ test_async_update_locked_multiple_callbacks = (
|
|||
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(
|
||||
|
@ -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
|
||||
|
||||
|
||||
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):
|
||||
"""Verify that SERVICE_RESET_FILTER_LIFE is registered and works."""
|
||||
assert await hass.services.async_call(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""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 pywemo
|
||||
|
@ -8,10 +8,9 @@ from homeassistant.components.homeassistant import (
|
|||
DOMAIN as HA_DOMAIN,
|
||||
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.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import entity_test_helpers
|
||||
|
||||
|
@ -32,60 +31,53 @@ def pywemo_bridge_light_fixture(pywemo_device):
|
|||
light.uniqueID = pywemo_device.serialnumber
|
||||
light.name = pywemo_device.name
|
||||
light.bridge = pywemo_device
|
||||
light.state = {"onoff": 0}
|
||||
light.state = {"onoff": 0, "available": True}
|
||||
pywemo_device.Lights = {pywemo_device.serialnumber: light}
|
||||
return light
|
||||
|
||||
|
||||
def _bypass_throttling():
|
||||
"""Bypass the util.Throttle on the update_lights method."""
|
||||
utcnow = dt_util.utcnow()
|
||||
|
||||
def increment_and_return_time():
|
||||
nonlocal utcnow
|
||||
utcnow += MIN_TIME_BETWEEN_SCANS
|
||||
return utcnow
|
||||
|
||||
return patch("homeassistant.util.utcnow", side_effect=increment_and_return_time)
|
||||
async def test_async_update_locked_callback_and_update(
|
||||
hass, pywemo_bridge_light, wemo_entity, pywemo_device
|
||||
):
|
||||
"""Test that a callback and a state update request can't both happen at the same time."""
|
||||
await entity_test_helpers.test_async_update_locked_callback_and_update(
|
||||
hass,
|
||||
pywemo_device,
|
||||
wemo_entity,
|
||||
)
|
||||
|
||||
|
||||
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."""
|
||||
pywemo_device.bridge_update.reset_mock()
|
||||
|
||||
with _bypass_throttling():
|
||||
await entity_test_helpers.test_async_update_locked_multiple_updates(
|
||||
hass,
|
||||
pywemo_registry,
|
||||
wemo_entity,
|
||||
pywemo_device,
|
||||
update_polling_method=pywemo_device.bridge_update,
|
||||
)
|
||||
await entity_test_helpers.test_async_update_locked_multiple_updates(
|
||||
hass,
|
||||
pywemo_device,
|
||||
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
|
||||
):
|
||||
"""Test that the entity becomes unavailable after a timeout, and that it recovers."""
|
||||
with _bypass_throttling():
|
||||
await entity_test_helpers.test_async_update_with_timeout_and_recovery(
|
||||
hass, wemo_entity, pywemo_device
|
||||
)
|
||||
"""Test that two device callback state updates do not proceed at the same time."""
|
||||
await entity_test_helpers.test_async_update_locked_multiple_callbacks(
|
||||
hass,
|
||||
pywemo_device,
|
||||
wemo_entity,
|
||||
)
|
||||
|
||||
|
||||
async def test_async_locked_update_with_exception(
|
||||
hass, pywemo_bridge_light, wemo_entity, pywemo_device
|
||||
async def test_available_after_update(
|
||||
hass, pywemo_registry, pywemo_device, pywemo_bridge_light, wemo_entity
|
||||
):
|
||||
"""Test that the entity becomes unavailable when communication is lost."""
|
||||
with _bypass_throttling():
|
||||
await entity_test_helpers.test_async_locked_update_with_exception(
|
||||
hass,
|
||||
wemo_entity,
|
||||
pywemo_device,
|
||||
update_polling_method=pywemo_device.bridge_update,
|
||||
)
|
||||
"""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
|
||||
)
|
||||
|
||||
|
||||
async def test_light_update_entity(
|
||||
|
@ -95,7 +87,7 @@ async def test_light_update_entity(
|
|||
await async_setup_component(hass, HA_DOMAIN, {})
|
||||
|
||||
# On state.
|
||||
pywemo_bridge_light.state = {"onoff": 1}
|
||||
pywemo_bridge_light.state["onoff"] = 1
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
|
@ -105,7 +97,7 @@ async def test_light_update_entity(
|
|||
assert hass.states.get(wemo_entity.entity_id).state == STATE_ON
|
||||
|
||||
# Off state.
|
||||
pywemo_bridge_light.state = {"onoff": 0}
|
||||
pywemo_bridge_light.state["onoff"] = 0
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
"""Tests for the Wemo standalone/non-bridge light entity."""
|
||||
|
||||
import pytest
|
||||
from pywemo.exceptions import ActionException
|
||||
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
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.setup import async_setup_component
|
||||
|
||||
|
@ -30,12 +32,17 @@ test_async_update_locked_multiple_callbacks = (
|
|||
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_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, LIGHT_DOMAIN
|
||||
)
|
||||
|
||||
|
||||
async def test_light_registry_state_callback(
|
||||
|
|
|
@ -78,62 +78,33 @@ class InsightTestTemplate:
|
|||
# in the scope of this test module. They will run using the pywemo_model from
|
||||
# this test module (Insight).
|
||||
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."""
|
||||
pywemo_device.subscription_update.return_value = False
|
||||
await entity_test_helpers.test_async_update_locked_multiple_updates(
|
||||
hass,
|
||||
pywemo_registry,
|
||||
wemo_entity,
|
||||
pywemo_device,
|
||||
update_polling_method=pywemo_device.update_insight_params,
|
||||
wemo_entity,
|
||||
)
|
||||
|
||||
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."""
|
||||
pywemo_device.subscription_update.return_value = False
|
||||
await entity_test_helpers.test_async_update_locked_multiple_callbacks(
|
||||
hass,
|
||||
pywemo_registry,
|
||||
wemo_entity,
|
||||
pywemo_device,
|
||||
update_polling_method=pywemo_device.update_insight_params,
|
||||
wemo_entity,
|
||||
)
|
||||
|
||||
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."""
|
||||
pywemo_device.subscription_update.return_value = False
|
||||
await entity_test_helpers.test_async_update_locked_callback_and_update(
|
||||
hass,
|
||||
pywemo_registry,
|
||||
wemo_entity,
|
||||
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,
|
||||
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):
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
"""Tests for the Wemo switch entity."""
|
||||
|
||||
import pytest
|
||||
from pywemo.exceptions import ActionException
|
||||
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
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.setup import async_setup_component
|
||||
|
||||
|
@ -30,12 +32,6 @@ test_async_update_locked_multiple_callbacks = (
|
|||
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(
|
||||
|
@ -78,3 +74,14 @@ async def test_switch_update_entity(hass, pywemo_registry, pywemo_device, wemo_e
|
|||
blocking=True,
|
||||
)
|
||||
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
|
||||
)
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
"""Tests for wemo_device.py."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
import async_timeout
|
||||
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.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.update_coordinator import UpdateFailed
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import MOCK_HOST
|
||||
|
||||
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(True))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pywemo_model():
|
||||
|
@ -36,5 +44,107 @@ async def test_async_register_device_longpress_fails(hass, pywemo_device):
|
|||
dr = device_registry.async_get(hass)
|
||||
device_entries = list(dr.devices.values())
|
||||
assert len(device_entries) == 1
|
||||
device_wrapper = wemo_device.async_get_device(hass, device_entries[0].id)
|
||||
assert device_wrapper.supports_long_press is False
|
||||
device = wemo_device.async_get_coordinator(hass, device_entries[0].id)
|
||||
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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue