Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions homeassistant/components/blebox/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 28 additions & 6 deletions homeassistant/components/blebox/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from datetime import timedelta
import logging
import math
from typing import Any

import blebox_uniapi.light
Expand All @@ -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__)
Expand Down Expand Up @@ -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."""
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
77 changes: 76 additions & 1 deletion tests/components/blebox/test_light.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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."""

Expand Down