diff --git a/CODEOWNERS b/CODEOWNERS index 86622690fb9..d2e756c0d0d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -584,7 +584,7 @@ homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yale_smart_alarm/* @gjohansson-ST homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 homeassistant/components/yandex_transport/* @rishatik92 @devbis -homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn +homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn @starkillerOG homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yi/* @bachya homeassistant/components/youless/* @gjong diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 2cb754ce6a7..2a4ba4eac55 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -6,7 +6,8 @@ from datetime import timedelta import logging import voluptuous as vol -from yeelight import Bulb, BulbException, discover_bulbs +from yeelight import BulbException, discover_bulbs +from yeelight.aio import KEY_CONNECTED, AsyncBulb from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( @@ -14,13 +15,15 @@ from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_NAME, - CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -46,7 +49,6 @@ CONF_NIGHTLIGHT_SWITCH = "nightlight_switch" DATA_CONFIG_ENTRIES = "config_entries" DATA_CUSTOM_EFFECTS = "custom_effects" -DATA_SCAN_INTERVAL = "scan_interval" DATA_DEVICE = "device" DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" DATA_PLATFORMS_LOADED = "platforms_loaded" @@ -65,7 +67,6 @@ ACTIVE_COLOR_FLOWING = "1" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" -SCAN_INTERVAL = timedelta(seconds=30) DISCOVERY_INTERVAL = timedelta(seconds=60) YEELIGHT_RGB_TRANSITION = "RGBTransition" @@ -114,7 +115,6 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.Schema( { vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_CUSTOM_EFFECTS): [ { vol.Required(CONF_NAME): cv.string, @@ -158,7 +158,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: hass.data[DOMAIN] = { DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}), DATA_CONFIG_ENTRIES: {}, - DATA_SCAN_INTERVAL: conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), } # Import manually configured devices @@ -196,14 +195,25 @@ async def _async_initialize( device = await _async_get_device(hass, host, entry) entry_data[DATA_DEVICE] = device + # start listening for local pushes + await device.bulb.async_listen(device.async_update_callback) + + # register stop callback to shutdown listening for local pushes + async def async_stop_listen_task(event): + """Stop listen thread.""" + _LOGGER.debug("Shutting down Yeelight Listener") + await device.bulb.async_stop_listening() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_listen_task) + entry.async_on_unload( async_dispatcher_connect( hass, DEVICE_INITIALIZED.format(host), _async_load_platforms ) ) - entry.async_on_unload(device.async_unload) - await device.async_setup() + # fetch initial state + asyncio.create_task(device.async_update()) @callback @@ -248,14 +258,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Otherwise fall through to discovery else: # manually added device - await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) + try: + await _async_initialize( + hass, entry, entry.data[CONF_HOST], device=device + ) + except BulbException as ex: + raise ConfigEntryNotReady from ex return True # discovery scanner = YeelightScanner.async_get(hass) async def _async_from_discovery(host: str) -> None: - await _async_initialize(hass, entry, host) + try: + await _async_initialize(hass, entry, host) + except BulbException: + _LOGGER.exception("Failed to connect to bulb at %s", host) scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True @@ -275,6 +293,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner = YeelightScanner.async_get(hass) scanner.async_unregister_callback(entry.data[CONF_ID]) + device = entry_data[DATA_DEVICE] + _LOGGER.debug("Shutting down Yeelight Listener") + await device.bulb.async_stop_listening() + _LOGGER.debug("Yeelight Listener stopped") + data_config_entries.pop(entry.entry_id) return True @@ -331,7 +354,7 @@ class YeelightScanner: if len(self._callbacks) == 0: self._async_stop_scan() - await asyncio.sleep(SCAN_INTERVAL.total_seconds()) + await asyncio.sleep(DISCOVERY_INTERVAL.total_seconds()) self._scan_task = self._hass.loop.create_task(self._async_scan()) @callback @@ -382,7 +405,6 @@ class YeelightDevice: self._capabilities = capabilities or {} self._device_type = None self._available = False - self._remove_time_tracker = None self._initialized = False self._name = host # Default name is host @@ -478,34 +500,36 @@ class YeelightDevice: return self._device_type - def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None): + async def async_turn_on( + self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None + ): """Turn on device.""" try: - self.bulb.turn_on( + await self.bulb.async_turn_on( duration=duration, light_type=light_type, power_mode=power_mode ) except BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) - def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): + async def async_turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn off device.""" try: - self.bulb.turn_off(duration=duration, light_type=light_type) + await self.bulb.async_turn_off(duration=duration, light_type=light_type) except BulbException as ex: _LOGGER.error( "Unable to turn the bulb off: %s, %s: %s", self._host, self.name, ex ) - def _update_properties(self): + async def _async_update_properties(self): """Read new properties from the device.""" if not self.bulb: return try: - self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) + await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True if not self._initialized: - self._initialize_device() + await self._async_initialize_device() except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -515,10 +539,10 @@ class YeelightDevice: return self._available - def _get_capabilities(self): + async def _async_get_capabilities(self): """Request device capabilities.""" try: - self.bulb.get_capabilities() + await self._hass.async_add_executor_job(self.bulb.get_capabilities) _LOGGER.debug( "Device %s, %s capabilities: %s", self._host, @@ -533,31 +557,24 @@ class YeelightDevice: ex, ) - def _initialize_device(self): - self._get_capabilities() + async def _async_initialize_device(self): + await self._async_get_capabilities() self._initialized = True - dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) + async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) - def update(self): + async def async_update(self): """Update device properties and send data updated signal.""" - self._update_properties() - dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) - - async def async_setup(self): - """Set up the device.""" - - async def _async_update(_): - await self._hass.async_add_executor_job(self.update) - - await _async_update(None) - self._remove_time_tracker = async_track_time_interval( - self._hass, _async_update, self._hass.data[DOMAIN][DATA_SCAN_INTERVAL] - ) + if self._initialized and self._available: + # No need to poll, already connected + return + await self._async_update_properties() + async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) @callback - def async_unload(self): - """Unload the device.""" - self._remove_time_tracker() + def async_update_callback(self, data): + """Update push from device.""" + self._available = data.get(KEY_CONNECTED, True) + async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) class YeelightEntity(Entity): @@ -597,9 +614,9 @@ class YeelightEntity(Entity): """No polling needed.""" return False - def update(self) -> None: + async def async_update(self) -> None: """Update the entity.""" - self._device.update() + await self._device.async_update() async def _async_get_device( @@ -609,7 +626,7 @@ async def _async_get_device( model = entry.options.get(CONF_MODEL) # Set up device - bulb = Bulb(host, model=model or None) + bulb = AsyncBulb(host, model=model or None) capabilities = await hass.async_add_executor_job(bulb.get_capabilities) return YeelightDevice(hass, host, entry.options, bulb, capabilities) diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 4fe3709cdd2..185bb504a1b 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -33,6 +33,7 @@ class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): self.async_write_ha_state, ) ) + await super().async_added_to_hass() @property def unique_id(self) -> str: diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 8d0a3b0ffd4..d2ddc92bb8d 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,7 +1,6 @@ """Light platform support for yeelight.""" from __future__ import annotations -from functools import partial import logging import voluptuous as vol @@ -234,17 +233,17 @@ def _parse_custom_effects(effects_config): return effects -def _cmd(func): +def _async_cmd(func): """Define a wrapper to catch exceptions from the bulb.""" - def _wrap(self, *args, **kwargs): + async def _async_wrap(self, *args, **kwargs): try: _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) - return func(self, *args, **kwargs) + return await func(self, *args, **kwargs) except BulbException as ex: _LOGGER.error("Error when calling %s: %s", func, ex) - return _wrap + return _async_wrap async def async_setup_entry( @@ -306,36 +305,27 @@ def _async_setup_services(hass: HomeAssistant): params = {**service_call.data} params.pop(ATTR_ENTITY_ID) params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS]) - await hass.async_add_executor_job(partial(entity.start_flow, **params)) + await entity.async_start_flow(**params) async def _async_set_color_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.COLOR, - *service_call.data[ATTR_RGB_COLOR], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.COLOR, + *service_call.data[ATTR_RGB_COLOR], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_hsv_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.HSV, - *service_call.data[ATTR_HS_COLOR], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.HSV, + *service_call.data[ATTR_HS_COLOR], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_color_temp_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.CT, - service_call.data[ATTR_KELVIN], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.CT, + service_call.data[ATTR_KELVIN], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_color_flow_scene(entity, service_call): @@ -344,24 +334,19 @@ def _async_setup_services(hass: HomeAssistant): action=Flow.actions[service_call.data[ATTR_ACTION]], transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]), ) - await hass.async_add_executor_job( - partial(entity.set_scene, SceneClass.CF, flow) - ) + await entity.async_set_scene(SceneClass.CF, flow) async def _async_set_auto_delay_off_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.AUTO_DELAY_OFF, - service_call.data[ATTR_BRIGHTNESS], - service_call.data[ATTR_MINUTES], - ) + await entity.async_set_scene( + SceneClass.AUTO_DELAY_OFF, + service_call.data[ATTR_BRIGHTNESS], + service_call.data[ATTR_MINUTES], ) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( - SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "set_mode" + SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "async_set_mode" ) platform.async_register_entity_service( SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow @@ -405,8 +390,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self.config = device.config self._color_temp = None - self._hs = None - self._rgb = None self._effect = None model_specs = self._bulb.get_model_specs() @@ -420,19 +403,16 @@ class YeelightGenericLight(YeelightEntity, LightEntity): else: self._custom_effects = {} - @callback - def _schedule_immediate_update(self): - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect( self.hass, DATA_UPDATED.format(self._device.host), - self._schedule_immediate_update, + self.async_write_ha_state, ) ) + await super().async_added_to_hass() @property def supported_features(self) -> int: @@ -502,16 +482,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity): @property def hs_color(self) -> tuple: """Return the color property.""" - return self._hs + hue = self._get_property("hue") + sat = self._get_property("sat") + if hue is None or sat is None: + return None + + return (int(hue), int(sat)) @property def rgb_color(self) -> tuple: """Return the color property.""" - return self._rgb + rgb = self._get_property("rgb") + + if rgb is None: + return None + + rgb = int(rgb) + blue = rgb & 0xFF + green = (rgb >> 8) & 0xFF + red = (rgb >> 16) & 0xFF + + return (red, green, blue) @property def effect(self): """Return the current effect.""" + if not self.device.is_color_flow_enabled: + return None return self._effect @property @@ -561,33 +558,9 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Return yeelight device.""" return self._device - def update(self): + async def async_update(self): """Update light properties.""" - self._hs = self._get_hs_from_properties() - self._rgb = self._get_rgb_from_properties() - if not self.device.is_color_flow_enabled: - self._effect = None - - def _get_hs_from_properties(self): - hue = self._get_property("hue") - sat = self._get_property("sat") - if hue is None or sat is None: - return None - - return (int(hue), int(sat)) - - def _get_rgb_from_properties(self): - rgb = self._get_property("rgb") - - if rgb is None: - return None - - rgb = int(rgb) - blue = rgb & 0xFF - green = (rgb >> 8) & 0xFF - red = (rgb >> 16) & 0xFF - - return (red, green, blue) + await self.device.async_update() def set_music_mode(self, music_mode) -> None: """Set the music mode on or off.""" @@ -599,53 +572,51 @@ class YeelightGenericLight(YeelightEntity, LightEntity): else: self._bulb.stop_music() - self.device.update() - - @_cmd - def set_brightness(self, brightness, duration) -> None: + @_async_cmd + async def async_set_brightness(self, brightness, duration) -> None: """Set bulb brightness.""" if brightness: _LOGGER.debug("Setting brightness: %s", brightness) - self._bulb.set_brightness( + await self._bulb.async_set_brightness( brightness / 255 * 100, duration=duration, light_type=self.light_type ) - @_cmd - def set_hs(self, hs_color, duration) -> None: + @_async_cmd + async def async_set_hs(self, hs_color, duration) -> None: """Set bulb's color.""" if hs_color and COLOR_MODE_HS in self.supported_color_modes: _LOGGER.debug("Setting HS: %s", hs_color) - self._bulb.set_hsv( + await self._bulb.async_set_hsv( hs_color[0], hs_color[1], duration=duration, light_type=self.light_type ) - @_cmd - def set_rgb(self, rgb, duration) -> None: + @_async_cmd + async def async_set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" if rgb and COLOR_MODE_RGB in self.supported_color_modes: _LOGGER.debug("Setting RGB: %s", rgb) - self._bulb.set_rgb( + await self._bulb.async_set_rgb( rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type ) - @_cmd - def set_colortemp(self, colortemp, duration) -> None: + @_async_cmd + async def async_set_colortemp(self, colortemp, duration) -> None: """Set bulb's color temperature.""" if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes: temp_in_k = mired_to_kelvin(colortemp) _LOGGER.debug("Setting color temp: %s K", temp_in_k) - self._bulb.set_color_temp( + await self._bulb.async_set_color_temp( temp_in_k, duration=duration, light_type=self.light_type ) - @_cmd - def set_default(self) -> None: + @_async_cmd + async def async_set_default(self) -> None: """Set current options as default.""" - self._bulb.set_default() + await self._bulb.async_set_default() - @_cmd - def set_flash(self, flash) -> None: + @_async_cmd + async def async_set_flash(self, flash) -> None: """Activate flash.""" if flash: if int(self._bulb.last_properties["color_mode"]) != 1: @@ -660,7 +631,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): count = 1 duration = transition * 2 - red, green, blue = color_util.color_hs_to_RGB(*self._hs) + red, green, blue = color_util.color_hs_to_RGB(*self.hs_color) transitions = [] transitions.append( @@ -675,18 +646,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity): flow = Flow(count=count, transitions=transitions) try: - self._bulb.start_flow(flow, light_type=self.light_type) + await self._bulb.async_start_flow(flow, light_type=self.light_type) except BulbException as ex: _LOGGER.error("Unable to set flash: %s", ex) - @_cmd - def set_effect(self, effect) -> None: + @_async_cmd + async def async_set_effect(self, effect) -> None: """Activate effect.""" if not effect: return if effect == EFFECT_STOP: - self._bulb.stop_flow(light_type=self.light_type) + await self._bulb.async_stop_flow(light_type=self.light_type) return if effect in self.custom_effects_names: @@ -705,12 +676,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return try: - self._bulb.start_flow(flow, light_type=self.light_type) + await self._bulb.async_start_flow(flow, light_type=self.light_type) self._effect = effect except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the bulb on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) @@ -723,15 +694,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity): if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self.device.turn_on( - duration=duration, - light_type=self.light_type, - power_mode=self._turn_on_power_mode, - ) + if not self.is_on: + await self.device.async_turn_on( + duration=duration, + light_type=self.light_type, + power_mode=self._turn_on_power_mode, + ) if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: try: - self.set_music_mode(self.config[CONF_MODE_MUSIC]) + await self.hass.async_add_executor_job( + self.set_music_mode, self.config[CONF_MODE_MUSIC] + ) except BulbException as ex: _LOGGER.error( "Unable to turn on music mode, consider disabling it: %s", ex @@ -739,12 +713,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity): try: # values checked for none in methods - self.set_hs(hs_color, duration) - self.set_rgb(rgb, duration) - self.set_colortemp(colortemp, duration) - self.set_brightness(brightness, duration) - self.set_flash(flash) - self.set_effect(effect) + await self.async_set_hs(hs_color, duration) + await self.async_set_rgb(rgb, duration) + await self.async_set_colortemp(colortemp, duration) + await self.async_set_brightness(brightness, duration) + await self.async_set_flash(flash) + await self.async_set_effect(effect) except BulbException as ex: _LOGGER.error("Unable to set bulb properties: %s", ex) return @@ -752,50 +726,48 @@ class YeelightGenericLight(YeelightEntity, LightEntity): # save the current state if we had a manual change. if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb): try: - self.set_default() + await self.async_set_default() except BulbException as ex: _LOGGER.error("Unable to set the defaults: %s", ex) return - self.device.update() - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off.""" + if not self.is_on: + return + duration = int(self.config[CONF_TRANSITION]) # in ms if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self.device.turn_off(duration=duration, light_type=self.light_type) - self.device.update() + await self.device.async_turn_off(duration=duration, light_type=self.light_type) - def set_mode(self, mode: str): + async def async_set_mode(self, mode: str): """Set a power mode.""" try: - self._bulb.set_power_mode(PowerMode[mode.upper()]) - self.device.update() + await self._bulb.async_set_power_mode(PowerMode[mode.upper()]) except BulbException as ex: _LOGGER.error("Unable to set the power mode: %s", ex) - def start_flow(self, transitions, count=0, action=ACTION_RECOVER): + async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER): """Start flow.""" try: flow = Flow( count=count, action=Flow.actions[action], transitions=transitions ) - self._bulb.start_flow(flow, light_type=self.light_type) - self.device.update() + await self._bulb.async_start_flow(flow, light_type=self.light_type) except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) - def set_scene(self, scene_class, *args): + async def async_set_scene(self, scene_class, *args): """ Set the light directly to the specified state. If the light is off, it will first be turned on. """ try: - self._bulb.set_scene(scene_class, *args) - self.device.update() + await self._bulb.async_set_scene(scene_class, *args) except BulbException as ex: _LOGGER.error("Unable to set scene: %s", ex) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 0bf6249b647..7b78f540289 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,10 +2,10 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.6.3"], - "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], + "requirements": ["yeelight==0.7.2"], + "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, - "iot_class": "local_polling", + "iot_class": "local_push", "dhcp": [{ "hostname": "yeelink-*" }], diff --git a/requirements_all.txt b/requirements_all.txt index 0f2e0fdf1c3..1ce754f9db1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2421,7 +2421,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.6.3 +yeelight==0.7.2 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d224c4112d..f5fc75e41ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1338,7 +1338,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.6.3 +yeelight==0.7.2 # homeassistant.components.youless youless-api==0.10 diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 5725880f942..9fa864d6213 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -1,5 +1,5 @@ """Tests for the Yeelight integration.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS @@ -84,16 +84,34 @@ def _mocked_bulb(cannot_connect=False): type(bulb).get_capabilities = MagicMock( return_value=None if cannot_connect else CAPABILITIES ) + type(bulb).async_get_properties = AsyncMock( + side_effect=BulbException if cannot_connect else None + ) type(bulb).get_properties = MagicMock( side_effect=BulbException if cannot_connect else None ) type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL]) - bulb.capabilities = CAPABILITIES + bulb.capabilities = CAPABILITIES.copy() bulb.model = MODEL bulb.bulb_type = BulbType.Color - bulb.last_properties = PROPERTIES + bulb.last_properties = PROPERTIES.copy() bulb.music_mode = False + bulb.async_get_properties = AsyncMock() + bulb.async_listen = AsyncMock() + bulb.async_stop_listening = AsyncMock() + bulb.async_update = AsyncMock() + bulb.async_turn_on = AsyncMock() + bulb.async_turn_off = AsyncMock() + bulb.async_set_brightness = AsyncMock() + bulb.async_set_color_temp = AsyncMock() + bulb.async_set_hsv = AsyncMock() + bulb.async_set_rgb = AsyncMock() + bulb.async_start_flow = AsyncMock() + bulb.async_stop_flow = AsyncMock() + bulb.async_set_power_mode = AsyncMock() + bulb.async_set_scene = AsyncMock() + bulb.async_set_default = AsyncMock() return bulb diff --git a/tests/components/yeelight/test_binary_sensor.py b/tests/components/yeelight/test_binary_sensor.py index f716469fc9a..472d8de4919 100644 --- a/tests/components/yeelight/test_binary_sensor.py +++ b/tests/components/yeelight/test_binary_sensor.py @@ -14,7 +14,7 @@ ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" async def test_nightlight(hass: HomeAssistant): """Test nightlight sensor.""" mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb ): await async_setup_component(hass, DOMAIN, YAML_CONFIGURATION) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 8994c8e3360..247630ecfc3 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -219,7 +219,7 @@ async def test_options(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with 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() @@ -241,7 +241,7 @@ async def test_options(hass: HomeAssistant): config[CONF_NIGHTLIGHT_SWITCH] = True user_input = {**config} user_input.pop(CONF_NAME) - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input ) diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 2d1113d1896..575ad4cb594 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,7 +1,7 @@ """Test Yeelight.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch -from yeelight import BulbType +from yeelight import BulbException, BulbType from homeassistant.components.yeelight import ( CONF_NIGHTLIGHT_SWITCH, @@ -56,7 +56,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): ) _discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}] - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( f"{MODULE}.discover_bulbs", return_value=_discovered_devices ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -65,14 +65,12 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( f"yeelight_color_{ID}" ) - entity_registry = er.async_get(hass) - assert entity_registry.async_get(binary_sensor_entity_id) is None - await hass.async_block_till_done() + type(mocked_bulb).async_get_properties = AsyncMock(None) - type(mocked_bulb).get_properties = MagicMock(None) - - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ + DATA_DEVICE + ].async_update() await hass.async_block_till_done() await hass.async_block_till_done() @@ -91,7 +89,7 @@ async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): side_effect=[OSError, CAPABILITIES, CAPABILITIES] ) - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -104,7 +102,9 @@ async def test_setup_discovery(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), 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() @@ -127,7 +127,7 @@ async def test_setup_import(hass: HomeAssistant): """Test import from yaml.""" mocked_bulb = _mocked_bulb() name = "yeelight" - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb ): assert await async_setup_component( @@ -162,7 +162,9 @@ async def test_unique_ids_device(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), 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() @@ -186,7 +188,9 @@ async def test_unique_ids_entry(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), 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() @@ -216,7 +220,7 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): mocked_bulb = _mocked_bulb(True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -225,15 +229,52 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( IP_ADDRESS.replace(".", "_") ) - entity_registry = er.async_get(hass) - assert entity_registry.async_get(binary_sensor_entity_id) is None type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES) type(mocked_bulb).get_properties = MagicMock(None) - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ + DATA_DEVICE + ].async_update() + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ + DATA_DEVICE + ].async_update_callback({}) await hass.async_block_till_done() await hass.async_block_till_done() entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None + + +async def test_async_listen_error_late_discovery(hass, caplog): + """Test the async listen error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + mocked_bulb.async_listen = AsyncMock(side_effect=BulbException) + + with _patch_discovery(MODULE), 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 config_entry.state is ConfigEntryState.LOADED + assert "Failed to connect to bulb at" in caplog.text + + +async def test_async_listen_error_has_host(hass: HomeAssistant): + """Test the async listen error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "127.0.0.1"} + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + mocked_bulb.async_listen = AsyncMock(side_effect=BulbException) + + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 9283514cb70..9a1f632242b 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,6 +1,6 @@ """Test the Yeelight light.""" import logging -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from yeelight import ( BulbException, @@ -131,7 +131,9 @@ async def test_services(hass: HomeAssistant, caplog): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), 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() @@ -146,8 +148,11 @@ async def test_services(hass: HomeAssistant, caplog): err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) # success - mocked_method = MagicMock() - setattr(type(mocked_bulb), method, mocked_method) + if method.startswith("async_"): + mocked_method = AsyncMock() + else: + mocked_method = MagicMock() + setattr(mocked_bulb, method, mocked_method) await hass.services.async_call(domain, service, data, blocking=True) if payload is None: mocked_method.assert_called_once() @@ -161,8 +166,11 @@ async def test_services(hass: HomeAssistant, caplog): # failure if failure_side_effect: - mocked_method = MagicMock(side_effect=failure_side_effect) - setattr(type(mocked_bulb), method, mocked_method) + if method.startswith("async_"): + mocked_method = AsyncMock(side_effect=failure_side_effect) + else: + mocked_method = MagicMock(side_effect=failure_side_effect) + setattr(mocked_bulb, method, mocked_method) await hass.services.async_call(domain, service, data, blocking=True) assert ( len([x for x in caplog.records if x.levelno == logging.ERROR]) @@ -173,6 +181,7 @@ async def test_services(hass: HomeAssistant, caplog): brightness = 100 rgb_color = (0, 128, 255) transition = 2 + mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -186,30 +195,30 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() mocked_bulb.start_music.reset_mock() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_brightness.reset_mock() - mocked_bulb.set_color_temp.assert_not_called() - mocked_bulb.set_color_temp.reset_mock() - mocked_bulb.set_hsv.assert_not_called() - mocked_bulb.set_hsv.reset_mock() - mocked_bulb.set_rgb.assert_called_once_with( + mocked_bulb.async_set_brightness.reset_mock() + mocked_bulb.async_set_color_temp.assert_not_called() + mocked_bulb.async_set_color_temp.reset_mock() + mocked_bulb.async_set_hsv.assert_not_called() + mocked_bulb.async_set_hsv.reset_mock() + mocked_bulb.async_set_rgb.assert_called_once_with( *rgb_color, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_rgb.reset_mock() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.start_flow.reset_mock() - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) - mocked_bulb.stop_flow.reset_mock() + mocked_bulb.async_set_rgb.reset_mock() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_start_flow.reset_mock() + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_stop_flow.reset_mock() # turn_on hs_color brightness = 100 @@ -228,35 +237,36 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() mocked_bulb.start_music.reset_mock() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_brightness.reset_mock() - mocked_bulb.set_color_temp.assert_not_called() - mocked_bulb.set_color_temp.reset_mock() - mocked_bulb.set_hsv.assert_called_once_with( + mocked_bulb.async_set_brightness.reset_mock() + mocked_bulb.async_set_color_temp.assert_not_called() + mocked_bulb.async_set_color_temp.reset_mock() + mocked_bulb.async_set_hsv.assert_called_once_with( *hs_color, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_hsv.reset_mock() - mocked_bulb.set_rgb.assert_not_called() - mocked_bulb.set_rgb.reset_mock() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.start_flow.reset_mock() - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) - mocked_bulb.stop_flow.reset_mock() + mocked_bulb.async_set_hsv.reset_mock() + mocked_bulb.async_set_rgb.assert_not_called() + mocked_bulb.async_set_rgb.reset_mock() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_start_flow.reset_mock() + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_stop_flow.reset_mock() # turn_on color_temp brightness = 100 color_temp = 200 transition = 1 + mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -270,31 +280,32 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_color_temp.assert_called_once_with( + mocked_bulb.async_set_color_temp.assert_called_once_with( color_temperature_mired_to_kelvin(color_temp), duration=transition * 1000, light_type=LightType.Main, ) - mocked_bulb.set_hsv.assert_not_called() - mocked_bulb.set_rgb.assert_not_called() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_set_hsv.assert_not_called() + mocked_bulb.async_set_rgb.assert_not_called() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.last_properties["power"] = "off" # turn_on nightlight await _async_test_service( SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_NIGHTLIGHT}, - "turn_on", + "async_turn_on", payload={ "duration": DEFAULT_TRANSITION, "light_type": LightType.Main, @@ -303,11 +314,12 @@ async def test_services(hass: HomeAssistant, caplog): domain="light", ) + mocked_bulb.last_properties["power"] = "on" # turn_off await _async_test_service( SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: transition}, - "turn_off", + "async_turn_off", domain="light", payload={"duration": transition * 1000, "light_type": LightType.Main}, ) @@ -317,7 +329,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE: "rgb"}, - "set_power_mode", + "async_set_power_mode", [PowerMode[mode.upper()]], ) @@ -328,7 +340,7 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], }, - "start_flow", + "async_start_flow", ) # set_color_scene @@ -339,7 +351,7 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_RGB_COLOR: [10, 20, 30], ATTR_BRIGHTNESS: 50, }, - "set_scene", + "async_set_scene", [SceneClass.COLOR, 10, 20, 30, 50], ) @@ -347,7 +359,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_HSV_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: [180, 50], ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.HSV, 180, 50, 50], ) @@ -355,7 +367,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_COLOR_TEMP_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_KELVIN: 4000, ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.CT, 4000, 50], ) @@ -366,14 +378,14 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], }, - "set_scene", + "async_set_scene", ) # set_auto_delay_off_scene await _async_test_service( SERVICE_SET_AUTO_DELAY_OFF_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MINUTES: 1, ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.AUTO_DELAY_OFF, 50, 1], ) @@ -401,6 +413,7 @@ async def test_services(hass: HomeAssistant, caplog): failure_side_effect=None, ) # test _cmd wrapper error handler + mocked_bulb.last_properties["power"] = "off" err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) type(mocked_bulb).turn_on = MagicMock() type(mocked_bulb).set_brightness = MagicMock(side_effect=BulbException) @@ -424,8 +437,11 @@ async def test_device_types(hass: HomeAssistant, caplog): mocked_bulb.last_properties = properties async def _async_setup(config_entry): - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): - await hass.config_entries.async_setup(config_entry.entry_id) + with 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() + # We use asyncio.create_task now to avoid + # blocking starting so we need to block again await hass.async_block_till_done() async def _async_test( @@ -447,6 +463,7 @@ async def test_device_types(hass: HomeAssistant, caplog): await _async_setup(config_entry) state = hass.states.get(entity_id) + assert state.state == "on" target_properties["friendly_name"] = name target_properties["flowing"] = False @@ -481,6 +498,7 @@ async def test_device_types(hass: HomeAssistant, caplog): await hass.config_entries.async_unload(config_entry.entry_id) await config_entry.async_remove(hass) registry.async_clear_config_entry(config_entry.entry_id) + await hass.async_block_till_done() bright = round(255 * int(PROPERTIES["bright"]) / 100) current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100) @@ -841,7 +859,9 @@ async def test_effects(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), 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() @@ -850,8 +870,8 @@ async def test_effects(hass: HomeAssistant): ) == YEELIGHT_COLOR_EFFECT_LIST + ["mock_effect"] async def _async_test_effect(name, target=None, called=True): - mocked_start_flow = MagicMock() - type(mocked_bulb).start_flow = mocked_start_flow + async_mocked_start_flow = AsyncMock() + mocked_bulb.async_start_flow = async_mocked_start_flow await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -860,10 +880,10 @@ async def test_effects(hass: HomeAssistant): ) if not called: return - mocked_start_flow.assert_called_once() + async_mocked_start_flow.assert_called_once() if target is None: return - args, _ = mocked_start_flow.call_args + args, _ = async_mocked_start_flow.call_args flow = args[0] assert flow.count == target.count assert flow.action == target.action