diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 6a5fd32213a..19fe5c550ad 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -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.""" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index deda6ebf9ab..30861fa0001 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -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}" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 47329235863..d0f1eee2828 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -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"], diff --git a/requirements_all.txt b/requirements_all.txt index db505b3584a..8ab676e8870 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d5c83200ab..ebdf6abee6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 9b52ab5f53b..f4cae17a30c 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -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