mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Improve yeelight stability by moving timeout handling to upstream library (#56432)
This commit is contained in:
parent
f3ad4ca0cc
commit
9e2a29dc37
@ -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."""
|
||||||
|
@ -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}"
|
||||||
|
@ -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"],
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user