mirror of https://github.com/home-assistant/core
386 lines
14 KiB
Python
386 lines
14 KiB
Python
"""Test the thread websocket API."""
|
|
|
|
from unittest.mock import ANY, AsyncMock, MagicMock, Mock
|
|
|
|
import pytest
|
|
from zeroconf.asyncio import AsyncServiceInfo
|
|
|
|
from homeassistant.components.thread import discovery
|
|
from homeassistant.components.thread.const import DOMAIN
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from . import (
|
|
ROUTER_DISCOVERY_GOOGLE_1,
|
|
ROUTER_DISCOVERY_HASS,
|
|
ROUTER_DISCOVERY_HASS_BAD_DATA,
|
|
ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP,
|
|
ROUTER_DISCOVERY_HASS_MISSING_DATA,
|
|
ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XA,
|
|
ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XP,
|
|
ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP,
|
|
ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP,
|
|
ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE,
|
|
)
|
|
|
|
|
|
async def test_discover_routers(
|
|
hass: HomeAssistant, mock_async_zeroconf: MagicMock
|
|
) -> None:
|
|
"""Test discovering thread routers."""
|
|
mock_async_zeroconf.async_add_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_remove_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_get_service_info = AsyncMock()
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
discovered = []
|
|
removed = []
|
|
|
|
@callback
|
|
def router_discovered(key: str, data: discovery.ThreadRouterDiscoveryData) -> None:
|
|
"""Handle router discovered."""
|
|
discovered.append((key, data))
|
|
|
|
@callback
|
|
def router_removed(key: str) -> None:
|
|
"""Handle router removed."""
|
|
removed.append(key)
|
|
|
|
# Start Thread router discovery
|
|
thread_disovery = discovery.ThreadRouterDiscovery(
|
|
hass, router_discovered, router_removed
|
|
)
|
|
await thread_disovery.async_start()
|
|
|
|
mock_async_zeroconf.async_add_service_listener.assert_called_once_with(
|
|
"_meshcop._udp.local.", ANY
|
|
)
|
|
listener: discovery.ThreadRouterDiscovery.ThreadServiceListener = (
|
|
mock_async_zeroconf.async_add_service_listener.mock_calls[0][1][1]
|
|
)
|
|
|
|
# Discover a service
|
|
mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(
|
|
**ROUTER_DISCOVERY_HASS
|
|
)
|
|
listener.add_service(
|
|
None, ROUTER_DISCOVERY_HASS["type_"], ROUTER_DISCOVERY_HASS["name"]
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert len(discovered) == 1
|
|
assert len(removed) == 0
|
|
assert discovered[-1] == (
|
|
"aeeb2f594b570bbf",
|
|
discovery.ThreadRouterDiscoveryData(
|
|
instance_name="HomeAssistant OpenThreadBorderRouter #0BBF",
|
|
addresses=["192.168.0.115"],
|
|
border_agent_id="230c6a1ac57f6f4be262acf32e5ef52c",
|
|
brand="homeassistant",
|
|
extended_address="aeeb2f594b570bbf",
|
|
extended_pan_id="e60fc7c186212ce5",
|
|
model_name="OpenThreadBorderRouter",
|
|
network_name="OpenThread HC",
|
|
server="core-silabs-multiprotocol.local.",
|
|
thread_version="1.3.0",
|
|
unconfigured=None,
|
|
vendor_name="HomeAssistant",
|
|
),
|
|
)
|
|
|
|
# Discover another service - we don't care if zeroconf considers this an update
|
|
mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(
|
|
**ROUTER_DISCOVERY_GOOGLE_1
|
|
)
|
|
listener.update_service(
|
|
None, ROUTER_DISCOVERY_GOOGLE_1["type_"], ROUTER_DISCOVERY_GOOGLE_1["name"]
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert len(discovered) == 2
|
|
assert len(removed) == 0
|
|
assert discovered[-1] == (
|
|
"f6a99b425a67abed",
|
|
discovery.ThreadRouterDiscoveryData(
|
|
instance_name="Google-Nest-Hub-#ABED",
|
|
addresses=["192.168.0.124"],
|
|
border_agent_id="bc3740c3e963aa8735bebecd7cc503c7",
|
|
brand="google",
|
|
extended_address="f6a99b425a67abed",
|
|
extended_pan_id="9e75e256f61409a3",
|
|
model_name="Google Nest Hub",
|
|
network_name="NEST-PAN-E1AF",
|
|
server="2d99f293-cd8e-2770-8dd2-6675de9fa000.local.",
|
|
thread_version="1.3.0",
|
|
unconfigured=None,
|
|
vendor_name="Google Inc.",
|
|
),
|
|
)
|
|
|
|
# Remove a service
|
|
listener.remove_service(
|
|
None, ROUTER_DISCOVERY_HASS["type_"], ROUTER_DISCOVERY_HASS["name"]
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert len(discovered) == 2
|
|
assert len(removed) == 1
|
|
assert removed[-1] == "aeeb2f594b570bbf"
|
|
|
|
# Remove the service again
|
|
listener.remove_service(
|
|
None, ROUTER_DISCOVERY_HASS["type_"], ROUTER_DISCOVERY_HASS["name"]
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert len(discovered) == 2
|
|
assert len(removed) == 1
|
|
|
|
# Remove an unknown service
|
|
listener.remove_service(None, ROUTER_DISCOVERY_HASS["type_"], "unknown")
|
|
await hass.async_block_till_done()
|
|
assert len(discovered) == 2
|
|
assert len(removed) == 1
|
|
|
|
# Stop Thread router discovery
|
|
await thread_disovery.async_stop()
|
|
mock_async_zeroconf.async_remove_service_listener.assert_called_once_with(listener)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("data", "unconfigured"),
|
|
[
|
|
(ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP, True),
|
|
(ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP, None),
|
|
(ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP, None),
|
|
(ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE, True),
|
|
],
|
|
)
|
|
async def test_discover_routers_unconfigured(
|
|
hass: HomeAssistant, mock_async_zeroconf: MagicMock, data, unconfigured
|
|
) -> None:
|
|
"""Test discovering thread routers and setting the unconfigured flag."""
|
|
mock_async_zeroconf.async_add_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_remove_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_get_service_info = AsyncMock()
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
# Start Thread router discovery
|
|
router_discovered_removed = Mock()
|
|
thread_disovery = discovery.ThreadRouterDiscovery(
|
|
hass, router_discovered_removed, router_discovered_removed
|
|
)
|
|
await thread_disovery.async_start()
|
|
listener: discovery.ThreadRouterDiscovery.ThreadServiceListener = (
|
|
mock_async_zeroconf.async_add_service_listener.mock_calls[0][1][1]
|
|
)
|
|
|
|
# Discover a service with bad or missing data
|
|
mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(**data)
|
|
listener.add_service(None, data["type_"], data["name"])
|
|
await hass.async_block_till_done()
|
|
router_discovered_removed.assert_called_once_with(
|
|
"aeeb2f594b570bbf",
|
|
discovery.ThreadRouterDiscoveryData(
|
|
instance_name="HomeAssistant OpenThreadBorderRouter #0BBF",
|
|
addresses=["192.168.0.115"],
|
|
border_agent_id="230c6a1ac57f6f4be262acf32e5ef52c",
|
|
brand="homeassistant",
|
|
extended_address="aeeb2f594b570bbf",
|
|
extended_pan_id="e60fc7c186212ce5",
|
|
model_name="OpenThreadBorderRouter",
|
|
network_name="OpenThread HC",
|
|
server="core-silabs-multiprotocol.local.",
|
|
thread_version="1.3.0",
|
|
unconfigured=unconfigured,
|
|
vendor_name="HomeAssistant",
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"data", [ROUTER_DISCOVERY_HASS_BAD_DATA, ROUTER_DISCOVERY_HASS_MISSING_DATA]
|
|
)
|
|
async def test_discover_routers_bad_or_missing_optional_data(
|
|
hass: HomeAssistant, mock_async_zeroconf: MagicMock, data
|
|
) -> None:
|
|
"""Test discovering thread routers with bad or missing vendor mDNS data."""
|
|
mock_async_zeroconf.async_add_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_remove_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_get_service_info = AsyncMock()
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
# Start Thread router discovery
|
|
router_discovered_removed = Mock()
|
|
thread_disovery = discovery.ThreadRouterDiscovery(
|
|
hass, router_discovered_removed, router_discovered_removed
|
|
)
|
|
await thread_disovery.async_start()
|
|
listener: discovery.ThreadRouterDiscovery.ThreadServiceListener = (
|
|
mock_async_zeroconf.async_add_service_listener.mock_calls[0][1][1]
|
|
)
|
|
|
|
# Discover a service with bad or missing data
|
|
mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(**data)
|
|
listener.add_service(None, data["type_"], data["name"])
|
|
await hass.async_block_till_done()
|
|
router_discovered_removed.assert_called_once_with(
|
|
"aeeb2f594b570bbf",
|
|
discovery.ThreadRouterDiscoveryData(
|
|
instance_name="HomeAssistant OpenThreadBorderRouter #0BBF",
|
|
addresses=["192.168.0.115"],
|
|
border_agent_id="230c6a1ac57f6f4be262acf32e5ef52c",
|
|
brand=None,
|
|
extended_address="aeeb2f594b570bbf",
|
|
extended_pan_id="e60fc7c186212ce5",
|
|
model_name="OpenThreadBorderRouter",
|
|
network_name="OpenThread HC",
|
|
server="core-silabs-multiprotocol.local.",
|
|
thread_version="1.3.0",
|
|
unconfigured=None,
|
|
vendor_name=None,
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"service",
|
|
[
|
|
ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XA,
|
|
ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XP,
|
|
],
|
|
)
|
|
async def test_discover_routers_bad_or_missing_mandatory_data(
|
|
hass: HomeAssistant, mock_async_zeroconf: MagicMock, service
|
|
) -> None:
|
|
"""Test discovering thread routers with missing mandatory mDNS data."""
|
|
mock_async_zeroconf.async_add_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_remove_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_get_service_info = AsyncMock()
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
# Start Thread router discovery
|
|
router_discovered_removed = Mock()
|
|
thread_disovery = discovery.ThreadRouterDiscovery(
|
|
hass, router_discovered_removed, router_discovered_removed
|
|
)
|
|
await thread_disovery.async_start()
|
|
listener: discovery.ThreadRouterDiscovery.ThreadServiceListener = (
|
|
mock_async_zeroconf.async_add_service_listener.mock_calls[0][1][1]
|
|
)
|
|
|
|
# Discover a service with missing mandatory data
|
|
mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(
|
|
**service
|
|
)
|
|
listener.add_service(
|
|
None,
|
|
service["type_"],
|
|
service["name"],
|
|
)
|
|
await hass.async_block_till_done()
|
|
router_discovered_removed.assert_not_called()
|
|
|
|
|
|
async def test_discover_routers_get_service_info_fails(
|
|
hass: HomeAssistant, mock_async_zeroconf: MagicMock
|
|
) -> None:
|
|
"""Test discovering thread routers with invalid mDNS data."""
|
|
mock_async_zeroconf.async_add_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_remove_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_get_service_info = AsyncMock()
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
# Start Thread router discovery
|
|
router_discovered_removed = Mock()
|
|
thread_disovery = discovery.ThreadRouterDiscovery(
|
|
hass, router_discovered_removed, router_discovered_removed
|
|
)
|
|
await thread_disovery.async_start()
|
|
listener: discovery.ThreadRouterDiscovery.ThreadServiceListener = (
|
|
mock_async_zeroconf.async_add_service_listener.mock_calls[0][1][1]
|
|
)
|
|
|
|
# Discover a service with missing data
|
|
mock_async_zeroconf.async_get_service_info.return_value = None
|
|
listener.add_service(
|
|
None, ROUTER_DISCOVERY_HASS["type_"], ROUTER_DISCOVERY_HASS["name"]
|
|
)
|
|
await hass.async_block_till_done()
|
|
router_discovered_removed.assert_not_called()
|
|
|
|
|
|
async def test_discover_routers_update_unchanged(
|
|
hass: HomeAssistant, mock_async_zeroconf: MagicMock
|
|
) -> None:
|
|
"""Test discovering thread routers with identical mDNS data in update."""
|
|
mock_async_zeroconf.async_add_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_remove_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_get_service_info = AsyncMock()
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
# Start Thread router discovery
|
|
router_discovered_removed = Mock()
|
|
thread_disovery = discovery.ThreadRouterDiscovery(
|
|
hass, router_discovered_removed, router_discovered_removed
|
|
)
|
|
await thread_disovery.async_start()
|
|
listener: discovery.ThreadRouterDiscovery.ThreadServiceListener = (
|
|
mock_async_zeroconf.async_add_service_listener.mock_calls[0][1][1]
|
|
)
|
|
|
|
# Discover a service
|
|
mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(
|
|
**ROUTER_DISCOVERY_HASS
|
|
)
|
|
listener.add_service(
|
|
None, ROUTER_DISCOVERY_HASS["type_"], ROUTER_DISCOVERY_HASS["name"]
|
|
)
|
|
await hass.async_block_till_done()
|
|
router_discovered_removed.assert_called_once()
|
|
|
|
# Update the service unchanged
|
|
mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(
|
|
**ROUTER_DISCOVERY_HASS
|
|
)
|
|
listener.update_service(
|
|
None, ROUTER_DISCOVERY_HASS["type_"], ROUTER_DISCOVERY_HASS["name"]
|
|
)
|
|
await hass.async_block_till_done()
|
|
router_discovered_removed.assert_called_once()
|
|
|
|
|
|
async def test_discover_routers_stop_twice(
|
|
hass: HomeAssistant, mock_async_zeroconf: MagicMock
|
|
) -> None:
|
|
"""Test discovering thread routers stopping discovery twice."""
|
|
mock_async_zeroconf.async_add_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_remove_service_listener = AsyncMock()
|
|
mock_async_zeroconf.async_get_service_info = AsyncMock()
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
# Start Thread router discovery
|
|
router_discovered_removed = Mock()
|
|
thread_disovery = discovery.ThreadRouterDiscovery(
|
|
hass, router_discovered_removed, router_discovered_removed
|
|
)
|
|
await thread_disovery.async_start()
|
|
|
|
# Stop Thread router discovery
|
|
await thread_disovery.async_stop()
|
|
mock_async_zeroconf.async_remove_service_listener.assert_called_once()
|
|
|
|
# Stop Thread router discovery again
|
|
await thread_disovery.async_stop()
|
|
mock_async_zeroconf.async_remove_service_listener.assert_called_once()
|