mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Yeelight local push updates (#51160)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
acf55f2f3a
commit
a23da30c29
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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-*"
|
||||
}],
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user