Implement color_mode support for kulersky (#52080)

This commit is contained in:
Erik Montnemery 2021-06-29 16:31:29 +02:00 committed by GitHub
parent f1b40b683d
commit 8a00c3a2f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 106 additions and 93 deletions

View file

@ -8,11 +8,8 @@ import pykulersky
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_HS_COLOR, ATTR_RGBW_COLOR,
ATTR_WHITE_VALUE, COLOR_MODE_RGBW,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_WHITE_VALUE,
LightEntity, LightEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -20,14 +17,11 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
import homeassistant.util.color as color_util
from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE
DISCOVERY_INTERVAL = timedelta(seconds=60) DISCOVERY_INTERVAL = timedelta(seconds=60)
@ -71,10 +65,9 @@ class KulerskyLight(LightEntity):
def __init__(self, light: pykulersky.Light) -> None: def __init__(self, light: pykulersky.Light) -> None:
"""Initialize a Kuler Sky light.""" """Initialize a Kuler Sky light."""
self._light = light self._light = light
self._hs_color = None
self._brightness = None
self._white_value = None
self._available = None self._available = None
self._attr_supported_color_modes = {COLOR_MODE_RGBW}
self._attr_color_mode = COLOR_MODE_RGBW
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass.""" """Run when entity about to be added to hass."""
@ -112,30 +105,10 @@ class KulerskyLight(LightEntity):
"manufacturer": "Brightech", "manufacturer": "Brightech",
} }
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_KULERSKY
@property
def brightness(self):
"""Return the brightness of the light."""
return self._brightness
@property
def hs_color(self):
"""Return the hs color."""
return self._hs_color
@property
def white_value(self):
"""Return the white value of this light between 0..255."""
return self._white_value
@property @property
def is_on(self): def is_on(self):
"""Return true if light is on.""" """Return true if light is on."""
return self._brightness > 0 or self._white_value > 0 return self.brightness > 0
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -144,24 +117,21 @@ class KulerskyLight(LightEntity):
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on.""" """Instruct the light to turn on."""
default_hs = (0, 0) if self._hs_color is None else self._hs_color default_rgbw = (255,) * 4 if self.rgbw_color is None else self.rgbw_color
hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) rgbw = kwargs.get(ATTR_RGBW_COLOR, default_rgbw)
default_brightness = 0 if self._brightness is None else self._brightness default_brightness = 0 if self.brightness is None else self.brightness
brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness)
default_white_value = 255 if self._white_value is None else self._white_value if brightness == 0 and not kwargs:
white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value)
if brightness == 0 and white_value == 0 and not kwargs:
# If the light would be off, and no additional parameters were # If the light would be off, and no additional parameters were
# passed, just turn the light on full brightness. # passed, just turn the light on full brightness.
brightness = 255 brightness = 255
white_value = 255 rgbw = (255,) * 4
rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) rgbw_scaled = [round(x * brightness / 255) for x in rgbw]
await self._light.set_color(*rgb, white_value) await self._light.set_color(*rgbw_scaled)
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off.""" """Instruct the light to turn off."""
@ -173,7 +143,7 @@ class KulerskyLight(LightEntity):
if not self._available: if not self._available:
await self._light.connect() await self._light.connect()
# pylint: disable=invalid-name # pylint: disable=invalid-name
r, g, b, w = await self._light.get_color() rgbw = await self._light.get_color()
except pykulersky.PykulerskyException as exc: except pykulersky.PykulerskyException as exc:
if self._available: if self._available:
_LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc) _LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc)
@ -183,7 +153,10 @@ class KulerskyLight(LightEntity):
_LOGGER.info("Reconnected to %s", self._light.address) _LOGGER.info("Reconnected to %s", self._light.address)
self._available = True self._available = True
hsv = color_util.color_RGB_to_hsv(r, g, b) brightness = max(rgbw)
self._hs_color = hsv[:2] if not brightness:
self._brightness = int(round((hsv[2] / 100) * 255)) rgbw_normalized = [0, 0, 0, 0]
self._white_value = w else:
rgbw_normalized = [round(x * 255 / brightness) for x in rgbw]
self._attr_brightness = brightness
self._attr_rgbw_color = tuple(rgbw_normalized)

View file

@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
import pykulersky import pykulersky
import pytest import pytest
from pytest import approx
from homeassistant import setup from homeassistant import setup
from homeassistant.components.kulersky.const import ( from homeassistant.components.kulersky.const import (
@ -17,14 +18,9 @@ from homeassistant.components.light import (
ATTR_RGB_COLOR, ATTR_RGB_COLOR,
ATTR_RGBW_COLOR, ATTR_RGBW_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR, ATTR_XY_COLOR,
COLOR_MODE_HS,
COLOR_MODE_RGBW, COLOR_MODE_RGBW,
SCAN_INTERVAL, SCAN_INTERVAL,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_WHITE_VALUE,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -72,12 +68,10 @@ async def test_init(hass, mock_light):
"""Test platform setup.""" """Test platform setup."""
state = hass.states.get("light.bedroom") state = hass.states.get("light.bedroom")
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert state.attributes == { assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom", ATTR_FRIENDLY_NAME: "Bedroom",
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS ATTR_SUPPORTED_FEATURES: 0,
| SUPPORT_COLOR
| SUPPORT_WHITE_VALUE,
} }
with patch.object(hass.loop, "stop"): with patch.object(hass.loop, "stop"):
@ -129,7 +123,7 @@ async def test_light_turn_on(hass, mock_light):
await hass.async_block_till_done() await hass.async_block_till_done()
mock_light.set_color.assert_called_with(255, 255, 255, 255) mock_light.set_color.assert_called_with(255, 255, 255, 255)
mock_light.get_color.return_value = (50, 50, 50, 255) mock_light.get_color.return_value = (50, 50, 50, 50)
await hass.services.async_call( await hass.services.async_call(
"light", "light",
"turn_on", "turn_on",
@ -137,9 +131,33 @@ async def test_light_turn_on(hass, mock_light):
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_light.set_color.assert_called_with(50, 50, 50, 255) mock_light.set_color.assert_called_with(50, 50, 50, 50)
mock_light.get_color.return_value = (50, 45, 25, 255) mock_light.get_color.return_value = (50, 25, 13, 6)
await hass.services.async_call(
"light",
"turn_on",
{ATTR_ENTITY_ID: "light.bedroom", ATTR_RGBW_COLOR: (255, 128, 64, 32)},
blocking=True,
)
await hass.async_block_till_done()
mock_light.set_color.assert_called_with(50, 25, 13, 6)
# RGB color is converted to RGBW by assigning the white component to the white
# channel, see color_rgb_to_rgbw
mock_light.get_color.return_value = (0, 17, 50, 17)
await hass.services.async_call(
"light",
"turn_on",
{ATTR_ENTITY_ID: "light.bedroom", ATTR_RGB_COLOR: (64, 128, 255)},
blocking=True,
)
await hass.async_block_till_done()
mock_light.set_color.assert_called_with(0, 17, 50, 17)
# HS color is converted to RGBW by assigning the white component to the white
# channel, see color_rgb_to_rgbw
mock_light.get_color.return_value = (50, 41, 0, 50)
await hass.services.async_call( await hass.services.async_call(
"light", "light",
"turn_on", "turn_on",
@ -147,18 +165,7 @@ async def test_light_turn_on(hass, mock_light):
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_light.set_color.assert_called_with(50, 41, 0, 50)
mock_light.set_color.assert_called_with(50, 45, 25, 255)
mock_light.get_color.return_value = (220, 201, 110, 180)
await hass.services.async_call(
"light",
"turn_on",
{ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180},
blocking=True,
)
await hass.async_block_till_done()
mock_light.set_color.assert_called_with(50, 45, 25, 180)
async def test_light_turn_off(hass, mock_light): async def test_light_turn_off(hass, mock_light):
@ -180,12 +187,10 @@ async def test_light_update(hass, mock_light):
state = hass.states.get("light.bedroom") state = hass.states.get("light.bedroom")
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert state.attributes == { assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom", ATTR_FRIENDLY_NAME: "Bedroom",
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS ATTR_SUPPORTED_FEATURES: 0,
| SUPPORT_COLOR
| SUPPORT_WHITE_VALUE,
} }
# Test an exception during discovery # Test an exception during discovery
@ -196,12 +201,50 @@ async def test_light_update(hass, mock_light):
state = hass.states.get("light.bedroom") state = hass.states.get("light.bedroom")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
assert state.attributes == { assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom", ATTR_FRIENDLY_NAME: "Bedroom",
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS ATTR_SUPPORTED_FEATURES: 0,
| SUPPORT_COLOR }
| SUPPORT_WHITE_VALUE,
mock_light.get_color.side_effect = None
mock_light.get_color.return_value = (80, 160, 255, 0)
utcnow = utcnow + SCAN_INTERVAL
async_fire_time_changed(hass, utcnow)
await hass.async_block_till_done()
state = hass.states.get("light.bedroom")
assert state.state == STATE_ON
assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom",
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: 0,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
ATTR_BRIGHTNESS: 255,
ATTR_HS_COLOR: (approx(212.571), approx(68.627)),
ATTR_RGB_COLOR: (80, 160, 255),
ATTR_RGBW_COLOR: (80, 160, 255, 0),
ATTR_XY_COLOR: (approx(0.17), approx(0.193)),
}
mock_light.get_color.side_effect = None
mock_light.get_color.return_value = (80, 160, 200, 255)
utcnow = utcnow + SCAN_INTERVAL
async_fire_time_changed(hass, utcnow)
await hass.async_block_till_done()
state = hass.states.get("light.bedroom")
assert state.state == STATE_ON
assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom",
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: 0,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
ATTR_BRIGHTNESS: 255,
ATTR_HS_COLOR: (approx(199.701), approx(26.275)),
ATTR_RGB_COLOR: (188, 233, 255),
ATTR_RGBW_COLOR: (80, 160, 200, 255),
ATTR_XY_COLOR: (approx(0.259), approx(0.306)),
} }
mock_light.get_color.side_effect = None mock_light.get_color.side_effect = None
@ -212,17 +255,14 @@ async def test_light_update(hass, mock_light):
state = hass.states.get("light.bedroom") state = hass.states.get("light.bedroom")
assert state.state == STATE_ON assert state.state == STATE_ON
assert state.attributes == { assert dict(state.attributes) == {
ATTR_FRIENDLY_NAME: "Bedroom", ATTR_FRIENDLY_NAME: "Bedroom",
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS ATTR_SUPPORTED_FEATURES: 0,
| SUPPORT_COLOR
| SUPPORT_WHITE_VALUE,
ATTR_COLOR_MODE: COLOR_MODE_RGBW, ATTR_COLOR_MODE: COLOR_MODE_RGBW,
ATTR_BRIGHTNESS: 200, ATTR_BRIGHTNESS: 240,
ATTR_HS_COLOR: (200, 60), ATTR_HS_COLOR: (approx(200.0), approx(27.059)),
ATTR_RGB_COLOR: (102, 203, 255), ATTR_RGB_COLOR: (186, 232, 255),
ATTR_RGBW_COLOR: (102, 203, 255, 240), ATTR_RGBW_COLOR: (85, 170, 212, 255),
ATTR_WHITE_VALUE: 240, ATTR_XY_COLOR: (approx(0.257), approx(0.305)),
ATTR_XY_COLOR: (0.184, 0.261),
} }