Skip to content

Commit

Permalink
Add unconfigured flag to thread discovery data (#89230)
Browse files Browse the repository at this point in the history
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
  • Loading branch information
emontnemery and balloob authored Mar 10, 2023
1 parent 9e1ba85 commit fde205c
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 15 deletions.
27 changes: 22 additions & 5 deletions homeassistant/components/thread/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging
from typing import cast

from python_otbr_api.mdns import StateBitmap
from zeroconf import BadTypeInNameException, DNSPointer, ServiceListener, Zeroconf
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf

Expand All @@ -29,14 +30,15 @@
class ThreadRouterDiscoveryData:
"""Thread router discovery data."""

addresses: list[str] | None
brand: str | None
extended_pan_id: str | None
model_name: str | None
network_name: str | None
server: str | None
vendor_name: str | None
addresses: list[str] | None
thread_version: str | None
unconfigured: bool | None
vendor_name: str | None


def async_discovery_data_from_service(
Expand All @@ -59,15 +61,30 @@ def try_decode(value: bytes | None) -> str | None:
server = service.server
vendor_name = try_decode(service.properties.get(b"vn"))
thread_version = try_decode(service.properties.get(b"tv"))
unconfigured = None
brand = KNOWN_BRANDS.get(vendor_name)
if brand == "homeassistant":
# Attempt to detect incomplete configuration
if (state_bitmap_b := service.properties.get(b"sb")) is not None:
try:
state_bitmap = StateBitmap.from_bytes(state_bitmap_b)
if not state_bitmap.is_active:
unconfigured = True
except ValueError:
_LOGGER.debug("Failed to decode state bitmap in service %s", service)
if service.properties.get(b"at") is None:
unconfigured = True

return ThreadRouterDiscoveryData(
brand=KNOWN_BRANDS.get(vendor_name),
addresses=service.parsed_addresses(),
brand=brand,
extended_pan_id=ext_pan_id.hex() if ext_pan_id is not None else None,
model_name=model_name,
network_name=network_name,
server=server,
vendor_name=vendor_name,
addresses=service.parsed_addresses(),
thread_version=thread_version,
unconfigured=unconfigured,
vendor_name=vendor_name,
)


Expand Down
106 changes: 106 additions & 0 deletions tests/components/thread/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,109 @@
},
"interface_index": None,
}


ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP = {
"type_": "_meshcop._udp.local.",
"name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.",
"addresses": [b"\xc0\xa8\x00s"],
"port": 49153,
"weight": 0,
"priority": 0,
"server": "core-silabs-multiprotocol.local.",
"properties": {
b"rv": b"1",
b"vn": b"HomeAssistant",
b"mn": b"OpenThreadBorderRouter",
b"nn": b"OpenThread HC",
b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5",
b"tv": b"1.3.0",
b"xa": b"\xae\xeb/YKW\x0b\xbf",
b"sb": b"\x00\x00\x01\xb1",
b"pt": b"\x8f\x06Q~",
b"sq": b"3",
b"bb": b"\xf0\xbf",
b"dn": b"DefaultDomain",
},
"interface_index": None,
}


ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP = {
"type_": "_meshcop._udp.local.",
"name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.",
"addresses": [b"\xc0\xa8\x00s"],
"port": 49153,
"weight": 0,
"priority": 0,
"server": "core-silabs-multiprotocol.local.",
"properties": {
b"rv": b"1",
b"vn": b"HomeAssistant",
b"mn": b"OpenThreadBorderRouter",
b"nn": b"OpenThread HC",
b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5",
b"tv": b"1.3.0",
b"xa": b"\xae\xeb/YKW\x0b\xbf",
b"at": b"\x00\x00\x00\x00\x00\x01\x00\x00",
b"pt": b"\x8f\x06Q~",
b"sq": b"3",
b"bb": b"\xf0\xbf",
b"dn": b"DefaultDomain",
},
"interface_index": None,
}


ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP = {
"type_": "_meshcop._udp.local.",
"name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.",
"addresses": [b"\xc0\xa8\x00s"],
"port": 49153,
"weight": 0,
"priority": 0,
"server": "core-silabs-multiprotocol.local.",
"properties": {
b"rv": b"1",
b"vn": b"HomeAssistant",
b"mn": b"OpenThreadBorderRouter",
b"nn": b"OpenThread HC",
b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5",
b"tv": b"1.3.0",
b"xa": b"\xae\xeb/YKW\x0b\xbf",
b"sb": b"\xff\x00\x01\xb1",
b"at": b"\x00\x00\x00\x00\x00\x01\x00\x00",
b"pt": b"\x8f\x06Q~",
b"sq": b"3",
b"bb": b"\xf0\xbf",
b"dn": b"DefaultDomain",
},
"interface_index": None,
}


ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE = {
"type_": "_meshcop._udp.local.",
"name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.",
"addresses": [b"\xc0\xa8\x00s"],
"port": 49153,
"weight": 0,
"priority": 0,
"server": "core-silabs-multiprotocol.local.",
"properties": {
b"rv": b"1",
b"vn": b"HomeAssistant",
b"mn": b"OpenThreadBorderRouter",
b"nn": b"OpenThread HC",
b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5",
b"tv": b"1.3.0",
b"xa": b"\xae\xeb/YKW\x0b\xbf",
b"sb": b"\x00\x00\x01\x31",
b"at": b"\x00\x00\x00\x00\x00\x01\x00\x00",
b"pt": b"\x8f\x06Q~",
b"sq": b"3",
b"bb": b"\xf0\xbf",
b"dn": b"DefaultDomain",
},
"interface_index": None,
}
69 changes: 63 additions & 6 deletions tests/components/thread/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
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,
ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP,
ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP,
ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE,
)


