Reconnect and retry yeelight commands after previous wifi drop out (#57741)
This commit is contained in:
parent
5aba8a7c81
commit
427f2a085b
6 changed files with 92 additions and 29 deletions
|
@ -6,7 +6,6 @@ import contextlib
|
|||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
import logging
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from async_upnp_client.search import SsdpSearchListener
|
||||
|
@ -163,9 +162,6 @@ UPDATE_REQUEST_PROPERTIES = [
|
|||
"active_mode",
|
||||
]
|
||||
|
||||
BULB_NETWORK_EXCEPTIONS = (socket.error,)
|
||||
BULB_EXCEPTIONS = (BulbException, asyncio.TimeoutError, *BULB_NETWORK_EXCEPTIONS)
|
||||
|
||||
|
||||
PLATFORMS = ["binary_sensor", "light"]
|
||||
|
||||
|
@ -270,7 +266,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
try:
|
||||
device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
|
||||
await _async_initialize(hass, entry, device)
|
||||
except BULB_EXCEPTIONS as ex:
|
||||
except (asyncio.TimeoutError, OSError, BulbException) as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
@ -594,13 +590,20 @@ class YeelightDevice:
|
|||
self._available = True
|
||||
if not self._initialized:
|
||||
self._initialized = True
|
||||
except BULB_NETWORK_EXCEPTIONS as ex:
|
||||
except OSError 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
|
||||
except BULB_EXCEPTIONS as ex:
|
||||
except asyncio.TimeoutError as ex:
|
||||
_LOGGER.debug(
|
||||
"timed out while trying to update device %s, %s: %s",
|
||||
self._host,
|
||||
self.name,
|
||||
ex,
|
||||
)
|
||||
except BulbException as ex:
|
||||
_LOGGER.debug(
|
||||
"Unable to update device %s, %s: %s", self._host, self.name, ex
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Light platform support for yeelight."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
|
||||
|
@ -8,6 +9,7 @@ import voluptuous as vol
|
|||
import yeelight
|
||||
from yeelight import Bulb, Flow, RGBTransition, SleepTransition, flows
|
||||
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
|
||||
from yeelight.main import BulbException
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
|
@ -51,8 +53,6 @@ from . import (
|
|||
ATTR_COUNT,
|
||||
ATTR_MODE_MUSIC,
|
||||
ATTR_TRANSITIONS,
|
||||
BULB_EXCEPTIONS,
|
||||
BULB_NETWORK_EXCEPTIONS,
|
||||
CONF_FLOW_PARAMS,
|
||||
CONF_MODE_MUSIC,
|
||||
CONF_NIGHTLIGHT_SWITCH,
|
||||
|
@ -243,23 +243,33 @@ def _async_cmd(func):
|
|||
"""Define a wrapper to catch exceptions from the bulb."""
|
||||
|
||||
async def _async_wrap(self, *args, **kwargs):
|
||||
try:
|
||||
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
|
||||
return await func(self, *args, **kwargs)
|
||||
except BULB_NETWORK_EXCEPTIONS as ex:
|
||||
# A network error happened, the bulb is likely offline now
|
||||
self.device.async_mark_unavailable()
|
||||
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}"
|
||||
) 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}: {exc_message}"
|
||||
) from ex
|
||||
for attempts in range(2):
|
||||
try:
|
||||
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
|
||||
return await func(self, *args, **kwargs)
|
||||
except asyncio.TimeoutError as ex:
|
||||
# The wifi likely dropped, so we want to retry once since
|
||||
# python-yeelight will auto reconnect
|
||||
exc_message = str(ex) or type(ex)
|
||||
if attempts == 0:
|
||||
continue
|
||||
raise HomeAssistantError(
|
||||
f"Timed out when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
|
||||
) from ex
|
||||
except OSError as ex:
|
||||
# A network error happened, the bulb is likely offline now
|
||||
self.device.async_mark_unavailable()
|
||||
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}"
|
||||
) from ex
|
||||
except BulbException 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}: {exc_message}"
|
||||
) from ex
|
||||
|
||||
return _async_wrap
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "yeelight",
|
||||
"name": "Yeelight",
|
||||
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
||||
"requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.8"],
|
||||
"requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.8"],
|
||||
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
|
|
|
@ -2447,7 +2447,7 @@ yalesmartalarmclient==0.3.4
|
|||
yalexs==1.1.13
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.7
|
||||
yeelight==0.7.8
|
||||
|
||||
# homeassistant.components.yeelightsunflower
|
||||
yeelightsunflower==0.0.10
|
||||
|
|
|
@ -1412,7 +1412,7 @@ yalesmartalarmclient==0.3.4
|
|||
yalexs==1.1.13
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.7
|
||||
yeelight==0.7.8
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==0.14
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""Test Yeelight."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from yeelight import BulbException, BulbType
|
||||
from yeelight.aio import KEY_CONNECTED
|
||||
|
||||
|
@ -507,3 +509,51 @@ async def test_connection_dropped_resyncs_properties(hass: HomeAssistant):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_oserror_on_first_update_results_in_unavailable(hass: HomeAssistant):
|
||||
"""Test that an OSError on first update results in unavailable."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=ID,
|
||||
data={CONF_HOST: "127.0.0.1"},
|
||||
options={CONF_NAME: "Test name"},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
mocked_bulb = _mocked_bulb()
|
||||
mocked_bulb.async_get_properties = AsyncMock(side_effect=OSError)
|
||||
|
||||
with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.test_name").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception", [BulbException, asyncio.TimeoutError])
|
||||
async def test_non_oserror_exception_on_first_update(
|
||||
hass: HomeAssistant, exception: Exception
|
||||
):
|
||||
"""Test that an exceptions other than OSError on first update do not result in unavailable.
|
||||
|
||||
The unavailable state will come as a push update in this case
|
||||
"""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=ID,
|
||||
data={CONF_HOST: "127.0.0.1"},
|
||||
options={CONF_NAME: "Test name"},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
mocked_bulb = _mocked_bulb()
|
||||
mocked_bulb.async_get_properties = AsyncMock(side_effect=exception)
|
||||
|
||||
with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.test_name").state != STATE_UNAVAILABLE
|
||||
|
|
Loading…
Add table
Reference in a new issue