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/yale_smart_alarm/* @gjohansson-ST
homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 homeassistant/components/yamaha_musiccast/* @vigonotion @micha91
homeassistant/components/yandex_transport/* @rishatik92 @devbis 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/yeelightsunflower/* @lindsaymarkward
homeassistant/components/yi/* @bachya homeassistant/components/yi/* @bachya
homeassistant/components/youless/* @gjong homeassistant/components/youless/* @gjong

View File

@ -6,7 +6,8 @@ from datetime import timedelta
import logging import logging
import voluptuous as vol 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.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady
from homeassistant.const import ( from homeassistant.const import (
@ -14,13 +15,15 @@ from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_ID, CONF_ID,
CONF_NAME, CONF_NAME,
CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv 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.entity import DeviceInfo, Entity
from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -46,7 +49,6 @@ CONF_NIGHTLIGHT_SWITCH = "nightlight_switch"
DATA_CONFIG_ENTRIES = "config_entries" DATA_CONFIG_ENTRIES = "config_entries"
DATA_CUSTOM_EFFECTS = "custom_effects" DATA_CUSTOM_EFFECTS = "custom_effects"
DATA_SCAN_INTERVAL = "scan_interval"
DATA_DEVICE = "device" DATA_DEVICE = "device"
DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher"
DATA_PLATFORMS_LOADED = "platforms_loaded" DATA_PLATFORMS_LOADED = "platforms_loaded"
@ -65,7 +67,6 @@ ACTIVE_COLOR_FLOWING = "1"
NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light"
SCAN_INTERVAL = timedelta(seconds=30)
DISCOVERY_INTERVAL = timedelta(seconds=60) DISCOVERY_INTERVAL = timedelta(seconds=60)
YEELIGHT_RGB_TRANSITION = "RGBTransition" YEELIGHT_RGB_TRANSITION = "RGBTransition"
@ -114,7 +115,6 @@ CONFIG_SCHEMA = vol.Schema(
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{ {
vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_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.Optional(CONF_CUSTOM_EFFECTS): [
{ {
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
@ -158,7 +158,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
hass.data[DOMAIN] = { hass.data[DOMAIN] = {
DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}), DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}),
DATA_CONFIG_ENTRIES: {}, DATA_CONFIG_ENTRIES: {},
DATA_SCAN_INTERVAL: conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
} }
# Import manually configured devices # Import manually configured devices
@ -196,14 +195,25 @@ async def _async_initialize(
device = await _async_get_device(hass, host, entry) device = await _async_get_device(hass, host, entry)
entry_data[DATA_DEVICE] = device 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( entry.async_on_unload(
async_dispatcher_connect( async_dispatcher_connect(
hass, DEVICE_INITIALIZED.format(host), _async_load_platforms hass, DEVICE_INITIALIZED.format(host), _async_load_platforms
) )
) )
entry.async_on_unload(device.async_unload) # fetch initial state
await device.async_setup() asyncio.create_task(device.async_update())
@callback @callback
@ -248,14 +258,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Otherwise fall through to discovery # Otherwise fall through to discovery
else: else:
# manually added device # 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 return True
# discovery # discovery
scanner = YeelightScanner.async_get(hass) scanner = YeelightScanner.async_get(hass)
async def _async_from_discovery(host: str) -> None: 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) scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery)
return True return True
@ -275,6 +293,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
scanner = YeelightScanner.async_get(hass) scanner = YeelightScanner.async_get(hass)
scanner.async_unregister_callback(entry.data[CONF_ID]) 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) data_config_entries.pop(entry.entry_id)
return True return True
@ -331,7 +354,7 @@ class YeelightScanner:
if len(self._callbacks) == 0: if len(self._callbacks) == 0:
self._async_stop_scan() 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()) self._scan_task = self._hass.loop.create_task(self._async_scan())
@callback @callback
@ -382,7 +405,6 @@ class YeelightDevice:
self._capabilities = capabilities or {} self._capabilities = capabilities or {}
self._device_type = None self._device_type = None
self._available = False self._available = False
self._remove_time_tracker = None
self._initialized = False self._initialized = False
self._name = host # Default name is host self._name = host # Default name is host
@ -478,34 +500,36 @@ class YeelightDevice:
return self._device_type 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.""" """Turn on device."""
try: try:
self.bulb.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 BulbException as ex:
_LOGGER.error("Unable to turn the bulb on: %s", 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.""" """Turn off device."""
try: 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: except BulbException 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
) )
def _update_properties(self): async def _async_update_properties(self):
"""Read new properties from the device.""" """Read new properties from the device."""
if not self.bulb: if not self.bulb:
return return
try: try:
self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES)
self._available = True self._available = True
if not self._initialized: if not self._initialized:
self._initialize_device() await self._async_initialize_device()
except BulbException as ex: except BulbException as ex:
if self._available: # just inform once if self._available: # just inform once
_LOGGER.error( _LOGGER.error(
@ -515,10 +539,10 @@ class YeelightDevice:
return self._available return self._available
def _get_capabilities(self): async def _async_get_capabilities(self):
"""Request device capabilities.""" """Request device capabilities."""
try: try:
self.bulb.get_capabilities() await self._hass.async_add_executor_job(self.bulb.get_capabilities)
_LOGGER.debug( _LOGGER.debug(
"Device %s, %s capabilities: %s", "Device %s, %s capabilities: %s",
self._host, self._host,
@ -533,31 +557,24 @@ class YeelightDevice:
ex, ex,
) )
def _initialize_device(self): async def _async_initialize_device(self):
self._get_capabilities() await self._async_get_capabilities()
self._initialized = True 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.""" """Update device properties and send data updated signal."""
self._update_properties() if self._initialized and self._available:
dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) # No need to poll, already connected
return
async def async_setup(self): await self._async_update_properties()
"""Set up the device.""" async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))
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]
)
@callback @callback
def async_unload(self): def async_update_callback(self, data):
"""Unload the device.""" """Update push from device."""
self._remove_time_tracker() self._available = data.get(KEY_CONNECTED, True)
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))
class YeelightEntity(Entity): class YeelightEntity(Entity):
@ -597,9 +614,9 @@ class YeelightEntity(Entity):
"""No polling needed.""" """No polling needed."""
return False return False
def update(self) -> None: async def async_update(self) -> None:
"""Update the entity.""" """Update the entity."""
self._device.update() await self._device.async_update()
async def _async_get_device( async def _async_get_device(
@ -609,7 +626,7 @@ async def _async_get_device(
model = entry.options.get(CONF_MODEL) model = entry.options.get(CONF_MODEL)
# Set up device # 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) capabilities = await hass.async_add_executor_job(bulb.get_capabilities)
return YeelightDevice(hass, host, entry.options, bulb, capabilities) return YeelightDevice(hass, host, entry.options, bulb, capabilities)

View File

@ -33,6 +33,7 @@ class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity):
self.async_write_ha_state, self.async_write_ha_state,
) )
) )
await super().async_added_to_hass()
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:

View File

@ -1,7 +1,6 @@
"""Light platform support for yeelight.""" """Light platform support for yeelight."""
from __future__ import annotations from __future__ import annotations
from functools import partial
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -234,17 +233,17 @@ def _parse_custom_effects(effects_config):
return effects return effects
def _cmd(func): def _async_cmd(func):
"""Define a wrapper to catch exceptions from the bulb.""" """Define a wrapper to catch exceptions from the bulb."""
def _wrap(self, *args, **kwargs): async def _async_wrap(self, *args, **kwargs):
try: try:
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs) _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: except BulbException as ex:
_LOGGER.error("Error when calling %s: %s", func, ex) _LOGGER.error("Error when calling %s: %s", func, ex)
return _wrap return _async_wrap
async def async_setup_entry( async def async_setup_entry(
@ -306,36 +305,27 @@ def _async_setup_services(hass: HomeAssistant):
params = {**service_call.data} params = {**service_call.data}
params.pop(ATTR_ENTITY_ID) params.pop(ATTR_ENTITY_ID)
params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS]) 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): async def _async_set_color_scene(entity, service_call):
await hass.async_add_executor_job( await entity.async_set_scene(
partial( SceneClass.COLOR,
entity.set_scene, *service_call.data[ATTR_RGB_COLOR],
SceneClass.COLOR, service_call.data[ATTR_BRIGHTNESS],
*service_call.data[ATTR_RGB_COLOR],
service_call.data[ATTR_BRIGHTNESS],
)
) )
async def _async_set_hsv_scene(entity, service_call): async def _async_set_hsv_scene(entity, service_call):
await hass.async_add_executor_job( await entity.async_set_scene(
partial( SceneClass.HSV,
entity.set_scene, *service_call.data[ATTR_HS_COLOR],
SceneClass.HSV, service_call.data[ATTR_BRIGHTNESS],
*service_call.data[ATTR_HS_COLOR],
service_call.data[ATTR_BRIGHTNESS],
)
) )
async def _async_set_color_temp_scene(entity, service_call): async def _async_set_color_temp_scene(entity, service_call):
await hass.async_add_executor_job( await entity.async_set_scene(
partial( SceneClass.CT,
entity.set_scene, service_call.data[ATTR_KELVIN],
SceneClass.CT, service_call.data[ATTR_BRIGHTNESS],
service_call.data[ATTR_KELVIN],
service_call.data[ATTR_BRIGHTNESS],
)
) )
async def _async_set_color_flow_scene(entity, service_call): 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]], action=Flow.actions[service_call.data[ATTR_ACTION]],
transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]), transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]),
) )
await hass.async_add_executor_job( await entity.async_set_scene(SceneClass.CF, flow)
partial(entity.set_scene, SceneClass.CF, flow)
)
async def _async_set_auto_delay_off_scene(entity, service_call): async def _async_set_auto_delay_off_scene(entity, service_call):
await hass.async_add_executor_job( await entity.async_set_scene(
partial( SceneClass.AUTO_DELAY_OFF,
entity.set_scene, service_call.data[ATTR_BRIGHTNESS],
SceneClass.AUTO_DELAY_OFF, service_call.data[ATTR_MINUTES],
service_call.data[ATTR_BRIGHTNESS],
service_call.data[ATTR_MINUTES],
)
) )
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service( 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( platform.async_register_entity_service(
SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow
@ -405,8 +390,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
self.config = device.config self.config = device.config
self._color_temp = None self._color_temp = None
self._hs = None
self._rgb = None
self._effect = None self._effect = None
model_specs = self._bulb.get_model_specs() model_specs = self._bulb.get_model_specs()
@ -420,19 +403,16 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
else: else:
self._custom_effects = {} self._custom_effects = {}
@callback
def _schedule_immediate_update(self):
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Handle entity which will be added.""" """Handle entity which will be added."""
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,
DATA_UPDATED.format(self._device.host), DATA_UPDATED.format(self._device.host),
self._schedule_immediate_update, self.async_write_ha_state,
) )
) )
await super().async_added_to_hass()
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
@ -502,16 +482,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
@property @property
def hs_color(self) -> tuple: def hs_color(self) -> tuple:
"""Return the color property.""" """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 @property
def rgb_color(self) -> tuple: def rgb_color(self) -> tuple:
"""Return the color property.""" """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 @property
def effect(self): def effect(self):
"""Return the current effect.""" """Return the current effect."""
if not self.device.is_color_flow_enabled:
return None
return self._effect return self._effect
@property @property
@ -561,33 +558,9 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
"""Return yeelight device.""" """Return yeelight device."""
return self._device return self._device
def update(self): async def async_update(self):
"""Update light properties.""" """Update light properties."""
self._hs = self._get_hs_from_properties() await self.device.async_update()
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)
def set_music_mode(self, music_mode) -> None: def set_music_mode(self, music_mode) -> None:
"""Set the music mode on or off.""" """Set the music mode on or off."""
@ -599,53 +572,51 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
else: else:
self._bulb.stop_music() self._bulb.stop_music()
self.device.update() @_async_cmd
async def async_set_brightness(self, brightness, duration) -> None:
@_cmd
def set_brightness(self, brightness, duration) -> None:
"""Set bulb brightness.""" """Set bulb brightness."""
if brightness: if brightness:
_LOGGER.debug("Setting brightness: %s", 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 brightness / 255 * 100, duration=duration, light_type=self.light_type
) )
@_cmd @_async_cmd
def set_hs(self, hs_color, duration) -> None: async def async_set_hs(self, hs_color, duration) -> None:
"""Set bulb's color.""" """Set bulb's color."""
if hs_color and COLOR_MODE_HS in self.supported_color_modes: if hs_color and COLOR_MODE_HS in self.supported_color_modes:
_LOGGER.debug("Setting HS: %s", hs_color) _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 hs_color[0], hs_color[1], duration=duration, light_type=self.light_type
) )
@_cmd @_async_cmd
def set_rgb(self, rgb, duration) -> None: async def async_set_rgb(self, rgb, duration) -> None:
"""Set bulb's color.""" """Set bulb's color."""
if rgb and COLOR_MODE_RGB in self.supported_color_modes: if rgb and COLOR_MODE_RGB in self.supported_color_modes:
_LOGGER.debug("Setting RGB: %s", rgb) _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 rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type
) )
@_cmd @_async_cmd
def set_colortemp(self, colortemp, duration) -> None: async def async_set_colortemp(self, colortemp, duration) -> None:
"""Set bulb's color temperature.""" """Set bulb's color temperature."""
if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes: if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes:
temp_in_k = mired_to_kelvin(colortemp) temp_in_k = mired_to_kelvin(colortemp)
_LOGGER.debug("Setting color temp: %s K", temp_in_k) _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 temp_in_k, duration=duration, light_type=self.light_type
) )
@_cmd @_async_cmd
def set_default(self) -> None: async def async_set_default(self) -> None:
"""Set current options as default.""" """Set current options as default."""
self._bulb.set_default() await self._bulb.async_set_default()
@_cmd @_async_cmd
def set_flash(self, flash) -> None: async def async_set_flash(self, flash) -> None:
"""Activate flash.""" """Activate flash."""
if flash: if flash:
if int(self._bulb.last_properties["color_mode"]) != 1: if int(self._bulb.last_properties["color_mode"]) != 1:
@ -660,7 +631,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
count = 1 count = 1
duration = transition * 2 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 = []
transitions.append( transitions.append(
@ -675,18 +646,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
flow = Flow(count=count, transitions=transitions) flow = Flow(count=count, transitions=transitions)
try: 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: except BulbException as ex:
_LOGGER.error("Unable to set flash: %s", ex) _LOGGER.error("Unable to set flash: %s", ex)
@_cmd @_async_cmd
def set_effect(self, effect) -> None: async def async_set_effect(self, effect) -> None:
"""Activate effect.""" """Activate effect."""
if not effect: if not effect:
return return
if effect == EFFECT_STOP: 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 return
if effect in self.custom_effects_names: if effect in self.custom_effects_names:
@ -705,12 +676,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
return return
try: 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 self._effect = effect
except BulbException as ex: except BulbException as ex:
_LOGGER.error("Unable to set effect: %s", 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.""" """Turn the bulb on."""
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
colortemp = kwargs.get(ATTR_COLOR_TEMP) colortemp = kwargs.get(ATTR_COLOR_TEMP)
@ -723,15 +694,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
self.device.turn_on( if not self.is_on:
duration=duration, await self.device.async_turn_on(
light_type=self.light_type, duration=duration,
power_mode=self._turn_on_power_mode, light_type=self.light_type,
) power_mode=self._turn_on_power_mode,
)
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
try: 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: except BulbException 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
@ -739,12 +713,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
try: try:
# values checked for none in methods # values checked for none in methods
self.set_hs(hs_color, duration) await self.async_set_hs(hs_color, duration)
self.set_rgb(rgb, duration) await self.async_set_rgb(rgb, duration)
self.set_colortemp(colortemp, duration) await self.async_set_colortemp(colortemp, duration)
self.set_brightness(brightness, duration) await self.async_set_brightness(brightness, duration)
self.set_flash(flash) await self.async_set_flash(flash)
self.set_effect(effect) await self.async_set_effect(effect)
except BulbException as ex: except BulbException as ex:
_LOGGER.error("Unable to set bulb properties: %s", ex) _LOGGER.error("Unable to set bulb properties: %s", ex)
return return
@ -752,50 +726,48 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
# save the current state if we had a manual change. # save the current state if we had a manual change.
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:
self.set_default() await self.async_set_default()
except BulbException as ex: except BulbException as ex:
_LOGGER.error("Unable to set the defaults: %s", ex) _LOGGER.error("Unable to set the defaults: %s", ex)
return return
self.device.update()
def turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn off.""" """Turn off."""
if not self.is_on:
return
duration = int(self.config[CONF_TRANSITION]) # in ms duration = int(self.config[CONF_TRANSITION]) # in ms
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
self.device.turn_off(duration=duration, light_type=self.light_type) await self.device.async_turn_off(duration=duration, light_type=self.light_type)
self.device.update()
def set_mode(self, mode: str): async def async_set_mode(self, mode: str):
"""Set a power mode.""" """Set a power mode."""
try: try:
self._bulb.set_power_mode(PowerMode[mode.upper()]) await self._bulb.async_set_power_mode(PowerMode[mode.upper()])
self.device.update()
except BulbException as ex: except BulbException as ex:
_LOGGER.error("Unable to set the power mode: %s", 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.""" """Start flow."""
try: try:
flow = Flow( flow = Flow(
count=count, action=Flow.actions[action], transitions=transitions count=count, action=Flow.actions[action], transitions=transitions
) )
self._bulb.start_flow(flow, light_type=self.light_type) await self._bulb.async_start_flow(flow, light_type=self.light_type)
self.device.update()
except BulbException as ex: except BulbException as ex:
_LOGGER.error("Unable to set effect: %s", 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. Set the light directly to the specified state.
If the light is off, it will first be turned on. If the light is off, it will first be turned on.
""" """
try: try:
self._bulb.set_scene(scene_class, *args) await self._bulb.async_set_scene(scene_class, *args)
self.device.update()
except BulbException as ex: except BulbException as ex:
_LOGGER.error("Unable to set scene: %s", ex) _LOGGER.error("Unable to set scene: %s", ex)

View File

@ -2,10 +2,10 @@
"domain": "yeelight", "domain": "yeelight",
"name": "Yeelight", "name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.6.3"], "requirements": ["yeelight==0.7.2"],
"codeowners": ["@rytilahti", "@zewelor", "@shenxn"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
"config_flow": true, "config_flow": true,
"iot_class": "local_polling", "iot_class": "local_push",
"dhcp": [{ "dhcp": [{
"hostname": "yeelink-*" "hostname": "yeelink-*"
}], }],

View File

@ -2421,7 +2421,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13 yalexs==1.1.13
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.6.3 yeelight==0.7.2
# homeassistant.components.yeelightsunflower # homeassistant.components.yeelightsunflower
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10

View File

@ -1338,7 +1338,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13 yalexs==1.1.13
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.6.3 yeelight==0.7.2
# homeassistant.components.youless # homeassistant.components.youless
youless-api==0.10 youless-api==0.10

View File

@ -1,5 +1,5 @@
"""Tests for the Yeelight integration.""" """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 import BulbException, BulbType
from yeelight.main import _MODEL_SPECS from yeelight.main import _MODEL_SPECS
@ -84,16 +84,34 @@ def _mocked_bulb(cannot_connect=False):
type(bulb).get_capabilities = MagicMock( type(bulb).get_capabilities = MagicMock(
return_value=None if cannot_connect else CAPABILITIES 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( type(bulb).get_properties = MagicMock(
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 bulb.capabilities = CAPABILITIES.copy()
bulb.model = MODEL bulb.model = MODEL
bulb.bulb_type = BulbType.Color bulb.bulb_type = BulbType.Color
bulb.last_properties = PROPERTIES bulb.last_properties = PROPERTIES.copy()
bulb.music_mode = False 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 return bulb

View File

@ -14,7 +14,7 @@ ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight"
async def test_nightlight(hass: HomeAssistant): async def test_nightlight(hass: HomeAssistant):
"""Test nightlight sensor.""" """Test nightlight sensor."""
mocked_bulb = _mocked_bulb() 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 f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb
): ):
await async_setup_component(hass, DOMAIN, YAML_CONFIGURATION) 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) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb() 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -241,7 +241,7 @@ async def test_options(hass: HomeAssistant):
config[CONF_NIGHTLIGHT_SWITCH] = True config[CONF_NIGHTLIGHT_SWITCH] = True
user_input = {**config} user_input = {**config}
user_input.pop(CONF_NAME) 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( result2 = await hass.config_entries.options.async_configure(
result["flow_id"], user_input result["flow_id"], user_input
) )

View File

@ -1,7 +1,7 @@
"""Test Yeelight.""" """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 ( from homeassistant.components.yeelight import (
CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH,
@ -56,7 +56,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
) )
_discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}] _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 f"{MODULE}.discover_bulbs", return_value=_discovered_devices
): ):
assert await hass.config_entries.async_setup(config_entry.entry_id) 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( binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format(
f"yeelight_color_{ID}" 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) await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][
DATA_DEVICE
hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() ].async_update()
await hass.async_block_till_done() await hass.async_block_till_done()
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] 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) assert not await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -104,7 +102,9 @@ async def test_setup_discovery(hass: HomeAssistant):
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb() 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -127,7 +127,7 @@ async def test_setup_import(hass: HomeAssistant):
"""Test import from yaml.""" """Test import from yaml."""
mocked_bulb = _mocked_bulb() mocked_bulb = _mocked_bulb()
name = "yeelight" 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 f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb
): ):
assert await async_setup_component( assert await async_setup_component(
@ -162,7 +162,9 @@ async def test_unique_ids_device(hass: HomeAssistant):
mocked_bulb = _mocked_bulb() mocked_bulb = _mocked_bulb()
mocked_bulb.bulb_type = BulbType.WhiteTempMood 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -186,7 +188,9 @@ async def test_unique_ids_entry(hass: HomeAssistant):
mocked_bulb = _mocked_bulb() mocked_bulb = _mocked_bulb()
mocked_bulb.bulb_type = BulbType.WhiteTempMood 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() 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 = _mocked_bulb(True)
mocked_bulb.bulb_type = BulbType.WhiteTempMood 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 f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb
): ):
assert await hass.config_entries.async_setup(config_entry.entry_id) 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( binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format(
IP_ADDRESS.replace(".", "_") 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_capabilities = MagicMock(CAPABILITIES)
type(mocked_bulb).get_properties = MagicMock(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()
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()
await hass.async_block_till_done() await hass.async_block_till_done()
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
assert entity_registry.async_get(binary_sensor_entity_id) is not None 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.""" """Test the Yeelight light."""
import logging import logging
from unittest.mock import MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from yeelight import ( from yeelight import (
BulbException, BulbException,
@ -131,7 +131,9 @@ async def test_services(hass: HomeAssistant, caplog):
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb() 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() 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]) err_count = len([x for x in caplog.records if x.levelno == logging.ERROR])
# success # success
mocked_method = MagicMock() if method.startswith("async_"):
setattr(type(mocked_bulb), method, mocked_method) mocked_method = AsyncMock()
else:
mocked_method = MagicMock()
setattr(mocked_bulb, method, mocked_method)
await hass.services.async_call(domain, service, data, blocking=True) await hass.services.async_call(domain, service, data, blocking=True)
if payload is None: if payload is None:
mocked_method.assert_called_once() mocked_method.assert_called_once()
@ -161,8 +166,11 @@ async def test_services(hass: HomeAssistant, caplog):
# failure # failure
if failure_side_effect: if failure_side_effect:
mocked_method = MagicMock(side_effect=failure_side_effect) if method.startswith("async_"):
setattr(type(mocked_bulb), method, mocked_method) 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) await hass.services.async_call(domain, service, data, blocking=True)
assert ( assert (
len([x for x in caplog.records if x.levelno == logging.ERROR]) 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 brightness = 100
rgb_color = (0, 128, 255) rgb_color = (0, 128, 255)
transition = 2 transition = 2
mocked_bulb.last_properties["power"] = "off"
await hass.services.async_call( await hass.services.async_call(
"light", "light",
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -186,30 +195,30 @@ async def test_services(hass: HomeAssistant, caplog):
}, },
blocking=True, blocking=True,
) )
mocked_bulb.turn_on.assert_called_once_with( mocked_bulb.async_turn_on.assert_called_once_with(
duration=transition * 1000, duration=transition * 1000,
light_type=LightType.Main, light_type=LightType.Main,
power_mode=PowerMode.NORMAL, 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.assert_called_once()
mocked_bulb.start_music.reset_mock() 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 brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main
) )
mocked_bulb.set_brightness.reset_mock() mocked_bulb.async_set_brightness.reset_mock()
mocked_bulb.set_color_temp.assert_not_called() mocked_bulb.async_set_color_temp.assert_not_called()
mocked_bulb.set_color_temp.reset_mock() mocked_bulb.async_set_color_temp.reset_mock()
mocked_bulb.set_hsv.assert_not_called() mocked_bulb.async_set_hsv.assert_not_called()
mocked_bulb.set_hsv.reset_mock() mocked_bulb.async_set_hsv.reset_mock()
mocked_bulb.set_rgb.assert_called_once_with( mocked_bulb.async_set_rgb.assert_called_once_with(
*rgb_color, duration=transition * 1000, light_type=LightType.Main *rgb_color, duration=transition * 1000, light_type=LightType.Main
) )
mocked_bulb.set_rgb.reset_mock() mocked_bulb.async_set_rgb.reset_mock()
mocked_bulb.start_flow.assert_called_once() # flash mocked_bulb.async_start_flow.assert_called_once() # flash
mocked_bulb.start_flow.reset_mock() mocked_bulb.async_start_flow.reset_mock()
mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main)
mocked_bulb.stop_flow.reset_mock() mocked_bulb.async_stop_flow.reset_mock()
# turn_on hs_color # turn_on hs_color
brightness = 100 brightness = 100
@ -228,35 +237,36 @@ async def test_services(hass: HomeAssistant, caplog):
}, },
blocking=True, blocking=True,
) )
mocked_bulb.turn_on.assert_called_once_with( mocked_bulb.async_turn_on.assert_called_once_with(
duration=transition * 1000, duration=transition * 1000,
light_type=LightType.Main, light_type=LightType.Main,
power_mode=PowerMode.NORMAL, 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.assert_called_once()
mocked_bulb.start_music.reset_mock() 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 brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main
) )
mocked_bulb.set_brightness.reset_mock() mocked_bulb.async_set_brightness.reset_mock()
mocked_bulb.set_color_temp.assert_not_called() mocked_bulb.async_set_color_temp.assert_not_called()
mocked_bulb.set_color_temp.reset_mock() mocked_bulb.async_set_color_temp.reset_mock()
mocked_bulb.set_hsv.assert_called_once_with( mocked_bulb.async_set_hsv.assert_called_once_with(
*hs_color, duration=transition * 1000, light_type=LightType.Main *hs_color, duration=transition * 1000, light_type=LightType.Main
) )
mocked_bulb.set_hsv.reset_mock() mocked_bulb.async_set_hsv.reset_mock()
mocked_bulb.set_rgb.assert_not_called() mocked_bulb.async_set_rgb.assert_not_called()
mocked_bulb.set_rgb.reset_mock() mocked_bulb.async_set_rgb.reset_mock()
mocked_bulb.start_flow.assert_called_once() # flash mocked_bulb.async_start_flow.assert_called_once() # flash
mocked_bulb.start_flow.reset_mock() mocked_bulb.async_start_flow.reset_mock()
mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main)
mocked_bulb.stop_flow.reset_mock() mocked_bulb.async_stop_flow.reset_mock()
# turn_on color_temp # turn_on color_temp
brightness = 100 brightness = 100
color_temp = 200 color_temp = 200
transition = 1 transition = 1
mocked_bulb.last_properties["power"] = "off"
await hass.services.async_call( await hass.services.async_call(
"light", "light",
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -270,31 +280,32 @@ async def test_services(hass: HomeAssistant, caplog):
}, },
blocking=True, blocking=True,
) )
mocked_bulb.turn_on.assert_called_once_with( mocked_bulb.async_turn_on.assert_called_once_with(
duration=transition * 1000, duration=transition * 1000,
light_type=LightType.Main, light_type=LightType.Main,
power_mode=PowerMode.NORMAL, 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.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 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), color_temperature_mired_to_kelvin(color_temp),
duration=transition * 1000, duration=transition * 1000,
light_type=LightType.Main, light_type=LightType.Main,
) )
mocked_bulb.set_hsv.assert_not_called() mocked_bulb.async_set_hsv.assert_not_called()
mocked_bulb.set_rgb.assert_not_called() mocked_bulb.async_set_rgb.assert_not_called()
mocked_bulb.start_flow.assert_called_once() # flash mocked_bulb.async_start_flow.assert_called_once() # flash
mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main)
mocked_bulb.last_properties["power"] = "off"
# turn_on nightlight # turn_on nightlight
await _async_test_service( await _async_test_service(
SERVICE_TURN_ON, SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_NIGHTLIGHT}, {ATTR_ENTITY_ID: ENTITY_NIGHTLIGHT},
"turn_on", "async_turn_on",
payload={ payload={
"duration": DEFAULT_TRANSITION, "duration": DEFAULT_TRANSITION,
"light_type": LightType.Main, "light_type": LightType.Main,
@ -303,11 +314,12 @@ async def test_services(hass: HomeAssistant, caplog):
domain="light", domain="light",
) )
mocked_bulb.last_properties["power"] = "on"
# turn_off # turn_off
await _async_test_service( await _async_test_service(
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: transition}, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: transition},
"turn_off", "async_turn_off",
domain="light", domain="light",
payload={"duration": transition * 1000, "light_type": LightType.Main}, payload={"duration": transition * 1000, "light_type": LightType.Main},
) )
@ -317,7 +329,7 @@ async def test_services(hass: HomeAssistant, caplog):
await _async_test_service( await _async_test_service(
SERVICE_SET_MODE, SERVICE_SET_MODE,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE: "rgb"}, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE: "rgb"},
"set_power_mode", "async_set_power_mode",
[PowerMode[mode.upper()]], [PowerMode[mode.upper()]],
) )
@ -328,7 +340,7 @@ async def test_services(hass: HomeAssistant, caplog):
ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_ENTITY_ID: ENTITY_LIGHT,
ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}],
}, },
"start_flow", "async_start_flow",
) )
# set_color_scene # set_color_scene
@ -339,7 +351,7 @@ async def test_services(hass: HomeAssistant, caplog):
ATTR_RGB_COLOR: [10, 20, 30], ATTR_RGB_COLOR: [10, 20, 30],
ATTR_BRIGHTNESS: 50, ATTR_BRIGHTNESS: 50,
}, },
"set_scene", "async_set_scene",
[SceneClass.COLOR, 10, 20, 30, 50], [SceneClass.COLOR, 10, 20, 30, 50],
) )
@ -347,7 +359,7 @@ async def test_services(hass: HomeAssistant, caplog):
await _async_test_service( await _async_test_service(
SERVICE_SET_HSV_SCENE, SERVICE_SET_HSV_SCENE,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: [180, 50], ATTR_BRIGHTNESS: 50}, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: [180, 50], ATTR_BRIGHTNESS: 50},
"set_scene", "async_set_scene",
[SceneClass.HSV, 180, 50, 50], [SceneClass.HSV, 180, 50, 50],
) )
@ -355,7 +367,7 @@ async def test_services(hass: HomeAssistant, caplog):
await _async_test_service( await _async_test_service(
SERVICE_SET_COLOR_TEMP_SCENE, SERVICE_SET_COLOR_TEMP_SCENE,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_KELVIN: 4000, ATTR_BRIGHTNESS: 50}, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_KELVIN: 4000, ATTR_BRIGHTNESS: 50},
"set_scene", "async_set_scene",
[SceneClass.CT, 4000, 50], [SceneClass.CT, 4000, 50],
) )
@ -366,14 +378,14 @@ async def test_services(hass: HomeAssistant, caplog):
ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_ENTITY_ID: ENTITY_LIGHT,
ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}],
}, },
"set_scene", "async_set_scene",
) )
# set_auto_delay_off_scene # set_auto_delay_off_scene
await _async_test_service( await _async_test_service(
SERVICE_SET_AUTO_DELAY_OFF_SCENE, SERVICE_SET_AUTO_DELAY_OFF_SCENE,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MINUTES: 1, ATTR_BRIGHTNESS: 50}, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MINUTES: 1, ATTR_BRIGHTNESS: 50},
"set_scene", "async_set_scene",
[SceneClass.AUTO_DELAY_OFF, 50, 1], [SceneClass.AUTO_DELAY_OFF, 50, 1],
) )
@ -401,6 +413,7 @@ async def test_services(hass: HomeAssistant, caplog):
failure_side_effect=None, failure_side_effect=None,
) )
# test _cmd wrapper error handler # 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]) err_count = len([x for x in caplog.records if x.levelno == logging.ERROR])
type(mocked_bulb).turn_on = MagicMock() type(mocked_bulb).turn_on = MagicMock()
type(mocked_bulb).set_brightness = MagicMock(side_effect=BulbException) 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 mocked_bulb.last_properties = properties
async def _async_setup(config_entry): async def _async_setup(config_entry):
with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
await hass.config_entries.async_setup(config_entry.entry_id) 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() await hass.async_block_till_done()
async def _async_test( async def _async_test(
@ -447,6 +463,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
await _async_setup(config_entry) await _async_setup(config_entry)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == "on" assert state.state == "on"
target_properties["friendly_name"] = name target_properties["friendly_name"] = name
target_properties["flowing"] = False 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 hass.config_entries.async_unload(config_entry.entry_id)
await config_entry.async_remove(hass) await config_entry.async_remove(hass)
registry.async_clear_config_entry(config_entry.entry_id) registry.async_clear_config_entry(config_entry.entry_id)
await hass.async_block_till_done()
bright = round(255 * int(PROPERTIES["bright"]) / 100) bright = round(255 * int(PROPERTIES["bright"]) / 100)
current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 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) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb() 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -850,8 +870,8 @@ async def test_effects(hass: HomeAssistant):
) == YEELIGHT_COLOR_EFFECT_LIST + ["mock_effect"] ) == YEELIGHT_COLOR_EFFECT_LIST + ["mock_effect"]
async def _async_test_effect(name, target=None, called=True): async def _async_test_effect(name, target=None, called=True):
mocked_start_flow = MagicMock() async_mocked_start_flow = AsyncMock()
type(mocked_bulb).start_flow = mocked_start_flow mocked_bulb.async_start_flow = async_mocked_start_flow
await hass.services.async_call( await hass.services.async_call(
"light", "light",
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -860,10 +880,10 @@ async def test_effects(hass: HomeAssistant):
) )
if not called: if not called:
return return
mocked_start_flow.assert_called_once() async_mocked_start_flow.assert_called_once()
if target is None: if target is None:
return return
args, _ = mocked_start_flow.call_args args, _ = async_mocked_start_flow.call_args
flow = args[0] flow = args[0]
assert flow.count == target.count assert flow.count == target.count
assert flow.action == target.action assert flow.action == target.action