Expand Down Expand Up @@ -67,14 +71,15 @@ def router_removed(key: str) -> None:
assert discovered[-1] == (
"aeeb2f594b570bbf",
discovery.ThreadRouterDiscoveryData(
addresses=["192.168.0.115"],
brand="homeassistant",
extended_pan_id="e60fc7c186212ce5",
model_name="OpenThreadBorderRouter",
network_name="OpenThread HC",
server="core-silabs-multiprotocol.local.",
vendor_name="HomeAssistant",
thread_version="1.3.0",
addresses=["192.168.0.115"],
unconfigured=None,
vendor_name="HomeAssistant",
),
)

Expand All @@ -91,14 +96,15 @@ def router_removed(key: str) -> None:
assert discovered[-1] == (
"f6a99b425a67abed",
discovery.ThreadRouterDiscoveryData(
addresses=["192.168.0.124"],
brand="google",
extended_pan_id="9e75e256f61409a3",
model_name="Google Nest Hub",
network_name="NEST-PAN-E1AF",
server="2d99f293-cd8e-2770-8dd2-6675de9fa000.local.",
vendor_name="Google Inc.",
thread_version="1.3.0",
addresses=["192.168.0.124"],
unconfigured=None,
vendor_name="Google Inc.",
),
)

Expand Down Expand Up @@ -130,6 +136,56 @@ def router_removed(key: str) -> None:
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: None, data, unconfigured
) -> 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(
addresses=["192.168.0.115"],
brand="homeassistant",
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)
)
Expand Down Expand Up @@ -161,14 +217,15 @@ async def test_discover_routers_bad_data(
router_discovered_removed.assert_called_once_with(
"aeeb2f594b570bbf",
discovery.ThreadRouterDiscoveryData(
addresses=["192.168.0.115"],
brand=None,
extended_pan_id="e60fc7c186212ce5",
model_name="OpenThreadBorderRouter",
network_name="OpenThread HC",
server="core-silabs-multiprotocol.local.",
vendor_name=None,
thread_version="1.3.0",
addresses=["192.168.0.115"],
unconfigured=None,
vendor_name=None,
),
)

Expand Down
10 changes: 6 additions & 4 deletions tests/components/thread/test_websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,15 @@ async def test_discover_routers(
assert msg == {
"event": {
"data": {
"addresses": ["192.168.0.115"],
"brand": "homeassistant",
"extended_pan_id": "e60fc7c186212ce5",
"model_name": "OpenThreadBorderRouter",
"network_name": "OpenThread HC",
"server": "core-silabs-multiprotocol.local.",
"vendor_name": "HomeAssistant",
"addresses": ["192.168.0.115"],
"thread_version": "1.3.0",
"unconfigured": None,
"vendor_name": "HomeAssistant",
},
"key": "aeeb2f594b570bbf",
"type": "router_discovered",
Expand All @@ -261,14 +262,15 @@ async def test_discover_routers(
assert msg == {
"event": {
"data": {
"addresses": ["192.168.0.124"],
"brand": "google",
"extended_pan_id": "9e75e256f61409a3",
"model_name": "Google Nest Hub",
"network_name": "NEST-PAN-E1AF",
"server": "2d99f293-cd8e-2770-8dd2-6675de9fa000.local.",
"vendor_name": "Google Inc.",
"thread_version": "1.3.0",
"addresses": ["192.168.0.124"],
"unconfigured": None,
"vendor_name": "Google Inc.",
},
"key": "f6a99b425a67abed",
"type": "router_discovered",
Expand Down

0 comments on commit fde205c

Please sign in to comment.