diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index e9ea19223023ca..22ccb678adc32e 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -13,3 +13,6 @@ DEFAULT_HOST = "192.168.0.2" DEFAULT_PORT = 80 + +LIGHT_MAX_MIREDS = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds +LIGHT_MIN_MIREDS = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 33fff1d71da1ab..6ab85b100df885 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +import math from typing import Any import blebox_uniapi.light @@ -24,6 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxConfigEntry +from .const import LIGHT_MAX_MIREDS, LIGHT_MIN_MIREDS from .entity import BleBoxEntity _LOGGER = logging.getLogger(__name__) @@ -58,8 +60,8 @@ async def async_setup_entry( class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity): """Representation of BleBox lights.""" - _attr_max_mireds = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds - _attr_min_mireds = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds + _attr_max_mireds = LIGHT_MAX_MIREDS + _attr_min_mireds = LIGHT_MIN_MIREDS def __init__(self, feature: blebox_uniapi.light.Light) -> None: """Initialize a BleBox light.""" @@ -80,7 +82,7 @@ def brightness(self): @property def color_temp(self): """Return color temperature.""" - return self._feature.color_temp + return self._color_temp_from_native_scale(self._feature.color_temp) @property def color_mode(self): @@ -130,23 +132,43 @@ def rgbww_color(self): return None return tuple(blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgbww_hex)) + def _color_temp_to_native_scale(self, x): + """Convert color temperature from mireds to native Blebox temperature scale.""" + scaled = ((x - self.min_mireds) / (self.max_mireds - self.min_mireds)) * 255 + # note: within the operating temperature range here the mired + # scale has less "integer steps" (~216) than the native scale used + # by blebox devices. Thus we need to use rounding method that is opposite + # to the one used in _color_temp_from_native_scale in order to avoid + # temperature value jumping by one step when the temparatur value is read + # back from the device + bounded = max(min(math.floor(scaled), 255), 0) + return int(bounded) + + def _color_temp_from_native_scale(self, x): + """Convert color temperature from native Blebox temperature scale to mireds.""" + scaled = (x / 255) * (self.max_mireds - self.min_mireds) + self.min_mireds + # note: see _color_temp_to_native_scale for explanation of rounding method + bounded = max(min(math.ceil(scaled), self.max_mireds), self.min_mireds) + return int(bounded) + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - rgbw = kwargs.get(ATTR_RGBW_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) color_temp = kwargs.get(ATTR_COLOR_TEMP) rgbww = kwargs.get(ATTR_RGBWW_COLOR) + rgb = kwargs.get(ATTR_RGB_COLOR) + feature = self._feature value = feature.sensible_on_value - rgb = kwargs.get(ATTR_RGB_COLOR) if rgbw is not None: value = list(rgbw) + if color_temp is not None: value = feature.return_color_temp_with_brightness( - int(color_temp), self.brightness + self._color_temp_to_native_scale(color_temp), self.brightness ) if rgbww is not None: diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index bfa5478265bccb..618d51a0ad2137 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -1,13 +1,15 @@ """BleBox light entities tests.""" import logging -from unittest.mock import AsyncMock, PropertyMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock import blebox_uniapi import pytest +from homeassistant.components.blebox.const import LIGHT_MAX_MIREDS, LIGHT_MIN_MIREDS from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, @@ -329,6 +331,32 @@ def wlightbox_fixture(): return (feature, "light.wlightbox_color") +@pytest.fixture(name="wlightbox_ctx2") +def wlightbox_ctx2_fixture() -> tuple[MagicMock, str]: + """Return a default light entity mock.""" + + feature = mock_feature( + "lights", + blebox_uniapi.light.Light, + unique_id="BleBox-wLightBox-1afe34e750b8-color", + full_name="wLightBox-ctx2", + device_class=None, + is_on=None, + supports_color=True, + supports_white=True, + white_value=None, + rgbw_hex=None, + color_mode=blebox_uniapi.light.BleboxColorMode.CTx2, + effect="NONE", + effect_list=["NONE", "PL", "POLICE"], + # color_temp=3, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My wLightBox") + type(product).model = PropertyMock(return_value="wLightBox") + return feature, "light.wlightbox_ctx2" + + async def test_wlightbox_init( wlightbox, hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: @@ -425,6 +453,53 @@ def apply_color(value, color_value): assert state.attributes[ATTR_RGBW_COLOR] == (0xC1, 0xD2, 0xF3, 0xC7) +# note: it is a good idea to test wide range of values including those +# outside of the operating range to make sure that bounds are properly +# corrected +@pytest.mark.parametrize("temp_requested", [1, 100, 150, 200, 300, 400]) +async def test_wlightbox_on_temp( + hass: HomeAssistant, wlightbox_ctx2: tuple[MagicMock, str], temp_requested: int +) -> None: + """Test light on with temperature change.""" + + feature_mock, entity_id = wlightbox_ctx2 + + # note: testing color temperature modification is tricky as neither + # blebox_uniapi nor blebox devices operate on color_temperature concept. + # Instead, it uses a mixture of brightness and temperature value encoded + # with known formula that is exposed by return_color_temp_with_brightness() + # method. To make testing simpler let's hijack the requested value + # with transient nonlocal value. + transient_temp: int = -1 + + def return_color_temp_with_brightness(value, brightness): + nonlocal transient_temp + transient_temp = value + return [0x00, 0x39, 0xB0, 0xFF] + + def turn_on(_): + feature_mock.is_on = True + feature_mock.color_temp = transient_temp + + feature_mock.return_color_temp_with_brightness = return_color_temp_with_brightness + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + await async_setup_entity(hass, entity_id) + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_COLOR_TEMP: temp_requested}, + blocking=True, + ) + + state = hass.states.get(entity_id) + mired_temp = state.attributes[ATTR_COLOR_TEMP] + assert state.state == STATE_ON + assert LIGHT_MIN_MIREDS <= mired_temp <= LIGHT_MAX_MIREDS + assert mired_temp == max(min(mired_temp, LIGHT_MAX_MIREDS), LIGHT_MIN_MIREDS) + assert 0 <= transient_temp <= 255 + + async def test_wlightbox_on_to_last_color(wlightbox, hass: HomeAssistant) -> None: """Test light on."""