Restore yeelight workaround for failing to update state after on/off (#57400)

pull/57413/head
J. Nick Koston 2021-10-09 21:01:45 -10:00 committed by GitHub
parent 45b60b8346
commit a58085639e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 116 additions and 5 deletions

View File

@ -36,8 +36,8 @@ from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
STATE_CHANGE_TIME = 0.25 # seconds
STATE_CHANGE_TIME = 0.40 # seconds
POWER_STATE_CHANGE_TIME = 1 # seconds
DOMAIN = "yeelight"
DATA_YEELIGHT = DOMAIN

View File

@ -38,6 +38,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_call_later
import homeassistant.util.color as color_util
from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired,
@ -62,6 +63,7 @@ from . import (
DATA_DEVICE,
DATA_UPDATED,
DOMAIN,
POWER_STATE_CHANGE_TIME,
YEELIGHT_FLOW_TRANSITION_SCHEMA,
YeelightEntity,
)
@ -247,7 +249,7 @@ def _async_cmd(func):
except BULB_NETWORK_EXCEPTIONS as ex:
# A network error happened, the bulb is likely offline now
self.device.async_mark_unavailable()
self.async_write_ha_state()
self.async_state_changed()
exc_message = str(ex) or type(ex)
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
@ -419,13 +421,22 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
else:
self._custom_effects = {}
self._unexpected_state_check = None
@callback
def async_state_changed(self):
"""Call when the device changes state."""
if not self._device.available:
self._async_cancel_pending_state_check()
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Handle entity which will be added."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
DATA_UPDATED.format(self._device.host),
self.async_write_ha_state,
self.async_state_changed,
)
)
await super().async_added_to_hass()
@ -760,6 +771,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
await self.async_set_default()
self._async_schedule_state_check(True)
@callback
def _async_cancel_pending_state_check(self):
"""Cancel a pending state check."""
if self._unexpected_state_check:
self._unexpected_state_check()
self._unexpected_state_check = None
@callback
def _async_schedule_state_check(self, expected_power_state):
"""Schedule a poll if the change failed to get pushed back to us.
Some devices (mainly nightlights) will not send back the on state
so we need to force a refresh.
"""
self._async_cancel_pending_state_check()
async def _async_update_if_state_unexpected(*_):
self._unexpected_state_check = None
if self.is_on != expected_power_state:
await self.device.async_update(True)
self._unexpected_state_check = async_call_later(
self.hass, POWER_STATE_CHANGE_TIME, _async_update_if_state_unexpected
)
@_async_cmd
async def _async_turn_off(self, duration) -> None:
"""Turn off with a given transition duration wrapped with _async_cmd."""
@ -775,6 +813,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
await self._async_turn_off(duration)
self._async_schedule_state_check(False)
@_async_cmd
async def async_set_mode(self, mode: str):

View File

@ -1,5 +1,6 @@
"""Test the Yeelight light."""
import asyncio
from datetime import timedelta
import logging
import socket
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
@ -98,6 +99,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.color import (
color_hs_to_RGB,
color_hs_to_xy,
@ -121,7 +123,7 @@ from . import (
_patch_discovery_interval,
)
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
CONFIG_ENTRY_DATA = {
CONF_HOST: IP_ADDRESS,
@ -1377,3 +1379,73 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant):
assert state.state == "on"
# bg_power off should not set the brightness to 0
assert state.attributes[ATTR_BRIGHTNESS] == 128
async def test_state_fails_to_update_triggers_update(hass: HomeAssistant):
"""Ensure we call async_get_properties if the turn on/off fails to update the state."""
mocked_bulb = _mocked_bulb()
properties = {**PROPERTIES}
properties.pop("active_mode")
properties["color_mode"] = "3" # HSV
mocked_bulb.last_properties = properties
mocked_bulb.bulb_type = BulbType.Color
config_entry = MockConfigEntry(
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
)
config_entry.add_to_hass(hass)
with _patch_discovery(), _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()
# We use asyncio.create_task now to avoid
# blocking starting so we need to block again
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 1
mocked_bulb.last_properties["power"] = "off"
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
mocked_bulb.last_properties["power"] = "on"
for _ in range(5):
await hass.services.async_call(
"light",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_off.mock_calls) == 5
# Even with five calls we only do one state request
# since each successive call should cancel the unexpected
# state check
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 3
# But if the state is correct no calls
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 3