From 2db102e0239ae937b58c0e68b36260a50da78835 Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Wed, 10 Feb 2021 16:08:39 -0300 Subject: [PATCH] Add WiLight Cover (#46065) Co-authored-by: J. Nick Koston --- homeassistant/components/wilight/__init__.py | 5 +- .../components/wilight/config_flow.py | 4 +- homeassistant/components/wilight/const.py | 14 -- homeassistant/components/wilight/cover.py | 105 ++++++++++++++ homeassistant/components/wilight/light.py | 17 +-- .../components/wilight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wilight/__init__.py | 13 +- tests/components/wilight/test_config_flow.py | 2 +- tests/components/wilight/test_cover.py | 136 ++++++++++++++++++ tests/components/wilight/test_init.py | 2 +- 12 files changed, 264 insertions(+), 40 deletions(-) delete mode 100644 homeassistant/components/wilight/const.py create mode 100644 homeassistant/components/wilight/cover.py create mode 100644 tests/components/wilight/test_cover.py diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 97b48257103..0e08fec2c31 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -1,16 +1,17 @@ """The WiLight integration.""" import asyncio +from pywilight.const import DOMAIN + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import Entity -from .const import DOMAIN from .parent_device import WiLightParent # List the platforms that you want to support. -PLATFORMS = ["fan", "light"] +PLATFORMS = ["cover", "fan", "light"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index 3f1b12395ba..c3148a4d045 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components import ssdp from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow from homeassistant.const import CONF_HOST -from .const import DOMAIN # pylint: disable=unused-import +DOMAIN = "wilight" CONF_SERIAL_NUMBER = "serial_number" CONF_MODEL_NAME = "model_name" @@ -15,7 +15,7 @@ CONF_MODEL_NAME = "model_name" WILIGHT_MANUFACTURER = "All Automacao Ltda" # List the components supported by this integration. -ALLOWED_WILIGHT_COMPONENTS = ["light", "fan"] +ALLOWED_WILIGHT_COMPONENTS = ["cover", "fan", "light"] class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/wilight/const.py b/homeassistant/components/wilight/const.py deleted file mode 100644 index a3d77da44ef..00000000000 --- a/homeassistant/components/wilight/const.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Constants for the WiLight integration.""" - -DOMAIN = "wilight" - -# Item types -ITEM_LIGHT = "light" - -# Light types -LIGHT_ON_OFF = "light_on_off" -LIGHT_DIMMER = "light_dimmer" -LIGHT_COLOR = "light_rgb" - -# Light service support -SUPPORT_NONE = 0 diff --git a/homeassistant/components/wilight/cover.py b/homeassistant/components/wilight/cover.py new file mode 100644 index 00000000000..bbe723b413a --- /dev/null +++ b/homeassistant/components/wilight/cover.py @@ -0,0 +1,105 @@ +"""Support for WiLight Cover.""" + +from pywilight.const import ( + COVER_V1, + DOMAIN, + ITEM_COVER, + WL_CLOSE, + WL_CLOSING, + WL_OPEN, + WL_OPENING, + WL_STOP, + WL_STOPPED, +) + +from homeassistant.components.cover import ATTR_POSITION, CoverEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import WiLightDevice + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up WiLight covers from a config entry.""" + parent = hass.data[DOMAIN][entry.entry_id] + + # Handle a discovered WiLight device. + entities = [] + for item in parent.api.items: + if item["type"] != ITEM_COVER: + continue + index = item["index"] + item_name = item["name"] + if item["sub_type"] != COVER_V1: + continue + entity = WiLightCover(parent.api, index, item_name) + entities.append(entity) + + async_add_entities(entities) + + +def wilight_to_hass_position(value): + """Convert wilight position 1..255 to hass format 0..100.""" + return min(100, round((value * 100) / 255)) + + +def hass_to_wilight_position(value): + """Convert hass position 0..100 to wilight 1..255 scale.""" + return min(255, round((value * 255) / 100)) + + +class WiLightCover(WiLightDevice, CoverEntity): + """Representation of a WiLights cover.""" + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + if "position_current" in self._status: + return wilight_to_hass_position(self._status["position_current"]) + return None + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + if "motor_state" not in self._status: + return None + return self._status["motor_state"] == WL_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + if "motor_state" not in self._status: + return None + return self._status["motor_state"] == WL_CLOSING + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if "motor_state" not in self._status or "position_current" not in self._status: + return None + return ( + self._status["motor_state"] == WL_STOPPED + and wilight_to_hass_position(self._status["position_current"]) == 0 + ) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self._client.cover_command(self._index, WL_OPEN) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + await self._client.cover_command(self._index, WL_CLOSE) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = hass_to_wilight_position(kwargs[ATTR_POSITION]) + await self._client.set_cover_position(self._index, position) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._client.cover_command(self._index, WL_STOP) diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py index e4bf504165d..2f3c9e3c5f2 100644 --- a/homeassistant/components/wilight/light.py +++ b/homeassistant/components/wilight/light.py @@ -1,5 +1,14 @@ """Support for WiLight lights.""" +from pywilight.const import ( + DOMAIN, + ITEM_LIGHT, + LIGHT_COLOR, + LIGHT_DIMMER, + LIGHT_ON_OFF, + SUPPORT_NONE, +) + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, @@ -11,14 +20,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import WiLightDevice -from .const import ( - DOMAIN, - ITEM_LIGHT, - LIGHT_COLOR, - LIGHT_DIMMER, - LIGHT_ON_OFF, - SUPPORT_NONE, -) def entities_from_discovered_wilight(hass, api_device): diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index c9f4fb049fc..5b8a93c6039 100644 --- a/homeassistant/components/wilight/manifest.json +++ b/homeassistant/components/wilight/manifest.json @@ -3,7 +3,7 @@ "name": "WiLight", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wilight", - "requirements": ["pywilight==0.0.66"], + "requirements": ["pywilight==0.0.68"], "ssdp": [ { "manufacturer": "All Automacao Ltda" diff --git a/requirements_all.txt b/requirements_all.txt index d0959d87e8f..4c890be38df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1901,7 +1901,7 @@ pywebpush==1.9.2 pywemo==0.6.1 # homeassistant.components.wilight -pywilight==0.0.66 +pywilight==0.0.68 # homeassistant.components.xeoma pyxeoma==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec4bce771f6..4f7c0344da7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -971,7 +971,7 @@ pywebpush==1.9.2 pywemo==0.6.1 # homeassistant.components.wilight -pywilight==0.0.66 +pywilight==0.0.68 # homeassistant.components.zerproc pyzerproc==0.4.7 diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index e1c31345235..7ee7f0119a4 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -1,4 +1,7 @@ """Tests for the WiLight component.""" + +from pywilight.const import DOMAIN + from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER, @@ -10,7 +13,6 @@ from homeassistant.components.wilight.config_flow import ( CONF_MODEL_NAME, CONF_SERIAL_NUMBER, ) -from homeassistant.components.wilight.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.helpers.typing import HomeAssistantType @@ -24,6 +26,7 @@ UPNP_MODEL_NAME_P_B = "WiLight 0102001800010009-10010010" UPNP_MODEL_NAME_DIMMER = "WiLight 0100001700020009-10010010" UPNP_MODEL_NAME_COLOR = "WiLight 0107001800020009-11010" UPNP_MODEL_NAME_LIGHT_FAN = "WiLight 0104001800010009-10" +UPNP_MODEL_NAME_COVER = "WiLight 0103001800010009-10" UPNP_MODEL_NUMBER = "123456789012345678901234567890123456" UPNP_SERIAL = "000000000099" UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56" @@ -53,14 +56,6 @@ MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER = { ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, } -MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN = { - ATTR_SSDP_LOCATION: SSDP_LOCATION, - ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_LIGHT_FAN, - ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, - ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, -} - async def setup_integration( hass: HomeAssistantType, diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index d44780092ec..9888dbe3ef9 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -2,12 +2,12 @@ from unittest.mock import patch import pytest +from pywilight.const import DOMAIN from homeassistant.components.wilight.config_flow import ( CONF_MODEL_NAME, CONF_SERIAL_NUMBER, ) -from homeassistant.components.wilight.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.data_entry_flow import ( diff --git a/tests/components/wilight/test_cover.py b/tests/components/wilight/test_cover.py new file mode 100644 index 00000000000..85f62c9d120 --- /dev/null +++ b/tests/components/wilight/test_cover.py @@ -0,0 +1,136 @@ +"""Tests for the WiLight integration.""" +from unittest.mock import patch + +import pytest +import pywilight + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + HOST, + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_COVER, + UPNP_MODEL_NUMBER, + UPNP_SERIAL, + WILIGHT_ID, + setup_integration, +) + + +@pytest.fixture(name="dummy_device_from_host_cover") +def mock_dummy_device_from_host_light_fan(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_COVER, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", + return_value=device, + ): + yield device + + +async def test_loading_cover( + hass: HomeAssistantType, + dummy_device_from_host_cover, +) -> None: + """Test the WiLight configuration entry loading.""" + + entry = await setup_integration(hass) + assert entry + assert entry.unique_id == WILIGHT_ID + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # First segment of the strip + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_CLOSED + + entry = entity_registry.async_get("cover.wl000000000099_1") + assert entry + assert entry.unique_id == "WL000000000099_0" + + +async def test_open_close_cover_state( + hass: HomeAssistantType, dummy_device_from_host_cover +) -> None: + """Test the change of state of the cover.""" + await setup_integration(hass) + + # Open + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_OPENING + + # Close + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_CLOSING + + # Set position + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50, ATTR_ENTITY_ID: "cover.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 50 + + # Stop + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_OPEN diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index c1557fb44d3..1441564b640 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -3,8 +3,8 @@ from unittest.mock import patch import pytest import pywilight +from pywilight.const import DOMAIN -from homeassistant.components.wilight.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED,