Improve yeelight stability by moving timeout handling to upstream library (#56432)

This commit is contained in:
J. Nick Koston 2021-09-20 12:32:01 -05:00 committed by GitHub
parent f3ad4ca0cc
commit 9e2a29dc37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 72 additions and 16 deletions

View File

@ -164,8 +164,8 @@ UPDATE_REQUEST_PROPERTIES = [
"active_mode", "active_mode",
] ]
BULB_NETWORK_EXCEPTIONS = (socket.error, asyncio.TimeoutError) BULB_NETWORK_EXCEPTIONS = (socket.error,)
BULB_EXCEPTIONS = (BulbException, *BULB_NETWORK_EXCEPTIONS) BULB_EXCEPTIONS = (BulbException, asyncio.TimeoutError, *BULB_NETWORK_EXCEPTIONS)
PLATFORMS = ["binary_sensor", "light"] PLATFORMS = ["binary_sensor", "light"]
@ -612,9 +612,6 @@ class YeelightDevice:
@property @property
def is_nightlight_enabled(self) -> bool: def is_nightlight_enabled(self) -> bool:
"""Return true / false if nightlight is currently enabled.""" """Return true / false if nightlight is currently enabled."""
if self.bulb is None:
return False
# Only ceiling lights have active_mode, from SDK docs: # Only ceiling lights have active_mode, from SDK docs:
# active_mode 0: daylight mode / 1: moonlight mode (ceiling light only) # active_mode 0: daylight mode / 1: moonlight mode (ceiling light only)
if self._active_mode is not None: if self._active_mode is not None:
@ -652,23 +649,22 @@ class YeelightDevice:
async def _async_update_properties(self): async def _async_update_properties(self):
"""Read new properties from the device.""" """Read new properties from the device."""
if not self.bulb:
return
try: try:
await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES) await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES)
self._available = True self._available = True
if not self._initialized: if not self._initialized:
self._initialized = True self._initialized = True
async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host))
except BULB_EXCEPTIONS as ex: except BULB_NETWORK_EXCEPTIONS as ex:
if self._available: # just inform once if self._available: # just inform once
_LOGGER.error( _LOGGER.error(
"Unable to update device %s, %s: %s", self._host, self.name, ex "Unable to update device %s, %s: %s", self._host, self.name, ex
) )
self._available = False self._available = False
except BULB_EXCEPTIONS as ex:
return self._available _LOGGER.debug(
"Unable to update device %s, %s: %s", self._host, self.name, ex
)
async def async_setup(self): async def async_setup(self):
"""Fetch capabilities and setup name if available.""" """Fetch capabilities and setup name if available."""

View File

@ -6,7 +6,7 @@ import math
import voluptuous as vol import voluptuous as vol
import yeelight import yeelight
from yeelight import Bulb, BulbException, Flow, RGBTransition, SleepTransition, flows from yeelight import Bulb, Flow, RGBTransition, SleepTransition, flows
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
from homeassistant.components.light import ( from homeassistant.components.light import (
@ -50,6 +50,7 @@ from . import (
ATTR_COUNT, ATTR_COUNT,
ATTR_MODE_MUSIC, ATTR_MODE_MUSIC,
ATTR_TRANSITIONS, ATTR_TRANSITIONS,
BULB_EXCEPTIONS,
BULB_NETWORK_EXCEPTIONS, BULB_NETWORK_EXCEPTIONS,
CONF_FLOW_PARAMS, CONF_FLOW_PARAMS,
CONF_MODE_MUSIC, CONF_MODE_MUSIC,
@ -250,7 +251,7 @@ def _async_cmd(func):
raise HomeAssistantError( 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}: {ex}"
) from ex ) from ex
except BulbException as ex: except BULB_EXCEPTIONS as ex:
# The bulb likely responded but had an error # The bulb likely responded but had an error
raise HomeAssistantError( 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}: {ex}"

View File

@ -2,7 +2,7 @@
"domain": "yeelight", "domain": "yeelight",
"name": "Yeelight", "name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.7.4", "async-upnp-client==0.21.2"], "requirements": ["yeelight==0.7.5", "async-upnp-client==0.21.2"],
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
"config_flow": true, "config_flow": true,
"dependencies": ["network"], "dependencies": ["network"],

View File

@ -2448,7 +2448,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13 yalexs==1.1.13
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.4 yeelight==0.7.5
# homeassistant.components.yeelightsunflower # homeassistant.components.yeelightsunflower
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10

View File

@ -1389,7 +1389,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13 yalexs==1.1.13
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.4 yeelight==0.7.5
# homeassistant.components.youless # homeassistant.components.youless
youless-api==0.12 youless-api==0.12

View File

@ -1,6 +1,7 @@
"""Test the Yeelight light.""" """Test the Yeelight light."""
import asyncio import asyncio
import logging import logging
import socket
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
import pytest import pytest
@ -505,6 +506,64 @@ async def test_services(hass: HomeAssistant, caplog):
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 55}, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 55},
blocking=True, blocking=True,
) )
assert hass.states.get(ENTITY_LIGHT).state == STATE_OFF
mocked_bulb.async_set_brightness = AsyncMock(side_effect=socket.error)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 55},
blocking=True,
)
assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE
async def test_update_errors(hass: HomeAssistant, caplog):
"""Test update errors."""
assert await async_setup_component(hass, "homeassistant", {})
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
**CONFIG_ENTRY_DATA,
CONF_MODE_MUSIC: True,
CONF_SAVE_ON_CHANGE: True,
CONF_NIGHTLIGHT_SWITCH: True,
},
)
config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb()
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()
assert hass.states.get(ENTITY_LIGHT).state == STATE_ON
assert hass.states.get(ENTITY_NIGHTLIGHT).state == STATE_OFF
# Timeout usually means the bulb is overloaded with commands
# but will still respond eventually.
mocked_bulb.async_get_properties = AsyncMock(side_effect=asyncio.TimeoutError)
await hass.services.async_call(
"light",
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_LIGHT},
blocking=True,
)
assert hass.states.get(ENTITY_LIGHT).state == STATE_ON
# socket.error usually means the bulb dropped the connection
# or lost wifi, then came back online and forced the existing
# connection closed with a TCP RST
mocked_bulb.async_get_properties = AsyncMock(side_effect=socket.error)
await hass.services.async_call(
"light",
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_LIGHT},
blocking=True,
)
assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE