1 """Support for Xiaomi Yeelight WiFi color bulb."""
3 from __future__
import annotations
6 from collections.abc
import ValuesView
8 from datetime
import datetime
9 from functools
import partial
10 from ipaddress
import IPv4Address
12 from typing
import Self
13 from urllib.parse
import urlparse
15 from async_upnp_client.search
import SsdpSearchListener
16 from async_upnp_client.utils
import CaseInsensitiveDict
18 from homeassistant
import config_entries
28 DISCOVERY_SEARCH_INTERVAL,
35 _LOGGER = logging.getLogger(__name__)
41 future.set_result(
None)
45 """Scan for Yeelight devices."""
47 _scanner: Self |
None =
None
51 def async_get(cls, hass: HomeAssistant) -> YeelightScanner:
52 """Get scanner instance."""
57 def __init__(self, hass: HomeAssistant) ->
None:
58 """Initialize class."""
60 self._host_discovered_events: dict[str, list[asyncio.Event]] = {}
61 self._unique_id_capabilities: dict[str, CaseInsensitiveDict] = {}
62 self._host_capabilities: dict[str, CaseInsensitiveDict] = {}
64 self._listeners: list[SsdpSearchListener] = []
65 self.
_setup_future_setup_future: asyncio.Future[
None] |
None =
None
68 """Set up the scanner."""
74 connected_futures: list[asyncio.Future[
None]] = []
76 future = self.
_hass_hass.loop.create_future()
77 connected_futures.append(future)
78 source = (
str(source_ip), 0)
79 self._listeners.append(
82 search_target=SSDP_ST,
85 connect_callback=partial(_set_future_if_not_done, future),
89 results = await asyncio.gather(
91 create_eager_task(listener.async_start())
92 for listener
in self._listeners
94 return_exceptions=
True,
97 for idx, result
in enumerate(results):
98 if not isinstance(result, Exception):
101 "Failed to setup listener for %s: %s",
102 self._listeners[idx].source,
105 failed_listeners.append(self._listeners[idx])
108 for listener
in failed_listeners:
109 self._listeners.
remove(listener)
111 await asyncio.wait(connected_futures)
113 self.
_hass_hass, self.
async_scanasync_scan, DISCOVERY_INTERVAL, cancel_on_shutdown=
True
119 """Build the list of ssdp sources."""
120 adapters = await network.async_get_adapters(self.
_hass_hass)
121 sources: set[IPv4Address] = set()
122 if network.async_only_default_interface_enabled(adapters):
123 sources.add(IPv4Address(
"0.0.0.0"))
128 for source_ip
in await network.async_get_enabled_source_ips(self.
_hass_hass)
129 if isinstance(source_ip, IPv4Address)
and not source_ip.is_loopback
133 """Discover bulbs."""
134 _LOGGER.debug(
"Yeelight discover with interval %s", DISCOVERY_SEARCH_INTERVAL)
136 for _
in range(DISCOVERY_ATTEMPTS):
138 await asyncio.sleep(DISCOVERY_SEARCH_INTERVAL.total_seconds())
139 return self._unique_id_capabilities.values()
143 """Send discovery packets."""
144 _LOGGER.debug(
"Yeelight scanning")
145 for listener
in self._listeners:
146 listener.async_search()
149 """Get capabilities via SSDP."""
150 if host
in self._host_capabilities:
151 return self._host_capabilities[host]
153 host_event = asyncio.Event()
154 self._host_discovered_events.setdefault(host, []).append(host_event)
157 for listener
in self._listeners:
158 listener.async_search((host, SSDP_TARGET[1]))
160 with contextlib.suppress(TimeoutError):
161 async
with asyncio.timeout(DISCOVERY_TIMEOUT):
162 await host_event.wait()
164 self._host_discovered_events[host].
remove(host_event)
165 return self._host_capabilities.
get(host)
169 def _async_start_flow(*_) -> None:
170 discovery_flow.async_create_flow(
173 context={
"source": config_entries.SOURCE_SSDP},
177 ssdp_headers=response,
185 self.
_hass_hass, 1,
HassJob(_async_start_flow, cancel_on_shutdown=
True)
190 """Process a discovery."""
191 _LOGGER.debug(
"Discovered via SSDP: %s", headers)
192 unique_id = headers[
"id"]
193 host = urlparse(headers[
"location"]).hostname
195 current_entry = self._unique_id_capabilities.
get(unique_id)
197 if not current_entry
or host != urlparse(current_entry[
"location"]).hostname:
198 _LOGGER.debug(
"Yeelight discovered with %s", headers)
200 self._host_capabilities[host] = headers
201 self._unique_id_capabilities[unique_id] = headers
202 for event
in self._host_discovered_events.
get(host, []):
CaseInsensitiveDict|None async_get_capabilities(self, str host)
None async_scan(self, datetime|None _=None)
YeelightScanner async_get(cls, HomeAssistant hass)
ValuesView[CaseInsensitiveDict] async_discover(self)
None _async_discovered_by_ssdp(self, CaseInsensitiveDict response)
None _async_process_entry(self, CaseInsensitiveDict headers)
None __init__(self, HomeAssistant hass)
set[IPv4Address] _async_build_source_set(self)
bool remove(self, _T matcher)
web.Response get(self, web.Request request, str config_key)
None _set_future_if_not_done(asyncio.Future[None] future)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)