core/homeassistant/components/wemo/wemo_device.py

153 lines
5.5 KiB
Python

"""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
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_NAME,
CONF_PARAMS,
CONF_TYPE,
CONF_UNIQUE_ID,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import async_get as async_get_device_registry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, WEMO_SUBSCRIPTION_EVENT
_LOGGER = logging.getLogger(__name__)
class DeviceCoordinator(DataUpdateCoordinator):
"""Home Assistant wrapper for a pyWeMo device."""
def __init__(self, hass: HomeAssistant, wemo: WeMoDevice, device_id: str) -> None:
"""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,
{
CONF_DEVICE_ID: self.device_id,
CONF_NAME: self.wemo.name,
CONF_TYPE: event_type,
CONF_PARAMS: params,
CONF_UNIQUE_ID: self.wemo.serialnumber,
},
)
else:
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):
return {
"name": wemo.name,
"identifiers": {(DOMAIN, wemo.serialnumber)},
"model": wemo.model_name,
"manufacturer": "Belkin",
}
async def async_register_device(
hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice
) -> 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)
)
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:
await hass.async_add_executor_job(wemo.ensure_long_press_virtual_device)
# Temporarily handling all exceptions for #52996 & pywemo/pywemo/issues/276
# Replace this with `except: PyWeMoException` after upstream has been fixed.
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Failed to enable long press support for device: %s", wemo.name
)
device.supports_long_press = False
return device
@callback
def async_get_coordinator(hass: HomeAssistant, device_id: str) -> DeviceCoordinator:
"""Return DeviceCoordinator for device_id."""
return hass.data[DOMAIN]["devices"][device_id]