1 """The SSDP integration."""
3 from __future__
import annotations
6 from collections.abc
import Callable, Coroutine, Mapping
7 from dataclasses
import dataclass, field
8 from datetime
import timedelta
10 from functools
import partial
11 from ipaddress
import IPv4Address, IPv6Address
15 from typing
import TYPE_CHECKING, Any
16 from urllib.parse
import urljoin
17 import xml.etree.ElementTree
as ET
19 from async_upnp_client.aiohttp
import AiohttpSessionRequester
20 from async_upnp_client.const
import (
27 from async_upnp_client.description_cache
import DescriptionCache
28 from async_upnp_client.server
import UpnpServer, UpnpServerDevice, UpnpServerService
29 from async_upnp_client.ssdp
import (
31 determine_source_target,
32 fix_ipv6_address_scope_id,
35 from async_upnp_client.ssdp_listener
import SsdpDevice, SsdpDeviceTracker, SsdpListener
36 from async_upnp_client.utils
import CaseInsensitiveDict
38 from homeassistant
import config_entries
41 EVENT_HOMEASSISTANT_STARTED,
42 EVENT_HOMEASSISTANT_STOP,
44 __version__
as current_version,
46 from homeassistant.core import Event, HassJob, HomeAssistant, callback
as core_callback
61 SSDP_SCANNER =
"scanner"
62 UPNP_SERVER =
"server"
63 UPNP_SERVER_MIN_PORT = 40000
64 UPNP_SERVER_MAX_PORT = 40100
67 IPV4_BROADCAST = IPv4Address(
"255.255.255.255")
70 ATTR_SSDP_LOCATION =
"ssdp_location"
71 ATTR_SSDP_ST =
"ssdp_st"
72 ATTR_SSDP_NT =
"ssdp_nt"
73 ATTR_SSDP_UDN =
"ssdp_udn"
74 ATTR_SSDP_USN =
"ssdp_usn"
75 ATTR_SSDP_EXT =
"ssdp_ext"
76 ATTR_SSDP_SERVER =
"ssdp_server"
77 ATTR_SSDP_BOOTID =
"BOOTID.UPNP.ORG"
78 ATTR_SSDP_NEXTBOOTID =
"NEXTBOOTID.UPNP.ORG"
82 ATTR_UPNP_DEVICE_TYPE =
"deviceType"
83 ATTR_UPNP_FRIENDLY_NAME =
"friendlyName"
84 ATTR_UPNP_MANUFACTURER =
"manufacturer"
85 ATTR_UPNP_MANUFACTURER_URL =
"manufacturerURL"
86 ATTR_UPNP_MODEL_DESCRIPTION =
"modelDescription"
87 ATTR_UPNP_MODEL_NAME =
"modelName"
88 ATTR_UPNP_MODEL_NUMBER =
"modelNumber"
89 ATTR_UPNP_MODEL_URL =
"modelURL"
90 ATTR_UPNP_SERIAL =
"serialNumber"
91 ATTR_UPNP_SERVICE_LIST =
"serviceList"
94 ATTR_UPNP_PRESENTATION_URL =
"presentationURL"
96 ATTR_HA_MATCHING_DOMAINS =
"x_homeassistant_matching_domains"
98 PRIMARY_MATCH_KEYS = [
99 ATTR_UPNP_MANUFACTURER,
101 ATTR_UPNP_DEVICE_TYPE,
103 ATTR_UPNP_MANUFACTURER_URL,
106 _LOGGER = logging.getLogger(__name__)
109 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
112 @dataclass(slots=True)
114 """Prepared info from ssdp/upnp entries."""
118 upnp: Mapping[str, Any]
119 ssdp_location: str |
None =
None
120 ssdp_nt: str |
None =
None
121 ssdp_udn: str |
None =
None
122 ssdp_ext: str |
None =
None
123 ssdp_server: str |
None =
None
124 ssdp_headers: Mapping[str, Any] = field(default_factory=dict)
125 ssdp_all_locations: set[str] = field(default_factory=set)
126 x_homeassistant_matching_domains: set[str] = field(default_factory=set)
129 SsdpChange = Enum(
"SsdpChange",
"ALIVE BYEBYE UPDATE")
130 type SsdpHassJobCallback = HassJob[
131 [SsdpServiceInfo, SsdpChange], Coroutine[Any, Any,
None] |
None
134 SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = {
135 SsdpSource.SEARCH_ALIVE: SsdpChange.ALIVE,
136 SsdpSource.SEARCH_CHANGED: SsdpChange.ALIVE,
137 SsdpSource.ADVERTISEMENT_ALIVE: SsdpChange.ALIVE,
138 SsdpSource.ADVERTISEMENT_BYEBYE: SsdpChange.BYEBYE,
139 SsdpSource.ADVERTISEMENT_UPDATE: SsdpChange.UPDATE,
144 """Format error message."""
145 return f
"Exception in SSDP callback {name}: {args}"
151 callback: Callable[[SsdpServiceInfo, SsdpChange], Coroutine[Any, Any,
None] |
None],
152 match_dict: dict[str, str] |
None =
None,
153 ) -> Callable[[],
None]:
154 """Register to receive a callback on ssdp broadcast.
156 Returns a callback that can be used to cancel the registration.
158 scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
162 partial(_format_err,
str(callback)),
164 f
"ssdp callback {match_dict}",
166 return await scanner.async_register_callback(job, match_dict)
171 hass: HomeAssistant, udn: str, st: str
172 ) -> SsdpServiceInfo |
None:
173 """Fetch the discovery info cache."""
174 scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
175 return await scanner.async_get_discovery_info_by_udn_st(udn, st)
180 hass: HomeAssistant, st: str
181 ) -> list[SsdpServiceInfo]:
182 """Fetch all the entries matching the st."""
183 scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
184 return await scanner.async_get_discovery_info_by_st(st)
189 hass: HomeAssistant, udn: str
190 ) -> list[SsdpServiceInfo]:
191 """Fetch all the entries matching the udn."""
192 scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
193 return await scanner.async_get_discovery_info_by_udn(udn)
197 """Build the list of ssdp sources."""
200 for source_ip
in await network.async_get_enabled_source_ips(hass)
201 if not source_ip.is_loopback
202 and not source_ip.is_global
203 and (source_ip.version == 6
and source_ip.scope_id
or source_ip.version == 4)
207 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
208 """Set up the SSDP integration."""
213 scanner =
Scanner(hass, integration_matchers)
215 hass.data[DOMAIN] = {
216 SSDP_SCANNER: scanner,
220 await scanner.async_start()
221 await server.async_start()
229 callbacks: list[SsdpHassJobCallback],
230 discovery_info: SsdpServiceInfo,
231 ssdp_change: SsdpChange,
233 for callback
in callbacks:
235 hass.async_run_hass_job(
236 callback, discovery_info, ssdp_change, background=
True
239 _LOGGER.exception(
"Failed to callback info: %s", discovery_info)
244 headers: CaseInsensitiveDict, lower_match_dict: dict[str, str]
246 for header, val
in lower_match_dict.items():
248 if header
not in headers:
250 elif headers.get_lower(header) != val:
256 """Optimized integration matching."""
259 """Init optimized integration matching."""
261 dict[str, dict[str, list[tuple[str, dict[str, str]]]]] |
None
266 self, integration_matchers: dict[str, list[dict[str, str]]]
268 """Build matchers by key.
270 Here we convert the primary match keys into their own
271 dicts so we can do lookups of the primary match
272 key to find the match dict.
275 for key
in PRIMARY_MATCH_KEYS:
277 for domain, matchers
in integration_matchers.items():
278 for matcher
in matchers:
279 if match_value := matcher.get(key):
280 matchers_by_key.setdefault(match_value, []).append(
286 """Find domains matching the passed CaseInsensitiveDict."""
290 for key, matchers_by_key
in self.
_match_by_key_match_by_key.items()
291 if (match_value := info_with_desc.get(key))
292 for domain, matcher
in matchers_by_key.get(match_value, ())
293 if info_with_desc.items() >= matcher.items()
298 """Class to manage SSDP searching and SSDP advertisements."""
301 self, hass: HomeAssistant, integration_matchers: IntegrationMatchers
303 """Initialize class."""
305 self.
_cancel_scan_cancel_scan: Callable[[],
None] |
None =
None
306 self._ssdp_listeners: list[SsdpListener] = []
308 self._callbacks: list[tuple[SsdpHassJobCallback, dict[str, str]]] = []
314 """Get all seen devices."""
318 self, callback: SsdpHassJobCallback, match_dict: dict[str, str] |
None =
None
319 ) -> Callable[[],
None]:
320 """Register a callback."""
321 if match_dict
is None:
322 lower_match_dict = {}
324 lower_match_dict = {k.lower(): v
for k, v
in match_dict.items()}
329 for headers
in ssdp_device.all_combined_headers.values():
340 callback_entry = (callback, lower_match_dict)
341 self._callbacks.append(callback_entry)
344 def _async_remove_callback() -> None:
345 self._callbacks.
remove(callback_entry)
347 return _async_remove_callback
350 """Stop the scanner."""
357 """Stop the SSDP listeners."""
358 await asyncio.gather(
360 create_eager_task(listener.async_stop())
361 for listener
in self._ssdp_listeners
363 return_exceptions=
True,
367 """Scan for new entries using ssdp listeners."""
372 """Scan for new entries using multicase target."""
373 for ssdp_listener
in self._ssdp_listeners:
374 await ssdp_listener.async_search()
377 """Scan for new entries using broadcast target."""
381 for listener
in self._ssdp_listeners:
383 await listener.async_search((
str(IPV4_BROADCAST), SSDP_PORT))
386 """Start the scanners."""
388 requester = AiohttpSessionRequester(session,
True, 10)
393 self.
hasshass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.
async_stopasync_stop)
395 self.
hasshass, self.
async_scanasync_scan, SCAN_INTERVAL, name=
"SSDP scanner"
400 config_entries.signal_discovered_config_entry_removed(DOMAIN),
408 """Start the SSDP Listeners."""
411 source_ip_str =
str(source_ip)
412 if source_ip.version == 6:
413 source_tuple: AddressTupleVXType = (
417 int(getattr(source_ip,
"scope_id")),
420 source_tuple = (source_ip_str, 0)
421 source, target = determine_source_target(source_tuple)
422 source = fix_ipv6_address_scope_id(source)
or source
423 self._ssdp_listeners.append(
431 results = await asyncio.gather(
433 create_eager_task(listener.async_start())
434 for listener
in self._ssdp_listeners
436 return_exceptions=
True,
438 failed_listeners = []
439 for idx, result
in enumerate(results):
440 if isinstance(result, Exception):
442 "Failed to setup listener for %s: %s",
443 self._ssdp_listeners[idx].source,
446 failed_listeners.append(self._ssdp_listeners[idx])
447 for listener
in failed_listeners:
448 self._ssdp_listeners.
remove(listener)
453 combined_headers: CaseInsensitiveDict,
454 ) -> list[SsdpHassJobCallback]:
455 """Return a list of callbacks that match."""
458 for callback, lower_match_dict
in self._callbacks
464 ssdp_device: SsdpDevice,
465 dst: DeviceOrServiceType,
468 """Handle a device/service change."""
470 "SSDP: ssdp_device: %s, dst: %s, source: %s", ssdp_device, dst, source
475 location = ssdp_device.location
476 _, info_desc = self.
_description_cache_description_cache.peek_description_dict(location)
477 if info_desc
is None:
479 self.
hasshass.async_create_background_task(
481 ssdp_device, dst, source
483 name=f
"ssdp_info_desc_lookup_{location}",
493 ssdp_device: SsdpDevice,
494 dst: DeviceOrServiceType,
497 """Handle a device/service change."""
498 location = ssdp_device.location
508 ssdp_device: SsdpDevice,
509 dst: DeviceOrServiceType,
511 info_desc: Mapping[str, Any],
512 skip_callbacks: bool =
False,
514 """Handle a device/service change."""
515 matching_domains: set[str] = set()
516 combined_headers = ssdp_device.combined_headers(dst)
520 if source != SsdpSource.SEARCH_ALIVE:
522 CaseInsensitiveDict(combined_headers.as_dict(), **info_desc)
527 and not matching_domains
528 and source != SsdpSource.ADVERTISEMENT_BYEBYE
533 ssdp_device, combined_headers, info_desc
535 discovery_info.x_homeassistant_matching_domains = matching_domains
537 if callbacks
and not skip_callbacks:
538 ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source]
542 if source == SsdpSource.ADVERTISEMENT_BYEBYE:
546 _LOGGER.debug(
"Discovery info: %s", discovery_info)
548 if not matching_domains:
551 discovery_key = discovery_flow.DiscoveryKey(
552 domain=DOMAIN, key=ssdp_device.udn, version=1
554 for domain
in matching_domains:
555 _LOGGER.debug(
"Discovered %s at %s", domain, ssdp_device.location)
556 discovery_flow.async_create_flow(
559 {
"source": config_entries.SOURCE_SSDP},
561 discovery_key=discovery_key,
565 self, byebye_discovery_info: SsdpServiceInfo
567 """Dismiss all discoveries for the given address."""
568 for flow
in self.
hasshass.config_entries.flow.async_progress_by_init_data_type(
570 lambda service_info: bool(
571 service_info.ssdp_st == byebye_discovery_info.ssdp_st
572 and service_info.ssdp_location == byebye_discovery_info.ssdp_location
575 self.
hasshass.config_entries.flow.async_abort(flow[
"flow_id"])
578 self, location: str |
None
579 ) -> Mapping[str, str]:
580 """Get description dict."""
584 has_description, description = cache.peek_description_dict(location)
586 return description
or {}
588 return await cache.async_get_description_dict(location)
or {}
591 self, ssdp_device: SsdpDevice, headers: CaseInsensitiveDict
592 ) -> SsdpServiceInfo:
593 """Combine the headers and description into discovery_info.
595 Building this is a bit expensive so we only do it on demand.
597 location = headers[
"location"]
600 ssdp_device, headers, info_desc
604 self, udn: str, st: str
605 ) -> SsdpServiceInfo |
None:
606 """Return discovery_info for a udn and st."""
608 if ssdp_device.udn == udn:
609 if headers := ssdp_device.combined_headers(st):
616 """Return matching discovery_infos for a st."""
620 if (headers := ssdp_device.combined_headers(st))
624 """Return matching discovery_infos for a udn."""
628 for headers
in ssdp_device.all_combined_headers.values()
629 if ssdp_device.udn == udn
635 entry: config_entries.ConfigEntry,
637 """Handle config entry changes."""
641 for discovery_key
in entry.discovery_keys[DOMAIN]:
642 if discovery_key.version != 1
or not isinstance(discovery_key.key, str):
644 udn = discovery_key.key
645 _LOGGER.debug(
"Rediscover service %s", udn)
648 if ssdp_device.udn != udn:
650 for dst
in ssdp_device.all_combined_headers:
651 has_cached_desc, info_desc = cache.peek_description_dict(
654 if has_cached_desc
and info_desc:
665 ssdp_device: SsdpDevice,
666 combined_headers: CaseInsensitiveDict,
667 info_desc: Mapping[str, Any],
668 ) -> SsdpServiceInfo:
669 """Convert headers and description to discovery_info."""
670 ssdp_usn = combined_headers[
"usn"]
671 ssdp_st = combined_headers.get_lower(
"st")
672 if isinstance(info_desc, CaseInsensitiveDict):
673 upnp_info = {**info_desc.as_dict()}
675 upnp_info = {**info_desc}
681 ssdp_st = combined_headers[
"nt"]
684 if ATTR_UPNP_UDN
not in upnp_info:
686 upnp_info[ATTR_UPNP_UDN] = udn
691 ssdp_ext=combined_headers.get_lower(
"ext"),
692 ssdp_server=combined_headers.get_lower(
"server"),
693 ssdp_location=combined_headers.get_lower(
"location"),
694 ssdp_udn=combined_headers.get_lower(
"_udn"),
695 ssdp_nt=combined_headers.get_lower(
"nt"),
696 ssdp_headers=combined_headers,
698 ssdp_all_locations=set(ssdp_device.locations),
703 """Get the UDN from the USN."""
706 if usn.startswith(
"uuid:"):
707 return usn.split(
"::")[0]
714 DEVICE_DEFINITION = DeviceInfo(
715 device_type=
"urn:home-assistant.io:device:HomeAssistant:1",
716 friendly_name=
"filled_later_on",
717 manufacturer=
"Home Assistant",
718 manufacturer_url=
"https://www.home-assistant.io",
719 model_description=
None,
720 model_name=
"filled_later_on",
721 model_number=current_version,
722 model_url=
"https://www.home-assistant.io",
723 serial_number=
"filled_later_on",
724 udn=
"filled_later_on",
726 presentation_url=
"https://my.home-assistant.io/",
730 mimetype=
"image/png",
734 url=
"/static/icons/favicon-1024x1024.png",
737 mimetype=
"image/png",
741 url=
"/static/icons/favicon-512x512.png",
744 mimetype=
"image/png",
748 url=
"/static/icons/favicon-384x384.png",
751 mimetype=
"image/png",
755 url=
"/static/icons/favicon-192x192.png",
758 xml=ET.Element(
"server_device"),
760 EMBEDDED_DEVICES: list[type[UpnpServerDevice]] = []
761 SERVICES: list[type[UpnpServerService]] = []
765 """Get a free TCP port."""
766 family = socket.AF_INET
if is_ipv4_address(source)
else socket.AF_INET6
767 test_socket = socket.socket(family, socket.SOCK_STREAM)
768 test_socket.setblocking(
False)
769 test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
771 for port
in range(UPNP_SERVER_MIN_PORT, UPNP_SERVER_MAX_PORT):
772 addr = (source[0],) + (port,) + source[2:]
774 test_socket.bind(addr)
776 if port == UPNP_SERVER_MAX_PORT - 1:
781 raise RuntimeError(
"unreachable")
785 """Class to be visible via SSDP searching and advertisements."""
788 """Initialize class."""
790 self._upnp_servers: list[UpnpServer] = []
793 """Start the server."""
794 bus = self.
hasshass.bus
795 bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.
async_stopasync_stop)
796 bus.async_listen_once(
797 EVENT_HOMEASSISTANT_STARTED,
802 """Get Unique Device Name for this instance."""
803 instance_id = await async_get_instance_id(self.
hasshass)
804 return f
"uuid:{instance_id[0:8]}-{instance_id[8:12]}-{instance_id[12:16]}-{instance_id[16:20]}-{instance_id[20:32]}".upper()
807 """Start the UPnP/SSDP servers."""
811 model_name = system_info[
"installation_type"]
813 presentation_url =
get_url(self.
hasshass, allow_ip=
True, prefer_external=
False)
814 except NoURLAvailableError:
816 "Could not set up UPnP/SSDP server, as a presentation URL could"
817 " not be determined; Please configure your internal URL"
818 " in the Home Assistant general configuration"
822 serial_number = await async_get_instance_id(self.
hasshass)
823 HassUpnpServiceDevice.DEVICE_DEFINITION = (
824 HassUpnpServiceDevice.DEVICE_DEFINITION._replace(
826 friendly_name=f
"{self.hass.config.location_name} (Home Assistant)",
827 model_name=model_name,
828 presentation_url=presentation_url,
829 serial_number=serial_number,
834 for index, icon
in enumerate(HassUpnpServiceDevice.DEVICE_DEFINITION.icons):
835 new_url = urljoin(presentation_url, icon.url)
836 HassUpnpServiceDevice.DEVICE_DEFINITION.icons[index] = icon._replace(
843 source_ip_str =
str(source_ip)
844 if source_ip.version == 6:
845 source_tuple: AddressTupleVXType = (
849 int(getattr(source_ip,
"scope_id")),
852 source_tuple = (source_ip_str, 0)
853 source, target = determine_source_target(source_tuple)
854 source = fix_ipv6_address_scope_id(source)
or source
856 _LOGGER.debug(
"Binding UPnP HTTP server to: %s:%s", source_ip, http_port)
857 self._upnp_servers.append(
862 server_device=HassUpnpServiceDevice,
866 results = await asyncio.gather(
867 *(upnp_server.async_start()
for upnp_server
in self._upnp_servers),
868 return_exceptions=
True,
871 for idx, result
in enumerate(results):
872 if isinstance(result, Exception):
874 "Failed to setup server for %s: %s",
875 self._upnp_servers[idx].source,
878 failed_servers.append(self._upnp_servers[idx])
879 for server
in failed_servers:
880 self._upnp_servers.
remove(server)
883 """Stop the server."""
887 """Stop UPnP/SSDP servers."""
888 for server
in self._upnp_servers:
889 await server.async_stop()
None async_setup(self, dict[str, list[dict[str, str]]] integration_matchers)
set[str] async_matching_domains(self, CaseInsensitiveDict info_with_desc)
None _ssdp_listener_callback(self, SsdpDevice ssdp_device, DeviceOrServiceType dst, SsdpSource source)
None __init__(self, HomeAssistant hass, IntegrationMatchers integration_matchers)
Callable[[], None] async_register_callback(self, SsdpHassJobCallback callback, dict[str, str]|None match_dict=None)
list[SsdpServiceInfo] async_get_discovery_info_by_st(self, str st)
None async_scan_broadcast(self, *Any _)
None _ssdp_listener_process_callback_with_lookup(self, SsdpDevice ssdp_device, DeviceOrServiceType dst, SsdpSource source)
None async_scan(self, *Any _)
SsdpServiceInfo|None async_get_discovery_info_by_udn_st(self, str udn, str st)
list[SsdpDevice] _ssdp_devices(self)
None _handle_config_entry_removed(self, config_entries.ConfigEntry entry)
None async_stop(self, *Any _)
None async_scan_multicast(self, *Any _)
SsdpServiceInfo _async_headers_to_discovery_info(self, SsdpDevice ssdp_device, CaseInsensitiveDict headers)
None _async_dismiss_discoveries(self, SsdpServiceInfo byebye_discovery_info)
None _ssdp_listener_process_callback(self, SsdpDevice ssdp_device, DeviceOrServiceType dst, SsdpSource source, Mapping[str, Any] info_desc, bool skip_callbacks=False)
Mapping[str, str] _async_get_description_dict(self, str|None location)
list[SsdpHassJobCallback] _async_get_matching_callbacks(self, CaseInsensitiveDict combined_headers)
None _async_stop_ssdp_listeners(self)
None _async_start_ssdp_listeners(self)
list[SsdpServiceInfo] async_get_discovery_info_by_udn(self, str udn)
None __init__(self, HomeAssistant hass)
None _async_stop_upnp_servers(self)
None async_stop(self, *Any _)
str _async_get_instance_udn(self)
None _async_start_upnp_servers(self, Event event)
bool remove(self, _T matcher)
int _async_find_next_available_port(AddressTupleVXType source)
None _async_process_callbacks(HomeAssistant hass, list[SsdpHassJobCallback] callbacks, SsdpServiceInfo discovery_info, SsdpChange ssdp_change)
Callable[[], None] async_register_callback(HomeAssistant hass, Callable[[SsdpServiceInfo, SsdpChange], Coroutine[Any, Any, None]|None] callback, dict[str, str]|None match_dict=None)
list[SsdpServiceInfo] async_get_discovery_info_by_st(HomeAssistant hass, str st)
SsdpServiceInfo|None async_get_discovery_info_by_udn_st(HomeAssistant hass, str udn, str st)
set[IPv4Address|IPv6Address] async_build_source_set(HomeAssistant hass)
bool _async_headers_match(CaseInsensitiveDict headers, dict[str, str] lower_match_dict)
SsdpServiceInfo discovery_info_from_headers_and_description(SsdpDevice ssdp_device, CaseInsensitiveDict combined_headers, Mapping[str, Any] info_desc)
bool async_setup(HomeAssistant hass, ConfigType config)
list[SsdpServiceInfo] async_get_discovery_info_by_udn(HomeAssistant hass, str udn)
str _format_err(str name, *Any args)
str|None _udn_from_usn(str|None usn)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
bool time(HomeAssistant hass, dt_time|str|None before=None, dt_time|str|None after=None, str|Container[str]|None weekday=None)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
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)
str get_url(HomeAssistant hass, *bool require_current_request=False, bool require_ssl=False, bool require_standard_port=False, bool require_cloud=False, bool allow_internal=True, bool allow_external=True, bool allow_cloud=True, bool|None allow_ip=None, bool|None prefer_external=None, bool prefer_cloud=False)
dict[str, Any] async_get_system_info(HomeAssistant hass)
dict[str, list[dict[str, str]]] async_get_ssdp(HomeAssistant hass)
bool is_ipv4_address(str address)