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 datetime import timedelta
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
import logging
|
import logging
|
||||||
import socket
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from async_upnp_client.search import SsdpSearchListener
|
from async_upnp_client.search import SsdpSearchListener
|
||||||
|
@ -163,9 +162,6 @@ UPDATE_REQUEST_PROPERTIES = [
|
||||||
"active_mode",
|
"active_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
BULB_NETWORK_EXCEPTIONS = (socket.error,)
|
|
||||||
BULB_EXCEPTIONS = (BulbException, asyncio.TimeoutError, *BULB_NETWORK_EXCEPTIONS)
|
|
||||||
|
|
||||||
|
|
||||||
PLATFORMS = ["binary_sensor", "light"]
|
PLATFORMS = ["binary_sensor", "light"]
|
||||||
|
|
||||||
|
@ -270,7 +266,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
try:
|
try:
|
||||||
device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
|
device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
|
||||||
await _async_initialize(hass, entry, device)
|
await _async_initialize(hass, entry, device)
|
||||||
except BULB_EXCEPTIONS as ex:
|
except (asyncio.TimeoutError, OSError, BulbException) as ex:
|
||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady from ex
|
||||||
|
|
||||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
@ -594,13 +590,20 @@ class YeelightDevice:
|
||||||
self._available = True
|
self._available = True
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
except BULB_NETWORK_EXCEPTIONS as ex:
|
except OSError 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:
|
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(
|
_LOGGER.debug(
|
||||||
"Unable to update device %s, %s: %s", self._host, self.name, ex
|
"Unable to update device %s, %s: %s", self._host, self.name, ex
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""Light platform support for yeelight."""
|
"""Light platform support for yeelight."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
@ -8,6 +9,7 @@ import voluptuous as vol
|
||||||
import yeelight
|
import yeelight
|
||||||
from yeelight import Bulb, 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 yeelight.main import BulbException
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
|
@ -51,8 +53,6 @@ from . import (
|
||||||
ATTR_COUNT,
|
ATTR_COUNT,
|
||||||
ATTR_MODE_MUSIC,
|
ATTR_MODE_MUSIC,
|
||||||
ATTR_TRANSITIONS,
|
ATTR_TRANSITIONS,
|
||||||
BULB_EXCEPTIONS,
|
|
||||||
BULB_NETWORK_EXCEPTIONS,
|
|
||||||
CONF_FLOW_PARAMS,
|
CONF_FLOW_PARAMS,
|
||||||
CONF_MODE_MUSIC,
|
CONF_MODE_MUSIC,
|
||||||
CONF_NIGHTLIGHT_SWITCH,
|
CONF_NIGHTLIGHT_SWITCH,
|
||||||
|
@ -243,10 +243,20 @@ def _async_cmd(func):
|
||||||
"""Define a wrapper to catch exceptions from the bulb."""
|
"""Define a wrapper to catch exceptions from the bulb."""
|
||||||
|
|
||||||
async def _async_wrap(self, *args, **kwargs):
|
async def _async_wrap(self, *args, **kwargs):
|
||||||
|
for attempts in range(2):
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
|
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
|
||||||
return await func(self, *args, **kwargs)
|
return await func(self, *args, **kwargs)
|
||||||
except BULB_NETWORK_EXCEPTIONS as ex:
|
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
|
# A network error happened, the bulb is likely offline now
|
||||||
self.device.async_mark_unavailable()
|
self.device.async_mark_unavailable()
|
||||||
self.async_state_changed()
|
self.async_state_changed()
|
||||||
|
@ -254,7 +264,7 @@ def _async_cmd(func):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
|
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
|
||||||
) from ex
|
) from ex
|
||||||
except BULB_EXCEPTIONS as ex:
|
except BulbException as ex:
|
||||||
# The bulb likely responded but had an error
|
# The bulb likely responded but had an error
|
||||||
exc_message = str(ex) or type(ex)
|
exc_message = str(ex) or type(ex)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
|
|
|
@ -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.7", "async-upnp-client==0.22.8"],
|
"requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.8"],
|
||||||
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
|
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["network"],
|
"dependencies": ["network"],
|
||||||
|
|
|
@ -2447,7 +2447,7 @@ yalesmartalarmclient==0.3.4
|
||||||
yalexs==1.1.13
|
yalexs==1.1.13
|
||||||
|
|
||||||
# homeassistant.components.yeelight
|
# homeassistant.components.yeelight
|
||||||
yeelight==0.7.7
|
yeelight==0.7.8
|
||||||
|
|
||||||
# homeassistant.components.yeelightsunflower
|
# homeassistant.components.yeelightsunflower
|
||||||
yeelightsunflower==0.0.10
|
yeelightsunflower==0.0.10
|
||||||
|
|
|
@ -1412,7 +1412,7 @@ yalesmartalarmclient==0.3.4
|
||||||
yalexs==1.1.13
|
yalexs==1.1.13
|
||||||
|
|
||||||
# homeassistant.components.yeelight
|
# homeassistant.components.yeelight
|
||||||
yeelight==0.7.7
|
yeelight==0.7.8
|
||||||
|
|
||||||
# homeassistant.components.youless
|
# homeassistant.components.youless
|
||||||
youless-api==0.14
|
youless-api==0.14
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
"""Test Yeelight."""
|
"""Test Yeelight."""
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from yeelight import BulbException, BulbType
|
from yeelight import BulbException, BulbType
|
||||||
from yeelight.aio import KEY_CONNECTED
|
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()
|
await hass.async_block_till_done()
|
||||||
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
|
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
Add a link
Reference in a new issue