Skip to content

Commit

Permalink
Fix yeelight state when controlled outside of Home Assistant (#56964)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco committed Oct 3, 2021
1 parent d0827a9 commit 1aeab65
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 159 deletions.
24 changes: 22 additions & 2 deletions homeassistant/components/yeelight/__init__.py
Expand Up @@ -36,6 +36,9 @@

_LOGGER = logging.getLogger(__name__)

STATE_CHANGE_TIME = 0.25 # seconds


DOMAIN = "yeelight"
DATA_YEELIGHT = DOMAIN
DATA_UPDATED = "yeelight_{}_data_updated"
Expand Down Expand Up @@ -546,6 +549,17 @@ def async_unregister_callback(self, unique_id):
self._async_stop_scan()


def update_needs_bg_power_workaround(data):
"""Check if a push update needs the bg_power workaround.
Some devices will push the incorrect state for bg_power.
To work around this any time we are pushed an update
with bg_power, we force poll state which will be correct.
"""
return "bg_power" in data


class YeelightDevice:
"""Represents single Yeelight device."""

Expand Down Expand Up @@ -692,12 +706,18 @@ async def async_update(self, force=False):
await self._async_update_properties()
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))

async def _async_forced_update(self, _now):
"""Call a forced update."""
await self.async_update(True)

@callback
def async_update_callback(self, data):
"""Update push from device."""
was_available = self._available
self._available = data.get(KEY_CONNECTED, True)
if self._did_first_update and not was_available and self._available:
if update_needs_bg_power_workaround(data) or (
self._did_first_update and not was_available and self._available
):
# On reconnect the properties may be out of sync
#
# We need to make sure the DEVICE_INITIALIZED dispatcher is setup
Expand All @@ -708,7 +728,7 @@ def async_update_callback(self, data):
# to be called when async_setup_entry reaches the end of the
# function
#
asyncio.create_task(self.async_update(True))
async_call_later(self._hass, STATE_CHANGE_TIME, self._async_forced_update)
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))


Expand Down
69 changes: 32 additions & 37 deletions homeassistant/components/yeelight/light.py
@@ -1,7 +1,6 @@
"""Light platform support for yeelight."""
from __future__ import annotations

import asyncio
import logging
import math

Expand Down Expand Up @@ -210,9 +209,6 @@
}


STATE_CHANGE_TIME = 0.25 # seconds


@callback
def _transitions_config_parser(transitions):
"""Parse transitions config into initialized objects."""
Expand Down Expand Up @@ -252,13 +248,15 @@ async def _async_wrap(self, *args, **kwargs):
# A network error happened, the bulb is likely offline now
self.device.async_mark_unavailable()
self.async_write_ha_state()
exc_message = str(ex) or type(ex)
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}"
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
) from ex
except BULB_EXCEPTIONS as ex:
# The bulb likely responded but had an error
exc_message = str(ex) or type(ex)
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}"
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
) from ex

return _async_wrap
Expand Down Expand Up @@ -762,11 +760,6 @@ async def async_turn_on(self, **kwargs) -> None:
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
await self.async_set_default()

# Some devices (mainly nightlights) will not send back the on state so we need to force a refresh
await asyncio.sleep(STATE_CHANGE_TIME)
if not self.is_on:
await self.device.async_update(True)

@_async_cmd
async def _async_turn_off(self, duration) -> None:
"""Turn off with a given transition duration wrapped with _async_cmd."""
Expand All @@ -782,10 +775,6 @@ async def async_turn_off(self, **kwargs) -> None:
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s

await self._async_turn_off(duration)
# Some devices will not send back the off state so we need to force a refresh
await asyncio.sleep(STATE_CHANGE_TIME)
if self.is_on:
await self.device.async_update(True)

@_async_cmd
async def async_set_mode(self, mode: str):
Expand Down Expand Up @@ -850,20 +839,34 @@ def _turn_on_power_mode(self):
return PowerMode.NORMAL


class YeelightColorLightWithoutNightlightSwitch(
YeelightColorLightSupport, YeelightGenericLight
):
"""Representation of a Color Yeelight light."""
class YeelightWithoutNightlightSwitchMixIn:
"""A mix-in for yeelights without a nightlight switch."""

@property
def _brightness_property(self):
# If the nightlight is not active, we do not
# want to "current_brightness" since it will check
# "bg_power" and main light could still be on
if self.device.is_nightlight_enabled:
return "current_brightness"
return "nl_br"
return super()._brightness_property

@property
def color_temp(self) -> int:
"""Return the color temperature."""
if self.device.is_nightlight_enabled:
# Enabling the nightlight locks the colortemp to max
return self._max_mireds
return super().color_temp


class YeelightColorLightWithoutNightlightSwitch(
YeelightColorLightSupport,
YeelightWithoutNightlightSwitchMixIn,
YeelightGenericLight,
):
"""Representation of a Color Yeelight light."""


class YeelightColorLightWithNightlightSwitch(
YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight
Expand All @@ -880,19 +883,12 @@ def is_on(self) -> bool:


class YeelightWhiteTempWithoutNightlightSwitch(
YeelightWhiteTempLightSupport, YeelightGenericLight
YeelightWhiteTempLightSupport,
YeelightWithoutNightlightSwitchMixIn,
YeelightGenericLight,
):
"""White temp light, when nightlight switch is not set to light."""

@property
def _brightness_property(self):
# If the nightlight is not active, we do not
# want to "current_brightness" since it will check
# "bg_power" and main light could still be on
if self.device.is_nightlight_enabled:
return "current_brightness"
return super()._brightness_property


class YeelightWithNightLight(
YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight
Expand All @@ -911,6 +907,9 @@ def is_on(self) -> bool:
class YeelightNightLightMode(YeelightGenericLight):
"""Representation of a Yeelight when in nightlight mode."""

_attr_color_mode = COLOR_MODE_BRIGHTNESS
_attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}

@property
def unique_id(self) -> str:
"""Return a unique ID."""
Expand Down Expand Up @@ -941,8 +940,9 @@ def _turn_on_power_mode(self):
return PowerMode.MOONLIGHT

@property
def _predefined_effects(self):
return YEELIGHT_TEMP_ONLY_EFFECT_LIST
def supported_features(self):
"""Flag no supported features."""
return 0


class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode):
Expand All @@ -962,11 +962,6 @@ class YeelightNightLightModeWithoutBrightnessControl(YeelightNightLightMode):
_attr_color_mode = COLOR_MODE_ONOFF
_attr_supported_color_modes = {COLOR_MODE_ONOFF}

@property
def supported_features(self):
"""Flag no supported features."""
return 0


class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch):
"""Representation of a Yeelight which has ambilight support.
Expand Down
5 changes: 4 additions & 1 deletion tests/components/yeelight/test_init.py
Expand Up @@ -13,6 +13,7 @@
DATA_DEVICE,
DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
STATE_CHANGE_TIME,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
Expand Down Expand Up @@ -458,6 +459,8 @@ async def test_connection_dropped_resyncs_properties(hass: HomeAssistant):
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 1
mocked_bulb._async_callback({KEY_CONNECTED: True})
await hass.async_block_till_done()
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(seconds=STATE_CHANGE_TIME)
)
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 2

0 comments on commit 1aeab65

Please sign in to comment.