1
0
mirror of https://github.com/esphome/esphome.git synced 2025-02-12 16:08:19 +00:00

Merge branch 'mdns_cache' into integration

This commit is contained in:
J. Nick Koston 2023-11-12 18:18:47 -06:00
commit a29d04fda3
No known key found for this signature in database
3 changed files with 192 additions and 72 deletions

View File

@ -0,0 +1,56 @@
from __future__ import annotations
import asyncio
import threading
class ThreadedAsyncEvent:
"""This is a shim to allow the asyncio event to be used in a threaded context.
When more of the code is moved to asyncio, this can be removed.
"""
def __init__(self) -> None:
"""Initialize the ThreadedAsyncEvent."""
self.event = threading.Event()
self.async_event: asyncio.Event | None = None
self.loop: asyncio.AbstractEventLoop | None = None
def async_setup(
self, loop: asyncio.AbstractEventLoop, async_event: asyncio.Event
) -> None:
"""Set the asyncio.Event instance."""
self.loop = loop
self.async_event = async_event
def async_set(self) -> None:
"""Set the asyncio.Event instance."""
self.async_event.set()
self.event.set()
def set(self) -> None:
"""Set the event."""
self.loop.call_soon_threadsafe(self.async_event.set)
self.event.set()
def wait(self) -> None:
"""Wait for the event."""
self.event.wait()
async def async_wait(self) -> None:
"""Wait the event async."""
await self.async_event.wait()
def clear(self) -> None:
"""Clear the event."""
self.loop.call_soon_threadsafe(self.async_event.clear)
self.event.clear()
def async_clear(self) -> None:
"""Clear the event async."""
self.async_event.clear()
self.event.clear()
def is_set(self) -> bool:
"""Return if the event is set."""
return self.event.is_set()

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import base64 import base64
import binascii import binascii
import codecs import codecs
@ -47,11 +48,12 @@ from esphome.storage_json import (
from esphome.util import get_serial_ports, shlex_quote from esphome.util import get_serial_ports, shlex_quote
from esphome.zeroconf import ( from esphome.zeroconf import (
ESPHOME_SERVICE_TYPE, ESPHOME_SERVICE_TYPE,
AsyncEsphomeZeroconf,
DashboardBrowser, DashboardBrowser,
DashboardImportDiscovery, DashboardImportDiscovery,
DashboardStatus, DashboardStatus,
EsphomeZeroconf,
) )
from .async_adapter import ThreadedAsyncEvent
from .util import friendly_name_slugify, password_hash from .util import friendly_name_slugify, password_hash
@ -289,7 +291,10 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
self._use_popen = os.name == "nt" self._use_popen = os.name == "nt"
@authenticated @authenticated
def on_message(self, message): async def on_message( # pylint: disable=invalid-overridden-method
self, message: str
) -> None:
# Since tornado 4.5, on_message is allowed to be a coroutine
# Messages are always JSON, 500 when not # Messages are always JSON, 500 when not
json_message = json.loads(message) json_message = json.loads(message)
type_ = json_message["type"] type_ = json_message["type"]
@ -299,14 +304,14 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
_LOGGER.warning("Requested unknown message type %s", type_) _LOGGER.warning("Requested unknown message type %s", type_)
return return
handlers[type_](self, json_message) await handlers[type_](self, json_message)
@websocket_method("spawn") @websocket_method("spawn")
def handle_spawn(self, json_message): async def handle_spawn(self, json_message: dict[str, Any]) -> None:
if self._proc is not None: if self._proc is not None:
# spawn can only be called once # spawn can only be called once
return return
command = self.build_command(json_message) command = await self.build_command(json_message)
_LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command)) _LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command))
if self._use_popen: if self._use_popen:
@ -337,7 +342,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
return self._proc is not None and self._proc.returncode is None return self._proc is not None and self._proc.returncode is None
@websocket_method("stdin") @websocket_method("stdin")
def handle_stdin(self, json_message): async def handle_stdin(self, json_message: dict[str, Any]) -> None:
if not self.is_process_active: if not self.is_process_active:
return return
data = json_message["data"] data = json_message["data"]
@ -346,7 +351,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
self._proc.stdin.write(data) self._proc.stdin.write(data)
@tornado.gen.coroutine @tornado.gen.coroutine
def _redirect_stdout(self): def _redirect_stdout(self) -> None:
reg = b"[\n\r]" reg = b"[\n\r]"
while True: while True:
@ -365,7 +370,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
_LOGGER.debug("> stdout: %s", data) _LOGGER.debug("> stdout: %s", data)
self.write_message({"event": "line", "data": data}) self.write_message({"event": "line", "data": data})
def _stdout_thread(self): def _stdout_thread(self) -> None:
if not self._use_popen: if not self._use_popen:
return return
while True: while True:
@ -378,13 +383,13 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
self._proc.wait(1.0) self._proc.wait(1.0)
self._queue.put_nowait(None) self._queue.put_nowait(None)
def _proc_on_exit(self, returncode): def _proc_on_exit(self, returncode: int) -> None:
if not self._is_closed: if not self._is_closed:
# Check if the proc was not forcibly closed # Check if the proc was not forcibly closed
_LOGGER.info("Process exited with return code %s", returncode) _LOGGER.info("Process exited with return code %s", returncode)
self.write_message({"event": "exit", "code": returncode}) self.write_message({"event": "exit", "code": returncode})
def on_close(self): def on_close(self) -> None:
# Check if proc exists (if 'start' has been run) # Check if proc exists (if 'start' has been run)
if self.is_process_active: if self.is_process_active:
_LOGGER.debug("Terminating process") _LOGGER.debug("Terminating process")
@ -395,7 +400,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
# Shutdown proc on WS close # Shutdown proc on WS close
self._is_closed = True self._is_closed = True
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
raise NotImplementedError raise NotImplementedError
@ -405,7 +410,9 @@ DASHBOARD_COMMAND = ["esphome", "--dashboard"]
class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
"""Base class for commands that require a port.""" """Base class for commands that require a port."""
def run_command(self, args: list[str], json_message: dict[str, Any]) -> list[str]: async def run_command(
self, args: list[str], json_message: dict[str, Any]
) -> list[str]:
"""Build the command to run.""" """Build the command to run."""
configuration = json_message["configuration"] configuration = json_message["configuration"]
config_file = settings.rel_path(configuration) config_file = settings.rel_path(configuration)
@ -414,7 +421,7 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
port == "OTA" port == "OTA"
and (mdns := MDNS_CONTAINER.get_mdns()) and (mdns := MDNS_CONTAINER.get_mdns())
and (host_name := mdns.filename_to_host_name_thread_safe(configuration)) and (host_name := mdns.filename_to_host_name_thread_safe(configuration))
and (address := mdns.resolve_host_thread_safe(host_name)) and (address := await mdns.async_resolve_host(host_name))
): ):
port = address port = address
@ -428,15 +435,15 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
class EsphomeLogsHandler(EsphomePortCommandWebSocket): class EsphomeLogsHandler(EsphomePortCommandWebSocket):
def build_command(self, json_message: dict[str, Any]) -> list[str]: async def build_command(self, json_message: dict[str, Any]) -> list[str]:
"""Build the command to run.""" """Build the command to run."""
return self.run_command(["logs"], json_message) return await self.run_command(["logs"], json_message)
class EsphomeRenameHandler(EsphomeCommandWebSocket): class EsphomeRenameHandler(EsphomeCommandWebSocket):
old_name: str old_name: str
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
self.old_name = json_message["configuration"] self.old_name = json_message["configuration"]
return [ return [
@ -457,19 +464,19 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket):
class EsphomeUploadHandler(EsphomePortCommandWebSocket): class EsphomeUploadHandler(EsphomePortCommandWebSocket):
def build_command(self, json_message: dict[str, Any]) -> list[str]: async def build_command(self, json_message: dict[str, Any]) -> list[str]:
"""Build the command to run.""" """Build the command to run."""
return self.run_command(["upload"], json_message) return await self.run_command(["upload"], json_message)
class EsphomeRunHandler(EsphomePortCommandWebSocket): class EsphomeRunHandler(EsphomePortCommandWebSocket):
def build_command(self, json_message: dict[str, Any]) -> list[str]: async def build_command(self, json_message: dict[str, Any]) -> list[str]:
"""Build the command to run.""" """Build the command to run."""
return self.run_command(["run"], json_message) return await self.run_command(["run"], json_message)
class EsphomeCompileHandler(EsphomeCommandWebSocket): class EsphomeCompileHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
command = [*DASHBOARD_COMMAND, "compile"] command = [*DASHBOARD_COMMAND, "compile"]
if json_message.get("only_generate", False): if json_message.get("only_generate", False):
@ -479,7 +486,7 @@ class EsphomeCompileHandler(EsphomeCommandWebSocket):
class EsphomeValidateHandler(EsphomeCommandWebSocket): class EsphomeValidateHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
command = [*DASHBOARD_COMMAND, "config", config_file] command = [*DASHBOARD_COMMAND, "config", config_file]
if not settings.streamer_mode: if not settings.streamer_mode:
@ -488,29 +495,29 @@ class EsphomeValidateHandler(EsphomeCommandWebSocket):
class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
class EsphomeCleanHandler(EsphomeCommandWebSocket): class EsphomeCleanHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
return [*DASHBOARD_COMMAND, "clean", config_file] return [*DASHBOARD_COMMAND, "clean", config_file]
class EsphomeVscodeHandler(EsphomeCommandWebSocket): class EsphomeVscodeHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
return [*DASHBOARD_COMMAND, "-q", "vscode", "dummy"] return [*DASHBOARD_COMMAND, "-q", "vscode", "dummy"]
class EsphomeAceEditorHandler(EsphomeCommandWebSocket): class EsphomeAceEditorHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
return [*DASHBOARD_COMMAND, "-q", "vscode", "--ace", settings.config_dir] return [*DASHBOARD_COMMAND, "-q", "vscode", "--ace", settings.config_dir]
class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): class EsphomeUpdateAllHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): async def build_command(self, json_message: dict[str, Any]) -> list[str]:
return [*DASHBOARD_COMMAND, "update-all", settings.config_dir] return [*DASHBOARD_COMMAND, "update-all", settings.config_dir]
@ -970,13 +977,13 @@ class BoardsRequestHandler(BaseHandler):
self.write(json.dumps(output)) self.write(json.dumps(output))
class MDNSStatusThread(threading.Thread): class MDNSStatus:
"""Thread that updates the mdns status.""" """Class that updates the mdns status."""
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the MDNSStatusThread.""" """Initialize the MDNSStatus class."""
super().__init__() super().__init__()
self.zeroconf: EsphomeZeroconf | None = None self.aiozc: AsyncEsphomeZeroconf | None = None
# This is the current mdns state for each host (True, False, None) # This is the current mdns state for each host (True, False, None)
self.host_mdns_state: dict[str, bool | None] = {} self.host_mdns_state: dict[str, bool | None] = {}
# This is the hostnames to filenames mapping # This is the hostnames to filenames mapping
@ -984,23 +991,23 @@ class MDNSStatusThread(threading.Thread):
self.filename_to_host_name: dict[str, str] = {} self.filename_to_host_name: dict[str, str] = {}
# This is a set of host names to track (i.e no_mdns = false) # This is a set of host names to track (i.e no_mdns = false)
self.host_name_with_mdns_enabled: set[set] = set() self.host_name_with_mdns_enabled: set[set] = set()
self._refresh_hosts() self._loop = asyncio.get_running_loop()
def filename_to_host_name_thread_safe(self, filename: str) -> str | None: def filename_to_host_name_thread_safe(self, filename: str) -> str | None:
"""Resolve a filename to an address in a thread-safe manner.""" """Resolve a filename to an address in a thread-safe manner."""
return self.filename_to_host_name.get(filename) return self.filename_to_host_name.get(filename)
def resolve_host_thread_safe(self, host_name: str) -> str | None: async def async_resolve_host(self, host_name: str) -> str | None:
"""Resolve a host name to an address in a thread-safe manner.""" """Resolve a host name to an address in a thread-safe manner."""
if zc := self.zeroconf: if aiozc := self.aiozc:
# Currently we do not do any I/O and only # Currently we do not do any I/O and only
# return the cached result (timeout=0) # return the cached result (timeout=0)
return zc.resolve_host(host_name, 0) return await aiozc.async_resolve_host(host_name)
return None return None
def _refresh_hosts(self): async def async_refresh_hosts(self):
"""Refresh the hosts to track.""" """Refresh the hosts to track."""
entries = _list_dashboard_entries() entries = await self._loop.run_in_executor(None, _list_dashboard_entries)
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
host_mdns_state = self.host_mdns_state host_mdns_state = self.host_mdns_state
host_name_to_filename = self.host_name_to_filename host_name_to_filename = self.host_name_to_filename
@ -1029,11 +1036,11 @@ class MDNSStatusThread(threading.Thread):
host_name_to_filename[name] = filename host_name_to_filename[name] = filename
filename_to_host_name[filename] = name filename_to_host_name[filename] = name
def run(self): async def async_run(self) -> None:
global IMPORT_RESULT global IMPORT_RESULT
zc = EsphomeZeroconf() aiozc = AsyncEsphomeZeroconf()
self.zeroconf = zc self.aiozc = aiozc
host_mdns_state = self.host_mdns_state host_mdns_state = self.host_mdns_state
host_name_to_filename = self.host_name_to_filename host_name_to_filename = self.host_name_to_filename
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
@ -1046,22 +1053,23 @@ class MDNSStatusThread(threading.Thread):
filename = host_name_to_filename[name] filename = host_name_to_filename[name]
PING_RESULT[filename] = result PING_RESULT[filename] = result
self._refresh_hosts()
stat = DashboardStatus(on_update) stat = DashboardStatus(on_update)
imports = DashboardImportDiscovery() imports = DashboardImportDiscovery()
browser = DashboardBrowser( browser = DashboardBrowser(
zc, ESPHOME_SERVICE_TYPE, [stat.browser_callback, imports.browser_callback] aiozc.zeroconf,
ESPHOME_SERVICE_TYPE,
[stat.browser_callback, imports.browser_callback],
) )
while not STOP_EVENT.is_set(): while not STOP_EVENT.is_set():
self._refresh_hosts() await self.async_refresh_hosts()
IMPORT_RESULT = imports.import_state IMPORT_RESULT = imports.import_state
PING_REQUEST.wait() await PING_REQUEST.async_wait()
PING_REQUEST.clear() PING_REQUEST.async_clear()
browser.cancel() await browser.async_cancel()
zc.close() await aiozc.async_close()
self.zeroconf = None self.aiozc = None
class PingStatusThread(threading.Thread): class PingStatusThread(threading.Thread):
@ -1241,21 +1249,21 @@ class UndoDeleteRequestHandler(BaseHandler):
class MDNSContainer: class MDNSContainer:
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the MDNSContainer.""" """Initialize the MDNSContainer."""
self._mdns: MDNSStatusThread | None = None self._mdns: MDNSStatus | None = None
def set_mdns(self, mdns: MDNSStatusThread) -> None: def set_mdns(self, mdns: MDNSStatus) -> None:
"""Set the MDNSStatusThread instance.""" """Set the MDNSStatus instance."""
self._mdns = mdns self._mdns = mdns
def get_mdns(self) -> MDNSStatusThread | None: def get_mdns(self) -> MDNSStatus | None:
"""Return the MDNSStatusThread instance.""" """Return the MDNSStatus instance."""
return self._mdns return self._mdns
PING_RESULT: dict = {} PING_RESULT: dict = {}
IMPORT_RESULT = {} IMPORT_RESULT = {}
STOP_EVENT = threading.Event() STOP_EVENT = threading.Event()
PING_REQUEST = threading.Event() PING_REQUEST = ThreadedAsyncEvent()
MQTT_PING_REQUEST = threading.Event() MQTT_PING_REQUEST = threading.Event()
MDNS_CONTAINER = MDNSContainer() MDNS_CONTAINER = MDNSContainer()
@ -1520,6 +1528,16 @@ def start_web_server(args):
storage.save(path) storage.save(path)
settings.cookie_secret = storage.cookie_secret settings.cookie_secret = storage.cookie_secret
try:
asyncio.run(async_start_web_server(args))
except KeyboardInterrupt:
pass
async def async_start_web_server(args):
loop = asyncio.get_event_loop()
PING_REQUEST.async_setup(loop, asyncio.Event())
app = make_app(args.verbose) app = make_app(args.verbose)
if args.socket is not None: if args.socket is not None:
_LOGGER.info( _LOGGER.info(
@ -1544,27 +1562,35 @@ def start_web_server(args):
webbrowser.open(f"http://{args.address}:{args.port}") webbrowser.open(f"http://{args.address}:{args.port}")
mdns_task: asyncio.Task | None = None
ping_status_thread: PingStatusThread | None = None
if settings.status_use_ping: if settings.status_use_ping:
status_thread = PingStatusThread() ping_status_thread = PingStatusThread()
ping_status_thread.start()
else: else:
status_thread = MDNSStatusThread() mdns_status = MDNSStatus()
MDNS_CONTAINER.set_mdns(status_thread) await mdns_status.async_refresh_hosts()
status_thread.start() MDNS_CONTAINER.set_mdns(mdns_status)
mdns_task = asyncio.create_task(mdns_status.async_run())
if settings.status_use_mqtt: if settings.status_use_mqtt:
status_thread_mqtt = MqttStatusThread() status_thread_mqtt = MqttStatusThread()
status_thread_mqtt.start() status_thread_mqtt.start()
shutdown_event = asyncio.Event()
try: try:
tornado.ioloop.IOLoop.current().start() await shutdown_event.wait()
except KeyboardInterrupt: finally:
_LOGGER.info("Shutting down...") _LOGGER.info("Shutting down...")
STOP_EVENT.set() STOP_EVENT.set()
PING_REQUEST.set() PING_REQUEST.set()
status_thread.join() if ping_status_thread:
ping_status_thread.join()
MDNS_CONTAINER.set_mdns(None) MDNS_CONTAINER.set_mdns(None)
mdns_task.cancel()
if settings.status_use_mqtt: if settings.status_use_mqtt:
status_thread_mqtt.join() status_thread_mqtt.join()
MQTT_PING_REQUEST.set() MQTT_PING_REQUEST.set()
if args.socket is not None: if args.socket is not None:
os.remove(args.socket) os.remove(args.socket)
await asyncio.sleep(0)

View File

@ -1,22 +1,21 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable from typing import Callable
from zeroconf import ( from zeroconf import IPVersion, ServiceInfo, ServiceStateChange, Zeroconf
IPVersion, from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
ServiceBrowser,
ServiceInfo,
ServiceStateChange,
Zeroconf,
)
from esphome.storage_json import StorageJSON, ext_storage_path from esphome.storage_json import StorageJSON, ext_storage_path
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_BACKGROUND_TASKS: set[asyncio.Task] = set()
class HostResolver(ServiceInfo): class HostResolver(ServiceInfo):
"""Resolve a host name to an IP address.""" """Resolve a host name to an IP address."""
@ -65,7 +64,7 @@ class DiscoveredImport:
network: str network: str
class DashboardBrowser(ServiceBrowser): class DashboardBrowser(AsyncServiceBrowser):
"""A class to browse for ESPHome nodes.""" """A class to browse for ESPHome nodes."""
@ -94,7 +93,28 @@ class DashboardImportDiscovery:
# Ignore updates for devices that are not in the import state # Ignore updates for devices that are not in the import state
return return
info = zeroconf.get_service_info(service_type, name) info = AsyncServiceInfo(
service_type,
name,
)
if info.load_from_cache(zeroconf):
self._process_service_info(name, info)
return
task = asyncio.create_task(
self._async_process_service_info(zeroconf, info, service_type, name)
)
_BACKGROUND_TASKS.add(task)
task.add_done_callback(_BACKGROUND_TASKS.discard)
async def _async_process_service_info(
self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str
) -> None:
"""Process a service info."""
if await info.async_request(zeroconf):
self._process_service_info(name, info)
def _process_service_info(self, name: str, info: ServiceInfo) -> None:
"""Process a service info."""
_LOGGER.debug("-> resolved info: %s", info) _LOGGER.debug("-> resolved info: %s", info)
if info is None: if info is None:
return return
@ -146,14 +166,32 @@ class DashboardImportDiscovery:
) )
def _make_host_resolver(host: str) -> HostResolver:
"""Create a new HostResolver for the given host name."""
name = host.partition(".")[0]
info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}")
return info
class EsphomeZeroconf(Zeroconf): class EsphomeZeroconf(Zeroconf):
def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: def resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
"""Resolve a host name to an IP address.""" """Resolve a host name to an IP address."""
name = host.partition(".")[0] info = _make_host_resolver(host)
info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}")
if ( if (
info.load_from_cache(self) info.load_from_cache(self)
or (timeout and info.request(self, timeout * 1000)) or (timeout and info.request(self, timeout * 1000))
) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
return str(addresses[0]) return str(addresses[0])
return None return None
class AsyncEsphomeZeroconf(AsyncZeroconf):
async def async_resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
"""Resolve a host name to an IP address."""
info = _make_host_resolver(host)
if (
info.load_from_cache(self.zeroconf)
or (timeout and await info.async_request(self.zeroconf, timeout * 1000))
) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
return str(addresses[0])
return None