mirror of
https://github.com/home-assistant/core.git
synced 2026-06-24 23:55:21 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 793cbfd328 | |||
| df3406e58d | |||
| d90bc24ee3 | |||
| cffbb385cb | |||
| b96aa49b8a | |||
| 1f724ba6c4 |
@@ -5,6 +5,7 @@ from functools import partial
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from aiohasupervisor import SupervisorBadRequestError, SupervisorError
|
||||
from aiohasupervisor.models import (
|
||||
@@ -16,7 +17,7 @@ from aiohasupervisor.models import (
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import RefreshToken, User
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components import frontend, network
|
||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||
from homeassistant.components.onboarding import async_is_onboarded
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
@@ -30,6 +31,7 @@ from homeassistant.helpers import (
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@@ -54,14 +56,19 @@ from .auth import async_setup_auth_view
|
||||
from .config import HassioConfig
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_UPDATE_KEY,
|
||||
ATTR_WS_EVENT,
|
||||
DATA_COMPONENT,
|
||||
DATA_CONFIG_STORE,
|
||||
DATA_HASSIO_HOST,
|
||||
DATA_HASSIO_SUPERVISOR_USER,
|
||||
DATA_KEY_SUPERVISOR_ISSUES,
|
||||
DOMAIN,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
MAIN_COORDINATOR,
|
||||
STATS_COORDINATOR,
|
||||
UPDATE_KEY_NETWORK,
|
||||
)
|
||||
from .coordinator import (
|
||||
HassioAddOnDataUpdateCoordinator,
|
||||
@@ -385,6 +392,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
entry.async_on_unload(hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config))
|
||||
|
||||
@callback
|
||||
def _async_supervisor_event(event: dict[str, Any]) -> None:
|
||||
"""Reload network adapters when Supervisor reports a network change."""
|
||||
if (
|
||||
event.get(ATTR_WS_EVENT) == EVENT_SUPERVISOR_UPDATE
|
||||
and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_NETWORK
|
||||
):
|
||||
entry.async_create_background_task(
|
||||
hass, network.async_reload_adapters(hass), "hassio_reload_adapters"
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, EVENT_SUPERVISOR_EVENT, _async_supervisor_event)
|
||||
)
|
||||
|
||||
async def update_hass_api(refresh_token: RefreshToken) -> None:
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
# hass.config.api is always set here: hassio depends on http, and the
|
||||
|
||||
@@ -91,6 +91,7 @@ EVENT_ISSUE_CHANGED = "issue_changed"
|
||||
EVENT_ISSUE_REMOVED = "issue_removed"
|
||||
EVENT_JOB = "job"
|
||||
|
||||
UPDATE_KEY_NETWORK = "network"
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
STARTUP_COMPLETE = "complete"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "hassio",
|
||||
"name": "Home Assistant Supervisor",
|
||||
"after_dependencies": ["network"],
|
||||
"codeowners": ["@home-assistant/supervisor"],
|
||||
"dependencies": ["http", "repairs"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
from homeassistant.util import package
|
||||
|
||||
@@ -17,9 +18,10 @@ from .const import (
|
||||
LOOPBACK_TARGET_IP,
|
||||
MDNS_TARGET_IP,
|
||||
PUBLIC_TARGET_IP,
|
||||
SIGNAL_NETWORK_ADAPTERS_CHANGED,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .network import Network, async_get_loaded_network, async_get_network
|
||||
from .network import DATA_NETWORK, Network, async_get_loaded_network, async_get_network
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +53,14 @@ def async_get_loaded_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
return async_get_loaded_network(hass).adapters
|
||||
|
||||
|
||||
async def async_reload_adapters(hass: HomeAssistant) -> None:
|
||||
"""Reload the network adapters and notify listeners if they changed."""
|
||||
if DATA_NETWORK not in hass.data:
|
||||
return
|
||||
if await async_get_loaded_network(hass).async_reload():
|
||||
async_dispatcher_send(hass, SIGNAL_NETWORK_ADAPTERS_CHANGED)
|
||||
|
||||
|
||||
async def async_get_source_ip(
|
||||
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
||||
) -> str:
|
||||
|
||||
@@ -19,6 +19,8 @@ MDNS_TARGET_IP: Final = "224.0.0.251"
|
||||
PUBLIC_TARGET_IP: Final = "8.8.8.8"
|
||||
IPV4_BROADCAST_ADDR: Final = "255.255.255.255"
|
||||
|
||||
SIGNAL_NETWORK_ADAPTERS_CHANGED: Final = "network_adapters_changed"
|
||||
|
||||
NETWORK_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
|
||||
@@ -63,6 +63,18 @@ class Network:
|
||||
self.adapters = await async_load_adapters()
|
||||
await storage_load_task
|
||||
|
||||
async def async_reload(self) -> bool:
|
||||
"""Reload adapters from the system, returning True if they changed.
|
||||
|
||||
Freshly loaded adapters are unconfigured (all disabled), so the new
|
||||
set must be configured before it can be compared against the current,
|
||||
already configured adapters.
|
||||
"""
|
||||
previous = self.adapters
|
||||
self.adapters = await async_load_adapters()
|
||||
self.async_configure()
|
||||
return self.adapters != previous
|
||||
|
||||
@callback
|
||||
def async_configure(self) -> None:
|
||||
"""Configure from storage."""
|
||||
|
||||
@@ -11,6 +11,7 @@ from zeroconf import InterfaceChoice, IPVersion
|
||||
from zeroconf.asyncio import AsyncServiceInfo
|
||||
|
||||
from homeassistant.components import network
|
||||
from homeassistant.components.network import SIGNAL_NETWORK_ADAPTERS_CHANGED
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_CLOSE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
@@ -18,6 +19,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, instance_id
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_homekit, async_get_zeroconf
|
||||
@@ -174,6 +176,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.data[DATA_DISCOVERY] = discovery
|
||||
websocket_api.async_setup(hass)
|
||||
|
||||
@callback
|
||||
def _async_adapters_changed() -> None:
|
||||
"""Reconcile the zeroconf sockets when the network adapters change."""
|
||||
zc_args = _async_get_zc_args(hass)
|
||||
# async_update_interfaces serializes concurrent calls and no-ops when
|
||||
# the interface set is unchanged, so overlapping tasks are safe.
|
||||
hass.async_create_background_task(
|
||||
aio_zc.async_update_interfaces(
|
||||
interfaces=zc_args["interfaces"],
|
||||
ip_version=zc_args["ip_version"],
|
||||
),
|
||||
"zeroconf_update_interfaces",
|
||||
)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_NETWORK_ADAPTERS_CHANGED, _async_adapters_changed
|
||||
)
|
||||
|
||||
async def _async_zeroconf_hass_start(hass: HomeAssistant, comp: str) -> None:
|
||||
"""Expose Home Assistant on zeroconf when it starts.
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ from homeassistant.components.hassio import (
|
||||
from homeassistant.components.hassio.config import STORAGE_KEY
|
||||
from homeassistant.components.hassio.const import (
|
||||
DATA_KEY_SUPERVISOR_ISSUES,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
HASSIO_MAIN_UPDATE_INTERVAL,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
)
|
||||
@@ -67,6 +68,7 @@ from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -252,6 +254,37 @@ async def test_setup_onboarding_supervisor_update_error(
|
||||
supervisor_client.supervisor.update.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
@pytest.mark.parametrize(
|
||||
("update_key", "expected_calls"),
|
||||
[("network", 1), ("supervisor", 0)],
|
||||
ids=["network", "supervisor"],
|
||||
)
|
||||
async def test_supervisor_network_event_reloads_adapters(
|
||||
hass: HomeAssistant,
|
||||
update_key: str,
|
||||
expected_calls: int,
|
||||
) -> None:
|
||||
"""Test only a Supervisor network event reloads the network adapters."""
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
assert await async_setup_component(hass, DOMAIN, {"hassio": {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch("homeassistant.components.network.async_reload_adapters") as mock_reload:
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
{
|
||||
"event": "supervisor_update",
|
||||
"update_key": update_key,
|
||||
"data": {},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_reload.mock_calls) == expected_calls
|
||||
|
||||
|
||||
async def test_setup_app_panel(hass: HomeAssistant) -> None:
|
||||
"""Test app panel is registered."""
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test the Network Configuration."""
|
||||
|
||||
from copy import deepcopy
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
@@ -13,12 +14,15 @@ from homeassistant.components.network.const import (
|
||||
ATTR_CONFIGURED_ADAPTERS,
|
||||
DOMAIN,
|
||||
MDNS_TARGET_IP,
|
||||
SIGNAL_NETWORK_ADAPTERS_CHANGED,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.network.network import async_get_loaded_network
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import LOOPBACK_IPADDR, NO_LOOPBACK_IPADDR
|
||||
@@ -700,6 +704,49 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_socket_loopback")
|
||||
async def test_async_reload_adapters(hass: HomeAssistant) -> None:
|
||||
"""Test reloading adapters dispatches a signal only when they change."""
|
||||
with patch(
|
||||
"homeassistant.components.network.network.async_load_adapters",
|
||||
return_value=deepcopy(_ADAPTERS_WITH_MANUAL_CONFIG),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
signals: list[None] = []
|
||||
|
||||
@callback
|
||||
def _track() -> None:
|
||||
signals.append(None)
|
||||
|
||||
async_dispatcher_connect(hass, SIGNAL_NETWORK_ADAPTERS_CHANGED, _track)
|
||||
|
||||
# The same hardware loads unconfigured (all disabled); once configured it
|
||||
# matches the current adapters, so no signal is dispatched.
|
||||
unconfigured = deepcopy(_ADAPTERS_WITH_MANUAL_CONFIG)
|
||||
for adapter in unconfigured:
|
||||
adapter["enabled"] = False
|
||||
with patch(
|
||||
"homeassistant.components.network.network.async_load_adapters",
|
||||
return_value=unconfigured,
|
||||
):
|
||||
await network.async_reload_adapters(hass)
|
||||
assert signals == []
|
||||
|
||||
# A physically different adapter set dispatches a signal.
|
||||
changed = deepcopy(_ADAPTERS_WITH_MANUAL_CONFIG)
|
||||
changed[1]["ipv4"][0]["address"] = "192.168.1.99"
|
||||
with patch(
|
||||
"homeassistant.components.network.network.async_load_adapters",
|
||||
return_value=changed,
|
||||
):
|
||||
await network.async_reload_adapters(hass)
|
||||
assert len(signals) == 1
|
||||
# Read the singleton directly; async_get_loaded_adapters is globally mocked.
|
||||
assert async_get_loaded_network(hass).adapters == changed
|
||||
|
||||
|
||||
async def test_async_get_announce_addresses(hass: HomeAssistant) -> None:
|
||||
"""Test addresses for mDNS/etc announcement."""
|
||||
first_ip = "172.16.1.5"
|
||||
|
||||
@@ -14,6 +14,7 @@ from zeroconf.asyncio import AsyncServiceInfo
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.network import SIGNAL_NETWORK_ADAPTERS_CHANGED
|
||||
from homeassistant.components.zeroconf import discovery
|
||||
from homeassistant.const import (
|
||||
EVENT_COMPONENT_LOADED,
|
||||
@@ -25,6 +26,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.generated import zeroconf as zc_gen
|
||||
from homeassistant.helpers.discovery_flow import DiscoveryKey
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.setup import ATTR_COMPONENT, async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
@@ -199,6 +201,22 @@ async def test_setup(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> Non
|
||||
assert await zeroconf.async_get_async_instance(hass) is mock_async_zeroconf
|
||||
|
||||
|
||||
async def test_network_adapters_changed_updates_interfaces(
|
||||
hass: HomeAssistant, mock_async_zeroconf: MagicMock
|
||||
) -> None:
|
||||
"""Test zeroconf reconciles its sockets when the network adapters change."""
|
||||
with patch.object(hass.config_entries.flow, "async_init"):
|
||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_dispatcher_send(hass, SIGNAL_NETWORK_ADAPTERS_CHANGED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_async_zeroconf.async_update_interfaces.mock_calls == [
|
||||
call(interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_async_zeroconf")
|
||||
async def test_setup_with_overly_long_url_and_name(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
|
||||
@@ -1486,6 +1486,7 @@ def mock_async_zeroconf(mock_zeroconf: MagicMock) -> Generator[MagicMock]:
|
||||
zc.async_unregister_service = AsyncMock()
|
||||
zc.async_register_service = AsyncMock()
|
||||
zc.async_update_service = AsyncMock()
|
||||
zc.async_update_interfaces = AsyncMock()
|
||||
zc.zeroconf = Mock(spec=Zeroconf)
|
||||
zc.zeroconf.async_wait_for_start = AsyncMock()
|
||||
# DNSCache has strong Cython type checks, and MagicMock does not work
|
||||
|
||||
@@ -586,8 +586,8 @@ async def test_discovery_requirements_mqtt(hass: HomeAssistant) -> None:
|
||||
) as mock_process:
|
||||
await async_get_integration_with_requirements(hass, "mqtt_comp")
|
||||
|
||||
assert len(mock_process.mock_calls) == 2
|
||||
# one for mqtt and one for hassio
|
||||
assert len(mock_process.mock_calls) == 3
|
||||
# one for mqtt, one for hassio, and one for network (hassio after_dependency)
|
||||
assert mock_process.mock_calls[0][1][1] == mqtt.requirements
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user