From 52e76e7a05a9fcf3fac30d76e4e734ba59eabae6 Mon Sep 17 00:00:00 2001 From: Pedro Miguel Cruz <43161378+pmiguelcruz@users.noreply.github.com> Date: Tue, 25 Aug 2026 06:41:01 -0400 Subject: [PATCH] Fix Yeelight ambilight effect state (#178787) --- homeassistant/components/yeelight/device.py | 10 --- homeassistant/components/yeelight/light.py | 19 +++-- tests/components/yeelight/test_light.py | 82 +++++++++++++++++++++ 3 files changed, 96 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py index e827f13742ab..6443ace057dd 100644 --- a/homeassistant/components/yeelight/device.py +++ b/homeassistant/components/yeelight/device.py @@ -12,7 +12,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from .const import ( - ACTIVE_COLOR_FLOWING, ACTIVE_MODE_NIGHTLIGHT, DATA_UPDATED, STATE_CHANGE_TIME, @@ -142,19 +141,10 @@ class YeelightDevice: return False - @property - def is_color_flow_enabled(self) -> bool: - """Return true / false if color flow is currently running.""" - return self._color_flow and int(self._color_flow) == ACTIVE_COLOR_FLOWING - @property def _active_mode(self): return self.bulb.last_properties.get("active_mode") - @property - def _color_flow(self): - return self.bulb.last_properties.get("flowing") - @property def _nightlight_brightness(self): return self.bulb.last_properties.get("nl_br") diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 8ddc715f907b..2fb54accdc95 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -40,6 +40,7 @@ from homeassistant.util import color as color_util from . import YEELIGHT_FLOW_TRANSITION_SCHEMA, YeelightConfigEntry from .const import ( ACTION_RECOVER, + ACTIVE_COLOR_FLOWING, ATTR_ACTION, ATTR_COUNT, ATTR_MODE_MUSIC, @@ -549,7 +550,12 @@ class YeelightBaseLight(YeelightEntity, LightEntity): @override def effect(self) -> str | None: """Return the current effect.""" - return self._effect if self.device.is_color_flow_enabled else None + return self._effect if self._is_color_flow_enabled else None + + @property + def _is_color_flow_enabled(self) -> bool: + color_flow = self._get_property("flowing") + return bool(color_flow) and int(color_flow) == ACTIVE_COLOR_FLOWING @property def _bulb(self) -> AsyncBulb: @@ -583,7 +589,7 @@ class YeelightBaseLight(YeelightEntity, LightEntity): def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" attributes = { - "flowing": self.device.is_color_flow_enabled, + "flowing": self._is_color_flow_enabled, "music_mode": self._bulb.music_mode, } @@ -655,7 +661,7 @@ class YeelightBaseLight(YeelightEntity, LightEntity): ): return if ( - not self.device.is_color_flow_enabled + not self._is_color_flow_enabled and self.color_mode == ColorMode.HS and self.hs_color == hs_color ): @@ -680,7 +686,7 @@ class YeelightBaseLight(YeelightEntity, LightEntity): ): return if ( - not self.device.is_color_flow_enabled + not self._is_color_flow_enabled and self.color_mode == ColorMode.RGB and self.rgb_color == rgb ): @@ -706,7 +712,7 @@ class YeelightBaseLight(YeelightEntity, LightEntity): return if ( - not self.device.is_color_flow_enabled + not self._is_color_flow_enabled and self.color_mode == ColorMode.COLOR_TEMP and self.color_temp_kelvin == temp_in_k ): @@ -764,6 +770,8 @@ class YeelightBaseLight(YeelightEntity, LightEntity): if effect == EFFECT_STOP: await self._bulb.async_stop_flow(light_type=self.light_type) + self._effect = None + await self.device.async_update(True) return if effect in self.custom_effects_names: @@ -783,6 +791,7 @@ class YeelightBaseLight(YeelightEntity, LightEntity): await self._bulb.async_start_flow(flow, light_type=self.light_type) self._effect = effect + await self.device.async_update(True) @_async_cmd async def _async_turn_on(self, duration) -> None: diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 4b1b336fa7a0..ee60c4af8efe 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -57,6 +57,7 @@ from homeassistant.components.yeelight.const import ( DEFAULT_SAVE_ON_CHANGE, DEFAULT_TRANSITION, DOMAIN, + UPDATE_REQUEST_PROPERTIES, YEELIGHT_HSV_TRANSACTION, YEELIGHT_RGB_TRANSITION, YEELIGHT_SLEEP_TRANSACTION, @@ -1251,6 +1252,87 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant) -> None: assert state.attributes[ATTR_BRIGHTNESS] == 128 +async def test_ambilight_effect(hass: HomeAssistant) -> None: + """Test an effect on the ambilight is tracked separately from the main light.""" + mocked_bulb = _mocked_bulb() + capabilities = {**CAPABILITIES, "model": "ceiling10"} + mocked_bulb.last_properties = { + **PROPERTIES, + "flowing": "0", + "bg_flowing": "0", + } + mocked_bulb.bulb_type = BulbType.WhiteTempMood + + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry.add_to_hass(hass) + with ( + _patch_discovery(capabilities=capabilities), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + main_light_entity_id = "light.yeelight_ceiling10_0x15243f" + ambilight_entity_id = f"{main_light_entity_id}_ambilight" + mocked_bulb.async_get_properties.reset_mock() + + async def _async_get_properties(properties: list[str]) -> None: + assert properties == UPDATE_REQUEST_PROPERTIES + assert mocked_bulb.async_start_flow.await_count > 0 + mocked_bulb.last_properties["bg_flowing"] = "1" + + mocked_bulb.async_get_properties.side_effect = _async_get_properties + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ambilight_entity_id, ATTR_EFFECT: EFFECT_DISCO}, + blocking=True, + ) + + mocked_bulb.async_start_flow.assert_awaited_once_with( + ANY, light_type=LightType.Ambient + ) + mocked_bulb.async_get_properties.assert_awaited_once_with(UPDATE_REQUEST_PROPERTIES) + + assert hass.states.get(main_light_entity_id).attributes.get(ATTR_EFFECT) is None + ambilight_state = hass.states.get(ambilight_entity_id) + assert ambilight_state.attributes[ATTR_EFFECT] == EFFECT_DISCO + assert ambilight_state.attributes["flowing"] is True + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ambilight_entity_id, ATTR_EFFECT: EFFECT_WHATSAPP}, + blocking=True, + ) + + assert mocked_bulb.async_get_properties.await_count == 2 + assert ( + hass.states.get(ambilight_entity_id).attributes[ATTR_EFFECT] == EFFECT_WHATSAPP + ) + + async def _async_get_stopped_properties(properties: list[str]) -> None: + assert properties == UPDATE_REQUEST_PROPERTIES + assert mocked_bulb.async_stop_flow.await_count > 0 + mocked_bulb.last_properties["bg_flowing"] = "0" + + mocked_bulb.async_get_properties.side_effect = _async_get_stopped_properties + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ambilight_entity_id, ATTR_EFFECT: EFFECT_STOP}, + blocking=True, + ) + + mocked_bulb.async_stop_flow.assert_awaited_once_with(light_type=LightType.Ambient) + assert mocked_bulb.async_get_properties.await_count == 3 + ambilight_state = hass.states.get(ambilight_entity_id) + assert ambilight_state.attributes.get(ATTR_EFFECT) is None + assert ambilight_state.attributes["flowing"] is False + + async def test_state_fails_to_update_triggers_update(hass: HomeAssistant) -> None: """Ensure async_get_properties is called on failed state update.""" mocked_bulb = _mocked_bulb()