2023-07-19 22:33:28 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-06-30 17:00:22 +02:00
|
|
|
import asyncio
|
2021-09-17 17:44:30 +02:00
|
|
|
import contextlib
|
2019-02-11 16:56:18 +01:00
|
|
|
import socket
|
2021-06-30 17:00:22 +02:00
|
|
|
from dataclasses import dataclass
|
2021-09-17 17:44:30 +02:00
|
|
|
from ipaddress import IPv4Address, IPv6Address
|
2023-07-19 22:33:28 +02:00
|
|
|
from typing import Union, cast
|
2019-02-11 16:56:18 +01:00
|
|
|
|
2023-10-16 01:05:23 +02:00
|
|
|
from zeroconf import IPVersion, Zeroconf
|
|
|
|
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf
|
2019-02-11 16:56:18 +01:00
|
|
|
|
2021-09-14 12:44:52 +02:00
|
|
|
from .core import APIConnectionError, ResolveAPIError
|
2019-02-11 16:56:18 +01:00
|
|
|
|
2023-10-16 01:05:23 +02:00
|
|
|
ZeroconfInstanceType = Union[Zeroconf, AsyncZeroconf, None]
|
2019-02-11 16:56:18 +01:00
|
|
|
|
|
|
|
|
2021-06-30 17:00:22 +02:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
class Sockaddr:
|
|
|
|
pass
|
2019-02-11 16:56:18 +01:00
|
|
|
|
2021-06-30 17:00:22 +02:00
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
class IPv4Sockaddr(Sockaddr):
|
|
|
|
address: str
|
|
|
|
port: int
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
class IPv6Sockaddr(Sockaddr):
|
|
|
|
address: str
|
|
|
|
port: int
|
|
|
|
flowinfo: int
|
|
|
|
scope_id: int
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
class AddrInfo:
|
|
|
|
family: int
|
|
|
|
type: int
|
|
|
|
proto: int
|
|
|
|
sockaddr: Sockaddr
|
|
|
|
|
|
|
|
|
2021-09-07 18:52:54 +02:00
|
|
|
async def _async_zeroconf_get_service_info(
|
|
|
|
zeroconf_instance: ZeroconfInstanceType,
|
|
|
|
service_type: str,
|
|
|
|
service_name: str,
|
|
|
|
timeout: float,
|
2023-10-16 01:05:23 +02:00
|
|
|
) -> AsyncServiceInfo | None:
|
2021-06-30 17:00:22 +02:00
|
|
|
# Use or create zeroconf instance, ensure it's an AsyncZeroconf
|
2023-10-16 01:05:23 +02:00
|
|
|
async_zc_instance: AsyncZeroconf | None = None
|
2021-06-30 17:00:22 +02:00
|
|
|
if zeroconf_instance is None:
|
|
|
|
try:
|
2023-10-16 01:05:23 +02:00
|
|
|
async_zc_instance = AsyncZeroconf()
|
2021-06-30 17:00:22 +02:00
|
|
|
except Exception:
|
2021-09-14 12:44:52 +02:00
|
|
|
raise ResolveAPIError(
|
2021-06-30 17:00:22 +02:00
|
|
|
"Cannot start mDNS sockets, is this a docker container without "
|
|
|
|
"host network mode?"
|
|
|
|
)
|
2023-10-16 01:05:23 +02:00
|
|
|
zc = async_zc_instance.zeroconf
|
|
|
|
elif isinstance(zeroconf_instance, AsyncZeroconf):
|
|
|
|
zc = zeroconf_instance.zeroconf
|
|
|
|
elif isinstance(zeroconf_instance, Zeroconf):
|
2021-06-30 17:00:22 +02:00
|
|
|
zc = zeroconf_instance
|
|
|
|
else:
|
|
|
|
raise ValueError(
|
|
|
|
f"Invalid type passed for zeroconf_instance: {type(zeroconf_instance)}"
|
|
|
|
)
|
|
|
|
|
2019-02-11 16:56:18 +01:00
|
|
|
try:
|
2023-10-16 01:05:23 +02:00
|
|
|
info = AsyncServiceInfo(service_type, service_name)
|
|
|
|
if await info.async_request(zc, int(timeout * 1000)):
|
|
|
|
return info
|
2021-06-30 17:00:22 +02:00
|
|
|
except Exception as exc:
|
2021-09-14 12:44:52 +02:00
|
|
|
raise ResolveAPIError(
|
2021-09-07 18:52:54 +02:00
|
|
|
f"Error resolving mDNS {service_name} via mDNS: {exc}"
|
2021-06-30 17:00:22 +02:00
|
|
|
) from exc
|
|
|
|
finally:
|
2023-10-16 01:05:23 +02:00
|
|
|
if async_zc_instance:
|
|
|
|
await async_zc_instance.async_close()
|
2021-09-07 18:52:54 +02:00
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
async def _async_resolve_host_zeroconf(
|
|
|
|
host: str,
|
|
|
|
port: int,
|
|
|
|
*,
|
|
|
|
timeout: float = 3.0,
|
|
|
|
zeroconf_instance: ZeroconfInstanceType = None,
|
2023-07-19 22:33:28 +02:00
|
|
|
) -> list[AddrInfo]:
|
2021-09-07 18:52:54 +02:00
|
|
|
service_type = "_esphomelib._tcp.local."
|
|
|
|
service_name = f"{host}.{service_type}"
|
|
|
|
|
|
|
|
info = await _async_zeroconf_get_service_info(
|
2021-10-13 10:15:30 +02:00
|
|
|
zeroconf_instance, service_type, service_name, timeout
|
2021-09-07 18:52:54 +02:00
|
|
|
)
|
2021-06-30 17:00:22 +02:00
|
|
|
|
|
|
|
if info is None:
|
|
|
|
return []
|
|
|
|
|
2023-07-19 22:33:28 +02:00
|
|
|
addrs: list[AddrInfo] = []
|
2023-10-16 01:05:23 +02:00
|
|
|
for ip_address in info.ip_addresses_by_version(IPVersion.All):
|
2023-10-01 19:31:50 +02:00
|
|
|
is_ipv6 = ip_address.version == 6
|
2021-06-30 17:00:22 +02:00
|
|
|
sockaddr: Sockaddr
|
|
|
|
if is_ipv6:
|
|
|
|
sockaddr = IPv6Sockaddr(
|
2023-10-01 19:31:50 +02:00
|
|
|
address=str(ip_address),
|
2021-06-30 17:00:22 +02:00
|
|
|
port=port,
|
|
|
|
flowinfo=0,
|
|
|
|
scope_id=0,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
sockaddr = IPv4Sockaddr(
|
2023-10-01 19:31:50 +02:00
|
|
|
address=str(ip_address),
|
2021-06-30 17:00:22 +02:00
|
|
|
port=port,
|
|
|
|
)
|
|
|
|
|
|
|
|
addrs.append(
|
|
|
|
AddrInfo(
|
|
|
|
family=socket.AF_INET6 if is_ipv6 else socket.AF_INET,
|
|
|
|
type=socket.SOCK_STREAM,
|
|
|
|
proto=socket.IPPROTO_TCP,
|
|
|
|
sockaddr=sockaddr,
|
|
|
|
)
|
2021-06-18 17:57:02 +02:00
|
|
|
)
|
2021-06-30 17:00:22 +02:00
|
|
|
return addrs
|
2019-02-11 16:56:18 +01:00
|
|
|
|
2021-06-30 17:00:22 +02:00
|
|
|
|
2023-07-19 22:33:28 +02:00
|
|
|
async def _async_resolve_host_getaddrinfo(host: str, port: int) -> list[AddrInfo]:
|
2019-02-11 16:56:18 +01:00
|
|
|
try:
|
2021-09-17 19:22:20 +02:00
|
|
|
# Limit to TCP IP protocol and SOCK_STREAM
|
2021-10-13 10:15:30 +02:00
|
|
|
res = await asyncio.get_event_loop().getaddrinfo(
|
2021-09-17 19:22:20 +02:00
|
|
|
host, port, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP
|
|
|
|
)
|
2021-06-30 17:00:22 +02:00
|
|
|
except OSError as err:
|
2021-09-19 19:08:18 +02:00
|
|
|
raise APIConnectionError(f"Error resolving IP address: {err}")
|
2021-06-30 17:00:22 +02:00
|
|
|
|
2023-07-19 22:33:28 +02:00
|
|
|
addrs: list[AddrInfo] = []
|
2021-06-30 17:00:22 +02:00
|
|
|
for family, type_, proto, _, raw in res:
|
|
|
|
sockaddr: Sockaddr
|
|
|
|
if family == socket.AF_INET:
|
2023-07-19 22:33:28 +02:00
|
|
|
raw = cast(tuple[str, int], raw)
|
2021-06-30 17:00:22 +02:00
|
|
|
address, port = raw
|
|
|
|
sockaddr = IPv4Sockaddr(address=address, port=port)
|
|
|
|
elif family == socket.AF_INET6:
|
2023-07-19 22:33:28 +02:00
|
|
|
raw = cast(tuple[str, int, int, int], raw)
|
2021-06-30 17:00:22 +02:00
|
|
|
address, port, flowinfo, scope_id = raw
|
|
|
|
sockaddr = IPv6Sockaddr(
|
|
|
|
address=address, port=port, flowinfo=flowinfo, scope_id=scope_id
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
# Unknown family
|
|
|
|
continue
|
|
|
|
|
|
|
|
addrs.append(
|
|
|
|
AddrInfo(family=family, type=type_, proto=proto, sockaddr=sockaddr)
|
|
|
|
)
|
|
|
|
return addrs
|
|
|
|
|
2019-02-11 16:56:18 +01:00
|
|
|
|
2023-07-19 22:33:28 +02:00
|
|
|
def _async_ip_address_to_addrs(host: str, port: int) -> list[AddrInfo]:
|
2021-09-17 17:44:30 +02:00
|
|
|
"""Convert an ipaddress to AddrInfo."""
|
|
|
|
with contextlib.suppress(ValueError):
|
|
|
|
return [
|
|
|
|
AddrInfo(
|
|
|
|
family=socket.AF_INET6,
|
|
|
|
type=socket.SOCK_STREAM,
|
|
|
|
proto=socket.IPPROTO_TCP,
|
|
|
|
sockaddr=IPv6Sockaddr(
|
|
|
|
address=str(IPv6Address(host)), port=port, flowinfo=0, scope_id=0
|
|
|
|
),
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
with contextlib.suppress(ValueError):
|
|
|
|
return [
|
|
|
|
AddrInfo(
|
|
|
|
family=socket.AF_INET,
|
|
|
|
type=socket.SOCK_STREAM,
|
|
|
|
proto=socket.IPPROTO_TCP,
|
|
|
|
sockaddr=IPv4Sockaddr(
|
|
|
|
address=str(IPv4Address(host)),
|
|
|
|
port=port,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
2021-06-30 17:00:22 +02:00
|
|
|
async def async_resolve_host(
|
|
|
|
host: str,
|
|
|
|
port: int,
|
|
|
|
zeroconf_instance: ZeroconfInstanceType = None,
|
|
|
|
) -> AddrInfo:
|
2023-07-19 22:33:28 +02:00
|
|
|
addrs: list[AddrInfo] = []
|
2021-06-30 17:00:22 +02:00
|
|
|
|
|
|
|
zc_error = None
|
|
|
|
if host.endswith(".local"):
|
|
|
|
name = host[: -len(".local")]
|
|
|
|
try:
|
|
|
|
addrs.extend(
|
|
|
|
await _async_resolve_host_zeroconf(
|
2021-10-13 10:15:30 +02:00
|
|
|
name, port, zeroconf_instance=zeroconf_instance
|
2021-06-30 17:00:22 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
except APIConnectionError as err:
|
|
|
|
zc_error = err
|
|
|
|
|
2023-10-16 00:39:12 +02:00
|
|
|
else:
|
2021-09-17 17:44:30 +02:00
|
|
|
addrs.extend(_async_ip_address_to_addrs(host, port))
|
|
|
|
|
2021-06-30 17:00:22 +02:00
|
|
|
if not addrs:
|
2021-10-13 10:15:30 +02:00
|
|
|
addrs.extend(await _async_resolve_host_getaddrinfo(host, port))
|
2021-06-30 17:00:22 +02:00
|
|
|
|
|
|
|
if not addrs:
|
|
|
|
if zc_error:
|
|
|
|
# Only show ZC error if getaddrinfo also didn't work
|
|
|
|
raise zc_error
|
2021-09-14 12:44:52 +02:00
|
|
|
raise ResolveAPIError(f"Could not resolve host {host} - got no results from OS")
|
2021-06-30 17:00:22 +02:00
|
|
|
|
|
|
|
# Use first matching result
|
|
|
|
# Future: return all matches and use first working one
|
|
|
|
return addrs[0]
|