mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Fix race configuring zeroconf (#138425)
This commit is contained in:
parent
ab2e075b41
commit
bbbad90ca2
@ -150,6 +150,10 @@ LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
|
|||||||
"isal",
|
"isal",
|
||||||
# Set log levels
|
# Set log levels
|
||||||
"logger",
|
"logger",
|
||||||
|
# Ensure network config is available
|
||||||
|
# before hassio or any other integration is
|
||||||
|
# loaded that might create an aiohttp client session
|
||||||
|
"network",
|
||||||
# Error logging
|
# Error logging
|
||||||
"system_log",
|
"system_log",
|
||||||
"sentry",
|
"sentry",
|
||||||
|
@ -20,7 +20,7 @@ from .const import (
|
|||||||
PUBLIC_TARGET_IP,
|
PUBLIC_TARGET_IP,
|
||||||
)
|
)
|
||||||
from .models import Adapter
|
from .models import Adapter
|
||||||
from .network import Network, async_get_network
|
from .network import Network, async_get_loaded_network, async_get_network
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -34,6 +34,12 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
|||||||
return network.adapters
|
return network.adapters
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_loaded_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||||
|
"""Get the network adapter configuration."""
|
||||||
|
return async_get_loaded_network(hass).adapters
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
async def async_get_source_ip(
|
async def async_get_source_ip(
|
||||||
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
||||||
@ -74,7 +80,14 @@ async def async_get_enabled_source_ips(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> list[IPv4Address | IPv6Address]:
|
) -> list[IPv4Address | IPv6Address]:
|
||||||
"""Build the list of enabled source ips."""
|
"""Build the list of enabled source ips."""
|
||||||
adapters = await async_get_adapters(hass)
|
return async_get_enabled_source_ips_from_adapters(await async_get_adapters(hass))
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_enabled_source_ips_from_adapters(
|
||||||
|
adapters: list[Adapter],
|
||||||
|
) -> list[IPv4Address | IPv6Address]:
|
||||||
|
"""Build the list of enabled source ips."""
|
||||||
sources: list[IPv4Address | IPv6Address] = []
|
sources: list[IPv4Address | IPv6Address] = []
|
||||||
for adapter in adapters:
|
for adapter in adapters:
|
||||||
if not adapter["enabled"]:
|
if not adapter["enabled"]:
|
||||||
@ -151,5 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
async_register_websocket_commands,
|
async_register_websocket_commands,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await async_get_network(hass)
|
||||||
|
|
||||||
async_register_websocket_commands(hass)
|
async_register_websocket_commands(hass)
|
||||||
return True
|
return True
|
||||||
|
@ -12,8 +12,6 @@ DOMAIN: Final = "network"
|
|||||||
STORAGE_KEY: Final = "core.network"
|
STORAGE_KEY: Final = "core.network"
|
||||||
STORAGE_VERSION: Final = 1
|
STORAGE_VERSION: Final = 1
|
||||||
|
|
||||||
DATA_NETWORK: Final = "network"
|
|
||||||
|
|
||||||
ATTR_ADAPTERS: Final = "adapters"
|
ATTR_ADAPTERS: Final = "adapters"
|
||||||
ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
|
ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
|
||||||
DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
|
DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
|
||||||
|
@ -9,11 +9,12 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.singleton import singleton
|
from homeassistant.helpers.singleton import singleton
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.util.async_ import create_eager_task
|
from homeassistant.util.async_ import create_eager_task
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CONFIGURED_ADAPTERS,
|
ATTR_CONFIGURED_ADAPTERS,
|
||||||
DATA_NETWORK,
|
|
||||||
DEFAULT_CONFIGURED_ADAPTERS,
|
DEFAULT_CONFIGURED_ADAPTERS,
|
||||||
|
DOMAIN,
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
STORAGE_VERSION,
|
STORAGE_VERSION,
|
||||||
)
|
)
|
||||||
@ -22,8 +23,16 @@ from .util import async_load_adapters, enable_adapters, enable_auto_detected_ada
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_NETWORK: HassKey[Network] = HassKey(DOMAIN)
|
||||||
|
|
||||||
@singleton(DATA_NETWORK)
|
|
||||||
|
@callback
|
||||||
|
def async_get_loaded_network(hass: HomeAssistant) -> Network:
|
||||||
|
"""Get network singleton."""
|
||||||
|
return hass.data[DATA_NETWORK]
|
||||||
|
|
||||||
|
|
||||||
|
@singleton(DOMAIN)
|
||||||
async def async_get_network(hass: HomeAssistant) -> Network:
|
async def async_get_network(hass: HomeAssistant) -> Network:
|
||||||
"""Get network singleton."""
|
"""Get network singleton."""
|
||||||
network = Network(hass)
|
network = Network(hass)
|
||||||
|
@ -141,13 +141,13 @@ def async_get_async_zeroconf(hass: HomeAssistant) -> HaAsyncZeroconf:
|
|||||||
return _async_get_instance(hass)
|
return _async_get_instance(hass)
|
||||||
|
|
||||||
|
|
||||||
def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZeroconf:
|
def _async_get_instance(hass: HomeAssistant) -> HaAsyncZeroconf:
|
||||||
if DOMAIN in hass.data:
|
if DOMAIN in hass.data:
|
||||||
return cast(HaAsyncZeroconf, hass.data[DOMAIN])
|
return cast(HaAsyncZeroconf, hass.data[DOMAIN])
|
||||||
|
|
||||||
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
|
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
|
||||||
|
|
||||||
zeroconf = HaZeroconf(**zcargs)
|
zeroconf = HaZeroconf(**_async_get_zc_args(hass))
|
||||||
aio_zc = HaAsyncZeroconf(zc=zeroconf)
|
aio_zc = HaAsyncZeroconf(zc=zeroconf)
|
||||||
|
|
||||||
install_multiple_zeroconf_catcher(zeroconf)
|
install_multiple_zeroconf_catcher(zeroconf)
|
||||||
@ -175,12 +175,10 @@ def _async_zc_has_functional_dual_stack() -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
def _async_get_zc_args(hass: HomeAssistant) -> dict[str, Any]:
|
||||||
"""Set up Zeroconf and make Home Assistant discoverable."""
|
"""Get zeroconf arguments from config."""
|
||||||
zc_args: dict = {"ip_version": IPVersion.V4Only}
|
zc_args: dict[str, Any] = {"ip_version": IPVersion.V4Only}
|
||||||
|
adapters = network.async_get_loaded_adapters(hass)
|
||||||
adapters = await network.async_get_adapters(hass)
|
|
||||||
|
|
||||||
ipv6 = False
|
ipv6 = False
|
||||||
if _async_zc_has_functional_dual_stack():
|
if _async_zc_has_functional_dual_stack():
|
||||||
if any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
|
if any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
|
||||||
@ -195,7 +193,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
else:
|
else:
|
||||||
zc_args["interfaces"] = [
|
zc_args["interfaces"] = [
|
||||||
str(source_ip)
|
str(source_ip)
|
||||||
for source_ip in await network.async_get_enabled_source_ips(hass)
|
for source_ip in network.async_get_enabled_source_ips_from_adapters(
|
||||||
|
adapters
|
||||||
|
)
|
||||||
if not source_ip.is_loopback
|
if not source_ip.is_loopback
|
||||||
and not (isinstance(source_ip, IPv6Address) and source_ip.is_global)
|
and not (isinstance(source_ip, IPv6Address) and source_ip.is_global)
|
||||||
and not (
|
and not (
|
||||||
@ -207,8 +207,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
and zc_args["ip_version"] == IPVersion.V6Only
|
and zc_args["ip_version"] == IPVersion.V6Only
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
return zc_args
|
||||||
|
|
||||||
aio_zc = _async_get_instance(hass, **zc_args)
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up Zeroconf and make Home Assistant discoverable."""
|
||||||
|
aio_zc = _async_get_instance(hass)
|
||||||
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
||||||
zeroconf_types = await async_get_zeroconf(hass)
|
zeroconf_types = await async_get_zeroconf(hass)
|
||||||
homekit_models = await async_get_homekit(hass)
|
homekit_models = await async_get_homekit(hass)
|
||||||
|
@ -1090,7 +1090,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(
|
|||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
@ -1178,7 +1178,7 @@ async def test_async_detect_interfaces_setting_empty_route_linux(
|
|||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
@ -1212,7 +1212,7 @@ async def test_async_detect_interfaces_setting_empty_route_freebsd(
|
|||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
@ -1263,7 +1263,7 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_linux(
|
|||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
@ -1292,7 +1292,7 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd(
|
|||||||
patch.object(hass.config_entries.flow, "async_init"),
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
@ -1310,6 +1310,36 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_async_zeroconf")
|
||||||
|
async def test_async_detect_interfaces_explicitly_before_setup(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test interfaces are explicitly set with IPv6 before setup is called."""
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.zeroconf.sys.platform", "linux"),
|
||||||
|
patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc,
|
||||||
|
patch.object(hass.config_entries.flow, "async_init"),
|
||||||
|
patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.zeroconf.network.async_get_loaded_adapters",
|
||||||
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
||||||
|
side_effect=get_service_info_mock,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
# Call before async_setup has been called
|
||||||
|
await zeroconf.async_get_async_instance(hass)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_zc.mock_calls[0] == call(
|
||||||
|
interfaces=["192.168.1.5", "fe80::dead:beef:dead:beef%3"],
|
||||||
|
ip_version=IPVersion.All,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_no_name(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> None:
|
async def test_no_name(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> None:
|
||||||
"""Test fallback to Home for mDNS announcement if the name is missing."""
|
"""Test fallback to Home for mDNS announcement if the name is missing."""
|
||||||
hass.config.location_name = ""
|
hass.config.location_name = ""
|
||||||
|
@ -1180,15 +1180,31 @@ async def mqtt_mock_entry(
|
|||||||
@pytest.fixture(autouse=True, scope="session")
|
@pytest.fixture(autouse=True, scope="session")
|
||||||
def mock_network() -> Generator[None]:
|
def mock_network() -> Generator[None]:
|
||||||
"""Mock network."""
|
"""Mock network."""
|
||||||
with patch(
|
with (
|
||||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
patch(
|
||||||
return_value=[
|
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||||
Mock(
|
return_value=[
|
||||||
nice_name="eth0",
|
Mock(
|
||||||
ips=[Mock(is_IPv6=False, ip="10.10.10.10", network_prefix=24)],
|
nice_name="eth0",
|
||||||
index=0,
|
ips=[Mock(is_IPv6=False, ip="10.10.10.10", network_prefix=24)],
|
||||||
)
|
index=0,
|
||||||
],
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.network.async_get_loaded_adapters",
|
||||||
|
return_value=[
|
||||||
|
{
|
||||||
|
"auto": True,
|
||||||
|
"default": True,
|
||||||
|
"enabled": True,
|
||||||
|
"index": 0,
|
||||||
|
"ipv4": [{"address": "10.10.10.10", "network_prefix": 24}],
|
||||||
|
"ipv6": [],
|
||||||
|
"name": "eth0",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
),
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user