Yeelight local push updates (#51160)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
starkillerOG 2021-08-09 20:33:34 +02:00 committed by GitHub
parent acf55f2f3a
commit a23da30c29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 328 additions and 259 deletions

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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-*"
}],

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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
)

View file

@ -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

View file

@ -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