diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index c3a9c50a..747fa2dd 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.17.0" + "version": "1.17.1" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d42cccae..ce9f6922 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import base64 import bisect +import colorsys import datetime import functools import logging @@ -1549,18 +1550,31 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._state = False -def lerp_color( +def lerp_color_hsv( rgb1: tuple[int, int, int], rgb2: tuple[int, int, int], t: float, ) -> tuple[int, int, int]: - """Linearly interpolate between two RGB colors.""" - return ( - int(rgb1[0] + t * (rgb2[0] - rgb1[0])), - int(rgb1[1] + t * (rgb2[1] - rgb1[1])), - int(rgb1[2] + t * (rgb2[2] - rgb1[2])), + """Linearly interpolate between two RGB colors in HSV color space.""" + t = abs(t) + assert 0 <= t <= 1 + + # Convert RGB to HSV + hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) + hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) + + # Linear interpolation in HSV space + hsv = ( + hsv1[0] + t * (hsv2[0] - hsv1[0]), + hsv1[1] + t * (hsv2[1] - hsv1[1]), + hsv1[2] + t * (hsv2[2] - hsv1[2]), ) + # Convert back to RGB + rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) + assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" + return rgb + @dataclass(frozen=True) class SunLightSettings: @@ -1748,7 +1762,7 @@ class SunLightSettings: # This will result in a perceptible jump in color at sunset and sunrise # because the `color_temperature_to_rgb` function is not 100% accurate. min_color_rgb = color_temperature_to_rgb(self.min_color_temp) - rgb_color = lerp_color(min_color_rgb, self.sleep_rgb_color, percent) + rgb_color = lerp_color_hsv(min_color_rgb, self.sleep_rgb_color, percent) color_temp_kelvin = self.calc_color_temp_kelvin(percent) force_rgb_color = True else: diff --git a/tests/test_switch.py b/tests/test_switch.py index 9b679a86..360fec55 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -92,6 +92,7 @@ from custom_components.adaptive_lighting.switch import ( create_context, is_our_context, is_our_context_id, + lerp_color_hsv, ) _LOGGER = logging.getLogger(__name__) @@ -1701,3 +1702,19 @@ async def test_adapt_until_sleep_and_rgb_colors(hass): await switch._update_attrs_and_maybe_adapt_lights(context=context) assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS assert switch._settings["rgb_color"] == DEFAULT_SLEEP_RGB_COLOR + + +def test_lerp_color_hsv(): + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0) == (255, 0, 0) + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 1) == (0, 255, 0) + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0.5) == (255, 255, 0) + assert lerp_color_hsv((0, 0, 255), (255, 255, 255), 0.5) == (128, 255, 128) + + # Tests that the interpolation is consistent + for t in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]: + color = lerp_color_hsv((255, 0, 0), (0, 255, 0), t) + inverted_color = lerp_color_hsv((0, 255, 0), (255, 0, 0), 1 - t) + assert color == inverted_color + + with pytest.raises(AssertionError): + lerp_color_hsv((255, 0, 0), (0, 255, 0), 1.1)