Add WiLight Cover (#46065)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Leonardo Figueiro 2021-02-10 16:08:39 -03:00 committed by GitHub
parent cdd78316c4
commit 2db102e023
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 264 additions and 40 deletions

View file

@ -1,16 +1,17 @@
"""The WiLight integration.""" """The WiLight integration."""
import asyncio import asyncio
from pywilight.const import DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN
from .parent_device import WiLightParent from .parent_device import WiLightParent
# List the platforms that you want to support. # List the platforms that you want to support.
PLATFORMS = ["fan", "light"] PLATFORMS = ["cover", "fan", "light"]
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict):

View file

@ -7,7 +7,7 @@ from homeassistant.components import ssdp
from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from .const import DOMAIN # pylint: disable=unused-import DOMAIN = "wilight"
CONF_SERIAL_NUMBER = "serial_number" CONF_SERIAL_NUMBER = "serial_number"
CONF_MODEL_NAME = "model_name" CONF_MODEL_NAME = "model_name"
@ -15,7 +15,7 @@ CONF_MODEL_NAME = "model_name"
WILIGHT_MANUFACTURER = "All Automacao Ltda" WILIGHT_MANUFACTURER = "All Automacao Ltda"
# List the components supported by this integration. # List the components supported by this integration.
ALLOWED_WILIGHT_COMPONENTS = ["light", "fan"] ALLOWED_WILIGHT_COMPONENTS = ["cover", "fan", "light"]
class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): class WiLightFlowHandler(ConfigFlow, domain=DOMAIN):

View file

@ -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

View file

@ -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)

View file

@ -1,5 +1,14 @@
"""Support for WiLight lights.""" """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 ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_HS_COLOR, ATTR_HS_COLOR,
@ -11,14 +20,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import WiLightDevice 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): def entities_from_discovered_wilight(hass, api_device):

View file

@ -3,7 +3,7 @@
"name": "WiLight", "name": "WiLight",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wilight", "documentation": "https://www.home-assistant.io/integrations/wilight",
"requirements": ["pywilight==0.0.66"], "requirements": ["pywilight==0.0.68"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "All Automacao Ltda" "manufacturer": "All Automacao Ltda"

View file

@ -1901,7 +1901,7 @@ pywebpush==1.9.2
pywemo==0.6.1 pywemo==0.6.1
# homeassistant.components.wilight # homeassistant.components.wilight
pywilight==0.0.66 pywilight==0.0.68
# homeassistant.components.xeoma # homeassistant.components.xeoma
pyxeoma==1.4.1 pyxeoma==1.4.1

View file

@ -971,7 +971,7 @@ pywebpush==1.9.2
pywemo==0.6.1 pywemo==0.6.1
# homeassistant.components.wilight # homeassistant.components.wilight
pywilight==0.0.66 pywilight==0.0.68
# homeassistant.components.zerproc # homeassistant.components.zerproc
pyzerproc==0.4.7 pyzerproc==0.4.7

View file

@ -1,4 +1,7 @@
"""Tests for the WiLight component.""" """Tests for the WiLight component."""
from pywilight.const import DOMAIN
from homeassistant.components.ssdp import ( from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION, ATTR_SSDP_LOCATION,
ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MANUFACTURER,
@ -10,7 +13,6 @@ from homeassistant.components.wilight.config_flow import (
CONF_MODEL_NAME, CONF_MODEL_NAME,
CONF_SERIAL_NUMBER, CONF_SERIAL_NUMBER,
) )
from homeassistant.components.wilight.const import DOMAIN
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.helpers.typing import HomeAssistantType 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_DIMMER = "WiLight 0100001700020009-10010010"
UPNP_MODEL_NAME_COLOR = "WiLight 0107001800020009-11010" UPNP_MODEL_NAME_COLOR = "WiLight 0107001800020009-11010"
UPNP_MODEL_NAME_LIGHT_FAN = "WiLight 0104001800010009-10" UPNP_MODEL_NAME_LIGHT_FAN = "WiLight 0104001800010009-10"
UPNP_MODEL_NAME_COVER = "WiLight 0103001800010009-10"
UPNP_MODEL_NUMBER = "123456789012345678901234567890123456" UPNP_MODEL_NUMBER = "123456789012345678901234567890123456"
UPNP_SERIAL = "000000000099" UPNP_SERIAL = "000000000099"
UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56" 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, 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( async def setup_integration(
hass: HomeAssistantType, hass: HomeAssistantType,

View file

@ -2,12 +2,12 @@
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from pywilight.const import DOMAIN
from homeassistant.components.wilight.config_flow import ( from homeassistant.components.wilight.config_flow import (
CONF_MODEL_NAME, CONF_MODEL_NAME,
CONF_SERIAL_NUMBER, CONF_SERIAL_NUMBER,
) )
from homeassistant.components.wilight.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP from homeassistant.config_entries import SOURCE_SSDP
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
from homeassistant.data_entry_flow import ( from homeassistant.data_entry_flow import (

View file

@ -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

View file

@ -3,8 +3,8 @@ from unittest.mock import patch
import pytest import pytest
import pywilight import pywilight
from pywilight.const import DOMAIN
from homeassistant.components.wilight.const import DOMAIN
from homeassistant.config_entries import ( from homeassistant.config_entries import (
ENTRY_STATE_LOADED, ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED, ENTRY_STATE_NOT_LOADED,