From 24c40f4aadc89060f3061b2b4a3f3da79f7ae98e Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 9 Dec 2020 10:02:05 -0800 Subject: [PATCH 1/6] Use a singleton for the Wemo registry and fan services --- homeassistant/components/wemo/__init__.py | 11 ++++- homeassistant/components/wemo/entity.py | 5 +- homeassistant/components/wemo/fan.py | 56 ++++++++++++++++------- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index c656926ecff97..72a2846899d56 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -8,8 +8,10 @@ from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.singleton import singleton from .const import DOMAIN @@ -68,11 +70,16 @@ def coerce_host_port(value): ) +@singleton("f{__name__}.registry") +async def async_get_registry(hass: HomeAssistant) -> pywemo.SubscriptionRegistry: + """Return the pywemo subscription registry.""" + return await hass.async_add_executor_job(pywemo.SubscriptionRegistry) + + async def async_setup(hass, config): """Set up for WeMo devices.""" hass.data[DOMAIN] = { "config": config.get(DOMAIN, {}), - "registry": None, "pending": {}, } @@ -91,7 +98,7 @@ async def async_setup_entry(hass, entry): config = hass.data[DOMAIN].pop("config") # Keep track of WeMo device subscriptions for push updates - registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() + registry = await async_get_registry(hass) await hass.async_add_executor_job(registry.start) def stop_wemo(event): diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index e7c0712272ccb..77c0f04d3ff34 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -8,6 +8,7 @@ from homeassistant.helpers.entity import Entity +from . import async_get_registry from .const import DOMAIN as WEMO_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -97,13 +98,13 @@ 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"] + registry = await async_get_registry(self.hass) 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"] + registry = await async_get_registry(self.hass) await self.hass.async_add_executor_job(registry.unregister, self.wemo) def _subscription_callback( diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 0d5ded7b82849..d5ed962e47987 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from typing import List from pywemo.ouimeaux_device.api.service import ActionException import voluptuous as vol @@ -15,8 +16,10 @@ FanEntity, ) from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.singleton import singleton from .const import ( DOMAIN as WEMO_DOMAIN, @@ -93,24 +96,14 @@ RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up WeMo binary sensors.""" - entities = [] +@singleton(f"{__name__}.service") +async def _async_get_humidifiers(hass: HomeAssistant) -> List["WemoHumidifier"]: + """Singleton containing humidifier services. - async def _discovered_wemo(device): - """Handle a discovered Wemo device.""" - entity = WemoHumidifier(device) - entities.append(entity) - async_add_entities([entity]) - - 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") - ] - ) + Returns: + List of humidifiers used by the services. + """ + entities = [] def service_handle(service): """Handle the WeMo humidifier services.""" @@ -142,6 +135,25 @@ def service_handle(service): schema=RESET_FILTER_LIFE_SCHEMA, ) + return entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo binary sensors.""" + + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" + async_add_entities([WemoHumidifier(device)]) + + 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") + ] + ) + class WemoHumidifier(WemoSubscriptionEntity, FanEntity): """Representation of a WeMo humidifier.""" @@ -189,6 +201,16 @@ def supported_features(self) -> int: """Flag supported features.""" return SUPPORTED_FEATURES + async def async_added_to_hass(self) -> None: + """Wemo humidifier added to Home Assistant.""" + await super().async_added_to_hass() + (await _async_get_humidifiers(self.hass)).append(self) + + async def async_will_remove_from_hass(self) -> None: + """Wemo humidifier removed from hass.""" + await super().async_will_remove_from_hass() + (await _async_get_humidifiers(self.hass)).remove(self) + def _update(self, force_update=True): """Update the device state.""" try: From 16980514d0e1f3c48226be90c428ff2ce6aa802f Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 14 Dec 2020 23:25:58 -0800 Subject: [PATCH 2/6] Undo changes to the wemo subscription registry --- homeassistant/components/wemo/__init__.py | 11 ++--------- homeassistant/components/wemo/entity.py | 5 ++--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 72a2846899d56..c656926ecff97 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -8,10 +8,8 @@ from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.singleton import singleton from .const import DOMAIN @@ -70,16 +68,11 @@ def coerce_host_port(value): ) -@singleton("f{__name__}.registry") -async def async_get_registry(hass: HomeAssistant) -> pywemo.SubscriptionRegistry: - """Return the pywemo subscription registry.""" - return await hass.async_add_executor_job(pywemo.SubscriptionRegistry) - - async def async_setup(hass, config): """Set up for WeMo devices.""" hass.data[DOMAIN] = { "config": config.get(DOMAIN, {}), + "registry": None, "pending": {}, } @@ -98,7 +91,7 @@ async def async_setup_entry(hass, entry): config = hass.data[DOMAIN].pop("config") # Keep track of WeMo device subscriptions for push updates - registry = await async_get_registry(hass) + registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() await hass.async_add_executor_job(registry.start) def stop_wemo(event): diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 77c0f04d3ff34..e7c0712272ccb 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -8,7 +8,6 @@ from homeassistant.helpers.entity import Entity -from . import async_get_registry from .const import DOMAIN as WEMO_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -98,13 +97,13 @@ async def async_added_to_hass(self) -> None: """Wemo device added to Home Assistant.""" await super().async_added_to_hass() - registry = await async_get_registry(self.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 = await async_get_registry(self.hass) + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_executor_job(registry.unregister, self.wemo) def _subscription_callback( From 5b6e765ddce4dcb832cb0db8b59c6cdaf85f687b Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 14 Dec 2020 23:45:06 -0800 Subject: [PATCH 3/6] Use an entity service helper to register the Wemo fan services --- homeassistant/components/wemo/fan.py | 109 ++++++++------------------- 1 file changed, 30 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index d5ed962e47987..44628d639f3d7 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -2,7 +2,6 @@ import asyncio from datetime import timedelta import logging -from typing import List from pywemo.ouimeaux_device.api.service import ActionException import voluptuous as vol @@ -15,11 +14,8 @@ SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.singleton import singleton from .const import ( DOMAIN as WEMO_DOMAIN, @@ -84,58 +80,11 @@ if k not in [WEMO_FAN_LOW, WEMO_FAN_HIGH] } -SET_HUMIDITY_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_TARGET_HUMIDITY): vol.All( - vol.Coerce(float), vol.Range(min=0, max=100) - ), - } -) - -RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) - - -@singleton(f"{__name__}.service") -async def _async_get_humidifiers(hass: HomeAssistant) -> List["WemoHumidifier"]: - """Singleton containing humidifier services. - - Returns: - List of humidifiers used by the services. - """ - entities = [] - - def service_handle(service): - """Handle the WeMo humidifier services.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - - humidifiers = [entity for entity in entities if entity.entity_id in entity_ids] - - if service.service == SERVICE_SET_HUMIDITY: - target_humidity = service.data.get(ATTR_TARGET_HUMIDITY) - - for humidifier in humidifiers: - humidifier.set_humidity(target_humidity) - elif service.service == SERVICE_RESET_FILTER_LIFE: - for humidifier in humidifiers: - humidifier.reset_filter_life() - - # Register service(s) - hass.services.async_register( - WEMO_DOMAIN, - SERVICE_SET_HUMIDITY, - service_handle, - schema=SET_HUMIDITY_SCHEMA, - ) - - hass.services.async_register( - WEMO_DOMAIN, - SERVICE_RESET_FILTER_LIFE, - service_handle, - schema=RESET_FILTER_LIFE_SCHEMA, - ) - - return entities +SET_HUMIDITY_SCHEMA = { + vol.Required(ATTR_TARGET_HUMIDITY): vol.All( + vol.Coerce(float), vol.Range(min=0, max=100) + ), +} async def async_setup_entry(hass, config_entry, async_add_entities): @@ -154,6 +103,18 @@ async def _discovered_wemo(device): ] ) + platform = entity_platform.current_platform.get() + + # This will call WemoHumidifier.set_humidity(target_humidity=VALUE) + platform.async_register_entity_service( + SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, "set_humidity" + ) + + # This will call WemoHumidifier.reset_filter_life() + platform.async_register_entity_service( + SERVICE_RESET_FILTER_LIFE, {}, "reset_filter_life" + ) + class WemoHumidifier(WemoSubscriptionEntity, FanEntity): """Representation of a WeMo humidifier.""" @@ -201,16 +162,6 @@ def supported_features(self) -> int: """Flag supported features.""" return SUPPORTED_FEATURES - async def async_added_to_hass(self) -> None: - """Wemo humidifier added to Home Assistant.""" - await super().async_added_to_hass() - (await _async_get_humidifiers(self.hass)).append(self) - - async def async_will_remove_from_hass(self) -> None: - """Wemo humidifier removed from hass.""" - await super().async_will_remove_from_hass() - (await _async_get_humidifiers(self.hass)).remove(self) - def _update(self, force_update=True): """Update the device state.""" try: @@ -269,21 +220,21 @@ def set_speed(self, speed: str) -> None: self.schedule_update_ha_state() - def set_humidity(self, humidity: float) -> None: + def set_humidity(self, target_humidity: float) -> None: """Set the target humidity level for the Humidifier.""" - if humidity < 50: - target_humidity = WEMO_HUMIDITY_45 - elif 50 <= humidity < 55: - target_humidity = WEMO_HUMIDITY_50 - elif 55 <= humidity < 60: - target_humidity = WEMO_HUMIDITY_55 - elif 60 <= humidity < 100: - target_humidity = WEMO_HUMIDITY_60 - elif humidity >= 100: - target_humidity = WEMO_HUMIDITY_100 + if target_humidity < 50: + pywemo_humidity = WEMO_HUMIDITY_45 + elif 50 <= target_humidity < 55: + pywemo_humidity = WEMO_HUMIDITY_50 + elif 55 <= target_humidity < 60: + pywemo_humidity = WEMO_HUMIDITY_55 + elif 60 <= target_humidity < 100: + pywemo_humidity = WEMO_HUMIDITY_60 + elif target_humidity >= 100: + pywemo_humidity = WEMO_HUMIDITY_100 try: - self.wemo.set_humidity(target_humidity) + self.wemo.set_humidity(pywemo_humidity) except ActionException as err: _LOGGER.warning( "Error while setting humidity of device: %s (%s)", self.name, err From 27063513a8f71bd285a4308479934cb29e8640b5 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 15 Dec 2020 00:07:17 -0800 Subject: [PATCH 4/6] Fix Wemo fan test (missing ATTR_ENTITY_ID) --- tests/components/wemo/test_fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py index fe7298b40cda0..866a4403ab55c 100644 --- a/tests/components/wemo/test_fan.py +++ b/tests/components/wemo/test_fan.py @@ -87,7 +87,7 @@ async def test_fan_reset_filter_service(hass, pywemo_device, wemo_entity): assert await hass.services.async_call( DOMAIN, fan.SERVICE_RESET_FILTER_LIFE, - {fan.ATTR_ENTITY_ID: wemo_entity.entity_id}, + {ATTR_ENTITY_ID: wemo_entity.entity_id}, blocking=True, ) pywemo_device.reset_filter_life.assert_called_with() @@ -99,7 +99,7 @@ async def test_fan_set_humidity_service(hass, pywemo_device, wemo_entity): DOMAIN, fan.SERVICE_SET_HUMIDITY, { - fan.ATTR_ENTITY_ID: wemo_entity.entity_id, + ATTR_ENTITY_ID: wemo_entity.entity_id, fan.ATTR_TARGET_HUMIDITY: "50", }, blocking=True, From 9189b8a75dcd7ad8afb03d16f46a594f487e6c4f Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 15 Dec 2020 00:16:57 -0800 Subject: [PATCH 5/6] Use the function name directly rather than a string --- homeassistant/components/wemo/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 44628d639f3d7..0dca71a0d8d19 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -107,12 +107,12 @@ async def _discovered_wemo(device): # This will call WemoHumidifier.set_humidity(target_humidity=VALUE) platform.async_register_entity_service( - SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, "set_humidity" + SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, WemoHumidifier.set_humidity.__name__ ) # This will call WemoHumidifier.reset_filter_life() platform.async_register_entity_service( - SERVICE_RESET_FILTER_LIFE, {}, "reset_filter_life" + SERVICE_RESET_FILTER_LIFE, {}, WemoHumidifier.reset_filter_life.__name__ ) From 310684917aabd97087fab7c8b1e90f2136c84d6e Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 15 Dec 2020 00:36:58 -0800 Subject: [PATCH 6/6] Improve test coverage of the set_humidity service --- tests/components/wemo/test_fan.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py index 866a4403ab55c..38055ba972cb1 100644 --- a/tests/components/wemo/test_fan.py +++ b/tests/components/wemo/test_fan.py @@ -93,15 +93,28 @@ async def test_fan_reset_filter_service(hass, pywemo_device, wemo_entity): pywemo_device.reset_filter_life.assert_called_with() -async def test_fan_set_humidity_service(hass, pywemo_device, wemo_entity): +@pytest.mark.parametrize( + "test_input,expected", + [ + (0, fan.WEMO_HUMIDITY_45), + (45, fan.WEMO_HUMIDITY_45), + (50, fan.WEMO_HUMIDITY_50), + (55, fan.WEMO_HUMIDITY_55), + (60, fan.WEMO_HUMIDITY_60), + (100, fan.WEMO_HUMIDITY_100), + ], +) +async def test_fan_set_humidity_service( + hass, pywemo_device, wemo_entity, test_input, expected +): """Verify that SERVICE_SET_HUMIDITY is registered and works.""" assert await hass.services.async_call( DOMAIN, fan.SERVICE_SET_HUMIDITY, { ATTR_ENTITY_ID: wemo_entity.entity_id, - fan.ATTR_TARGET_HUMIDITY: "50", + fan.ATTR_TARGET_HUMIDITY: test_input, }, blocking=True, ) - pywemo_device.set_humidity.assert_called_with(fan.WEMO_HUMIDITY_50) + pywemo_device.set_humidity.assert_called_with(expected)