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

View File

@ -6,7 +6,7 @@ import math
import voluptuous as vol
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 homeassistant.components.light import (
@ -50,6 +50,7 @@ from . import (
ATTR_COUNT,
ATTR_MODE_MUSIC,
ATTR_TRANSITIONS,
BULB_EXCEPTIONS,
BULB_NETWORK_EXCEPTIONS,
CONF_FLOW_PARAMS,
CONF_MODE_MUSIC,
@ -250,7 +251,7 @@ def _async_cmd(func):
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}"
) from ex
except BulbException as ex:
except BULB_EXCEPTIONS as ex:
# The bulb likely responded but had an error
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}"

View File

@ -2,7 +2,7 @@
"domain": "yeelight",
"name": "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"],
"config_flow": true,
"dependencies": ["network"],

View File

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

View File

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

View File

@ -1,6 +1,7 @@
"""Test the Yeelight light."""
import asyncio
import logging
import socket
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
import pytest
@ -505,6 +506,64 @@ async def test_services(hass: HomeAssistant, caplog):
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 55},
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