mirror of
https://github.com/home-assistant/core.git
synced 2026-01-13 18:48:45 +00:00
Compare commits
1 Commits
dev
...
claude/ip-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7537e9216e |
@@ -16,7 +16,7 @@ from aiohasupervisor.models import GreenOptions, YellowOptions # noqa: F401
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.components import panel_custom
|
||||
from homeassistant.components import network, panel_custom
|
||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
@@ -41,6 +41,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.event import async_call_later
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -78,6 +79,7 @@ from .const import (
|
||||
ATTR_LOCATION,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_SLUG,
|
||||
ATTR_WS_EVENT,
|
||||
DATA_COMPONENT,
|
||||
DATA_CONFIG_STORE,
|
||||
DATA_CORE_INFO,
|
||||
@@ -89,6 +91,8 @@ from .const import (
|
||||
DATA_STORE,
|
||||
DATA_SUPERVISOR_INFO,
|
||||
DOMAIN,
|
||||
EVENT_NETWORK_CHANGED,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
HASSIO_UPDATE_INTERVAL,
|
||||
)
|
||||
from .coordinator import (
|
||||
@@ -380,6 +384,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass, hassio)
|
||||
issues_task = hass.async_create_task(issues.setup(), eager_start=True)
|
||||
|
||||
@callback
|
||||
def _async_handle_supervisor_events(event: dict[str, Any]) -> None:
|
||||
"""Handle supervisor events for network changes."""
|
||||
if event.get(ATTR_WS_EVENT) == EVENT_NETWORK_CHANGED:
|
||||
hass.async_create_task(network.async_notify_network_change(hass))
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, EVENT_SUPERVISOR_EVENT, _async_handle_supervisor_events
|
||||
)
|
||||
|
||||
async def async_service_handler(service: ServiceCall) -> None:
|
||||
"""Handle service calls for Hass.io."""
|
||||
api_endpoint = MAP_SERVICE_API[service.service]
|
||||
|
||||
@@ -70,6 +70,7 @@ EVENT_HEALTH_CHANGED = "health_changed"
|
||||
EVENT_SUPPORTED_CHANGED = "supported_changed"
|
||||
EVENT_ISSUE_CHANGED = "issue_changed"
|
||||
EVENT_ISSUE_REMOVED = "issue_removed"
|
||||
EVENT_NETWORK_CHANGED = "network_changed"
|
||||
EVENT_JOB = "job"
|
||||
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from ipaddress import IPv4Address, IPv6Address, ip_interface
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -22,7 +23,12 @@ from .const import (
|
||||
PUBLIC_TARGET_IP,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .network import Network, async_get_loaded_network, async_get_network
|
||||
from .network import (
|
||||
Network,
|
||||
NetworkChangeCallback,
|
||||
async_get_loaded_network,
|
||||
async_get_network,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -172,6 +178,32 @@ async def async_get_announce_addresses(hass: HomeAssistant) -> list[str]:
|
||||
return list(addresses)
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_network_change_callback(
|
||||
hass: HomeAssistant, callback_fn: NetworkChangeCallback
|
||||
) -> Callable[[], None]:
|
||||
"""Register a callback to be called when network adapters change.
|
||||
|
||||
Returns a function to unregister the callback.
|
||||
|
||||
The callback will be called with the new list of adapters when
|
||||
a network change is detected.
|
||||
"""
|
||||
network: Network = async_get_loaded_network(hass)
|
||||
return network.async_register_change_callback(callback_fn)
|
||||
|
||||
|
||||
async def async_notify_network_change(hass: HomeAssistant) -> None:
|
||||
"""Notify the network integration of a network change.
|
||||
|
||||
This will reload network adapters and notify all registered callbacks.
|
||||
This should be called when external systems (like the supervisor) detect
|
||||
a network change.
|
||||
"""
|
||||
network: Network = await async_get_network(hass)
|
||||
await network.async_notify_network_change()
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up network for Home Assistant."""
|
||||
# Avoid circular issue: http->network->websocket_api->http
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -23,6 +24,8 @@ from .util import async_load_adapters, enable_adapters, enable_auto_detected_ada
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type NetworkChangeCallback = Callable[[list[Adapter]], None]
|
||||
|
||||
DATA_NETWORK: HassKey[Network] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
@@ -48,11 +51,13 @@ class Network:
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the Network class."""
|
||||
self._hass = hass
|
||||
self._store = Store[dict[str, list[str]]](
|
||||
hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True
|
||||
)
|
||||
self._data: dict[str, list[str]] = {}
|
||||
self.adapters: list[Adapter] = []
|
||||
self._change_callbacks: list[NetworkChangeCallback] = []
|
||||
|
||||
@property
|
||||
def configured_adapters(self) -> list[str]:
|
||||
@@ -85,3 +90,34 @@ class Network:
|
||||
async def _async_save(self) -> None:
|
||||
"""Save preferences."""
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
@callback
|
||||
def async_register_change_callback(
|
||||
self, callback_fn: NetworkChangeCallback
|
||||
) -> Callable[[], None]:
|
||||
"""Register a callback to be called when network adapters change.
|
||||
|
||||
Returns a function to unregister the callback.
|
||||
"""
|
||||
self._change_callbacks.append(callback_fn)
|
||||
|
||||
@callback
|
||||
def unregister() -> None:
|
||||
"""Unregister the callback."""
|
||||
self._change_callbacks.remove(callback_fn)
|
||||
|
||||
return unregister
|
||||
|
||||
async def async_notify_network_change(self) -> None:
|
||||
"""Notify listeners of a network change.
|
||||
|
||||
This reloads network adapters and calls all registered callbacks.
|
||||
"""
|
||||
old_adapters = self.adapters
|
||||
self.adapters = await async_load_adapters()
|
||||
self.async_configure()
|
||||
|
||||
if old_adapters != self.adapters:
|
||||
_LOGGER.info("Network adapters changed: %s", self.adapters)
|
||||
for callback_fn in self._change_callbacks:
|
||||
callback_fn(self.adapters)
|
||||
|
||||
@@ -22,12 +22,16 @@ from homeassistant.components.hassio import (
|
||||
)
|
||||
from homeassistant.components.hassio.config import STORAGE_KEY
|
||||
from homeassistant.components.hassio.const import (
|
||||
ATTR_WS_EVENT,
|
||||
EVENT_NETWORK_CHANGED,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
HASSIO_UPDATE_INTERVAL,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
@@ -1388,3 +1392,26 @@ async def test_deprecated_installation_issue_supported_board(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_network_change_event(hass: HomeAssistant) -> None:
|
||||
"""Test network change event from supervisor triggers network notification."""
|
||||
with (
|
||||
patch.dict(os.environ, MOCK_ENVIRON),
|
||||
patch(
|
||||
"homeassistant.components.hassio.HassIO.is_connected",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.network.async_notify_network_change"
|
||||
) as mock_notify,
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_dispatcher_send(
|
||||
hass, EVENT_SUPERVISOR_EVENT, {ATTR_WS_EVENT: EVENT_NETWORK_CHANGED}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_notify.assert_called_once_with(hass)
|
||||
|
||||
@@ -850,3 +850,77 @@ async def test_repair_docker_host_network_without_host_networking(
|
||||
|
||||
assert (issue := issue_registry.async_get_issue(DOMAIN, "docker_host_network"))
|
||||
assert issue == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_socket",
|
||||
[
|
||||
[
|
||||
(MDNS_TARGET_IP, _mock_cond_socket(NO_LOOPBACK_IPADDR)),
|
||||
]
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_socket")
|
||||
async def test_network_change_callbacks(hass: HomeAssistant) -> None:
|
||||
"""Test network change callbacks are called only when adapters change."""
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
callback_calls = []
|
||||
|
||||
def mock_callback(adapters):
|
||||
"""Mock callback to track calls."""
|
||||
callback_calls.append(adapters)
|
||||
|
||||
# Register callback
|
||||
unregister = network.async_register_network_change_callback(hass, mock_callback)
|
||||
|
||||
# Get initial adapters
|
||||
network_obj = hass.data[DOMAIN]
|
||||
initial_adapters = network_obj.adapters
|
||||
|
||||
# Trigger network change with same adapters - callback should NOT be called
|
||||
with patch(
|
||||
"homeassistant.components.network.util.async_load_adapters",
|
||||
return_value=initial_adapters,
|
||||
):
|
||||
await network.async_notify_network_change(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(callback_calls) == 0
|
||||
|
||||
# Trigger network change with different adapters - callback SHOULD be called
|
||||
new_adapters = [
|
||||
{
|
||||
"index": 2,
|
||||
"auto": True,
|
||||
"default": True,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.2.10", "network_prefix": 24}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
}
|
||||
]
|
||||
with patch(
|
||||
"homeassistant.components.network.util.async_load_adapters",
|
||||
return_value=new_adapters,
|
||||
):
|
||||
await network.async_notify_network_change(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(callback_calls) == 1
|
||||
assert callback_calls[0] == new_adapters
|
||||
|
||||
# Unregister callback
|
||||
unregister()
|
||||
|
||||
# Trigger another change - callback should NOT be called
|
||||
with patch(
|
||||
"homeassistant.components.network.util.async_load_adapters",
|
||||
return_value=initial_adapters,
|
||||
):
|
||||
await network.async_notify_network_change(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(callback_calls) == 1
|
||||
|
||||
Reference in New Issue
Block a user