mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 22:27:07 +00:00
Make yeelight discovery async (#54711)
This commit is contained in:
parent
bca9360d52
commit
e7a0604a40
@ -2,13 +2,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from async_upnp_client.search import SSDPListener
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from yeelight import BulbException, discover_bulbs
|
from yeelight import BulbException
|
||||||
from yeelight.aio import KEY_CONNECTED, AsyncBulb
|
from yeelight.aio import KEY_CONNECTED, AsyncBulb
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
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 (
|
||||||
CONF_DEVICES,
|
CONF_DEVICES,
|
||||||
@ -24,6 +28,7 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
async_dispatcher_send,
|
async_dispatcher_send,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||||
|
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -69,6 +74,12 @@ ACTIVE_COLOR_FLOWING = "1"
|
|||||||
NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light"
|
NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light"
|
||||||
|
|
||||||
DISCOVERY_INTERVAL = timedelta(seconds=60)
|
DISCOVERY_INTERVAL = timedelta(seconds=60)
|
||||||
|
SSDP_TARGET = ("239.255.255.250", 1982)
|
||||||
|
SSDP_ST = "wifi_bulb"
|
||||||
|
DISCOVERY_ATTEMPTS = 3
|
||||||
|
DISCOVERY_SEARCH_INTERVAL = timedelta(seconds=2)
|
||||||
|
DISCOVERY_TIMEOUT = 2
|
||||||
|
|
||||||
|
|
||||||
YEELIGHT_RGB_TRANSITION = "RGBTransition"
|
YEELIGHT_RGB_TRANSITION = "RGBTransition"
|
||||||
YEELIGHT_HSV_TRANSACTION = "HSVTransition"
|
YEELIGHT_HSV_TRANSACTION = "HSVTransition"
|
||||||
@ -193,20 +204,12 @@ async def _async_initialize(
|
|||||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if not device:
|
if not device:
|
||||||
|
# get device and start listening for local pushes
|
||||||
device = await _async_get_device(hass, host, entry)
|
device = await _async_get_device(hass, host, entry)
|
||||||
|
|
||||||
|
await device.async_setup()
|
||||||
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
|
||||||
@ -251,7 +254,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
if entry.data.get(CONF_HOST):
|
if entry.data.get(CONF_HOST):
|
||||||
try:
|
try:
|
||||||
device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
|
device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
|
||||||
except OSError as ex:
|
except BulbException as ex:
|
||||||
# If CONF_ID is not valid we cannot fallback to discovery
|
# If CONF_ID is not valid we cannot fallback to discovery
|
||||||
# so we must retry by raising ConfigEntryNotReady
|
# so we must retry by raising ConfigEntryNotReady
|
||||||
if not entry.data.get(CONF_ID):
|
if not entry.data.get(CONF_ID):
|
||||||
@ -267,16 +270,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady from ex
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# discovery
|
async def _async_from_discovery(capabilities: dict[str, str]) -> None:
|
||||||
scanner = YeelightScanner.async_get(hass)
|
host = urlparse(capabilities["location"]).hostname
|
||||||
|
|
||||||
async def _async_from_discovery(host: str) -> None:
|
|
||||||
try:
|
try:
|
||||||
await _async_initialize(hass, entry, host)
|
await _async_initialize(hass, entry, host)
|
||||||
except BulbException:
|
except BulbException:
|
||||||
_LOGGER.exception("Failed to connect to bulb at %s", host)
|
_LOGGER.exception("Failed to connect to bulb at %s", host)
|
||||||
|
|
||||||
scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery)
|
# discovery
|
||||||
|
scanner = YeelightScanner.async_get(hass)
|
||||||
|
await scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -294,6 +297,7 @@ 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])
|
||||||
|
|
||||||
|
if DATA_DEVICE in entry_data:
|
||||||
device = entry_data[DATA_DEVICE]
|
device = entry_data[DATA_DEVICE]
|
||||||
_LOGGER.debug("Shutting down Yeelight Listener")
|
_LOGGER.debug("Shutting down Yeelight Listener")
|
||||||
await device.bulb.async_stop_listening()
|
await device.bulb.async_stop_listening()
|
||||||
@ -307,9 +311,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
@callback
|
@callback
|
||||||
def _async_unique_name(capabilities: dict) -> str:
|
def _async_unique_name(capabilities: dict) -> str:
|
||||||
"""Generate name from capabilities."""
|
"""Generate name from capabilities."""
|
||||||
model = capabilities["model"]
|
model = str(capabilities["model"]).replace("_", " ").title()
|
||||||
unique_id = capabilities["id"]
|
short_id = hex(int(capabilities["id"], 16))
|
||||||
return f"yeelight_{model}_{unique_id}"
|
return f"Yeelight {model} {short_id}"
|
||||||
|
|
||||||
|
|
||||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
@ -333,88 +337,147 @@ class YeelightScanner:
|
|||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize class."""
|
"""Initialize class."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._seen = {}
|
|
||||||
self._callbacks = {}
|
self._callbacks = {}
|
||||||
self._scan_task = None
|
self._host_discovered_events = {}
|
||||||
|
self._unique_id_capabilities = {}
|
||||||
|
self._host_capabilities = {}
|
||||||
|
self._track_interval = None
|
||||||
|
self._listener = None
|
||||||
|
self._connected_event = None
|
||||||
|
|
||||||
async def _async_scan(self):
|
async def async_setup(self):
|
||||||
_LOGGER.debug("Yeelight scanning")
|
"""Set up the scanner."""
|
||||||
# Run 3 times as packets can get lost
|
if self._connected_event:
|
||||||
for _ in range(3):
|
await self._connected_event.wait()
|
||||||
devices = await self._hass.async_add_executor_job(discover_bulbs)
|
return
|
||||||
for device in devices:
|
self._connected_event = asyncio.Event()
|
||||||
unique_id = device["capabilities"]["id"]
|
|
||||||
if unique_id in self._seen:
|
|
||||||
continue
|
|
||||||
host = device["ip"]
|
|
||||||
self._seen[unique_id] = host
|
|
||||||
_LOGGER.debug("Yeelight discovered at %s", host)
|
|
||||||
if unique_id in self._callbacks:
|
|
||||||
self._hass.async_create_task(self._callbacks[unique_id](host))
|
|
||||||
self._callbacks.pop(unique_id)
|
|
||||||
if len(self._callbacks) == 0:
|
|
||||||
self._async_stop_scan()
|
|
||||||
|
|
||||||
await asyncio.sleep(DISCOVERY_INTERVAL.total_seconds())
|
async def _async_connected():
|
||||||
self._scan_task = self._hass.loop.create_task(self._async_scan())
|
self._listener.async_search()
|
||||||
|
self._connected_event.set()
|
||||||
|
|
||||||
|
self._listener = SSDPListener(
|
||||||
|
async_callback=self._async_process_entry,
|
||||||
|
service_type=SSDP_ST,
|
||||||
|
target=SSDP_TARGET,
|
||||||
|
async_connect_callback=_async_connected,
|
||||||
|
)
|
||||||
|
await self._listener.async_start()
|
||||||
|
await self._connected_event.wait()
|
||||||
|
|
||||||
|
async def async_discover(self):
|
||||||
|
"""Discover bulbs."""
|
||||||
|
await self.async_setup()
|
||||||
|
for _ in range(DISCOVERY_ATTEMPTS):
|
||||||
|
self._listener.async_search()
|
||||||
|
await asyncio.sleep(DISCOVERY_SEARCH_INTERVAL.total_seconds())
|
||||||
|
return self._unique_id_capabilities.values()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_start_scan(self):
|
def async_scan(self, *_):
|
||||||
|
"""Send discovery packets."""
|
||||||
|
_LOGGER.debug("Yeelight scanning")
|
||||||
|
self._listener.async_search()
|
||||||
|
|
||||||
|
async def async_get_capabilities(self, host):
|
||||||
|
"""Get capabilities via SSDP."""
|
||||||
|
if host in self._host_capabilities:
|
||||||
|
return self._host_capabilities[host]
|
||||||
|
|
||||||
|
host_event = asyncio.Event()
|
||||||
|
self._host_discovered_events.setdefault(host, []).append(host_event)
|
||||||
|
await self.async_setup()
|
||||||
|
|
||||||
|
self._listener.async_search((host, SSDP_TARGET[1]))
|
||||||
|
|
||||||
|
with contextlib.suppress(asyncio.TimeoutError):
|
||||||
|
await asyncio.wait_for(host_event.wait(), timeout=DISCOVERY_TIMEOUT)
|
||||||
|
|
||||||
|
self._host_discovered_events[host].remove(host_event)
|
||||||
|
return self._host_capabilities.get(host)
|
||||||
|
|
||||||
|
def _async_discovered_by_ssdp(self, response):
|
||||||
|
@callback
|
||||||
|
def _async_start_flow(*_):
|
||||||
|
asyncio.create_task(
|
||||||
|
self._hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_SSDP},
|
||||||
|
data=response,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delay starting the flow in case the discovery is the result
|
||||||
|
# of another discovery
|
||||||
|
async_call_later(self._hass, 1, _async_start_flow)
|
||||||
|
|
||||||
|
async def _async_process_entry(self, response):
|
||||||
|
"""Process a discovery."""
|
||||||
|
_LOGGER.debug("Discovered via SSDP: %s", response)
|
||||||
|
unique_id = response["id"]
|
||||||
|
host = urlparse(response["location"]).hostname
|
||||||
|
if unique_id not in self._unique_id_capabilities:
|
||||||
|
_LOGGER.debug("Yeelight discovered with %s", response)
|
||||||
|
self._async_discovered_by_ssdp(response)
|
||||||
|
self._host_capabilities[host] = response
|
||||||
|
self._unique_id_capabilities[unique_id] = response
|
||||||
|
for event in self._host_discovered_events.get(host, []):
|
||||||
|
event.set()
|
||||||
|
if unique_id in self._callbacks:
|
||||||
|
self._hass.async_create_task(self._callbacks[unique_id](response))
|
||||||
|
self._callbacks.pop(unique_id)
|
||||||
|
if not self._callbacks:
|
||||||
|
self._async_stop_scan()
|
||||||
|
|
||||||
|
async def _async_start_scan(self):
|
||||||
"""Start scanning for Yeelight devices."""
|
"""Start scanning for Yeelight devices."""
|
||||||
_LOGGER.debug("Start scanning")
|
_LOGGER.debug("Start scanning")
|
||||||
# Use loop directly to avoid home assistant track this task
|
await self.async_setup()
|
||||||
self._scan_task = self._hass.loop.create_task(self._async_scan())
|
if not self._track_interval:
|
||||||
|
self._track_interval = async_track_time_interval(
|
||||||
|
self._hass, self.async_scan, DISCOVERY_INTERVAL
|
||||||
|
)
|
||||||
|
self.async_scan()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_stop_scan(self):
|
def _async_stop_scan(self):
|
||||||
"""Stop scanning."""
|
"""Stop scanning."""
|
||||||
_LOGGER.debug("Stop scanning")
|
if self._track_interval is None:
|
||||||
if self._scan_task is not None:
|
return
|
||||||
self._scan_task.cancel()
|
_LOGGER.debug("Stop scanning interval")
|
||||||
self._scan_task = None
|
self._track_interval()
|
||||||
|
self._track_interval = None
|
||||||
|
|
||||||
@callback
|
async def async_register_callback(self, unique_id, callback_func):
|
||||||
def async_register_callback(self, unique_id, callback_func):
|
|
||||||
"""Register callback function."""
|
"""Register callback function."""
|
||||||
host = self._seen.get(unique_id)
|
if capabilities := self._unique_id_capabilities.get(unique_id):
|
||||||
if host is not None:
|
self._hass.async_create_task(callback_func(capabilities))
|
||||||
self._hass.async_create_task(callback_func(host))
|
return
|
||||||
else:
|
|
||||||
self._callbacks[unique_id] = callback_func
|
self._callbacks[unique_id] = callback_func
|
||||||
if len(self._callbacks) == 1:
|
await self._async_start_scan()
|
||||||
self._async_start_scan()
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_unregister_callback(self, unique_id):
|
def async_unregister_callback(self, unique_id):
|
||||||
"""Unregister callback function."""
|
"""Unregister callback function."""
|
||||||
if unique_id not in self._callbacks:
|
self._callbacks.pop(unique_id, None)
|
||||||
return
|
if not self._callbacks:
|
||||||
self._callbacks.pop(unique_id)
|
|
||||||
if len(self._callbacks) == 0:
|
|
||||||
self._async_stop_scan()
|
self._async_stop_scan()
|
||||||
|
|
||||||
|
|
||||||
class YeelightDevice:
|
class YeelightDevice:
|
||||||
"""Represents single Yeelight device."""
|
"""Represents single Yeelight device."""
|
||||||
|
|
||||||
def __init__(self, hass, host, config, bulb, capabilities):
|
def __init__(self, hass, host, config, bulb):
|
||||||
"""Initialize device."""
|
"""Initialize device."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._config = config
|
self._config = config
|
||||||
self._host = host
|
self._host = host
|
||||||
self._bulb_device = bulb
|
self._bulb_device = bulb
|
||||||
self._capabilities = capabilities or {}
|
self._capabilities = {}
|
||||||
self._device_type = None
|
self._device_type = None
|
||||||
self._available = False
|
self._available = False
|
||||||
self._initialized = False
|
self._initialized = False
|
||||||
|
self._name = None
|
||||||
self._name = host # Default name is host
|
|
||||||
if capabilities:
|
|
||||||
# Generate name from model and id when capabilities is available
|
|
||||||
self._name = _async_unique_name(capabilities)
|
|
||||||
if config.get(CONF_NAME):
|
|
||||||
# Override default name when name is set in config
|
|
||||||
self._name = config[CONF_NAME]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bulb(self):
|
def bulb(self):
|
||||||
@ -444,7 +507,7 @@ class YeelightDevice:
|
|||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self):
|
||||||
"""Return configured/autodetected device model."""
|
"""Return configured/autodetected device model."""
|
||||||
return self._bulb_device.model
|
return self._bulb_device.model or self._capabilities.get("model")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fw_version(self):
|
def fw_version(self):
|
||||||
@ -530,7 +593,8 @@ class YeelightDevice:
|
|||||||
await self.bulb.async_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:
|
||||||
await self._async_initialize_device()
|
self._initialized = True
|
||||||
|
async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host))
|
||||||
except BulbException as ex:
|
except BulbException as ex:
|
||||||
if self._available: # just inform once
|
if self._available: # just inform once
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
@ -540,28 +604,18 @@ class YeelightDevice:
|
|||||||
|
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
async def _async_get_capabilities(self):
|
async def async_setup(self):
|
||||||
"""Request device capabilities."""
|
"""Fetch capabilities and setup name if available."""
|
||||||
try:
|
scanner = YeelightScanner.async_get(self._hass)
|
||||||
await self._hass.async_add_executor_job(self.bulb.get_capabilities)
|
self._capabilities = await scanner.async_get_capabilities(self._host) or {}
|
||||||
_LOGGER.debug(
|
if name := self._config.get(CONF_NAME):
|
||||||
"Device %s, %s capabilities: %s",
|
# Override default name when name is set in config
|
||||||
self._host,
|
self._name = name
|
||||||
self.name,
|
elif self._capabilities:
|
||||||
self.bulb.capabilities,
|
# Generate name from model and id when capabilities is available
|
||||||
)
|
self._name = _async_unique_name(self._capabilities)
|
||||||
except BulbException as ex:
|
else:
|
||||||
_LOGGER.error(
|
self._name = self._host # Default name is host
|
||||||
"Unable to get device capabilities %s, %s: %s",
|
|
||||||
self._host,
|
|
||||||
self.name,
|
|
||||||
ex,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_initialize_device(self):
|
|
||||||
await self._async_get_capabilities()
|
|
||||||
self._initialized = True
|
|
||||||
async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host))
|
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Update device properties and send data updated signal."""
|
"""Update device properties and send data updated signal."""
|
||||||
@ -628,6 +682,19 @@ async def _async_get_device(
|
|||||||
|
|
||||||
# Set up device
|
# Set up device
|
||||||
bulb = AsyncBulb(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)
|
device = YeelightDevice(hass, host, entry.options, bulb)
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
entry.async_on_unload(
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_listen_task)
|
||||||
|
)
|
||||||
|
|
||||||
|
return device
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
"""Config flow for Yeelight integration."""
|
"""Config flow for Yeelight integration."""
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yeelight
|
import yeelight
|
||||||
|
from yeelight.aio import AsyncBulb
|
||||||
|
|
||||||
from homeassistant import config_entries, exceptions
|
from homeassistant import config_entries, exceptions
|
||||||
from homeassistant.components.dhcp import IP_ADDRESS
|
from homeassistant.components.dhcp import IP_ADDRESS
|
||||||
@ -19,6 +21,7 @@ from . import (
|
|||||||
CONF_TRANSITION,
|
CONF_TRANSITION,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
||||||
|
YeelightScanner,
|
||||||
_async_unique_name,
|
_async_unique_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,6 +57,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._discovered_ip = discovery_info[IP_ADDRESS]
|
self._discovered_ip = discovery_info[IP_ADDRESS]
|
||||||
return await self._async_handle_discovery()
|
return await self._async_handle_discovery()
|
||||||
|
|
||||||
|
async def async_step_ssdp(self, discovery_info):
|
||||||
|
"""Handle discovery from ssdp."""
|
||||||
|
self._discovered_ip = urlparse(discovery_info["location"]).hostname
|
||||||
|
await self.async_set_unique_id(discovery_info["id"])
|
||||||
|
self._abort_if_unique_id_configured(
|
||||||
|
updates={CONF_HOST: self._discovered_ip}, reload_on_update=False
|
||||||
|
)
|
||||||
|
return await self._async_handle_discovery()
|
||||||
|
|
||||||
async def _async_handle_discovery(self):
|
async def _async_handle_discovery(self):
|
||||||
"""Handle any discovery."""
|
"""Handle any discovery."""
|
||||||
self.context[CONF_HOST] = self._discovered_ip
|
self.context[CONF_HOST] = self._discovered_ip
|
||||||
@ -62,7 +74,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_abort(reason="already_in_progress")
|
return self.async_abort(reason="already_in_progress")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._discovered_model = await self._async_try_connect(self._discovered_ip)
|
self._discovered_model = await self._async_try_connect(
|
||||||
|
self._discovered_ip, raise_on_progress=True
|
||||||
|
)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
|
||||||
@ -96,7 +110,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if not user_input.get(CONF_HOST):
|
if not user_input.get(CONF_HOST):
|
||||||
return await self.async_step_pick_device()
|
return await self.async_step_pick_device()
|
||||||
try:
|
try:
|
||||||
model = await self._async_try_connect(user_input[CONF_HOST])
|
model = await self._async_try_connect(
|
||||||
|
user_input[CONF_HOST], raise_on_progress=False
|
||||||
|
)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
else:
|
else:
|
||||||
@ -119,10 +135,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
unique_id = user_input[CONF_DEVICE]
|
unique_id = user_input[CONF_DEVICE]
|
||||||
capabilities = self._discovered_devices[unique_id]
|
capabilities = self._discovered_devices[unique_id]
|
||||||
await self.async_set_unique_id(unique_id)
|
await self.async_set_unique_id(unique_id, raise_on_progress=False)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
host = urlparse(capabilities["location"]).hostname
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=_async_unique_name(capabilities), data={CONF_ID: unique_id}
|
title=_async_unique_name(capabilities),
|
||||||
|
data={CONF_ID: unique_id, CONF_HOST: host},
|
||||||
)
|
)
|
||||||
|
|
||||||
configured_devices = {
|
configured_devices = {
|
||||||
@ -131,16 +149,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if entry.data[CONF_ID]
|
if entry.data[CONF_ID]
|
||||||
}
|
}
|
||||||
devices_name = {}
|
devices_name = {}
|
||||||
|
scanner = YeelightScanner.async_get(self.hass)
|
||||||
|
devices = await scanner.async_discover()
|
||||||
# Run 3 times as packets can get lost
|
# Run 3 times as packets can get lost
|
||||||
for _ in range(3):
|
for capabilities in devices:
|
||||||
devices = await self.hass.async_add_executor_job(yeelight.discover_bulbs)
|
|
||||||
for device in devices:
|
|
||||||
capabilities = device["capabilities"]
|
|
||||||
unique_id = capabilities["id"]
|
unique_id = capabilities["id"]
|
||||||
if unique_id in configured_devices:
|
if unique_id in configured_devices:
|
||||||
continue # ignore configured devices
|
continue # ignore configured devices
|
||||||
model = capabilities["model"]
|
model = capabilities["model"]
|
||||||
host = device["ip"]
|
host = urlparse(capabilities["location"]).hostname
|
||||||
name = f"{host} {model} {unique_id}"
|
name = f"{host} {model} {unique_id}"
|
||||||
self._discovered_devices[unique_id] = capabilities
|
self._discovered_devices[unique_id] = capabilities
|
||||||
devices_name[unique_id] = name
|
devices_name[unique_id] = name
|
||||||
@ -157,7 +174,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle import step."""
|
"""Handle import step."""
|
||||||
host = user_input[CONF_HOST]
|
host = user_input[CONF_HOST]
|
||||||
try:
|
try:
|
||||||
await self._async_try_connect(host)
|
await self._async_try_connect(host, raise_on_progress=False)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
_LOGGER.error("Failed to import %s: cannot connect", host)
|
_LOGGER.error("Failed to import %s: cannot connect", host)
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
@ -169,27 +186,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
|
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
|
||||||
|
|
||||||
async def _async_try_connect(self, host):
|
async def _async_try_connect(self, host, raise_on_progress=True):
|
||||||
"""Set up with options."""
|
"""Set up with options."""
|
||||||
self._async_abort_entries_match({CONF_HOST: host})
|
self._async_abort_entries_match({CONF_HOST: host})
|
||||||
|
|
||||||
bulb = yeelight.Bulb(host)
|
scanner = YeelightScanner.async_get(self.hass)
|
||||||
try:
|
capabilities = await scanner.async_get_capabilities(host)
|
||||||
capabilities = await self.hass.async_add_executor_job(bulb.get_capabilities)
|
|
||||||
if capabilities is None: # timeout
|
if capabilities is None: # timeout
|
||||||
_LOGGER.debug("Failed to get capabilities from %s: timeout", host)
|
_LOGGER.debug("Failed to get capabilities from %s: timeout", host)
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("Get capabilities: %s", capabilities)
|
_LOGGER.debug("Get capabilities: %s", capabilities)
|
||||||
await self.async_set_unique_id(capabilities["id"])
|
await self.async_set_unique_id(
|
||||||
|
capabilities["id"], raise_on_progress=raise_on_progress
|
||||||
|
)
|
||||||
return capabilities["model"]
|
return capabilities["model"]
|
||||||
except OSError as err:
|
|
||||||
_LOGGER.debug("Failed to get capabilities from %s: %s", host, err)
|
|
||||||
# Ignore the error since get_capabilities uses UDP discovery packet
|
|
||||||
# which does not work in all network environments
|
|
||||||
|
|
||||||
# Fallback to get properties
|
# Fallback to get properties
|
||||||
|
bulb = AsyncBulb(host)
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(bulb.get_properties)
|
await bulb.async_listen(lambda _: True)
|
||||||
|
await bulb.async_get_properties()
|
||||||
|
await bulb.async_stop_listening()
|
||||||
except yeelight.BulbException as err:
|
except yeelight.BulbException as err:
|
||||||
_LOGGER.error("Failed to get properties from %s: %s", host, err)
|
_LOGGER.error("Failed to get properties from %s: %s", host, err)
|
||||||
raise CannotConnect from err
|
raise CannotConnect from err
|
||||||
|
@ -905,7 +905,7 @@ class YeelightNightLightMode(YeelightGenericLight):
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the device if any."""
|
"""Return the name of the device if any."""
|
||||||
return f"{self.device.name} nightlight"
|
return f"{self.device.name} Nightlight"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
@ -997,7 +997,7 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch):
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the device if any."""
|
"""Return the name of the device if any."""
|
||||||
return f"{self.device.name} ambilight"
|
return f"{self.device.name} Ambilight"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _brightness_property(self):
|
def _brightness_property(self):
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"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.7.2"],
|
"requirements": ["yeelight==0.7.2", "async-upnp-client==0.20.0"],
|
||||||
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
|
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
@ -314,6 +314,7 @@ asterisk_mbox==0.5.0
|
|||||||
# homeassistant.components.dlna_dmr
|
# homeassistant.components.dlna_dmr
|
||||||
# homeassistant.components.ssdp
|
# homeassistant.components.ssdp
|
||||||
# homeassistant.components.upnp
|
# homeassistant.components.upnp
|
||||||
|
# homeassistant.components.yeelight
|
||||||
async-upnp-client==0.20.0
|
async-upnp-client==0.20.0
|
||||||
|
|
||||||
# homeassistant.components.supla
|
# homeassistant.components.supla
|
||||||
|
@ -205,6 +205,7 @@ arcam-fmj==0.7.0
|
|||||||
# homeassistant.components.dlna_dmr
|
# homeassistant.components.dlna_dmr
|
||||||
# homeassistant.components.ssdp
|
# homeassistant.components.ssdp
|
||||||
# homeassistant.components.upnp
|
# homeassistant.components.upnp
|
||||||
|
# homeassistant.components.yeelight
|
||||||
async-upnp-client==0.20.0
|
async-upnp-client==0.20.0
|
||||||
|
|
||||||
# homeassistant.components.aurora
|
# homeassistant.components.aurora
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
"""Tests for the Yeelight integration."""
|
"""Tests for the Yeelight integration."""
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from async_upnp_client.search import SSDPListener
|
||||||
from yeelight import BulbException, BulbType
|
from yeelight import BulbException, BulbType
|
||||||
from yeelight.main import _MODEL_SPECS
|
from yeelight.main import _MODEL_SPECS
|
||||||
|
|
||||||
|
from homeassistant.components import yeelight as hass_yeelight
|
||||||
from homeassistant.components.yeelight import (
|
from homeassistant.components.yeelight import (
|
||||||
CONF_MODE_MUSIC,
|
CONF_MODE_MUSIC,
|
||||||
CONF_NIGHTLIGHT_SWITCH_TYPE,
|
CONF_NIGHTLIGHT_SWITCH_TYPE,
|
||||||
@ -13,6 +17,7 @@ from homeassistant.components.yeelight import (
|
|||||||
YeelightScanner,
|
YeelightScanner,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME
|
from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
IP_ADDRESS = "192.168.1.239"
|
IP_ADDRESS = "192.168.1.239"
|
||||||
MODEL = "color"
|
MODEL = "color"
|
||||||
@ -23,13 +28,16 @@ CAPABILITIES = {
|
|||||||
"id": ID,
|
"id": ID,
|
||||||
"model": MODEL,
|
"model": MODEL,
|
||||||
"fw_ver": FW_VER,
|
"fw_ver": FW_VER,
|
||||||
|
"location": f"yeelight://{IP_ADDRESS}",
|
||||||
"support": "get_prop set_default set_power toggle set_bright start_cf stop_cf"
|
"support": "get_prop set_default set_power toggle set_bright start_cf stop_cf"
|
||||||
" set_scene cron_add cron_get cron_del set_ct_abx set_rgb",
|
" set_scene cron_add cron_get cron_del set_ct_abx set_rgb",
|
||||||
"name": "",
|
"name": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
NAME = "name"
|
NAME = "name"
|
||||||
UNIQUE_NAME = f"yeelight_{MODEL}_{ID}"
|
SHORT_ID = hex(int("0x000000000015243f", 16))
|
||||||
|
UNIQUE_NAME = f"yeelight_{MODEL}_{SHORT_ID}"
|
||||||
|
UNIQUE_FRIENDLY_NAME = f"Yeelight {MODEL.title()} {SHORT_ID}"
|
||||||
|
|
||||||
MODULE = "homeassistant.components.yeelight"
|
MODULE = "homeassistant.components.yeelight"
|
||||||
MODULE_CONFIG_FLOW = f"{MODULE}.config_flow"
|
MODULE_CONFIG_FLOW = f"{MODULE}.config_flow"
|
||||||
@ -81,8 +89,8 @@ CONFIG_ENTRY_DATA = {CONF_ID: ID}
|
|||||||
|
|
||||||
def _mocked_bulb(cannot_connect=False):
|
def _mocked_bulb(cannot_connect=False):
|
||||||
bulb = MagicMock()
|
bulb = MagicMock()
|
||||||
type(bulb).get_capabilities = MagicMock(
|
type(bulb).async_listen = AsyncMock(
|
||||||
return_value=None if cannot_connect else CAPABILITIES
|
side_effect=BulbException if cannot_connect else None
|
||||||
)
|
)
|
||||||
type(bulb).async_get_properties = AsyncMock(
|
type(bulb).async_get_properties = AsyncMock(
|
||||||
side_effect=BulbException if cannot_connect else None
|
side_effect=BulbException if cannot_connect else None
|
||||||
@ -98,7 +106,6 @@ def _mocked_bulb(cannot_connect=False):
|
|||||||
bulb.last_properties = PROPERTIES.copy()
|
bulb.last_properties = PROPERTIES.copy()
|
||||||
bulb.music_mode = False
|
bulb.music_mode = False
|
||||||
bulb.async_get_properties = AsyncMock()
|
bulb.async_get_properties = AsyncMock()
|
||||||
bulb.async_listen = AsyncMock()
|
|
||||||
bulb.async_stop_listening = AsyncMock()
|
bulb.async_stop_listening = AsyncMock()
|
||||||
bulb.async_update = AsyncMock()
|
bulb.async_update = AsyncMock()
|
||||||
bulb.async_turn_on = AsyncMock()
|
bulb.async_turn_on = AsyncMock()
|
||||||
@ -116,12 +123,43 @@ def _mocked_bulb(cannot_connect=False):
|
|||||||
return bulb
|
return bulb
|
||||||
|
|
||||||
|
|
||||||
def _patch_discovery(prefix, no_device=False):
|
def _patched_ssdp_listener(info, *args, **kwargs):
|
||||||
|
listener = SSDPListener(*args, **kwargs)
|
||||||
|
|
||||||
|
async def _async_callback(*_):
|
||||||
|
await listener.async_connect_callback()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_search(*_):
|
||||||
|
if info:
|
||||||
|
asyncio.create_task(listener.async_callback(info))
|
||||||
|
|
||||||
|
listener.async_start = _async_callback
|
||||||
|
listener.async_search = _async_search
|
||||||
|
return listener
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_discovery(no_device=False):
|
||||||
YeelightScanner._scanner = None # Clear class scanner to reset hass
|
YeelightScanner._scanner = None # Clear class scanner to reset hass
|
||||||
|
|
||||||
def _mocked_discovery(timeout=2, interface=False):
|
def _generate_fake_ssdp_listener(*args, **kwargs):
|
||||||
if no_device:
|
return _patched_ssdp_listener(
|
||||||
return []
|
None if no_device else CAPABILITIES,
|
||||||
return [{"ip": IP_ADDRESS, "port": 55443, "capabilities": CAPABILITIES}]
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
return patch(f"{prefix}.discover_bulbs", side_effect=_mocked_discovery)
|
return patch(
|
||||||
|
"homeassistant.components.yeelight.SSDPListener",
|
||||||
|
new=_generate_fake_ssdp_listener,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_discovery_interval():
|
||||||
|
return patch.object(
|
||||||
|
hass_yeelight, "DISCOVERY_SEARCH_INTERVAL", timedelta(seconds=0)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_discovery_timeout():
|
||||||
|
return patch.object(hass_yeelight, "DISCOVERY_TIMEOUT", 0.0001)
|
||||||
|
@ -6,7 +6,14 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import entity_component
|
from homeassistant.helpers import entity_component
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import MODULE, NAME, PROPERTIES, YAML_CONFIGURATION, _mocked_bulb
|
from . import (
|
||||||
|
MODULE,
|
||||||
|
NAME,
|
||||||
|
PROPERTIES,
|
||||||
|
YAML_CONFIGURATION,
|
||||||
|
_mocked_bulb,
|
||||||
|
_patch_discovery,
|
||||||
|
)
|
||||||
|
|
||||||
ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight"
|
ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight"
|
||||||
|
|
||||||
@ -14,9 +21,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}.AsyncBulb", return_value=mocked_bulb), patch(
|
with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", 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)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""Test the Yeelight config flow."""
|
"""Test the Yeelight config flow."""
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -25,14 +25,17 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM
|
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
CAPABILITIES,
|
||||||
ID,
|
ID,
|
||||||
IP_ADDRESS,
|
IP_ADDRESS,
|
||||||
MODULE,
|
MODULE,
|
||||||
MODULE_CONFIG_FLOW,
|
MODULE_CONFIG_FLOW,
|
||||||
NAME,
|
NAME,
|
||||||
UNIQUE_NAME,
|
UNIQUE_FRIENDLY_NAME,
|
||||||
_mocked_bulb,
|
_mocked_bulb,
|
||||||
_patch_discovery,
|
_patch_discovery,
|
||||||
|
_patch_discovery_interval,
|
||||||
|
_patch_discovery_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
@ -55,21 +58,23 @@ async def test_discovery(hass: HomeAssistant):
|
|||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "user"
|
||||||
assert not result["errors"]
|
assert not result["errors"]
|
||||||
|
|
||||||
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"):
|
with _patch_discovery(), _patch_discovery_interval():
|
||||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
assert result2["type"] == "form"
|
assert result2["type"] == "form"
|
||||||
assert result2["step_id"] == "pick_device"
|
assert result2["step_id"] == "pick_device"
|
||||||
assert not result2["errors"]
|
assert not result2["errors"]
|
||||||
|
|
||||||
with patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch(
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE}.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
f"{MODULE}.async_setup_entry", return_value=True
|
f"{MODULE}.async_setup_entry", return_value=True
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
result3 = await hass.config_entries.flow.async_configure(
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], {CONF_DEVICE: ID}
|
result["flow_id"], {CONF_DEVICE: ID}
|
||||||
)
|
)
|
||||||
assert result3["type"] == "create_entry"
|
assert result3["type"] == "create_entry"
|
||||||
assert result3["title"] == UNIQUE_NAME
|
assert result3["title"] == UNIQUE_FRIENDLY_NAME
|
||||||
assert result3["data"] == {CONF_ID: ID}
|
assert result3["data"] == {CONF_ID: ID, CONF_HOST: IP_ADDRESS}
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
mock_setup.assert_called_once()
|
mock_setup.assert_called_once()
|
||||||
mock_setup_entry.assert_called_once()
|
mock_setup_entry.assert_called_once()
|
||||||
@ -82,7 +87,7 @@ async def test_discovery(hass: HomeAssistant):
|
|||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "user"
|
||||||
assert not result["errors"]
|
assert not result["errors"]
|
||||||
|
|
||||||
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"):
|
with _patch_discovery(), _patch_discovery_interval():
|
||||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
assert result2["type"] == "abort"
|
assert result2["type"] == "abort"
|
||||||
assert result2["reason"] == "no_devices_found"
|
assert result2["reason"] == "no_devices_found"
|
||||||
@ -94,7 +99,9 @@ async def test_discovery_no_device(hass: HomeAssistant):
|
|||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight", no_device=True):
|
with _patch_discovery(
|
||||||
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval():
|
||||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
assert result2["type"] == "abort"
|
assert result2["type"] == "abort"
|
||||||
@ -114,26 +121,27 @@ async def test_import(hass: HomeAssistant):
|
|||||||
|
|
||||||
# Cannot connect
|
# Cannot connect
|
||||||
mocked_bulb = _mocked_bulb(cannot_connect=True)
|
mocked_bulb = _mocked_bulb(cannot_connect=True)
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
with _patch_discovery(
|
||||||
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
|
||||||
)
|
)
|
||||||
type(mocked_bulb).get_capabilities.assert_called_once()
|
|
||||||
type(mocked_bulb).get_properties.assert_called_once()
|
|
||||||
assert result["type"] == "abort"
|
assert result["type"] == "abort"
|
||||||
assert result["reason"] == "cannot_connect"
|
assert result["reason"] == "cannot_connect"
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
|
with _patch_discovery(), patch(
|
||||||
f"{MODULE}.async_setup", return_value=True
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
) as mock_setup, patch(
|
), patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch(
|
||||||
f"{MODULE}.async_setup_entry", return_value=True
|
f"{MODULE}.async_setup_entry", return_value=True
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
|
||||||
)
|
)
|
||||||
type(mocked_bulb).get_capabilities.assert_called_once()
|
|
||||||
assert result["type"] == "create_entry"
|
assert result["type"] == "create_entry"
|
||||||
assert result["title"] == DEFAULT_NAME
|
assert result["title"] == DEFAULT_NAME
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
@ -150,7 +158,9 @@ async def test_import(hass: HomeAssistant):
|
|||||||
|
|
||||||
# Duplicate
|
# Duplicate
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
with _patch_discovery(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
|
||||||
)
|
)
|
||||||
@ -169,7 +179,11 @@ async def test_manual(hass: HomeAssistant):
|
|||||||
|
|
||||||
# Cannot connect (timeout)
|
# Cannot connect (timeout)
|
||||||
mocked_bulb = _mocked_bulb(cannot_connect=True)
|
mocked_bulb = _mocked_bulb(cannot_connect=True)
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
with _patch_discovery(
|
||||||
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
||||||
)
|
)
|
||||||
@ -178,8 +192,11 @@ async def test_manual(hass: HomeAssistant):
|
|||||||
assert result2["errors"] == {"base": "cannot_connect"}
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
# Cannot connect (error)
|
# Cannot connect (error)
|
||||||
type(mocked_bulb).get_capabilities = MagicMock(side_effect=OSError)
|
with _patch_discovery(
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result3 = await hass.config_entries.flow.async_configure(
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
||||||
)
|
)
|
||||||
@ -187,9 +204,11 @@ async def test_manual(hass: HomeAssistant):
|
|||||||
|
|
||||||
# Success
|
# Success
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
|
with _patch_discovery(), _patch_discovery_timeout(), patch(
|
||||||
f"{MODULE}.async_setup", return_value=True
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
), patch(f"{MODULE}.async_setup_entry", return_value=True):
|
), patch(f"{MODULE}.async_setup", return_value=True), patch(
|
||||||
|
f"{MODULE}.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
result4 = await hass.config_entries.flow.async_configure(
|
result4 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
||||||
)
|
)
|
||||||
@ -203,7 +222,11 @@ async def test_manual(hass: HomeAssistant):
|
|||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
with _patch_discovery(
|
||||||
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
||||||
)
|
)
|
||||||
@ -219,7 +242,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}.AsyncBulb", return_value=mocked_bulb):
|
with _patch_discovery(), 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 +264,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}.AsyncBulb", return_value=mocked_bulb):
|
with _patch_discovery(), 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
|
||||||
)
|
)
|
||||||
@ -262,15 +285,18 @@ async def test_manual_no_capabilities(hass: HomeAssistant):
|
|||||||
assert not result["errors"]
|
assert not result["errors"]
|
||||||
|
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
type(mocked_bulb).get_capabilities = MagicMock(return_value=None)
|
with _patch_discovery(
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
), patch(
|
||||||
f"{MODULE}.async_setup", return_value=True
|
f"{MODULE}.async_setup", return_value=True
|
||||||
), patch(f"{MODULE}.async_setup_entry", return_value=True):
|
), patch(
|
||||||
|
f"{MODULE}.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
||||||
)
|
)
|
||||||
type(mocked_bulb).get_capabilities.assert_called_once()
|
|
||||||
type(mocked_bulb).get_properties.assert_called_once()
|
|
||||||
assert result["type"] == "create_entry"
|
assert result["type"] == "create_entry"
|
||||||
assert result["data"] == {CONF_HOST: IP_ADDRESS}
|
assert result["data"] == {CONF_HOST: IP_ADDRESS}
|
||||||
|
|
||||||
@ -280,39 +306,53 @@ async def test_discovered_by_homekit_and_dhcp(hass):
|
|||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_HOMEKIT},
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
||||||
data={"host": "1.2.3.4", "properties": {"id": "aa:bb:cc:dd:ee:ff"}},
|
data={"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
assert result["type"] == RESULT_TYPE_FORM
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
assert result["errors"] is None
|
assert result["errors"] is None
|
||||||
|
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_init(
|
result2 = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_DHCP},
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
data={"ip": "1.2.3.4", "macaddress": "aa:bb:cc:dd:ee:ff"},
|
data={"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
assert result2["type"] == RESULT_TYPE_ABORT
|
assert result2["type"] == RESULT_TYPE_ABORT
|
||||||
assert result2["reason"] == "already_in_progress"
|
assert result2["reason"] == "already_in_progress"
|
||||||
|
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result3 = await hass.config_entries.flow.async_init(
|
result3 = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_DHCP},
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
data={"ip": "1.2.3.4", "macaddress": "00:00:00:00:00:00"},
|
data={"ip": IP_ADDRESS, "macaddress": "00:00:00:00:00:00"},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
assert result3["type"] == RESULT_TYPE_ABORT
|
assert result3["type"] == RESULT_TYPE_ABORT
|
||||||
assert result3["reason"] == "already_in_progress"
|
assert result3["reason"] == "already_in_progress"
|
||||||
|
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", side_effect=CannotConnect):
|
with _patch_discovery(
|
||||||
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect
|
||||||
|
):
|
||||||
result3 = await hass.config_entries.flow.async_init(
|
result3 = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_DHCP},
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
data={"ip": "1.2.3.5", "macaddress": "00:00:00:00:00:01"},
|
data={"ip": "1.2.3.5", "macaddress": "00:00:00:00:00:01"},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
assert result3["type"] == RESULT_TYPE_ABORT
|
assert result3["type"] == RESULT_TYPE_ABORT
|
||||||
assert result3["reason"] == "cannot_connect"
|
assert result3["reason"] == "cannot_connect"
|
||||||
|
|
||||||
@ -335,17 +375,25 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data):
|
|||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": source}, data=data
|
DOMAIN, context={"source": source}, data=data
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result["type"] == RESULT_TYPE_FORM
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
assert result["errors"] is None
|
assert result["errors"] is None
|
||||||
|
|
||||||
with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch(
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE}.async_setup", return_value=True
|
||||||
|
) as mock_async_setup, patch(
|
||||||
f"{MODULE}.async_setup_entry", return_value=True
|
f"{MODULE}.async_setup_entry", return_value=True
|
||||||
) as mock_async_setup_entry:
|
) as mock_async_setup_entry:
|
||||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == "create_entry"
|
assert result2["type"] == "create_entry"
|
||||||
assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"}
|
assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"}
|
||||||
assert mock_async_setup.called
|
assert mock_async_setup.called
|
||||||
@ -370,10 +418,55 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data
|
|||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
type(mocked_bulb).get_capabilities = MagicMock(return_value=None)
|
with _patch_discovery(
|
||||||
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": source}, data=data
|
DOMAIN, context={"source": source}, data=data
|
||||||
)
|
)
|
||||||
assert result["type"] == RESULT_TYPE_ABORT
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
assert result["reason"] == "cannot_connect"
|
assert result["reason"] == "cannot_connect"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovered_ssdp(hass):
|
||||||
|
"""Test we can setup when discovered from ssdp."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
mocked_bulb = _mocked_bulb()
|
||||||
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=CAPABILITIES
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE}.async_setup", return_value=True
|
||||||
|
) as mock_async_setup, patch(
|
||||||
|
f"{MODULE}.async_setup_entry", return_value=True
|
||||||
|
) as mock_async_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"}
|
||||||
|
assert mock_async_setup.called
|
||||||
|
assert mock_async_setup_entry.called
|
||||||
|
|
||||||
|
mocked_bulb = _mocked_bulb()
|
||||||
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=CAPABILITIES
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Test Yeelight."""
|
"""Test Yeelight."""
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from datetime import timedelta
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from yeelight import BulbException, BulbType
|
from yeelight import BulbException, BulbType
|
||||||
|
|
||||||
@ -22,9 +23,9 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
CAPABILITIES,
|
|
||||||
CONFIG_ENTRY_DATA,
|
CONFIG_ENTRY_DATA,
|
||||||
ENTITY_AMBILIGHT,
|
ENTITY_AMBILIGHT,
|
||||||
ENTITY_BINARY_SENSOR,
|
ENTITY_BINARY_SENSOR,
|
||||||
@ -34,12 +35,14 @@ from . import (
|
|||||||
ID,
|
ID,
|
||||||
IP_ADDRESS,
|
IP_ADDRESS,
|
||||||
MODULE,
|
MODULE,
|
||||||
MODULE_CONFIG_FLOW,
|
SHORT_ID,
|
||||||
_mocked_bulb,
|
_mocked_bulb,
|
||||||
_patch_discovery,
|
_patch_discovery,
|
||||||
|
_patch_discovery_interval,
|
||||||
|
_patch_discovery_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
|
async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
|
||||||
@ -51,19 +54,15 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
|
|||||||
|
|
||||||
mocked_bulb = _mocked_bulb(True)
|
mocked_bulb = _mocked_bulb(True)
|
||||||
mocked_bulb.bulb_type = BulbType.WhiteTempMood
|
mocked_bulb.bulb_type = BulbType.WhiteTempMood
|
||||||
mocked_bulb.get_capabilities = MagicMock(
|
mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None])
|
||||||
side_effect=[OSError, CAPABILITIES, CAPABILITIES]
|
|
||||||
)
|
|
||||||
|
|
||||||
_discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}]
|
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery():
|
||||||
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)
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
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_{SHORT_ID}"
|
||||||
)
|
)
|
||||||
|
|
||||||
type(mocked_bulb).async_get_properties = AsyncMock(None)
|
type(mocked_bulb).async_get_properties = AsyncMock(None)
|
||||||
@ -77,6 +76,19 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
|
|||||||
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
|
||||||
|
|
||||||
|
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery():
|
||||||
|
# The discovery should update the ip address
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert config_entry.data[CONF_HOST] == IP_ADDRESS
|
||||||
|
|
||||||
|
# Make sure we can still reload with the new ip right after we change it
|
||||||
|
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery():
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity_registry.async_get(binary_sensor_entity_id) is not None
|
||||||
|
|
||||||
|
|
||||||
async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant):
|
async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant):
|
||||||
"""Test Yeelight ip changes and we fallback to discovery."""
|
"""Test Yeelight ip changes and we fallback to discovery."""
|
||||||
@ -85,9 +97,7 @@ async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant):
|
|||||||
|
|
||||||
mocked_bulb = _mocked_bulb(True)
|
mocked_bulb = _mocked_bulb(True)
|
||||||
mocked_bulb.bulb_type = BulbType.WhiteTempMood
|
mocked_bulb.bulb_type = BulbType.WhiteTempMood
|
||||||
mocked_bulb.get_capabilities = MagicMock(
|
mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None])
|
||||||
side_effect=[OSError, CAPABILITIES, CAPABILITIES]
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(f"{MODULE}.AsyncBulb", 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)
|
||||||
@ -102,9 +112,7 @@ 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(
|
with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
|
||||||
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,9 +135,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}.AsyncBulb", return_value=mocked_bulb), patch(
|
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery():
|
||||||
f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb
|
|
||||||
):
|
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -162,9 +168,7 @@ 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(
|
with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
|
||||||
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()
|
||||||
|
|
||||||
@ -188,9 +192,7 @@ 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(
|
with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
|
||||||
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()
|
||||||
|
|
||||||
@ -220,30 +222,13 @@ 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}.AsyncBulb", return_value=mocked_bulb), patch(
|
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(
|
||||||
f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb
|
no_device=True
|
||||||
):
|
), _patch_discovery_timeout(), _patch_discovery_interval():
|
||||||
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()
|
||||||
|
|
||||||
binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format(
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
IP_ADDRESS.replace(".", "_")
|
|
||||||
)
|
|
||||||
|
|
||||||
type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES)
|
|
||||||
type(mocked_bulb).get_properties = MagicMock(None)
|
|
||||||
|
|
||||||
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):
|
async def test_async_listen_error_late_discovery(hass, caplog):
|
||||||
@ -251,12 +236,9 @@ async def test_async_listen_error_late_discovery(hass, caplog):
|
|||||||
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
|
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb(cannot_connect=True)
|
||||||
mocked_bulb.async_listen = AsyncMock(side_effect=BulbException)
|
|
||||||
|
|
||||||
with _patch_discovery(MODULE), patch(
|
with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
|
||||||
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
|
|
||||||
):
|
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -264,17 +246,33 @@ async def test_async_listen_error_late_discovery(hass, caplog):
|
|||||||
assert "Failed to connect to bulb at" in caplog.text
|
assert "Failed to connect to bulb at" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
async def test_async_listen_error_has_host(hass: HomeAssistant):
|
async def test_async_listen_error_has_host_with_id(hass: HomeAssistant):
|
||||||
"""Test the async listen error."""
|
"""Test the async listen error."""
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "127.0.0.1"}
|
domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "127.0.0.1"}
|
||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
mocked_bulb = _mocked_bulb()
|
with _patch_discovery(
|
||||||
mocked_bulb.async_listen = AsyncMock(side_effect=BulbException)
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True)
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_listen_error_has_host_without_id(hass: HomeAssistant):
|
||||||
|
"""Test the async listen error but no id."""
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "127.0.0.1"})
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with _patch_discovery(
|
||||||
|
no_device=True
|
||||||
|
), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True)
|
||||||
|
):
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
@ -102,9 +102,10 @@ from . import (
|
|||||||
MODULE,
|
MODULE,
|
||||||
NAME,
|
NAME,
|
||||||
PROPERTIES,
|
PROPERTIES,
|
||||||
UNIQUE_NAME,
|
UNIQUE_FRIENDLY_NAME,
|
||||||
_mocked_bulb,
|
_mocked_bulb,
|
||||||
_patch_discovery,
|
_patch_discovery,
|
||||||
|
_patch_discovery_interval,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
@ -132,7 +133,7 @@ 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(
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
|
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)
|
||||||
@ -559,7 +560,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
model,
|
model,
|
||||||
target_properties,
|
target_properties,
|
||||||
nightlight_properties=None,
|
nightlight_properties=None,
|
||||||
name=UNIQUE_NAME,
|
name=UNIQUE_FRIENDLY_NAME,
|
||||||
entity_id=ENTITY_LIGHT,
|
entity_id=ENTITY_LIGHT,
|
||||||
):
|
):
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
@ -598,7 +599,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
assert hass.states.get(entity_id).state == "off"
|
assert hass.states.get(entity_id).state == "off"
|
||||||
state = hass.states.get(f"{entity_id}_nightlight")
|
state = hass.states.get(f"{entity_id}_nightlight")
|
||||||
assert state.state == "on"
|
assert state.state == "on"
|
||||||
nightlight_properties["friendly_name"] = f"{name} nightlight"
|
nightlight_properties["friendly_name"] = f"{name} Nightlight"
|
||||||
nightlight_properties["icon"] = "mdi:weather-night"
|
nightlight_properties["icon"] = "mdi:weather-night"
|
||||||
nightlight_properties["flowing"] = False
|
nightlight_properties["flowing"] = False
|
||||||
nightlight_properties["night_light"] = True
|
nightlight_properties["night_light"] = True
|
||||||
@ -893,7 +894,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"color_mode": "color_temp",
|
"color_mode": "color_temp",
|
||||||
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
},
|
},
|
||||||
name=f"{UNIQUE_NAME} ambilight",
|
name=f"{UNIQUE_FRIENDLY_NAME} Ambilight",
|
||||||
entity_id=f"{ENTITY_LIGHT}_ambilight",
|
entity_id=f"{ENTITY_LIGHT}_ambilight",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -914,7 +915,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"color_mode": "hs",
|
"color_mode": "hs",
|
||||||
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
},
|
},
|
||||||
name=f"{UNIQUE_NAME} ambilight",
|
name=f"{UNIQUE_FRIENDLY_NAME} Ambilight",
|
||||||
entity_id=f"{ENTITY_LIGHT}_ambilight",
|
entity_id=f"{ENTITY_LIGHT}_ambilight",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -935,7 +936,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"color_mode": "rgb",
|
"color_mode": "rgb",
|
||||||
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
},
|
},
|
||||||
name=f"{UNIQUE_NAME} ambilight",
|
name=f"{UNIQUE_FRIENDLY_NAME} Ambilight",
|
||||||
entity_id=f"{ENTITY_LIGHT}_ambilight",
|
entity_id=f"{ENTITY_LIGHT}_ambilight",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -969,7 +970,7 @@ 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(
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
|
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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user