2019-02-13 20:21:14 +00:00
|
|
|
"""Support for exposing Home Assistant via Zeroconf."""
|
2021-03-18 21:58:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-03-23 13:36:43 +00:00
|
|
|
from contextlib import suppress
|
2020-09-11 10:19:21 +00:00
|
|
|
import fnmatch
|
2020-09-13 23:06:19 +00:00
|
|
|
from functools import partial
|
2019-12-09 17:54:54 +00:00
|
|
|
import ipaddress
|
2016-04-10 22:34:04 +00:00
|
|
|
import logging
|
2019-06-05 15:13:40 +00:00
|
|
|
import socket
|
2021-03-30 16:48:04 +00:00
|
|
|
from typing import Any, TypedDict
|
2016-04-10 22:34:04 +00:00
|
|
|
|
2016-09-30 02:02:22 +00:00
|
|
|
import voluptuous as vol
|
2019-11-23 15:46:06 +00:00
|
|
|
from zeroconf import (
|
2020-08-14 03:00:39 +00:00
|
|
|
Error as ZeroconfError,
|
2020-05-06 14:16:59 +00:00
|
|
|
InterfaceChoice,
|
2020-07-15 18:26:40 +00:00
|
|
|
IPVersion,
|
2019-12-09 17:54:54 +00:00
|
|
|
NonUniqueNameException,
|
2019-11-23 15:46:06 +00:00
|
|
|
ServiceInfo,
|
|
|
|
ServiceStateChange,
|
|
|
|
Zeroconf,
|
|
|
|
)
|
2019-05-21 22:36:26 +00:00
|
|
|
|
2019-06-05 15:13:40 +00:00
|
|
|
from homeassistant import util
|
2019-10-13 21:16:27 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
EVENT_HOMEASSISTANT_START,
|
2020-07-22 00:18:43 +00:00
|
|
|
EVENT_HOMEASSISTANT_STARTED,
|
2019-12-09 17:54:54 +00:00
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
2019-10-13 21:16:27 +00:00
|
|
|
__version__,
|
|
|
|
)
|
2021-03-30 16:48:04 +00:00
|
|
|
from homeassistant.core import Event, HomeAssistant
|
2020-05-06 14:16:59 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2020-05-08 19:53:28 +00:00
|
|
|
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
2020-05-11 05:51:23 +00:00
|
|
|
from homeassistant.helpers.singleton import singleton
|
2020-08-05 13:50:56 +00:00
|
|
|
from homeassistant.loader import async_get_homekit, async_get_zeroconf
|
2016-04-10 22:34:04 +00:00
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
from .models import HaServiceBrowser, HaZeroconf
|
2020-08-12 14:08:33 +00:00
|
|
|
from .usage import install_multiple_zeroconf_catcher
|
|
|
|
|
2016-09-30 02:02:22 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2016-04-10 22:34:04 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN = "zeroconf"
|
2016-04-10 23:09:52 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
ZEROCONF_TYPE = "_home-assistant._tcp.local."
|
2021-01-07 18:44:34 +00:00
|
|
|
HOMEKIT_TYPES = [
|
|
|
|
"_hap._tcp.local.",
|
|
|
|
# Thread based devices
|
|
|
|
"_hap._udp.local.",
|
|
|
|
]
|
2016-04-10 22:34:04 +00:00
|
|
|
|
2020-05-06 14:16:59 +00:00
|
|
|
CONF_DEFAULT_INTERFACE = "default_interface"
|
2020-07-15 18:26:40 +00:00
|
|
|
CONF_IPV6 = "ipv6"
|
2021-03-25 01:04:55 +00:00
|
|
|
DEFAULT_DEFAULT_INTERFACE = True
|
2020-07-15 18:26:40 +00:00
|
|
|
DEFAULT_IPV6 = True
|
2020-05-06 14:16:59 +00:00
|
|
|
|
2020-05-12 19:59:29 +00:00
|
|
|
HOMEKIT_PAIRED_STATUS_FLAG = "sf"
|
|
|
|
HOMEKIT_MODEL = "md"
|
|
|
|
|
2020-08-21 12:31:17 +00:00
|
|
|
# Property key=value has a max length of 255
|
|
|
|
# so we use 230 to leave space for key=
|
|
|
|
MAX_PROPERTY_VALUE_LEN = 230
|
|
|
|
|
|
|
|
# Dns label max length
|
|
|
|
MAX_NAME_LEN = 63
|
|
|
|
|
2020-05-06 14:16:59 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
DOMAIN: vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(
|
|
|
|
CONF_DEFAULT_INTERFACE, default=DEFAULT_DEFAULT_INTERFACE
|
2020-07-15 18:26:40 +00:00
|
|
|
): cv.boolean,
|
|
|
|
vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean,
|
2020-05-06 14:16:59 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
2016-04-10 22:34:04 +00:00
|
|
|
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
class HaServiceInfo(TypedDict):
|
|
|
|
"""Prepared info from mDNS entries."""
|
|
|
|
|
|
|
|
host: str
|
|
|
|
port: int | None
|
|
|
|
hostname: str
|
|
|
|
type: str
|
|
|
|
name: str
|
|
|
|
properties: dict[str, Any]
|
|
|
|
|
|
|
|
|
2020-05-11 05:51:23 +00:00
|
|
|
@singleton(DOMAIN)
|
2021-03-30 16:48:04 +00:00
|
|
|
async def async_get_instance(hass: HomeAssistant) -> HaZeroconf:
|
2020-05-11 05:51:23 +00:00
|
|
|
"""Zeroconf instance to be shared with other integrations that use it."""
|
2020-09-13 23:06:19 +00:00
|
|
|
return await _async_get_instance(hass)
|
2020-05-11 05:51:23 +00:00
|
|
|
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf:
|
2020-07-13 17:20:18 +00:00
|
|
|
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
|
2020-07-15 18:26:40 +00:00
|
|
|
|
2020-09-13 23:06:19 +00:00
|
|
|
zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs))
|
2020-07-15 18:26:40 +00:00
|
|
|
|
2020-09-13 23:06:19 +00:00
|
|
|
install_multiple_zeroconf_catcher(zeroconf)
|
2020-05-11 05:51:23 +00:00
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
def _stop_zeroconf(_event: Event) -> None:
|
2020-05-11 05:51:23 +00:00
|
|
|
"""Stop Zeroconf."""
|
|
|
|
zeroconf.ha_close()
|
|
|
|
|
2020-09-13 23:06:19 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf)
|
2020-05-11 05:51:23 +00:00
|
|
|
|
|
|
|
return zeroconf
|
|
|
|
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
2016-04-10 23:02:07 +00:00
|
|
|
"""Set up Zeroconf and make Home Assistant discoverable."""
|
2020-07-15 18:26:40 +00:00
|
|
|
zc_config = config.get(DOMAIN, {})
|
2021-03-30 16:48:04 +00:00
|
|
|
zc_args: dict = {}
|
2020-09-13 23:06:19 +00:00
|
|
|
if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE):
|
|
|
|
zc_args["interfaces"] = InterfaceChoice.Default
|
|
|
|
if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
|
|
|
|
zc_args["ip_version"] = IPVersion.V4Only
|
|
|
|
|
|
|
|
zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args)
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
async def _async_zeroconf_hass_start(_event: Event) -> None:
|
2020-09-13 23:06:19 +00:00
|
|
|
"""Expose Home Assistant on zeroconf when it starts.
|
|
|
|
|
|
|
|
Wait till started or otherwise HTTP is not up and running.
|
|
|
|
"""
|
|
|
|
uuid = await hass.helpers.instance_id.async_get()
|
|
|
|
await hass.async_add_executor_job(
|
|
|
|
_register_hass_zc_service, hass, zeroconf, uuid
|
|
|
|
)
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
async def _async_zeroconf_hass_started(_event: Event) -> None:
|
2020-09-13 23:06:19 +00:00
|
|
|
"""Start the service browser."""
|
|
|
|
|
|
|
|
await _async_start_zeroconf_browser(hass, zeroconf)
|
|
|
|
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start)
|
|
|
|
hass.bus.async_listen_once(
|
|
|
|
EVENT_HOMEASSISTANT_STARTED, _async_zeroconf_hass_started
|
2020-05-11 05:51:23 +00:00
|
|
|
)
|
2020-05-20 06:37:32 +00:00
|
|
|
|
2020-09-13 23:06:19 +00:00
|
|
|
return True
|
2020-08-12 14:08:33 +00:00
|
|
|
|
2016-04-10 22:34:04 +00:00
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
def _register_hass_zc_service(
|
|
|
|
hass: HomeAssistant, zeroconf: HaZeroconf, uuid: str
|
|
|
|
) -> None:
|
2020-09-13 23:06:19 +00:00
|
|
|
# Get instance UUID
|
2020-08-21 12:31:17 +00:00
|
|
|
valid_location_name = _truncate_location_name_to_valid(hass.config.location_name)
|
|
|
|
|
2016-09-30 02:02:22 +00:00
|
|
|
params = {
|
2020-08-21 12:31:17 +00:00
|
|
|
"location_name": valid_location_name,
|
2020-05-20 06:37:32 +00:00
|
|
|
"uuid": uuid,
|
2019-07-31 19:25:30 +00:00
|
|
|
"version": __version__,
|
2020-06-05 19:43:58 +00:00
|
|
|
"external_url": "",
|
|
|
|
"internal_url": "",
|
2020-05-08 00:29:47 +00:00
|
|
|
# Old base URL, for backward compatibility
|
2020-06-05 19:43:58 +00:00
|
|
|
"base_url": "",
|
2019-11-23 15:46:06 +00:00
|
|
|
# Always needs authentication
|
2019-07-31 19:25:30 +00:00
|
|
|
"requires_api_password": True,
|
2016-09-30 02:02:22 +00:00
|
|
|
}
|
2016-04-10 22:34:04 +00:00
|
|
|
|
2020-05-20 06:37:32 +00:00
|
|
|
# Get instance URL's
|
2021-03-23 13:36:43 +00:00
|
|
|
with suppress(NoURLAvailableError):
|
2020-05-08 19:53:28 +00:00
|
|
|
params["external_url"] = get_url(hass, allow_internal=False)
|
2020-05-08 00:29:47 +00:00
|
|
|
|
2021-03-23 13:36:43 +00:00
|
|
|
with suppress(NoURLAvailableError):
|
2020-05-08 19:53:28 +00:00
|
|
|
params["internal_url"] = get_url(hass, allow_external=False)
|
2020-05-08 00:29:47 +00:00
|
|
|
|
|
|
|
# Set old base URL based on external or internal
|
|
|
|
params["base_url"] = params["external_url"] or params["internal_url"]
|
|
|
|
|
2019-06-05 15:13:40 +00:00
|
|
|
host_ip = util.get_local_ip()
|
|
|
|
|
|
|
|
try:
|
|
|
|
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
|
2020-04-04 20:09:11 +00:00
|
|
|
except OSError:
|
2019-06-05 15:13:40 +00:00
|
|
|
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
|
|
|
|
|
2020-08-21 12:31:17 +00:00
|
|
|
_suppress_invalid_properties(params)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
info = ServiceInfo(
|
|
|
|
ZEROCONF_TYPE,
|
2020-08-21 12:31:17 +00:00
|
|
|
name=f"{valid_location_name}.{ZEROCONF_TYPE}",
|
2020-05-20 06:37:32 +00:00
|
|
|
server=f"{uuid}.local.",
|
2019-07-31 19:25:30 +00:00
|
|
|
addresses=[host_ip_pton],
|
|
|
|
port=hass.http.server_port,
|
|
|
|
properties=params,
|
|
|
|
)
|
2019-05-16 19:04:20 +00:00
|
|
|
|
2020-09-13 23:06:19 +00:00
|
|
|
_LOGGER.info("Starting Zeroconf broadcast")
|
|
|
|
try:
|
|
|
|
zeroconf.register_service(info)
|
|
|
|
except NonUniqueNameException:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Home Assistant instance with identical name present in the local network"
|
|
|
|
)
|
2019-10-13 21:16:27 +00:00
|
|
|
|
2017-01-03 20:39:42 +00:00
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
async def _async_start_zeroconf_browser(
|
|
|
|
hass: HomeAssistant, zeroconf: HaZeroconf
|
|
|
|
) -> None:
|
2020-09-13 23:06:19 +00:00
|
|
|
"""Start the zeroconf browser."""
|
|
|
|
|
|
|
|
zeroconf_types = await async_get_zeroconf(hass)
|
|
|
|
homekit_models = await async_get_homekit(hass)
|
|
|
|
|
|
|
|
types = list(zeroconf_types)
|
2016-04-10 22:34:04 +00:00
|
|
|
|
2021-01-07 18:44:34 +00:00
|
|
|
for hk_type in HOMEKIT_TYPES:
|
|
|
|
if hk_type not in zeroconf_types:
|
|
|
|
types.append(hk_type)
|
2020-08-05 13:50:56 +00:00
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
def service_update(
|
|
|
|
zeroconf: Zeroconf,
|
|
|
|
service_type: str,
|
|
|
|
name: str,
|
|
|
|
state_change: ServiceStateChange,
|
|
|
|
) -> None:
|
2019-05-21 22:36:26 +00:00
|
|
|
"""Service state changed."""
|
2020-08-05 13:50:56 +00:00
|
|
|
nonlocal zeroconf_types
|
|
|
|
nonlocal homekit_models
|
|
|
|
|
2021-03-09 20:14:00 +00:00
|
|
|
if state_change == ServiceStateChange.Removed:
|
2019-05-31 18:58:48 +00:00
|
|
|
return
|
|
|
|
|
2020-08-14 03:00:39 +00:00
|
|
|
try:
|
|
|
|
service_info = zeroconf.get_service_info(service_type, name)
|
|
|
|
except ZeroconfError:
|
|
|
|
_LOGGER.exception("Failed to get info for device %s", name)
|
|
|
|
return
|
|
|
|
|
2020-05-14 16:58:40 +00:00
|
|
|
if not service_info:
|
|
|
|
# Prevent the browser thread from collapsing as
|
|
|
|
# service_info can be None
|
2020-05-27 11:31:26 +00:00
|
|
|
_LOGGER.debug("Failed to get info for device %s", name)
|
2020-05-14 16:58:40 +00:00
|
|
|
return
|
|
|
|
|
2019-05-31 18:58:48 +00:00
|
|
|
info = info_from_service(service_info)
|
2020-07-23 06:21:57 +00:00
|
|
|
if not info:
|
|
|
|
# Prevent the browser thread from collapsing
|
|
|
|
_LOGGER.debug("Failed to get addresses for device %s", name)
|
|
|
|
return
|
|
|
|
|
2019-05-31 18:58:48 +00:00
|
|
|
_LOGGER.debug("Discovered new device %s %s", name, info)
|
|
|
|
|
|
|
|
# If we can handle it as a HomeKit discovery, we do that here.
|
2021-01-07 18:44:34 +00:00
|
|
|
if service_type in HOMEKIT_TYPES:
|
2020-08-05 13:50:56 +00:00
|
|
|
discovery_was_forwarded = handle_homekit(hass, homekit_models, info)
|
2020-05-12 19:59:29 +00:00
|
|
|
# Continue on here as homekit_controller
|
|
|
|
# still needs to get updates on devices
|
|
|
|
# so it can see when the 'c#' field is updated.
|
|
|
|
#
|
|
|
|
# We only send updates to homekit_controller
|
|
|
|
# if the device is already paired in order to avoid
|
|
|
|
# offering a second discovery for the same device
|
|
|
|
if (
|
2020-07-14 00:45:05 +00:00
|
|
|
discovery_was_forwarded
|
2021-03-30 16:48:04 +00:00
|
|
|
and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]
|
2020-05-12 19:59:29 +00:00
|
|
|
):
|
|
|
|
try:
|
2020-05-14 16:58:40 +00:00
|
|
|
# 0 means paired and not discoverable by iOS clients)
|
2021-03-30 16:48:04 +00:00
|
|
|
if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]):
|
2020-05-12 19:59:29 +00:00
|
|
|
return
|
|
|
|
except ValueError:
|
|
|
|
# HomeKit pairing status unknown
|
|
|
|
# likely bad homekit data
|
|
|
|
return
|
2019-05-31 18:58:48 +00:00
|
|
|
|
2020-12-31 00:06:26 +00:00
|
|
|
if "name" in info:
|
2021-03-30 16:48:04 +00:00
|
|
|
lowercase_name: str | None = info["name"].lower()
|
2020-12-31 00:06:26 +00:00
|
|
|
else:
|
|
|
|
lowercase_name = None
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
if "macaddress" in info["properties"]:
|
|
|
|
uppercase_mac: str | None = info["properties"]["macaddress"].upper()
|
2020-12-31 00:06:26 +00:00
|
|
|
else:
|
|
|
|
uppercase_mac = None
|
|
|
|
|
2021-04-08 19:03:10 +00:00
|
|
|
if "manufacturer" in info["properties"]:
|
|
|
|
lowercase_manufacturer: str | None = info["properties"][
|
|
|
|
"manufacturer"
|
|
|
|
].lower()
|
|
|
|
else:
|
|
|
|
lowercase_manufacturer = None
|
|
|
|
|
2021-01-07 18:44:34 +00:00
|
|
|
# Not all homekit types are currently used for discovery
|
|
|
|
# so not all service type exist in zeroconf_types
|
|
|
|
for entry in zeroconf_types.get(service_type, []):
|
2020-09-11 10:19:21 +00:00
|
|
|
if len(entry) > 1:
|
2020-12-31 00:06:26 +00:00
|
|
|
if (
|
|
|
|
uppercase_mac is not None
|
|
|
|
and "macaddress" in entry
|
|
|
|
and not fnmatch.fnmatch(uppercase_mac, entry["macaddress"])
|
|
|
|
):
|
|
|
|
continue
|
|
|
|
if (
|
|
|
|
lowercase_name is not None
|
|
|
|
and "name" in entry
|
|
|
|
and not fnmatch.fnmatch(lowercase_name, entry["name"])
|
|
|
|
):
|
|
|
|
continue
|
2021-04-08 19:03:10 +00:00
|
|
|
if (
|
|
|
|
lowercase_manufacturer is not None
|
|
|
|
and "manufacturer" in entry
|
|
|
|
and not fnmatch.fnmatch(
|
|
|
|
lowercase_manufacturer, entry["manufacturer"]
|
|
|
|
)
|
|
|
|
):
|
|
|
|
continue
|
2020-09-11 10:19:21 +00:00
|
|
|
|
2019-05-31 18:58:48 +00:00
|
|
|
hass.add_job(
|
|
|
|
hass.config_entries.flow.async_init(
|
2020-09-11 10:19:21 +00:00
|
|
|
entry["domain"], context={"source": DOMAIN}, data=info
|
2021-03-30 16:48:04 +00:00
|
|
|
) # type: ignore
|
2019-05-31 18:58:48 +00:00
|
|
|
)
|
2019-05-29 21:20:06 +00:00
|
|
|
|
2020-09-13 23:06:19 +00:00
|
|
|
_LOGGER.debug("Starting Zeroconf browser")
|
|
|
|
HaServiceBrowser(zeroconf, types, handlers=[service_update])
|
2019-05-21 22:36:26 +00:00
|
|
|
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
def handle_homekit(
|
|
|
|
hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo
|
|
|
|
) -> bool:
|
2019-05-31 18:58:48 +00:00
|
|
|
"""Handle a HomeKit discovery.
|
|
|
|
|
|
|
|
Return if discovery was forwarded.
|
|
|
|
"""
|
|
|
|
model = None
|
2021-03-30 16:48:04 +00:00
|
|
|
props = info["properties"]
|
2019-05-31 18:58:48 +00:00
|
|
|
|
|
|
|
for key in props:
|
2020-05-12 19:59:29 +00:00
|
|
|
if key.lower() == HOMEKIT_MODEL:
|
2019-05-31 18:58:48 +00:00
|
|
|
model = props[key]
|
|
|
|
break
|
|
|
|
|
|
|
|
if model is None:
|
|
|
|
return False
|
|
|
|
|
2020-08-05 13:50:56 +00:00
|
|
|
for test_model in homekit_models:
|
2020-03-26 22:15:35 +00:00
|
|
|
if (
|
|
|
|
model != test_model
|
2020-04-05 14:01:41 +00:00
|
|
|
and not model.startswith(f"{test_model} ")
|
|
|
|
and not model.startswith(f"{test_model}-")
|
2020-03-26 22:15:35 +00:00
|
|
|
):
|
2019-05-31 18:58:48 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
hass.add_job(
|
|
|
|
hass.config_entries.flow.async_init(
|
2020-08-05 13:50:56 +00:00
|
|
|
homekit_models[test_model], context={"source": "homekit"}, data=info
|
2021-03-30 16:48:04 +00:00
|
|
|
) # type: ignore
|
2019-05-31 18:58:48 +00:00
|
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
def info_from_service(service: ServiceInfo) -> HaServiceInfo | None:
|
2019-05-21 22:36:26 +00:00
|
|
|
"""Return prepared info from mDNS entries."""
|
2021-03-30 16:48:04 +00:00
|
|
|
properties: dict[str, Any] = {"_raw": {}}
|
2019-05-21 22:36:26 +00:00
|
|
|
|
|
|
|
for key, value in service.properties.items():
|
2020-01-22 05:24:59 +00:00
|
|
|
# See https://ietf.org/rfc/rfc6763.html#section-6.4 and
|
|
|
|
# https://ietf.org/rfc/rfc6763.html#section-6.5 for expected encodings
|
|
|
|
# for property keys and values
|
2020-04-18 00:36:46 +00:00
|
|
|
try:
|
|
|
|
key = key.decode("ascii")
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
_LOGGER.debug(
|
|
|
|
"Ignoring invalid key provided by [%s]: %s", service.name, key
|
|
|
|
)
|
|
|
|
continue
|
|
|
|
|
2020-01-22 05:24:59 +00:00
|
|
|
properties["_raw"][key] = value
|
|
|
|
|
2021-03-23 13:36:43 +00:00
|
|
|
with suppress(UnicodeDecodeError):
|
2019-05-21 22:36:26 +00:00
|
|
|
if isinstance(value, bytes):
|
2020-01-22 05:24:59 +00:00
|
|
|
properties[key] = value.decode("utf-8")
|
2019-05-21 22:36:26 +00:00
|
|
|
|
2020-07-23 06:21:57 +00:00
|
|
|
if not service.addresses:
|
|
|
|
return None
|
|
|
|
|
2019-06-04 21:14:51 +00:00
|
|
|
address = service.addresses[0]
|
2019-05-21 22:36:26 +00:00
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
return {
|
|
|
|
"host": str(ipaddress.ip_address(address)),
|
|
|
|
"port": service.port,
|
|
|
|
"hostname": service.server,
|
|
|
|
"type": service.type,
|
|
|
|
"name": service.name,
|
|
|
|
"properties": properties,
|
2019-05-21 22:36:26 +00:00
|
|
|
}
|
|
|
|
|
2020-08-21 12:31:17 +00:00
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
def _suppress_invalid_properties(properties: dict) -> None:
|
2020-08-21 12:31:17 +00:00
|
|
|
"""Suppress any properties that will cause zeroconf to fail to startup."""
|
|
|
|
|
|
|
|
for prop, prop_value in properties.items():
|
|
|
|
if not isinstance(prop_value, str):
|
|
|
|
continue
|
|
|
|
|
|
|
|
if len(prop_value.encode("utf-8")) > MAX_PROPERTY_VALUE_LEN:
|
|
|
|
_LOGGER.error(
|
|
|
|
"The property '%s' was suppressed because it is longer than the maximum length of %d bytes: %s",
|
|
|
|
prop,
|
|
|
|
MAX_PROPERTY_VALUE_LEN,
|
|
|
|
prop_value,
|
|
|
|
)
|
|
|
|
properties[prop] = ""
|
|
|
|
|
|
|
|
|
2021-03-30 16:48:04 +00:00
|
|
|
def _truncate_location_name_to_valid(location_name: str) -> str:
|
2020-08-21 12:31:17 +00:00
|
|
|
"""Truncate or return the location name usable for zeroconf."""
|
|
|
|
if len(location_name.encode("utf-8")) < MAX_NAME_LEN:
|
|
|
|
return location_name
|
|
|
|
|
|
|
|
_LOGGER.warning(
|
|
|
|
"The location name was truncated because it is longer than the maximum length of %d bytes: %s",
|
|
|
|
MAX_NAME_LEN,
|
|
|
|
location_name,
|
|
|
|
)
|
|
|
|
return location_name.encode("utf-8")[:MAX_NAME_LEN].decode("utf-8", "ignore")
|