mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Ensure yeelights resync state if they are busy on first connect (#55333)
This commit is contained in:
parent
ed19fdd462
commit
10fa63775d
@ -163,6 +163,8 @@ UPDATE_REQUEST_PROPERTIES = [
|
|||||||
"active_mode",
|
"active_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
BULB_EXCEPTIONS = (BulbException, asyncio.TimeoutError)
|
||||||
|
|
||||||
PLATFORMS = ["binary_sensor", "light"]
|
PLATFORMS = ["binary_sensor", "light"]
|
||||||
|
|
||||||
|
|
||||||
@ -272,7 +274,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
if entry.data.get(CONF_HOST):
|
if entry.data.get(CONF_HOST):
|
||||||
try:
|
try:
|
||||||
device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
|
device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
# If CONF_ID is not valid we cannot fallback to discovery
|
# If CONF_ID is not valid we cannot fallback to discovery
|
||||||
# so we must retry by raising ConfigEntryNotReady
|
# so we must retry by raising ConfigEntryNotReady
|
||||||
if not entry.data.get(CONF_ID):
|
if not entry.data.get(CONF_ID):
|
||||||
@ -287,7 +289,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
host = urlparse(capabilities["location"]).hostname
|
host = urlparse(capabilities["location"]).hostname
|
||||||
try:
|
try:
|
||||||
await _async_initialize(hass, entry, host)
|
await _async_initialize(hass, entry, host)
|
||||||
except BulbException:
|
except BULB_EXCEPTIONS:
|
||||||
_LOGGER.exception("Failed to connect to bulb at %s", host)
|
_LOGGER.exception("Failed to connect to bulb at %s", host)
|
||||||
|
|
||||||
# discovery
|
# discovery
|
||||||
@ -552,6 +554,7 @@ class YeelightDevice:
|
|||||||
self._device_type = None
|
self._device_type = None
|
||||||
self._available = False
|
self._available = False
|
||||||
self._initialized = False
|
self._initialized = False
|
||||||
|
self._did_first_update = False
|
||||||
self._name = None
|
self._name = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -647,14 +650,14 @@ class YeelightDevice:
|
|||||||
await self.bulb.async_turn_on(
|
await self.bulb.async_turn_on(
|
||||||
duration=duration, light_type=light_type, power_mode=power_mode
|
duration=duration, light_type=light_type, power_mode=power_mode
|
||||||
)
|
)
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Unable to turn the bulb on: %s", ex)
|
_LOGGER.error("Unable to turn the bulb on: %s", ex)
|
||||||
|
|
||||||
async def async_turn_off(self, duration=DEFAULT_TRANSITION, light_type=None):
|
async def async_turn_off(self, duration=DEFAULT_TRANSITION, light_type=None):
|
||||||
"""Turn off device."""
|
"""Turn off device."""
|
||||||
try:
|
try:
|
||||||
await self.bulb.async_turn_off(duration=duration, light_type=light_type)
|
await self.bulb.async_turn_off(duration=duration, light_type=light_type)
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Unable to turn the bulb off: %s, %s: %s", self._host, self.name, ex
|
"Unable to turn the bulb off: %s, %s: %s", self._host, self.name, ex
|
||||||
)
|
)
|
||||||
@ -670,7 +673,7 @@ class YeelightDevice:
|
|||||||
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 BulbException as ex:
|
except BULB_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
|
||||||
@ -696,6 +699,7 @@ class YeelightDevice:
|
|||||||
|
|
||||||
async def async_update(self, force=False):
|
async def async_update(self, force=False):
|
||||||
"""Update device properties and send data updated signal."""
|
"""Update device properties and send data updated signal."""
|
||||||
|
self._did_first_update = True
|
||||||
if not force and self._initialized and self._available:
|
if not force and self._initialized and self._available:
|
||||||
# No need to poll unless force, already connected
|
# No need to poll unless force, already connected
|
||||||
return
|
return
|
||||||
@ -705,7 +709,20 @@ class YeelightDevice:
|
|||||||
@callback
|
@callback
|
||||||
def async_update_callback(self, data):
|
def async_update_callback(self, data):
|
||||||
"""Update push from device."""
|
"""Update push from device."""
|
||||||
|
was_available = self._available
|
||||||
self._available = data.get(KEY_CONNECTED, True)
|
self._available = data.get(KEY_CONNECTED, True)
|
||||||
|
if self._did_first_update and not was_available and self._available:
|
||||||
|
# On reconnect the properties may be out of sync
|
||||||
|
#
|
||||||
|
# We need to make sure the DEVICE_INITIALIZED dispatcher is setup
|
||||||
|
# before we can update on reconnect by checking self._did_first_update
|
||||||
|
#
|
||||||
|
# If the device drops the connection right away, we do not want to
|
||||||
|
# do a property resync via async_update since its about
|
||||||
|
# to be called when async_setup_entry reaches the end of the
|
||||||
|
# function
|
||||||
|
#
|
||||||
|
asyncio.create_task(self.async_update(True))
|
||||||
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))
|
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
@ -49,6 +49,7 @@ from . import (
|
|||||||
ATTR_COUNT,
|
ATTR_COUNT,
|
||||||
ATTR_MODE_MUSIC,
|
ATTR_MODE_MUSIC,
|
||||||
ATTR_TRANSITIONS,
|
ATTR_TRANSITIONS,
|
||||||
|
BULB_EXCEPTIONS,
|
||||||
CONF_FLOW_PARAMS,
|
CONF_FLOW_PARAMS,
|
||||||
CONF_MODE_MUSIC,
|
CONF_MODE_MUSIC,
|
||||||
CONF_NIGHTLIGHT_SWITCH,
|
CONF_NIGHTLIGHT_SWITCH,
|
||||||
@ -241,7 +242,7 @@ def _async_cmd(func):
|
|||||||
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 BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Error when calling %s: %s", func, ex)
|
_LOGGER.error("Error when calling %s: %s", func, ex)
|
||||||
|
|
||||||
return _async_wrap
|
return _async_wrap
|
||||||
@ -678,7 +679,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
flow = Flow(count=count, transitions=transitions)
|
flow = Flow(count=count, transitions=transitions)
|
||||||
try:
|
try:
|
||||||
await self._bulb.async_start_flow(flow, light_type=self.light_type)
|
await self._bulb.async_start_flow(flow, light_type=self.light_type)
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Unable to set flash: %s", ex)
|
_LOGGER.error("Unable to set flash: %s", ex)
|
||||||
|
|
||||||
@_async_cmd
|
@_async_cmd
|
||||||
@ -709,7 +710,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
try:
|
try:
|
||||||
await self._bulb.async_start_flow(flow, light_type=self.light_type)
|
await self._bulb.async_start_flow(flow, light_type=self.light_type)
|
||||||
self._effect = effect
|
self._effect = effect
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Unable to set effect: %s", ex)
|
_LOGGER.error("Unable to set effect: %s", ex)
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs) -> None:
|
async def async_turn_on(self, **kwargs) -> None:
|
||||||
@ -737,7 +738,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
self.set_music_mode, self.config[CONF_MODE_MUSIC]
|
self.set_music_mode, self.config[CONF_MODE_MUSIC]
|
||||||
)
|
)
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Unable to turn on music mode, consider disabling it: %s", ex
|
"Unable to turn on music mode, consider disabling it: %s", ex
|
||||||
)
|
)
|
||||||
@ -750,7 +751,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
await self.async_set_brightness(brightness, duration)
|
await self.async_set_brightness(brightness, duration)
|
||||||
await self.async_set_flash(flash)
|
await self.async_set_flash(flash)
|
||||||
await self.async_set_effect(effect)
|
await self.async_set_effect(effect)
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Unable to set bulb properties: %s", ex)
|
_LOGGER.error("Unable to set bulb properties: %s", ex)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -758,7 +759,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
|
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
|
||||||
try:
|
try:
|
||||||
await self.async_set_default()
|
await self.async_set_default()
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Unable to set the defaults: %s", ex)
|
_LOGGER.error("Unable to set the defaults: %s", ex)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -784,7 +785,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
"""Set a power mode."""
|
"""Set a power mode."""
|
||||||
try:
|
try:
|
||||||
await self._bulb.async_set_power_mode(PowerMode[mode.upper()])
|
await self._bulb.async_set_power_mode(PowerMode[mode.upper()])
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Unable to set the power mode: %s", ex)
|
_LOGGER.error("Unable to set the power mode: %s", ex)
|
||||||
|
|
||||||
async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER):
|
async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER):
|
||||||
@ -795,7 +796,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self._bulb.async_start_flow(flow, light_type=self.light_type)
|
await self._bulb.async_start_flow(flow, light_type=self.light_type)
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Unable to set effect: %s", ex)
|
_LOGGER.error("Unable to set effect: %s", ex)
|
||||||
|
|
||||||
async def async_set_scene(self, scene_class, *args):
|
async def async_set_scene(self, scene_class, *args):
|
||||||
@ -806,7 +807,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await self._bulb.async_set_scene(scene_class, *args)
|
await self._bulb.async_set_scene(scene_class, *args)
|
||||||
except BulbException as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
_LOGGER.error("Unable to set scene: %s", ex)
|
_LOGGER.error("Unable to set scene: %s", ex)
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,11 +90,33 @@ YAML_CONFIGURATION = {
|
|||||||
CONFIG_ENTRY_DATA = {CONF_ID: ID}
|
CONFIG_ENTRY_DATA = {CONF_ID: ID}
|
||||||
|
|
||||||
|
|
||||||
|
class MockAsyncBulb:
|
||||||
|
"""A mock for yeelight.aio.AsyncBulb."""
|
||||||
|
|
||||||
|
def __init__(self, model, bulb_type, cannot_connect):
|
||||||
|
"""Init the mock."""
|
||||||
|
self.model = model
|
||||||
|
self.bulb_type = bulb_type
|
||||||
|
self._async_callback = None
|
||||||
|
self._cannot_connect = cannot_connect
|
||||||
|
|
||||||
|
async def async_listen(self, callback):
|
||||||
|
"""Mock the listener."""
|
||||||
|
if self._cannot_connect:
|
||||||
|
raise BulbException
|
||||||
|
self._async_callback = callback
|
||||||
|
|
||||||
|
async def async_stop_listening(self):
|
||||||
|
"""Drop the listener."""
|
||||||
|
self._async_callback = None
|
||||||
|
|
||||||
|
def set_capabilities(self, capabilities):
|
||||||
|
"""Mock setting capabilities."""
|
||||||
|
self.capabilities = capabilities
|
||||||
|
|
||||||
|
|
||||||
def _mocked_bulb(cannot_connect=False):
|
def _mocked_bulb(cannot_connect=False):
|
||||||
bulb = MagicMock()
|
bulb = MockAsyncBulb(MODEL, BulbType.Color, cannot_connect)
|
||||||
type(bulb).async_listen = AsyncMock(
|
|
||||||
side_effect=BulbException if cannot_connect else None
|
|
||||||
)
|
|
||||||
type(bulb).async_get_properties = AsyncMock(
|
type(bulb).async_get_properties = AsyncMock(
|
||||||
side_effect=BulbException if cannot_connect else None
|
side_effect=BulbException if cannot_connect else None
|
||||||
)
|
)
|
||||||
@ -102,14 +124,10 @@ def _mocked_bulb(cannot_connect=False):
|
|||||||
side_effect=BulbException if cannot_connect else None
|
side_effect=BulbException if cannot_connect else None
|
||||||
)
|
)
|
||||||
type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL])
|
type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL])
|
||||||
|
|
||||||
bulb.capabilities = CAPABILITIES.copy()
|
bulb.capabilities = CAPABILITIES.copy()
|
||||||
bulb.model = MODEL
|
|
||||||
bulb.bulb_type = BulbType.Color
|
|
||||||
bulb.last_properties = PROPERTIES.copy()
|
bulb.last_properties = PROPERTIES.copy()
|
||||||
bulb.music_mode = False
|
bulb.music_mode = False
|
||||||
bulb.async_get_properties = AsyncMock()
|
bulb.async_get_properties = AsyncMock()
|
||||||
bulb.async_stop_listening = AsyncMock()
|
|
||||||
bulb.async_update = AsyncMock()
|
bulb.async_update = AsyncMock()
|
||||||
bulb.async_turn_on = AsyncMock()
|
bulb.async_turn_on = AsyncMock()
|
||||||
bulb.async_turn_off = AsyncMock()
|
bulb.async_turn_off = AsyncMock()
|
||||||
@ -122,7 +140,7 @@ def _mocked_bulb(cannot_connect=False):
|
|||||||
bulb.async_set_power_mode = AsyncMock()
|
bulb.async_set_power_mode = AsyncMock()
|
||||||
bulb.async_set_scene = AsyncMock()
|
bulb.async_set_scene = AsyncMock()
|
||||||
bulb.async_set_default = AsyncMock()
|
bulb.async_set_default = AsyncMock()
|
||||||
|
bulb.start_music = MagicMock()
|
||||||
return bulb
|
return bulb
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ from datetime import timedelta
|
|||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from yeelight import BulbException, BulbType
|
from yeelight import BulbException, BulbType
|
||||||
|
from yeelight.aio import KEY_CONNECTED
|
||||||
|
|
||||||
from homeassistant.components.yeelight import (
|
from homeassistant.components.yeelight import (
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
@ -414,3 +415,29 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant):
|
|||||||
|
|
||||||
assert config_entry.state is ConfigEntryState.LOADED
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
assert config_entry.data[CONF_ID] == ID
|
assert config_entry.data[CONF_ID] == ID
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connection_dropped_resyncs_properties(hass: HomeAssistant):
|
||||||
|
"""Test handling a connection drop results in a property resync."""
|
||||||
|
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()
|
||||||
|
|
||||||
|
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 len(mocked_bulb.async_get_properties.mock_calls) == 1
|
||||||
|
mocked_bulb._async_callback({KEY_CONNECTED: False})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mocked_bulb.async_get_properties.mock_calls) == 1
|
||||||
|
mocked_bulb._async_callback({KEY_CONNECTED: True})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user