mirror of https://github.com/home-assistant/core
Add network configuration integration (#50874)
Co-authored-by: Ruslan Sayfutdinov <ruslan@sayfutdinov.com> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
16e90f12ca
commit
64661ee2b7
|
@ -45,6 +45,7 @@ homeassistant.components.lock.*
|
|||
homeassistant.components.mailbox.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.nam.*
|
||||
homeassistant.components.network.*
|
||||
homeassistant.components.notify.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.onewire.*
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"media_source",
|
||||
"mobile_app",
|
||||
"my",
|
||||
"network",
|
||||
"person",
|
||||
"scene",
|
||||
"script",
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
"""The Network Configuration integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api.connection import ActiveConnection
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
ATTR_ADAPTERS,
|
||||
ATTR_CONFIGURED_ADAPTERS,
|
||||
DOMAIN,
|
||||
NETWORK_CONFIG_SCHEMA,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .network import Network
|
||||
|
||||
ZEROCONF_DOMAIN = "zeroconf" # cannot import from zeroconf due to circular dep
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
"""Get the network adapter configuration."""
|
||||
network: Network = hass.data[DOMAIN]
|
||||
return network.adapters
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up network for Home Assistant."""
|
||||
|
||||
hass.data[DOMAIN] = network = Network(hass)
|
||||
await network.async_setup()
|
||||
if ZEROCONF_DOMAIN in config:
|
||||
await network.async_migrate_from_zeroconf(config[ZEROCONF_DOMAIN])
|
||||
network.async_configure()
|
||||
|
||||
_LOGGER.debug("Adapters: %s", network.adapters)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_network_adapters)
|
||||
websocket_api.async_register_command(hass, websocket_network_adapters_configure)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "network"})
|
||||
@websocket_api.async_response
|
||||
async def websocket_network_adapters(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict,
|
||||
) -> None:
|
||||
"""Return network preferences."""
|
||||
network: Network = hass.data[DOMAIN]
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
ATTR_ADAPTERS: network.adapters,
|
||||
ATTR_CONFIGURED_ADAPTERS: network.configured_adapters,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "network/configure",
|
||||
vol.Required("config", default={}): NETWORK_CONFIG_SCHEMA,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def websocket_network_adapters_configure(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict,
|
||||
) -> None:
|
||||
"""Update network config."""
|
||||
network: Network = hass.data[DOMAIN]
|
||||
|
||||
await network.async_reconfig(msg["config"])
|
||||
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{ATTR_CONFIGURED_ADAPTERS: network.configured_adapters},
|
||||
)
|
|
@ -0,0 +1,27 @@
|
|||
"""Constants for the network integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN: Final = "network"
|
||||
STORAGE_KEY: Final = "core.network"
|
||||
STORAGE_VERSION: Final = 1
|
||||
|
||||
ATTR_ADAPTERS: Final = "adapters"
|
||||
ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
|
||||
DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
|
||||
|
||||
MDNS_TARGET_IP: Final = "224.0.0.251"
|
||||
|
||||
|
||||
NETWORK_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
ATTR_CONFIGURED_ADAPTERS, default=DEFAULT_CONFIGURED_ADAPTERS
|
||||
): vol.Schema(vol.All(cv.ensure_list, [cv.string])),
|
||||
}
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"domain": "network",
|
||||
"name": "Network Configuration",
|
||||
"documentation": "https://www.home-assistant.io/integrations/network",
|
||||
"requirements": ["ifaddr==0.1.7"],
|
||||
"codeowners": [],
|
||||
"dependencies": ["websocket_api"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_push"
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
"""Models helper class for the network integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class IPv6ConfiguredAddress(TypedDict):
|
||||
"""Represent an IPv6 address."""
|
||||
|
||||
address: str
|
||||
flowinfo: int
|
||||
scope_id: int
|
||||
network_prefix: int
|
||||
|
||||
|
||||
class IPv4ConfiguredAddress(TypedDict):
|
||||
"""Represent an IPv4 address."""
|
||||
|
||||
address: str
|
||||
network_prefix: int
|
||||
|
||||
|
||||
class Adapter(TypedDict):
|
||||
"""Configured network adapters."""
|
||||
|
||||
name: str
|
||||
enabled: bool
|
||||
auto: bool
|
||||
default: bool
|
||||
ipv6: list[IPv6ConfiguredAddress]
|
||||
ipv4: list[IPv4ConfiguredAddress]
|
|
@ -0,0 +1,78 @@
|
|||
"""Network helper class for the network integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import (
|
||||
ATTR_CONFIGURED_ADAPTERS,
|
||||
DEFAULT_CONFIGURED_ADAPTERS,
|
||||
NETWORK_CONFIG_SCHEMA,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .util import (
|
||||
adapters_with_exernal_addresses,
|
||||
async_load_adapters,
|
||||
enable_adapters,
|
||||
enable_auto_detected_adapters,
|
||||
)
|
||||
|
||||
|
||||
class Network:
|
||||
"""Network helper class for the network integration."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the Network class."""
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
self._data: dict[str, Any] = {}
|
||||
self.adapters: list[Adapter] = []
|
||||
|
||||
@property
|
||||
def configured_adapters(self) -> list[str]:
|
||||
"""Return the configured adapters."""
|
||||
return self._data.get(ATTR_CONFIGURED_ADAPTERS, DEFAULT_CONFIGURED_ADAPTERS)
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the network config."""
|
||||
await self.async_load()
|
||||
self.adapters = await async_load_adapters()
|
||||
|
||||
async def async_migrate_from_zeroconf(self, zc_config: dict[str, Any]) -> None:
|
||||
"""Migrate configuration from zeroconf."""
|
||||
if self._data or not zc_config:
|
||||
return
|
||||
|
||||
from homeassistant.components.zeroconf import ( # pylint: disable=import-outside-toplevel
|
||||
CONF_DEFAULT_INTERFACE,
|
||||
)
|
||||
|
||||
if zc_config.get(CONF_DEFAULT_INTERFACE) is False:
|
||||
self._data[ATTR_CONFIGURED_ADAPTERS] = adapters_with_exernal_addresses(
|
||||
self.adapters
|
||||
)
|
||||
await self._async_save()
|
||||
|
||||
@callback
|
||||
def async_configure(self) -> None:
|
||||
"""Configure from storage."""
|
||||
if not enable_adapters(self.adapters, self.configured_adapters):
|
||||
enable_auto_detected_adapters(self.adapters)
|
||||
|
||||
async def async_reconfig(self, config: dict[str, Any]) -> None:
|
||||
"""Reconfigure network."""
|
||||
config = NETWORK_CONFIG_SCHEMA(config)
|
||||
self._data[ATTR_CONFIGURED_ADAPTERS] = config[ATTR_CONFIGURED_ADAPTERS]
|
||||
self.async_configure()
|
||||
await self._async_save()
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load config."""
|
||||
if stored := await self._store.async_load():
|
||||
self._data = cast(dict, stored)
|
||||
|
||||
async def _async_save(self) -> None:
|
||||
"""Save preferences."""
|
||||
await self._store.async_save(self._data)
|
|
@ -0,0 +1,158 @@
|
|||
"""Network helper class for the network integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ipaddress import IPv4Address, IPv6Address, ip_address
|
||||
import logging
|
||||
import socket
|
||||
from typing import cast
|
||||
|
||||
import ifaddr
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import MDNS_TARGET_IP
|
||||
from .models import Adapter, IPv4ConfiguredAddress, IPv6ConfiguredAddress
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_load_adapters() -> list[Adapter]:
|
||||
"""Load adapters."""
|
||||
source_ip = async_get_source_ip(MDNS_TARGET_IP)
|
||||
source_ip_address = ip_address(source_ip) if source_ip else None
|
||||
|
||||
ha_adapters: list[Adapter] = [
|
||||
_ifaddr_adapter_to_ha(adapter, source_ip_address)
|
||||
for adapter in ifaddr.get_adapters()
|
||||
]
|
||||
|
||||
if not any(adapter["default"] and adapter["auto"] for adapter in ha_adapters):
|
||||
for adapter in ha_adapters:
|
||||
if _adapter_has_external_address(adapter):
|
||||
adapter["auto"] = True
|
||||
|
||||
return ha_adapters
|
||||
|
||||
|
||||
def enable_adapters(adapters: list[Adapter], enabled_interfaces: list[str]) -> bool:
|
||||
"""Enable configured adapters."""
|
||||
_reset_enabled_adapters(adapters)
|
||||
|
||||
if not enabled_interfaces:
|
||||
return False
|
||||
|
||||
found_adapter = False
|
||||
for adapter in adapters:
|
||||
if adapter["name"] in enabled_interfaces:
|
||||
adapter["enabled"] = True
|
||||
found_adapter = True
|
||||
|
||||
return found_adapter
|
||||
|
||||
|
||||
def enable_auto_detected_adapters(adapters: list[Adapter]) -> None:
|
||||
"""Enable auto detected adapters."""
|
||||
enable_adapters(
|
||||
adapters, [adapter["name"] for adapter in adapters if adapter["auto"]]
|
||||
)
|
||||
|
||||
|
||||
def adapters_with_exernal_addresses(adapters: list[Adapter]) -> list[str]:
|
||||
"""Enable all interfaces with an external address."""
|
||||
return [
|
||||
adapter["name"]
|
||||
for adapter in adapters
|
||||
if _adapter_has_external_address(adapter)
|
||||
]
|
||||
|
||||
|
||||
def _adapter_has_external_address(adapter: Adapter) -> bool:
|
||||
"""Adapter has a non-loopback and non-link-local address."""
|
||||
return any(
|
||||
_has_external_address(v4_config["address"]) for v4_config in adapter["ipv4"]
|
||||
) or any(
|
||||
_has_external_address(v6_config["address"]) for v6_config in adapter["ipv6"]
|
||||
)
|
||||
|
||||
|
||||
def _has_external_address(ip_str: str) -> bool:
|
||||
return _ip_address_is_external(ip_address(ip_str))
|
||||
|
||||
|
||||
def _ip_address_is_external(ip_addr: IPv4Address | IPv6Address) -> bool:
|
||||
return (
|
||||
not ip_addr.is_multicast
|
||||
and not ip_addr.is_loopback
|
||||
and not ip_addr.is_link_local
|
||||
)
|
||||
|
||||
|
||||
def _reset_enabled_adapters(adapters: list[Adapter]) -> None:
|
||||
for adapter in adapters:
|
||||
adapter["enabled"] = False
|
||||
|
||||
|
||||
def _ifaddr_adapter_to_ha(
|
||||
adapter: ifaddr.Adapter, next_hop_address: None | IPv4Address | IPv6Address
|
||||
) -> Adapter:
|
||||
"""Convert an ifaddr adapter to ha."""
|
||||
ip_v4s: list[IPv4ConfiguredAddress] = []
|
||||
ip_v6s: list[IPv6ConfiguredAddress] = []
|
||||
default = False
|
||||
auto = False
|
||||
|
||||
for ip_config in adapter.ips:
|
||||
if ip_config.is_IPv6:
|
||||
ip_addr = ip_address(ip_config.ip[0])
|
||||
ip_v6s.append(_ip_v6_from_adapter(ip_config))
|
||||
else:
|
||||
ip_addr = ip_address(ip_config.ip)
|
||||
ip_v4s.append(_ip_v4_from_adapter(ip_config))
|
||||
|
||||
if ip_addr == next_hop_address:
|
||||
default = True
|
||||
if _ip_address_is_external(ip_addr):
|
||||
auto = True
|
||||
|
||||
return {
|
||||
"name": adapter.nice_name,
|
||||
"enabled": False,
|
||||
"auto": auto,
|
||||
"default": default,
|
||||
"ipv4": ip_v4s,
|
||||
"ipv6": ip_v6s,
|
||||
}
|
||||
|
||||
|
||||
def _ip_v6_from_adapter(ip_config: ifaddr.IP) -> IPv6ConfiguredAddress:
|
||||
return {
|
||||
"address": ip_config.ip[0],
|
||||
"flowinfo": ip_config.ip[1],
|
||||
"scope_id": ip_config.ip[2],
|
||||
"network_prefix": ip_config.network_prefix,
|
||||
}
|
||||
|
||||
|
||||
def _ip_v4_from_adapter(ip_config: ifaddr.IP) -> IPv4ConfiguredAddress:
|
||||
return {
|
||||
"address": ip_config.ip,
|
||||
"network_prefix": ip_config.network_prefix,
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_source_ip(target_ip: str) -> str | None:
|
||||
"""Return the source ip that will reach target_ip."""
|
||||
test_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
test_sock.setblocking(False) # must be non-blocking for async
|
||||
try:
|
||||
test_sock.connect((target_ip, 1))
|
||||
return cast(str, test_sock.getsockname()[0])
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.debug(
|
||||
"The system could not auto detect the source ip for %s on your operating system",
|
||||
target_ip,
|
||||
)
|
||||
return None
|
||||
finally:
|
||||
test_sock.close()
|
|
@ -2,16 +2,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine, Iterable
|
||||
from collections.abc import Coroutine
|
||||
from contextlib import suppress
|
||||
import fnmatch
|
||||
import ipaddress
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any, TypedDict, cast
|
||||
|
||||
from pyroute2 import IPRoute
|
||||
import voluptuous as vol
|
||||
from zeroconf import (
|
||||
InterfaceChoice,
|
||||
|
@ -23,6 +21,8 @@ from zeroconf import (
|
|||
)
|
||||
|
||||
from homeassistant import config_entries, util
|
||||
from homeassistant.components import network
|
||||
from homeassistant.components.network.models import Adapter
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
|
@ -34,7 +34,6 @@ from homeassistant.data_entry_flow import FlowResult
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass
|
||||
from homeassistant.util.network import is_loopback
|
||||
|
||||
from .models import HaAsyncZeroconf, HaServiceBrowser, HaZeroconf
|
||||
from .usage import install_multiple_zeroconf_catcher
|
||||
|
@ -69,11 +68,14 @@ MAX_NAME_LEN = 63
|
|||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean,
|
||||
vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean,
|
||||
}
|
||||
DOMAIN: vol.All(
|
||||
cv.deprecated(CONF_DEFAULT_INTERFACE),
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean,
|
||||
vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
|
@ -132,49 +134,11 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero
|
|||
return aio_zc
|
||||
|
||||
|
||||
def _get_ip_route(dst_ip: str) -> Any:
|
||||
"""Get ip next hop."""
|
||||
return IPRoute().route("get", dst=dst_ip)
|
||||
|
||||
|
||||
def _first_ip_nexthop_from_route(routes: Iterable) -> None | str:
|
||||
"""Find the first RTA_PREFSRC in the routes."""
|
||||
_LOGGER.debug("Routes: %s", routes)
|
||||
for route in routes:
|
||||
for key, value in route["attrs"]:
|
||||
if key == "RTA_PREFSRC":
|
||||
return cast(str, value)
|
||||
return None
|
||||
|
||||
|
||||
async def async_detect_interfaces_setting(hass: HomeAssistant) -> InterfaceChoice:
|
||||
"""Auto detect the interfaces setting when unset."""
|
||||
routes = []
|
||||
try:
|
||||
routes = await hass.async_add_executor_job(_get_ip_route, MDNS_TARGET_IP)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
_LOGGER.debug(
|
||||
"The system could not auto detect routing data on your operating system; Zeroconf will broadcast on all interfaces",
|
||||
exc_info=ex,
|
||||
)
|
||||
return InterfaceChoice.All
|
||||
|
||||
if not (first_ip := _first_ip_nexthop_from_route(routes)):
|
||||
_LOGGER.debug(
|
||||
"The system could not auto detect the nexthop for %s on your operating system; Zeroconf will broadcast on all interfaces",
|
||||
MDNS_TARGET_IP,
|
||||
)
|
||||
return InterfaceChoice.All
|
||||
|
||||
if is_loopback(ip_address(first_ip)):
|
||||
_LOGGER.debug(
|
||||
"The next hop for %s is %s; Zeroconf will broadcast on all interfaces",
|
||||
MDNS_TARGET_IP,
|
||||
first_ip,
|
||||
)
|
||||
return InterfaceChoice.All
|
||||
|
||||
return InterfaceChoice.Default
|
||||
def _async_use_default_interface(adapters: list[Adapter]) -> bool:
|
||||
for adapter in adapters:
|
||||
if adapter["enabled"] and not adapter["default"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
|
@ -182,10 +146,18 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
|||
zc_config = config.get(DOMAIN, {})
|
||||
zc_args: dict = {}
|
||||
|
||||
if CONF_DEFAULT_INTERFACE not in zc_config:
|
||||
zc_args["interfaces"] = await async_detect_interfaces_setting(hass)
|
||||
elif zc_config[CONF_DEFAULT_INTERFACE]:
|
||||
adapters = await network.async_get_adapters(hass)
|
||||
if _async_use_default_interface(adapters):
|
||||
zc_args["interfaces"] = InterfaceChoice.Default
|
||||
else:
|
||||
interfaces = zc_args["interfaces"] = []
|
||||
for adapter in adapters:
|
||||
if not adapter["enabled"]:
|
||||
continue
|
||||
if ipv4s := adapter["ipv4"]:
|
||||
interfaces.append(ipv4s[0]["address"])
|
||||
elif ipv6s := adapter["ipv6"]:
|
||||
interfaces.append(ipv6s[0]["scope_id"])
|
||||
if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
|
||||
zc_args["ip_version"] = IPVersion.V4Only
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"domain": "zeroconf",
|
||||
"name": "Zero-configuration networking (zeroconf)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
|
||||
"requirements": ["zeroconf==0.31.0","pyroute2==0.5.18"],
|
||||
"dependencies": ["api"],
|
||||
"requirements": ["zeroconf==0.31.0"],
|
||||
"dependencies": ["network", "api"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_push"
|
||||
|
|
|
@ -19,12 +19,12 @@ emoji==1.2.0
|
|||
hass-nabucasa==0.43.0
|
||||
home-assistant-frontend==20210518.0
|
||||
httpx==0.18.0
|
||||
ifaddr==0.1.7
|
||||
jinja2>=3.0.1
|
||||
netdisco==2.8.3
|
||||
paho-mqtt==1.5.1
|
||||
pillow==8.1.2
|
||||
pip>=8.0.3,<20.3
|
||||
pyroute2==0.5.18
|
||||
python-slugify==4.0.1
|
||||
pyyaml==5.4.1
|
||||
requests==2.25.1
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -506,6 +506,17 @@ no_implicit_optional = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.network.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.notify.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -818,6 +818,9 @@ ibmiotf==0.3.4
|
|||
# homeassistant.components.ping
|
||||
icmplib==2.1.1
|
||||
|
||||
# homeassistant.components.network
|
||||
ifaddr==0.1.7
|
||||
|
||||
# homeassistant.components.iglo
|
||||
iglo==1.2.7
|
||||
|
||||
|
@ -1690,9 +1693,6 @@ pyrisco==0.3.1
|
|||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.3
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
pyroute2==0.5.18
|
||||
|
||||
# homeassistant.components.ruckus_unleashed
|
||||
pyruckus==0.12
|
||||
|
||||
|
|
|
@ -462,6 +462,9 @@ iaqualink==0.3.4
|
|||
# homeassistant.components.ping
|
||||
icmplib==2.1.1
|
||||
|
||||
# homeassistant.components.network
|
||||
ifaddr==0.1.7
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
influxdb-client==1.14.0
|
||||
|
||||
|
@ -947,9 +950,6 @@ pyrisco==0.3.1
|
|||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.3
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
pyroute2==0.5.18
|
||||
|
||||
# homeassistant.components.ruckus_unleashed
|
||||
pyruckus==0.12
|
||||
|
||||
|
|
|
@ -136,6 +136,8 @@ IGNORE_VIOLATIONS = {
|
|||
("demo", "openalpr_local"),
|
||||
# Migration wizard from zwave to ozw.
|
||||
"ozw",
|
||||
# Migration of settings from zeroconf to network
|
||||
("network", "zeroconf"),
|
||||
# This should become a helper method that integrations can submit data to
|
||||
("websocket_api", "lovelace"),
|
||||
("websocket_api", "shopping_list"),
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Network Configuration integration."""
|
|
@ -0,0 +1,446 @@
|
|||
"""Test the Network Configuration."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import ifaddr
|
||||
|
||||
from homeassistant.components import network
|
||||
from homeassistant.components.network.const import (
|
||||
ATTR_ADAPTERS,
|
||||
ATTR_CONFIGURED_ADAPTERS,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
_NO_LOOPBACK_IPADDR = "192.168.1.5"
|
||||
_LOOPBACK_IPADDR = "127.0.0.1"
|
||||
|
||||
|
||||
def _generate_mock_adapters():
|
||||
mock_lo0 = Mock(spec=ifaddr.Adapter)
|
||||
mock_lo0.nice_name = "lo0"
|
||||
mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")]
|
||||
mock_eth0 = Mock(spec=ifaddr.Adapter)
|
||||
mock_eth0.nice_name = "eth0"
|
||||
mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")]
|
||||
mock_eth1 = Mock(spec=ifaddr.Adapter)
|
||||
mock_eth1.nice_name = "eth1"
|
||||
mock_eth1.ips = [ifaddr.IP("192.168.1.5", 23, "eth1")]
|
||||
mock_vtun0 = Mock(spec=ifaddr.Adapter)
|
||||
mock_vtun0.nice_name = "vtun0"
|
||||
mock_vtun0.ips = [ifaddr.IP("169.254.3.2", 16, "vtun0")]
|
||||
return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0]
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_storage):
|
||||
"""Test without default interface config and the route returns a non-loopback address."""
|
||||
with patch(
|
||||
"homeassistant.components.network.util.socket.socket.getsockname",
|
||||
return_value=[_NO_LOOPBACK_IPADDR],
|
||||
), patch(
|
||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||
return_value=_generate_mock_adapters(),
|
||||
):
|
||||
assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
network_obj = hass.data[network.DOMAIN]
|
||||
assert network_obj.configured_adapters == []
|
||||
|
||||
assert network_obj.adapters == [
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "127.0.0.1", "network_prefix": 8}],
|
||||
"ipv6": [],
|
||||
"name": "lo0",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": True,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
||||
"ipv6": [],
|
||||
"name": "vtun0",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage):
|
||||
"""Test without default interface config and the route returns a loopback address."""
|
||||
with patch(
|
||||
"homeassistant.components.network.util.socket.socket.getsockname",
|
||||
return_value=[_LOOPBACK_IPADDR],
|
||||
), patch(
|
||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||
return_value=_generate_mock_adapters(),
|
||||
):
|
||||
assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
network_obj = hass.data[network.DOMAIN]
|
||||
assert network_obj.configured_adapters == []
|
||||
assert network_obj.adapters == [
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": True,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "127.0.0.1", "network_prefix": 8}],
|
||||
"ipv6": [],
|
||||
"name": "lo0",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
||||
"ipv6": [],
|
||||
"name": "vtun0",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage):
|
||||
"""Test without default interface config and the route returns nothing."""
|
||||
with patch(
|
||||
"homeassistant.components.network.util.socket.socket.getsockname",
|
||||
return_value=[],
|
||||
), patch(
|
||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||
return_value=_generate_mock_adapters(),
|
||||
):
|
||||
assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
network_obj = hass.data[network.DOMAIN]
|
||||
assert network_obj.configured_adapters == []
|
||||
assert network_obj.adapters == [
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "127.0.0.1", "network_prefix": 8}],
|
||||
"ipv6": [],
|
||||
"name": "lo0",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
||||
"ipv6": [],
|
||||
"name": "vtun0",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_exception(hass, hass_storage):
|
||||
"""Test without default interface config and the route throws an exception."""
|
||||
with patch(
|
||||
"homeassistant.components.network.util.socket.socket.getsockname",
|
||||
side_effect=AttributeError,
|
||||
), patch(
|
||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||
return_value=_generate_mock_adapters(),
|
||||
):
|
||||
assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
network_obj = hass.data[network.DOMAIN]
|
||||
assert network_obj.configured_adapters == []
|
||||
assert network_obj.adapters == [
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "127.0.0.1", "network_prefix": 8}],
|
||||
"ipv6": [],
|
||||
"name": "lo0",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
||||
"ipv6": [],
|
||||
"name": "vtun0",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_interfaces_configured_from_storage(hass, hass_storage):
|
||||
"""Test settings from storage are preferred over auto configure."""
|
||||
hass_storage[STORAGE_KEY] = {
|
||||
"version": STORAGE_VERSION,
|
||||
"key": STORAGE_KEY,
|
||||
"data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]},
|
||||
}
|
||||
with patch(
|
||||
"homeassistant.components.network.util.socket.socket.getsockname",
|
||||
return_value=[_NO_LOOPBACK_IPADDR],
|
||||
), patch(
|
||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||
return_value=_generate_mock_adapters(),
|
||||
):
|
||||
assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
network_obj = hass.data[network.DOMAIN]
|
||||
assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"]
|
||||
|
||||
assert network_obj.adapters == [
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "127.0.0.1", "network_prefix": 8}],
|
||||
"ipv6": [],
|
||||
"name": "lo0",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": True,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
||||
"ipv6": [],
|
||||
"name": "vtun0",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_interfaces_configured_from_storage_websocket_update(
|
||||
hass, hass_ws_client, hass_storage
|
||||
):
|
||||
"""Test settings from storage can be updated via websocket api."""
|
||||
hass_storage[STORAGE_KEY] = {
|
||||
"version": STORAGE_VERSION,
|
||||
"key": STORAGE_KEY,
|
||||
"data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]},
|
||||
}
|
||||
with patch(
|
||||
"homeassistant.components.network.util.socket.socket.getsockname",
|
||||
return_value=[_NO_LOOPBACK_IPADDR],
|
||||
), patch(
|
||||
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||
return_value=_generate_mock_adapters(),
|
||||
):
|
||||
assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
network_obj = hass.data[network.DOMAIN]
|
||||
assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"]
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json({"id": 1, "type": "network"})
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"][ATTR_CONFIGURED_ADAPTERS] == ["eth0", "eth1", "vtun0"]
|
||||
assert response["result"][ATTR_ADAPTERS] == [
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "127.0.0.1", "network_prefix": 8}],
|
||||
"ipv6": [],
|
||||
"name": "lo0",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": True,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
||||
"ipv6": [],
|
||||
"name": "vtun0",
|
||||
},
|
||||
]
|
||||
|
||||
await ws_client.send_json(
|
||||
{"id": 2, "type": "network/configure", "config": {ATTR_CONFIGURED_ADAPTERS: []}}
|
||||
)
|
||||
response = await ws_client.receive_json()
|
||||
assert response["result"][ATTR_CONFIGURED_ADAPTERS] == []
|
||||
|
||||
await ws_client.send_json({"id": 3, "type": "network"})
|
||||
response = await ws_client.receive_json()
|
||||
assert response["result"][ATTR_CONFIGURED_ADAPTERS] == []
|
||||
assert response["result"][ATTR_ADAPTERS] == [
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "127.0.0.1", "network_prefix": 8}],
|
||||
"ipv6": [],
|
||||
"name": "lo0",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": True,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
||||
"ipv6": [],
|
||||
"name": "vtun0",
|
||||
},
|
||||
]
|
|
@ -1,5 +1,5 @@
|
|||
"""Test Zeroconf component setup process."""
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange
|
||||
|
||||
|
@ -697,13 +697,27 @@ async def test_removed_ignored(hass, mock_zeroconf):
|
|||
assert mock_service_info.mock_calls[1][1][0] == "_service.updated.local."
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zeroconf):
|
||||
_ADAPTER_WITH_DEFAULT_ENABLED = [
|
||||
{
|
||||
"auto": True,
|
||||
"default": True,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_non_loopback_route(hass):
|
||||
"""Test without default interface config and the route returns a non-loopback address."""
|
||||
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
|
||||
with patch(
|
||||
"homeassistant.components.zeroconf.models.HaZeroconf"
|
||||
) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
|
||||
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
|
||||
), patch(
|
||||
"homeassistant.components.zeroconf.IPRoute.route",
|
||||
return_value=_ROUTE_NO_LOOPBACK,
|
||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
||||
return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
|
||||
), patch(
|
||||
"homeassistant.components.zeroconf.ServiceInfo",
|
||||
side_effect=get_service_info_mock,
|
||||
|
@ -712,47 +726,53 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zer
|
|||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.Default)
|
||||
assert mock_zc.mock_calls[0] == call(interfaces=InterfaceChoice.Default)
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_loopback_route(hass, mock_zeroconf):
|
||||
"""Test without default interface config and the route returns a loopback address."""
|
||||
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
|
||||
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
|
||||
), patch(
|
||||
"homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_LOOPBACK
|
||||
), patch(
|
||||
"homeassistant.components.zeroconf.ServiceInfo",
|
||||
side_effect=get_service_info_mock,
|
||||
):
|
||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All)
|
||||
_ADAPTERS_WITH_MANUAL_CONFIG = [
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
"enabled": False,
|
||||
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
||||
"ipv6": [],
|
||||
"name": "vtun0",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_empty_route(hass, mock_zeroconf):
|
||||
async def test_async_detect_interfaces_setting_empty_route(hass):
|
||||
"""Test without default interface config and the route returns nothing."""
|
||||
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
|
||||
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
|
||||
), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]), patch(
|
||||
"homeassistant.components.zeroconf.ServiceInfo",
|
||||
side_effect=get_service_info_mock,
|
||||
):
|
||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All)
|
||||
|
||||
|
||||
async def test_async_detect_interfaces_setting_exception(hass, mock_zeroconf):
|
||||
"""Test without default interface config and the route throws an exception."""
|
||||
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
|
||||
with patch(
|
||||
"homeassistant.components.zeroconf.models.HaZeroconf"
|
||||
) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
|
||||
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
|
||||
), patch(
|
||||
"homeassistant.components.zeroconf.IPRoute.route", side_effect=AttributeError
|
||||
"homeassistant.components.zeroconf.network.async_get_adapters",
|
||||
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
||||
), patch(
|
||||
"homeassistant.components.zeroconf.ServiceInfo",
|
||||
side_effect=get_service_info_mock,
|
||||
|
@ -761,4 +781,4 @@ async def test_async_detect_interfaces_setting_exception(hass, mock_zeroconf):
|
|||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All)
|
||||
assert mock_zc.mock_calls[0] == call(interfaces=[1, "192.168.1.5"])
|
||||
|
|
|
@ -361,7 +361,7 @@ async def test_discovery_requirements_ssdp(hass):
|
|||
) as mock_process:
|
||||
await async_get_integration_with_requirements(hass, "ssdp_comp")
|
||||
|
||||
assert len(mock_process.mock_calls) == 3
|
||||
assert len(mock_process.mock_calls) == 4
|
||||
assert mock_process.mock_calls[0][1][2] == ssdp.requirements
|
||||
# Ensure zeroconf is a dep for ssdp
|
||||
assert mock_process.mock_calls[1][1][1] == "zeroconf"
|
||||
|
@ -386,7 +386,7 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest):
|
|||
) as mock_process:
|
||||
await async_get_integration_with_requirements(hass, "comp")
|
||||
|
||||
assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http
|
||||
assert len(mock_process.mock_calls) == 3 # zeroconf also depends on http
|
||||
assert mock_process.mock_calls[0][1][2] == zeroconf.requirements
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue