diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 234e2547676..4abe18daa21 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -234,6 +234,7 @@ ATTR_EFFECT_LIST = "effect_list" # Apply an effect to the light, can be EFFECT_COLORLOOP. ATTR_EFFECT = "effect" EFFECT_COLORLOOP = "colorloop" +EFFECT_OFF = "off" EFFECT_RANDOM = "random" EFFECT_WHITE = "white" @@ -1060,6 +1061,51 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) return data + def __validate_color_mode( + self, + color_mode: ColorMode | str | None, + supported_color_modes: set[ColorMode] | set[str], + effect: str | None, + ) -> None: + """Validate the color mode.""" + if color_mode is None: + # The light is turned off + return + + if not effect or effect == EFFECT_OFF: + # No effect is active, the light must set color mode to one of the supported + # color modes + if color_mode in supported_color_modes: + return + # Increase severity to warning in 2024.3, reject in 2025.3 + _LOGGER.debug( + "%s: set to unsupported color_mode: %s, supported_color_modes: %s", + self.entity_id, + color_mode, + supported_color_modes, + ) + return + + # When an effect is active, the color mode should indicate what adjustments are + # supported by the effect. To make this possible, we allow the light to set its + # color mode to on_off, and to brightness if the light allows adjusting + # brightness, in addition to the otherwise supported color modes. + effect_color_modes = supported_color_modes | {ColorMode.ONOFF} + if brightness_supported(effect_color_modes): + effect_color_modes.add(ColorMode.BRIGHTNESS) + + if color_mode in effect_color_modes: + return + + # Increase severity to warning in 2024.3, reject in 2025.3 + _LOGGER.debug( + "%s: set to unsupported color_mode: %s, supported for effect: %s", + self.entity_id, + color_mode, + effect_color_modes, + ) + return + @final @property def state_attributes(self) -> dict[str, Any] | None: @@ -1074,14 +1120,13 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): _is_on = self.is_on color_mode = self._light_internal_color_mode if _is_on else None - if color_mode and color_mode not in legacy_supported_color_modes: - # Increase severity to warning in 2024.3, reject in 2025.3 - _LOGGER.debug( - "%s: set to unsupported color_mode: %s, supported_color_modes: %s", - self.entity_id, - color_mode, - legacy_supported_color_modes, - ) + effect: str | None + if LightEntityFeature.EFFECT in supported_features: + data[ATTR_EFFECT] = effect = self.effect if _is_on else None + else: + effect = None + + self.__validate_color_mode(color_mode, legacy_supported_color_modes, effect) data[ATTR_COLOR_MODE] = color_mode @@ -1140,9 +1185,6 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if color_mode: data.update(self._light_internal_convert_color(color_mode)) - if LightEntityFeature.EFFECT in supported_features: - data[ATTR_EFFECT] = self.effect if _is_on else None - return data @property diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 903002063e8..69f6a841737 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -2610,3 +2610,51 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> caplog.clear() assert entity.supported_features_compat is light.LightEntityFeature(1) assert "is using deprecated supported features values" not in caplog.text + + +@pytest.mark.parametrize( + ("color_mode", "supported_color_modes", "effect", "warning_expected"), + [ + (light.ColorMode.ONOFF, {light.ColorMode.ONOFF}, None, False), + # A light which supports brightness should not set its color mode to on_off + (light.ColorMode.ONOFF, {light.ColorMode.BRIGHTNESS}, None, True), + (light.ColorMode.ONOFF, {light.ColorMode.BRIGHTNESS}, light.EFFECT_OFF, True), + # Unless it renders an effect + (light.ColorMode.ONOFF, {light.ColorMode.BRIGHTNESS}, "effect", False), + (light.ColorMode.BRIGHTNESS, {light.ColorMode.BRIGHTNESS}, "effect", False), + (light.ColorMode.BRIGHTNESS, {light.ColorMode.BRIGHTNESS}, None, False), + # A light which supports color should not set its color mode to brightnes + (light.ColorMode.BRIGHTNESS, {light.ColorMode.HS}, None, True), + (light.ColorMode.BRIGHTNESS, {light.ColorMode.HS}, light.EFFECT_OFF, True), + (light.ColorMode.ONOFF, {light.ColorMode.HS}, None, True), + (light.ColorMode.ONOFF, {light.ColorMode.HS}, light.EFFECT_OFF, True), + # Unless it renders an effect + (light.ColorMode.BRIGHTNESS, {light.ColorMode.HS}, "effect", False), + (light.ColorMode.ONOFF, {light.ColorMode.HS}, "effect", False), + (light.ColorMode.HS, {light.ColorMode.HS}, "effect", False), + # A light which supports brightness should not set its color mode to hs even + # if rendering an effect + (light.ColorMode.HS, {light.ColorMode.BRIGHTNESS}, "effect", True), + ], +) +def test_report_invalid_color_mode( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + color_mode: str, + supported_color_modes: set[str], + effect: str | None, + warning_expected: bool, +) -> None: + """Test a light setting an invalid color mode.""" + + class MockLightEntityEntity(light.LightEntity): + _attr_color_mode = color_mode + _attr_effect = effect + _attr_is_on = True + _attr_supported_features = light.LightEntityFeature.EFFECT + _attr_supported_color_modes = supported_color_modes + + entity = MockLightEntityEntity() + entity._async_calculate_state() + expected_warning = f"set to unsupported color_mode: {color_mode}" + assert (expected_warning in caplog.text) is warning_expected