mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +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/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
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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-*"
|
||||||
}],
|
}],
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user