From d174c8265e8540bebb2958fd53ec82962ee1feb9 Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Mon, 25 Jan 2021 10:03:11 -0300 Subject: [PATCH] Add WiLight Fan (#39541) * Add WiLight Fan Add fan to WiLigt integration * Updated fan.py and test_fan.py * Creating new fan test * Update homeassistant/components/wilight/__init__.py OK! Done! Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/fan.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/fan.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/fan.py Co-authored-by: Martin Hjelmare * As MartinHjelmare requested * Update fan.py * Update tests/components/wilight/test_fan.py Co-authored-by: Martin Hjelmare * Update tests/components/wilight/test_fan.py Co-authored-by: Martin Hjelmare * Update test_fan.py As Martin Hjelmare suggested Co-authored-by: Martin Hjelmare --- homeassistant/components/wilight/__init__.py | 2 +- .../components/wilight/config_flow.py | 2 +- homeassistant/components/wilight/fan.py | 122 +++++++++++ .../components/wilight/manifest.json | 2 +- .../components/wilight/parent_device.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wilight/test_config_flow.py | 20 +- tests/components/wilight/test_fan.py | 206 ++++++++++++++++++ 9 files changed, 352 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/wilight/fan.py create mode 100644 tests/components/wilight/test_fan.py diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 8821190bd32..97b48257103 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -10,7 +10,7 @@ from .const import DOMAIN from .parent_device import WiLightParent # List the platforms that you want to support. -PLATFORMS = ["light"] +PLATFORMS = ["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 6bc81e363aa..3f1b12395ba 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -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"] +ALLOWED_WILIGHT_COMPONENTS = ["light", "fan"] class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py new file mode 100644 index 00000000000..6d8ad88d6c0 --- /dev/null +++ b/homeassistant/components/wilight/fan.py @@ -0,0 +1,122 @@ +"""Support for WiLight Fan.""" + +from pywilight.const import ( + DOMAIN, + FAN_V1, + ITEM_FAN, + WL_DIRECTION_FORWARD, + WL_DIRECTION_OFF, + WL_DIRECTION_REVERSE, + WL_SPEED_HIGH, + WL_SPEED_LOW, + WL_SPEED_MEDIUM, +) + +from homeassistant.components.fan import ( + DIRECTION_FORWARD, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SUPPORT_DIRECTION, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import WiLightDevice + +SUPPORTED_SPEEDS = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + +SUPPORTED_FEATURES = SUPPORT_SET_SPEED | SUPPORT_DIRECTION + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up WiLight lights 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_FAN: + continue + index = item["index"] + item_name = item["name"] + if item["sub_type"] != FAN_V1: + continue + entity = WiLightFan(parent.api, index, item_name) + entities.append(entity) + + async_add_entities(entities) + + +class WiLightFan(WiLightDevice, FanEntity): + """Representation of a WiLights fan.""" + + def __init__(self, api_device, index, item_name): + """Initialize the device.""" + super().__init__(api_device, index, item_name) + # Initialize the WiLights fan. + self._direction = WL_DIRECTION_FORWARD + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES + + @property + def icon(self): + """Return the icon of device based on its type.""" + return "mdi:fan" + + @property + def is_on(self): + """Return true if device is on.""" + return self._status.get("direction", WL_DIRECTION_OFF) != WL_DIRECTION_OFF + + @property + def speed(self) -> str: + """Return the current speed.""" + return self._status.get("speed", SPEED_HIGH) + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return SUPPORTED_SPEEDS + + @property + def current_direction(self) -> str: + """Return the current direction of the fan.""" + if "direction" in self._status: + if self._status["direction"] != WL_DIRECTION_OFF: + self._direction = self._status["direction"] + return self._direction + + async def async_turn_on(self, speed: str = None, **kwargs): + """Turn on the fan.""" + if speed is None: + await self._client.set_fan_direction(self._index, self._direction) + else: + await self.async_set_speed(speed) + + async def async_set_speed(self, speed: str): + """Set the speed of the fan.""" + wl_speed = WL_SPEED_HIGH + if speed == SPEED_LOW: + wl_speed = WL_SPEED_LOW + if speed == SPEED_MEDIUM: + wl_speed = WL_SPEED_MEDIUM + await self._client.set_fan_speed(self._index, wl_speed) + + async def async_set_direction(self, direction: str): + """Set the direction of the fan.""" + wl_direction = WL_DIRECTION_REVERSE + if direction == DIRECTION_FORWARD: + wl_direction = WL_DIRECTION_FORWARD + await self._client.set_fan_direction(self._index, wl_direction) + + async def async_turn_off(self, **kwargs): + """Turn the fan off.""" + await self._client.set_fan_direction(self._index, WL_DIRECTION_OFF) diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index bb20da2b1ce..c9f4fb049fc 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.65"], + "requirements": ["pywilight==0.0.66"], "ssdp": [ { "manufacturer": "All Automacao Ltda" diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py index a53bc352a7b..faf71b74f72 100644 --- a/homeassistant/components/wilight/parent_device.py +++ b/homeassistant/components/wilight/parent_device.py @@ -84,11 +84,9 @@ class WiLightParent: async def async_reset(self): """Reset api.""" - # If the initialization was wrong. - if self._api is None: - return True - - self._api.client.stop() + # If the initialization was not wrong. + if self._api is not None: + self._api.client.stop() def create_api_device(host): diff --git a/requirements_all.txt b/requirements_all.txt index 39b9d98dcba..6ca8f98331f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1892,7 +1892,7 @@ pywebpush==1.9.2 pywemo==0.6.1 # homeassistant.components.wilight -pywilight==0.0.65 +pywilight==0.0.66 # homeassistant.components.xeoma pyxeoma==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b44d21b78c7..5c14d798f66 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ pywebpush==1.9.2 pywemo==0.6.1 # homeassistant.components.wilight -pywilight==0.0.65 +pywilight==0.0.66 # homeassistant.components.zerproc pyzerproc==0.4.7 diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index 41a44cda174..d44780092ec 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -21,7 +21,6 @@ from tests.common import MockConfigEntry from tests.components.wilight import ( CONF_COMPONENTS, HOST, - MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN, MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER, MOCK_SSDP_DISCOVERY_INFO_P_B, MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER, @@ -32,7 +31,7 @@ from tests.components.wilight import ( @pytest.fixture(name="dummy_get_components_from_model_clear") -def mock_dummy_get_components_from_model(): +def mock_dummy_get_components_from_model_clear(): """Mock a clear components list.""" components = [] with patch( @@ -42,6 +41,17 @@ def mock_dummy_get_components_from_model(): yield components +@pytest.fixture(name="dummy_get_components_from_model_wrong") +def mock_dummy_get_components_from_model_wrong(): + """Mock a clear components list.""" + components = ["wrong"] + with patch( + "pywilight.get_components_from_model", + return_value=components, + ): + yield components + + async def test_show_ssdp_form(hass: HomeAssistantType) -> None: """Test that the ssdp confirmation form is served.""" @@ -96,10 +106,12 @@ async def test_ssdp_not_wilight_abort_3( assert result["reason"] == "not_wilight_device" -async def test_ssdp_not_supported_abort(hass: HomeAssistantType) -> None: +async def test_ssdp_not_supported_abort( + hass: HomeAssistantType, dummy_get_components_from_model_wrong +) -> None: """Test that the ssdp aborts not_supported.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN.copy() + discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) diff --git a/tests/components/wilight/test_fan.py b/tests/components/wilight/test_fan.py new file mode 100644 index 00000000000..9b656236b93 --- /dev/null +++ b/tests/components/wilight/test_fan.py @@ -0,0 +1,206 @@ +"""Tests for the WiLight integration.""" +from unittest.mock import patch + +import pytest +import pywilight + +from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_SPEED, + DIRECTION_FORWARD, + DIRECTION_REVERSE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_DIRECTION, + SERVICE_SET_SPEED, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + HOST, + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_LIGHT_FAN, + UPNP_MODEL_NUMBER, + UPNP_SERIAL, + WILIGHT_ID, + setup_integration, +) + + +@pytest.fixture(name="dummy_device_from_host_light_fan") +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_LIGHT_FAN, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", + return_value=device, + ): + yield device + + +async def test_loading_light_fan( + hass: HomeAssistantType, + dummy_device_from_host_light_fan, +) -> 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("fan.wl000000000099_2") + assert state + assert state.state == STATE_OFF + + entry = entity_registry.async_get("fan.wl000000000099_2") + assert entry + assert entry.unique_id == "WL000000000099_1" + + +async def test_on_off_fan_state( + hass: HomeAssistantType, dummy_device_from_host_light_fan +) -> None: + """Test the change of state of the fan switches.""" + await setup_integration(hass) + + # Turn on + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_ON + + # Turn on with speed + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_SPEED: SPEED_LOW, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_SPEED) == SPEED_LOW + + # Turn off + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_OFF + + +async def test_speed_fan_state( + hass: HomeAssistantType, dummy_device_from_host_light_fan +) -> None: + """Test the change of speed of the fan switches.""" + await setup_integration(hass) + + # Set speed Low + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_LOW, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.attributes.get(ATTR_SPEED) == SPEED_LOW + + # Set speed Medium + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_MEDIUM, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.attributes.get(ATTR_SPEED) == SPEED_MEDIUM + + # Set speed High + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_HIGH, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.attributes.get(ATTR_SPEED) == SPEED_HIGH + + +async def test_direction_fan_state( + hass: HomeAssistantType, dummy_device_from_host_light_fan +) -> None: + """Test the change of direction of the fan switches.""" + await setup_integration(hass) + + # Set direction Forward + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_DIRECTION, + {ATTR_DIRECTION: DIRECTION_FORWARD, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DIRECTION) == DIRECTION_FORWARD + + # Set direction Reverse + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_DIRECTION, + {ATTR_DIRECTION: DIRECTION_REVERSE, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.attributes.get(ATTR_DIRECTION) == DIRECTION_REVERSE