diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 48f27e91c79..fd907235b45 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -8,11 +8,8 @@ import pykulersky from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, + ATTR_RGBW_COLOR, + COLOR_MODE_RGBW, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -20,14 +17,11 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 _LOGGER = logging.getLogger(__name__) -SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE - DISCOVERY_INTERVAL = timedelta(seconds=60) @@ -71,10 +65,9 @@ class KulerskyLight(LightEntity): def __init__(self, light: pykulersky.Light) -> None: """Initialize a Kuler Sky light.""" self._light = light - self._hs_color = None - self._brightness = None - self._white_value = 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: """Run when entity about to be added to hass.""" @@ -112,30 +105,10 @@ class KulerskyLight(LightEntity): "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 def is_on(self): """Return true if light is on.""" - return self._brightness > 0 or self._white_value > 0 + return self.brightness > 0 @property def available(self) -> bool: @@ -144,24 +117,21 @@ class KulerskyLight(LightEntity): async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" - default_hs = (0, 0) if self._hs_color is None else self._hs_color - hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) + default_rgbw = (255,) * 4 if self.rgbw_color is None else self.rgbw_color + 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) - default_white_value = 255 if self._white_value is None else self._white_value - white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value) - - if brightness == 0 and white_value == 0 and not kwargs: + if brightness == 0 and not kwargs: # If the light would be off, and no additional parameters were # passed, just turn the light on full brightness. 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): """Instruct the light to turn off.""" @@ -173,7 +143,7 @@ class KulerskyLight(LightEntity): if not self._available: await self._light.connect() # pylint: disable=invalid-name - r, g, b, w = await self._light.get_color() + rgbw = await self._light.get_color() except pykulersky.PykulerskyException as exc: if self._available: _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) self._available = True - hsv = color_util.color_RGB_to_hsv(r, g, b) - self._hs_color = hsv[:2] - self._brightness = int(round((hsv[2] / 100) * 255)) - self._white_value = w + brightness = max(rgbw) + if not brightness: + rgbw_normalized = [0, 0, 0, 0] + else: + rgbw_normalized = [round(x * 255 / brightness) for x in rgbw] + self._attr_brightness = brightness + self._attr_rgbw_color = tuple(rgbw_normalized) diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index ea5eeb5a690..f51c79168d7 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch import pykulersky import pytest +from pytest import approx from homeassistant import setup from homeassistant.components.kulersky.const import ( @@ -17,14 +18,9 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, - COLOR_MODE_HS, COLOR_MODE_RGBW, SCAN_INTERVAL, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -72,12 +68,10 @@ async def test_init(hass, mock_light): """Test platform setup.""" state = hass.states.get("light.bedroom") assert state.state == STATE_OFF - assert state.attributes == { + assert dict(state.attributes) == { ATTR_FRIENDLY_NAME: "Bedroom", - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, } 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() 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( "light", "turn_on", @@ -137,9 +131,33 @@ async def test_light_turn_on(hass, mock_light): blocking=True, ) 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( "light", "turn_on", @@ -147,18 +165,7 @@ async def test_light_turn_on(hass, mock_light): blocking=True, ) await hass.async_block_till_done() - - 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) + mock_light.set_color.assert_called_with(50, 41, 0, 50) 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") assert state.state == STATE_OFF - assert state.attributes == { + assert dict(state.attributes) == { ATTR_FRIENDLY_NAME: "Bedroom", - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, } # Test an exception during discovery @@ -196,12 +201,50 @@ async def test_light_update(hass, mock_light): state = hass.states.get("light.bedroom") assert state.state == STATE_UNAVAILABLE - assert state.attributes == { + assert dict(state.attributes) == { ATTR_FRIENDLY_NAME: "Bedroom", - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, + } + + 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 @@ -212,17 +255,14 @@ async def test_light_update(hass, mock_light): state = hass.states.get("light.bedroom") assert state.state == STATE_ON - assert state.attributes == { + assert dict(state.attributes) == { ATTR_FRIENDLY_NAME: "Bedroom", - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, ATTR_COLOR_MODE: COLOR_MODE_RGBW, - ATTR_BRIGHTNESS: 200, - ATTR_HS_COLOR: (200, 60), - ATTR_RGB_COLOR: (102, 203, 255), - ATTR_RGBW_COLOR: (102, 203, 255, 240), - ATTR_WHITE_VALUE: 240, - ATTR_XY_COLOR: (0.184, 0.261), + ATTR_BRIGHTNESS: 240, + ATTR_HS_COLOR: (approx(200.0), approx(27.059)), + ATTR_RGB_COLOR: (186, 232, 255), + ATTR_RGBW_COLOR: (85, 170, 212, 255), + ATTR_XY_COLOR: (approx(0.257), approx(0.305)), }