Skip to content
Merged
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
4 changes: 4 additions & 0 deletions homeassistant/components/blebox/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@

DEFAULT_HOST = "192.168.0.2"
DEFAULT_PORT = 80


LIGHT_MAX_KELVINS = 6500 # 154 Mireds
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
51 changes: 42 additions & 9 deletions homeassistant/components/blebox/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

import blebox_uniapi.light
Expand All @@ -20,9 +21,9 @@
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util

from . import BleBoxConfigEntry
from .const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from .entity import BleBoxEntity

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -57,8 +58,8 @@ async def async_setup_entry(
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
"""Representation of BleBox lights."""

_attr_min_color_temp_kelvin = 2700 # 370 Mireds
_attr_max_color_temp_kelvin = 6500 # 154 Mireds
_attr_min_color_temp_kelvin = LIGHT_MIN_KELVINS
_attr_max_color_temp_kelvin = LIGHT_MAX_KELVINS

def __init__(self, feature: blebox_uniapi.light.Light) -> None:
"""Initialize a BleBox light."""
Expand All @@ -76,10 +77,43 @@ def brightness(self) -> int | None:
"""Return the name."""
return self._feature.brightness

def _color_temp_to_native_scale(self, x: int) -> int:
"""Convert color temperature from Kelvin to native BleBox scale (0-255).

BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
"""
scaled = (
(self._attr_max_color_temp_kelvin - x)
/ (self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin)
) * 255
# note: within the operating temperature range here the Kelvin
# scale has less "integer steps" 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 temperature 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: int) -> int:
"""Convert color temperature from native BleBox scale (0-255) to Kelvin.

BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
"""
scaled = self._attr_max_color_temp_kelvin - (x / 255) * (
self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin
)
# note: see _color_temp_to_native_scale for explanation of rounding method
bounded = max(
min(math.ceil(scaled), self._attr_max_color_temp_kelvin),
self._attr_min_color_temp_kelvin,
)
return int(bounded)

@property
def color_temp_kelvin(self) -> int:
"""Return the color temperature value in Kelvin."""
return color_util.color_temperature_mired_to_kelvin(self._feature.color_temp)
return self._color_temp_from_native_scale(self._feature.color_temp)

@property
def color_mode(self) -> ColorMode:
Expand Down Expand Up @@ -137,15 +171,16 @@ async def async_turn_on(self, **kwargs: Any) -> None:
effect = kwargs.get(ATTR_EFFECT)
color_temp_kelvin = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
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_kelvin is not None:
value = feature.return_color_temp_with_brightness(
int(color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)),
self._color_temp_to_native_scale(color_temp_kelvin),
self.brightness,
)

Expand All @@ -160,9 +195,7 @@ async def async_turn_on(self, **kwargs: Any) -> None:
if brightness is not None:
if self.color_mode == ColorMode.COLOR_TEMP:
value = feature.return_color_temp_with_brightness(
color_util.color_temperature_kelvin_to_mired(
self.color_temp_kelvin
),
self._color_temp_to_native_scale(self.color_temp_kelvin),
brightness,
)
else:
Expand Down
71 changes: 70 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_KELVINS, LIGHT_MIN_KELVINS
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_RGBW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
Expand Down Expand Up @@ -329,6 +331,73 @@ def wlightbox_fixture():
return (feature, "light.my_wlightbox_wlightbox_color")


@pytest.fixture(name="wlightbox_ct")
def wlightbox_ct_fixture() -> tuple[MagicMock, str]:
"""Return a default light entity mock for color temperature testing."""

feature = mock_feature(
"lights",
blebox_uniapi.light.Light,
unique_id="BleBox-wLightBox-1afe34e750b8-color",
full_name="wLightBox-ct",
device_class=None,
is_on=None,
supports_color=True,
supports_white=True,
white_value=None,
rgbw_hex=None,
color_mode=blebox_uniapi.light.BleboxColorMode.CT,
effect="NONE",
effect_list=["NONE", "PL", "POLICE"],
)
product = feature.product
type(product).name = PropertyMock(return_value="My wLightBox")
type(product).model = PropertyMock(return_value="wLightBox")
return feature, "light.my_wlightbox_wlightbox_ct"


@pytest.mark.parametrize("kelvin_requested", [1000, 2700, 3000, 4000, 5000, 6500, 8000])
async def test_wlightbox_on_color_temp(
hass: HomeAssistant,
wlightbox_ct: tuple[MagicMock, str],
kelvin_requested: int,
) -> None:
"""Test light on with color temperature change."""

feature_mock, entity_id = wlightbox_ct

# Capture the native scale value passed to the device to verify the
# conversion is correct without depending on blebox_uniapi internals.
transient_temp: int = -1

def return_color_temp_with_brightness(value: int, _brightness: int) -> list[int]:
nonlocal transient_temp
transient_temp = value
return [0x00, 0x39, 0xB0, 0xFF]

def turn_on(_: list[int]) -> None:
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_KELVIN: kelvin_requested},
blocking=True,
)

state = hass.states.get(entity_id)
assert state.state == STATE_ON
assert 0 <= transient_temp <= 255

kelvin_actual = state.attributes[ATTR_COLOR_TEMP_KELVIN]
assert LIGHT_MIN_KELVINS <= kelvin_actual <= LIGHT_MAX_KELVINS


async def test_wlightbox_init(
wlightbox, hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
Expand Down