From 82558156886fb2641a5be064ef7d824ffa2149a2 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 13 Dec 2020 03:02:45 -0800 Subject: [PATCH] Share wemo entity code to reduce duplicate boilerplate (#44113) --- .../components/wemo/binary_sensor.py | 97 +------------ homeassistant/components/wemo/entity.py | 124 ++++++++++++++++ homeassistant/components/wemo/fan.py | 95 +----------- homeassistant/components/wemo/light.py | 135 +----------------- homeassistant/components/wemo/switch.py | 105 ++------------ 5 files changed, 144 insertions(+), 412 deletions(-) create mode 100644 homeassistant/components/wemo/entity.py diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 44031e846c3..b6690ed6d28 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -2,13 +2,13 @@ import asyncio import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException 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 _LOGGER = logging.getLogger(__name__) @@ -30,72 +30,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoBinarySensor(BinarySensorEntity): +class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity): """Representation a WeMo binary sensor.""" - def __init__(self, device): - """Initialize the WeMo sensor.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serial_number = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo sensor.""" - _LOGGER.debug("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Wemo sensor added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_will_remove_from_hass(self) -> None: - """Wemo sensor removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - async def async_update(self): - """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 after 5 seconds, assume the - Wemo sensor 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: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - def _update(self, force_update=True): """Update the sensor state.""" try: @@ -108,33 +45,3 @@ class WemoBinarySensor(BinarySensorEntity): _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False self.wemo.reconnect_with_device() - - @property - def unique_id(self): - """Return the id of this WeMo sensor.""" - return self._serial_number - - @property - def name(self): - """Return the name of the service if any.""" - return self._name - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state - - @property - def available(self): - """Return true if sensor is available.""" - return self._available - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serial_number)}, - "model": self._model_name, - "manufacturer": "Belkin", - } diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py new file mode 100644 index 00000000000..e7c0712272c --- /dev/null +++ b/homeassistant/components/wemo/entity.py @@ -0,0 +1,124 @@ +"""Classes shared among Wemo entities.""" +import asyncio +import logging +from typing import Any, Dict, Optional + +import async_timeout +from pywemo import WeMoDevice + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN as WEMO_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class WemoEntity(Entity): + """Common methods for Wemo entities. + + Requires that subclasses implement the _update method. + """ + + def __init__(self, device: WeMoDevice) -> None: + """Initialize the WeMo device.""" + self.wemo = device + self._state = None + self._available = True + self._update_lock = None + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return self.wemo.name + + @property + def available(self) -> bool: + """Return true if switch is available.""" + return self._available + + def _update(self, force_update: Optional[bool] = 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 after 5 seconds, 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: + with async_timeout.timeout(5): + await asyncio.shield(self._async_locked_update(True)) + except asyncio.TimeoutError: + _LOGGER.warning("Lost connection to %s", self.name) + self._available = False + + async def _async_locked_update(self, force_update: bool) -> None: + """Try updating within an async lock.""" + async with self._update_lock: + await self.hass.async_add_executor_job(self._update, force_update) + + +class WemoSubscriptionEntity(WemoEntity): + """Common methods for Wemo devices that register for update callbacks.""" + + @property + def unique_id(self) -> str: + """Return the id of this WeMo device.""" + return self.wemo.serialnumber + + @property + def device_info(self) -> Dict[str, Any]: + """Return the device info.""" + return { + "name": self.name, + "identifiers": {(WEMO_DOMAIN, self.unique_id)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } + + @property + def is_on(self) -> bool: + """Return true if the state is on. Standby is on.""" + return self._state + + async def async_added_to_hass(self) -> None: + """Wemo device added to Home Assistant.""" + await super().async_added_to_hass() + + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.register, self.wemo) + registry.on(self.wemo, None, self._subscription_callback) + + async def async_will_remove_from_hass(self) -> None: + """Wemo device removed from hass.""" + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.unregister, self.wemo) + + def _subscription_callback( + self, _device: WeMoDevice, _type: str, _params: str + ) -> None: + """Update the state by the Wemo device.""" + _LOGGER.info("Subscription update for %s", self.name) + updated = self.wemo.subscription_update(_type, _params) + self.hass.add_job(self._async_locked_subscription_callback(not updated)) + + async def _async_locked_subscription_callback(self, force_update: bool) -> None: + """Handle an update from a subscription.""" + # If an update is in progress, we don't do anything + if self._update_lock.locked(): + return + + await self._async_locked_update(force_update) + self.async_write_ha_state() diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index c7325f776fa..0d5ded7b828 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException import voluptuous as vol @@ -24,6 +23,7 @@ from .const import ( SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY, ) +from .entity import WemoSubscriptionEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -143,15 +143,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoHumidifier(FanEntity): +class WemoHumidifier(WemoSubscriptionEntity, FanEntity): """Representation of a WeMo humidifier.""" def __init__(self, device): """Initialize the WeMo switch.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None + super().__init__(device) self._fan_mode = None self._target_humidity = None self._current_humidity = None @@ -159,54 +156,6 @@ class WemoHumidifier(FanEntity): self._filter_life = None self._filter_expired = None self._last_fan_on_mode = WEMO_FAN_MEDIUM - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - @property - def unique_id(self): - """Return the ID of this WeMo humidifier.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the humidifier if any.""" - return self._name - - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self._state - - @property - def available(self): - """Return true if switch is available.""" - return self._available - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def icon(self): @@ -240,44 +189,6 @@ class WemoHumidifier(FanEntity): """Flag supported features.""" return SUPPORTED_FEATURES - async def async_added_to_hass(self): - """Wemo humidifier added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_will_remove_from_hass(self) -> None: - """Wemo humidifier removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - async def async_update(self): - """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 after 5 seconds, assume the - Wemo humidifier 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: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - def _update(self, force_update=True): """Update the device state.""" try: diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 4c88c157f9b..1362c7d483c 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant import util @@ -22,6 +21,7 @@ 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) @@ -81,21 +81,17 @@ def setup_bridge(hass, bridge, async_add_entities): update_lights() -class WemoLight(LightEntity): +class WemoLight(WemoEntity, LightEntity): """Representation of a WeMo light.""" def __init__(self, device, update_lights): """Initialize the WeMo light.""" - self.wemo = device - self._state = None + super().__init__(device) self._update_lights = update_lights - self._available = True - self._update_lock = None self._brightness = None self._hs_color = None self._color_temp = None self._is_on = None - self._name = self.wemo.name self._unique_id = self.wemo.uniqueID self._model_name = type(self.wemo).__name__ @@ -107,18 +103,13 @@ class WemoLight(LightEntity): @property def unique_id(self): """Return the ID of this light.""" - return self._unique_id - - @property - def name(self): - """Return the name of the light.""" - return self._name + return self.wemo.uniqueID @property def device_info(self): """Return the device info.""" return { - "name": self._name, + "name": self.name, "identifiers": {(WEMO_DOMAIN, self._unique_id)}, "model": self._model_name, "manufacturer": "Belkin", @@ -149,11 +140,6 @@ class WemoLight(LightEntity): """Flag supported features.""" return SUPPORT_WEMO - @property - def available(self): - """Return if light is available.""" - return self._available - def turn_on(self, **kwargs): """Turn the light on.""" xy_color = None @@ -222,111 +208,14 @@ class WemoLight(LightEntity): else: self._hs_color = None - async def async_update(self): - """Synchronize state with bridge.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - -class WemoDimmer(LightEntity): +class WemoDimmer(WemoSubscriptionEntity, LightEntity): """Representation of a WeMo dimmer.""" def __init__(self, device): """Initialize the WeMo dimmer.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None + super().__init__(device) self._brightness = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.debug("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Wemo dimmer added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_will_remove_from_hass(self) -> None: - """Wemo dimmer removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - async def async_update(self): - """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 after 5 seconds, assume the - Wemo dimmer 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: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - @property - def unique_id(self): - """Return the ID of this WeMo dimmer.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the dimmer if any.""" - return self._name - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def supported_features(self): @@ -338,11 +227,6 @@ class WemoDimmer(LightEntity): """Return the brightness of this light between 1 and 100.""" return self._brightness - @property - def is_on(self): - """Return true if dimmer is on. Standby is on.""" - return self._state - def _update(self, force_update=True): """Update the device state.""" try: @@ -390,8 +274,3 @@ class WemoDimmer(LightEntity): self._available = False self.schedule_update_ha_state() - - @property - def available(self): - """Return if dimmer is available.""" - return self._available diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index e2210d0279a..50926e07a11 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -3,7 +3,6 @@ import asyncio from datetime import datetime, timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.switch import SwitchEntity @@ -12,6 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN +from .entity import WemoSubscriptionEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -49,57 +49,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoSwitch(SwitchEntity): +class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): """Representation of a WeMo switch.""" def __init__(self, device): """Initialize the WeMo switch.""" - self.wemo = device + super().__init__(device) self.insight_params = None self.maker_params = None self.coffeemaker_mode = None - self._state = None self._mode_string = None - self._available = True - self._update_lock = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - @property - def unique_id(self): - """Return the ID of this WeMo switch.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the switch if any.""" - return self._name - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def device_state_attributes(self): @@ -172,20 +131,10 @@ class WemoSwitch(SwitchEntity): return STATE_STANDBY return STATE_UNKNOWN - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self._state - - @property - def available(self): - """Return true if switch is available.""" - return self._available - @property def icon(self): """Return the icon of device based on its type.""" - if self._model_name == "CoffeeMaker": + if self.wemo.model_name == "CoffeeMaker": return "mdi:coffee" return None @@ -211,55 +160,17 @@ class WemoSwitch(SwitchEntity): self.schedule_update_ha_state() - async def async_added_to_hass(self): - """Wemo switch added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_will_remove_from_hass(self) -> None: - """Wemo switch removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - async def async_update(self): - """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 after 5 seconds, 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: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - def _update(self, force_update): + def _update(self, force_update=True): """Update the device state.""" try: self._state = self.wemo.get_state(force_update) - if self._model_name == "Insight": + 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._model_name == "Maker": + elif self.wemo.model_name == "Maker": self.maker_params = self.wemo.maker_params - elif self._model_name == "CoffeeMaker": + elif self.wemo.model_name == "CoffeeMaker": self.coffeemaker_mode = self.wemo.mode self._mode_string = self.wemo.mode_string