Fix intensity restoration and RGB output

This commit is contained in:
Bas Nijholt 2026-09-08 05:03:10 -07:00
commit 844c7ced85
9 changed files with 220 additions and 45 deletions

View file

@ -32,7 +32,7 @@ from .switch import (
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["switch", "number"] # "number" is the intensity dial
PLATFORMS = ["number", "switch"]
def _all_unique_names(value: list[dict[str, Any]]) -> list[dict[str, Any]]:
@ -110,7 +110,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
undo_listener = config_entry.add_update_listener(async_update_options)
data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener}
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
# Restore the number before the switch can send its first adaptation.
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_setups(config_entry, [platform])
return True

View file

@ -416,13 +416,10 @@ class SunLightSettings:
def intensity_floor_is_sleep(self) -> bool:
"""Whether the dial's 0% end is the sleep settings.
`adapt_until_sleep` forces it, whatever `intensity_floor` says. With
that option on, the adaptive colour temperature after sunset descends
*below* `min_color_temp` towards `sleep_color_temp`, so a
`min_color_temp` floor would sit above the adaptive value and turning
the dial down would make the light cooler -- backwards. The sleep
settings are the bottom of the curve there, so they are the only
sensible floor.
`adapt_until_sleep` forces it, whatever `intensity_floor` says. When
sleep is warmer than the minimum, the adaptive colour after sunset
goes below `min_color_temp`. Using the minimum endpoint could then
make dial-down cool the light during that period.
"""
return self.intensity_floor == "sleep" or self.adapt_until_sleep
@ -441,8 +438,7 @@ class SunLightSettings:
out = floor_value + (adaptive_value - floor_value) * intensity / 100
so 100 returns the adaptive value untouched and 0 returns the floor value.
Scaling towards zero instead (the obvious implementation) is wrong for a
mood dial.
Unlike multiplication towards zero, this retains the configured endpoint.
The floor is the sleep settings by default. ``intensity_floor:
minimum`` anchors it to ``min_brightness``/``min_color_temp`` instead --
@ -529,6 +525,14 @@ class SunLightSettings:
rgb_color,
is_sleep=is_sleep,
)
if (
not is_sleep
and self.intensity < 100
and self.intensity_floor_is_sleep
and self.sleep_rgb_or_color_temp == "rgb_color"
):
# Select the blended RGB target even on lights that also support CT.
force_rgb_color = True
# backwards compatibility for versions < 1.3.1 - see #403
color_temp_mired: float = math.floor(1000000 / color_temp_kelvin)

View file

@ -238,12 +238,11 @@ DOCS[CONF_ADAPT_UNTIL_SLEEP] = (
CONF_INTENSITY_FLOOR, DEFAULT_INTENSITY_FLOOR = "intensity_floor", "sleep"
DOCS[CONF_INTENSITY_FLOOR] = (
"What the bottom of the intensity dial means. `sleep` interpolates towards "
"`sleep_brightness`/`sleep_color_temp`, `minimum` towards "
"`min_brightness`/`min_color_temp`. Has no effect while "
"`transition_until_sleep` is enabled: the sleep settings are then the "
"bottom of the adaptive curve itself, so 0% is always the sleep "
"settings. 🎚️"
"What 0% on the intensity dial means. `sleep` blends towards "
"`sleep_brightness` and the configured sleep color; `minimum` towards "
"`min_brightness`/`min_color_temp`. `transition_until_sleep` forces the "
"sleep endpoint. Lower intensity dims only when the endpoint is below "
"the current adaptive value. 🎚️"
)
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0

View file

@ -8,9 +8,8 @@ Adapt Brightness / Adapt Color switches on the same device:
100% is the normal adaptive behaviour. 0% is the switch's floor, set by the
``intensity_floor`` option: its sleep settings (``sleep_brightness`` and
``sleep_color_temp``) by default, or its ``min_brightness``/``min_color_temp``.
Anything in between is a straight interpolation between the two, recomputed
continuously, so the sun still moves the light at every setting -- it is a
scaled adaptive curve, not a frozen snapshot.
Anything in between blends the two, recomputed continuously so the sun still
moves the light through a smaller range. At 0%, the target stays at the endpoint.
The value survives restarts via RestoreEntity, and is re-applied to the switch
whenever it changes so the lights follow immediately instead of waiting for the
@ -118,10 +117,9 @@ class AdaptiveIntensityNumber(NumberEntity, RestoreEntity):
last_state.state,
DEFAULT_INTENSITY,
)
# Push even at 100 so the switch and the entity can never disagree.
# Re-adapt only if this is an actual dimmed level being restored, so a
# plain restart does not push a redundant adaptation at every switch.
await self._push(adapt=self._value != DEFAULT_INTENSITY)
# The switch starts after this platform and owns startup adaptation,
# including the decision to skip it for only_once configurations.
await self._push(adapt=False)
async def async_set_native_value(self, value: float) -> None:
"""Set a new intensity and re-adapt the lights straight away."""
@ -131,11 +129,9 @@ class AdaptiveIntensityNumber(NumberEntity, RestoreEntity):
async def _push(self, *, adapt: bool) -> None:
switch = self._switch
if switch is None:
# Home Assistant does not guarantee that the "switch" platform
# finishes before "number", so the switch may not exist yet. Leave
# the value where AdaptiveSwitch.async_added_to_hass will find it
# rather than dropping a restored dial on the floor.
if switch is None or switch.hass is None or switch.is_on is None:
# The parent may not have started yet, or may be disabled in the
# entity registry. It adopts this value when added to Home Assistant.
entry_data = self.hass.data[DOMAIN][self._config_entry.entry_id]
entry_data[PENDING_INTENSITY] = self._value
_LOGGER.debug(

View file

@ -87,7 +87,7 @@
"sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
"sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
"sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
"intensity_floor": "What the bottom of the intensity dial means. `sleep` interpolates towards `sleep_brightness`/`sleep_color_temp`, `minimum` towards `min_brightness`/`min_color_temp`. Has no effect while `transition_until_sleep` is enabled: the sleep settings are then the bottom of the adaptive curve itself, so 0% is always the sleep settings. 🎚️",
"intensity_floor": "What 0% on the intensity dial means. `sleep` blends towards `sleep_brightness` and the configured sleep color; `minimum` towards `min_brightness`/`min_color_temp`. `transition_until_sleep` forces the sleep endpoint. Lower intensity dims only when the endpoint is below the current adaptive value. 🎚️",
"sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅",
"min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
"max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",

View file

@ -88,7 +88,7 @@
"sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
"sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
"sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
"intensity_floor": "What the bottom of the intensity dial means. `sleep` interpolates towards `sleep_brightness`/`sleep_color_temp`, `minimum` towards `min_brightness`/`min_color_temp`. Has no effect while `transition_until_sleep` is enabled: the sleep settings are then the bottom of the adaptive curve itself, so 0% is always the sleep settings. 🎚️",
"intensity_floor": "What 0% on the intensity dial means. `sleep` blends towards `sleep_brightness` and the configured sleep color; `minimum` towards `min_brightness`/`min_color_temp`. `transition_until_sleep` forces the sleep endpoint. Lower intensity dims only when the endpoint is below the current adaptive value. 🎚️",
"sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅",
"min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
"max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",