Bump python-otbr-api to 2.1.0 (#93790)
* Bump python-otbr-api to 2.1.0 * Fix tests
This commit is contained in:
parent
4596ff0ce5
commit
16d8c8d4d5
11 changed files with 41 additions and 51 deletions
|
@ -3,10 +3,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import python_otbr_api
|
import python_otbr_api
|
||||||
from python_otbr_api import tlv_parser
|
from python_otbr_api import tlv_parser
|
||||||
|
from python_otbr_api.tlv_parser import MeshcopTLVType
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
import yarl
|
||||||
|
|
||||||
|
@ -38,8 +40,8 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
thread_dataset_tlv = await async_get_preferred_dataset(self.hass)
|
thread_dataset_tlv = await async_get_preferred_dataset(self.hass)
|
||||||
if thread_dataset_tlv:
|
if thread_dataset_tlv:
|
||||||
dataset = tlv_parser.parse_tlv(thread_dataset_tlv)
|
dataset = tlv_parser.parse_tlv(thread_dataset_tlv)
|
||||||
if channel_str := dataset.get(tlv_parser.MeshcopTLVType.CHANNEL):
|
if channel := dataset.get(MeshcopTLVType.CHANNEL):
|
||||||
thread_dataset_channel = int(channel_str, base=16)
|
thread_dataset_channel = cast(tlv_parser.Channel, channel).channel
|
||||||
|
|
||||||
if thread_dataset_tlv is not None and (
|
if thread_dataset_tlv is not None and (
|
||||||
not allowed_channel or allowed_channel == thread_dataset_channel
|
not allowed_channel or allowed_channel == thread_dataset_channel
|
||||||
|
|
|
@ -8,5 +8,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["python-otbr-api==2.0.0"]
|
"requirements": ["python-otbr-api==2.1.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,12 @@ from collections.abc import Callable, Coroutine
|
||||||
import contextlib
|
import contextlib
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Concatenate, ParamSpec, TypeVar
|
from typing import Any, Concatenate, ParamSpec, TypeVar, cast
|
||||||
|
|
||||||
import python_otbr_api
|
import python_otbr_api
|
||||||
from python_otbr_api import tlv_parser
|
from python_otbr_api import tlv_parser
|
||||||
from python_otbr_api.pskc import compute_pskc
|
from python_otbr_api.pskc import compute_pskc
|
||||||
|
from python_otbr_api.tlv_parser import MeshcopTLVType
|
||||||
|
|
||||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||||
is_multiprotocol_url,
|
is_multiprotocol_url,
|
||||||
|
@ -146,14 +147,10 @@ async def _warn_on_channel_collision(
|
||||||
|
|
||||||
dataset = tlv_parser.parse_tlv(dataset_tlvs.hex())
|
dataset = tlv_parser.parse_tlv(dataset_tlvs.hex())
|
||||||
|
|
||||||
if (channel_s := dataset.get(tlv_parser.MeshcopTLVType.CHANNEL)) is None:
|
if (channel_s := dataset.get(MeshcopTLVType.CHANNEL)) is None:
|
||||||
delete_issue()
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
channel = int(channel_s, 16)
|
|
||||||
except ValueError:
|
|
||||||
delete_issue()
|
delete_issue()
|
||||||
return
|
return
|
||||||
|
channel = cast(tlv_parser.Channel, channel_s).channel
|
||||||
|
|
||||||
if channel == allowed_channel:
|
if channel == allowed_channel:
|
||||||
delete_issue()
|
delete_issue()
|
||||||
|
@ -186,20 +183,20 @@ def _warn_on_default_network_settings(
|
||||||
insecure = False
|
insecure = False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
network_key := dataset.get(tlv_parser.MeshcopTLVType.NETWORKKEY)
|
network_key := dataset.get(MeshcopTLVType.NETWORKKEY)
|
||||||
) is not None and bytes.fromhex(network_key) in INSECURE_NETWORK_KEYS:
|
) is not None and network_key.data in INSECURE_NETWORK_KEYS:
|
||||||
insecure = True
|
insecure = True
|
||||||
if (
|
if (
|
||||||
not insecure
|
not insecure
|
||||||
and tlv_parser.MeshcopTLVType.EXTPANID in dataset
|
and MeshcopTLVType.EXTPANID in dataset
|
||||||
and tlv_parser.MeshcopTLVType.NETWORKNAME in dataset
|
and MeshcopTLVType.NETWORKNAME in dataset
|
||||||
and tlv_parser.MeshcopTLVType.PSKC in dataset
|
and MeshcopTLVType.PSKC in dataset
|
||||||
):
|
):
|
||||||
ext_pan_id = dataset[tlv_parser.MeshcopTLVType.EXTPANID]
|
ext_pan_id = dataset[MeshcopTLVType.EXTPANID]
|
||||||
network_name = dataset[tlv_parser.MeshcopTLVType.NETWORKNAME]
|
network_name = cast(tlv_parser.NetworkName, dataset[MeshcopTLVType.NETWORKNAME])
|
||||||
pskc = bytes.fromhex(dataset[tlv_parser.MeshcopTLVType.PSKC])
|
pskc = dataset[MeshcopTLVType.PSKC].data
|
||||||
for passphrase in INSECURE_PASSPHRASES:
|
for passphrase in INSECURE_PASSPHRASES:
|
||||||
if pskc == compute_pskc(ext_pan_id, network_name, passphrase):
|
if pskc == compute_pskc(ext_pan_id.data, network_name.name, passphrase):
|
||||||
insecure = True
|
insecure = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
"""Websocket API for OTBR."""
|
"""Websocket API for OTBR."""
|
||||||
|
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import python_otbr_api
|
import python_otbr_api
|
||||||
from python_otbr_api import tlv_parser
|
from python_otbr_api import tlv_parser
|
||||||
|
from python_otbr_api.tlv_parser import MeshcopTLVType
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
|
@ -133,8 +136,8 @@ async def websocket_set_network(
|
||||||
connection.send_error(msg["id"], "unknown_dataset", "Unknown dataset")
|
connection.send_error(msg["id"], "unknown_dataset", "Unknown dataset")
|
||||||
return
|
return
|
||||||
dataset = tlv_parser.parse_tlv(dataset_tlv)
|
dataset = tlv_parser.parse_tlv(dataset_tlv)
|
||||||
if channel_str := dataset.get(tlv_parser.MeshcopTLVType.CHANNEL):
|
if channel := dataset.get(MeshcopTLVType.CHANNEL):
|
||||||
thread_dataset_channel = int(channel_str, base=16)
|
thread_dataset_channel = cast(tlv_parser.Channel, channel).channel
|
||||||
|
|
||||||
data: OTBRData = hass.data[DOMAIN]
|
data: OTBRData = hass.data[DOMAIN]
|
||||||
allowed_channel = await get_allowed_channel(hass, data.url)
|
allowed_channel = await get_allowed_channel(hass, data.url)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
"""Persistently store thread datasets."""
|
"""Persistently store thread datasets."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from contextlib import suppress
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from python_otbr_api import tlv_parser
|
from python_otbr_api import tlv_parser
|
||||||
|
from python_otbr_api.tlv_parser import MeshcopTLVType
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
@ -39,31 +39,33 @@ class DatasetEntry:
|
||||||
@property
|
@property
|
||||||
def channel(self) -> int | None:
|
def channel(self) -> int | None:
|
||||||
"""Return channel as an integer."""
|
"""Return channel as an integer."""
|
||||||
if (channel := self.dataset.get(tlv_parser.MeshcopTLVType.CHANNEL)) is None:
|
if (channel := self.dataset.get(MeshcopTLVType.CHANNEL)) is None:
|
||||||
return None
|
return None
|
||||||
with suppress(ValueError):
|
return cast(tlv_parser.Channel, channel).channel
|
||||||
return int(channel, 16)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def dataset(self) -> dict[tlv_parser.MeshcopTLVType, str]:
|
def dataset(self) -> dict[MeshcopTLVType, tlv_parser.MeshcopTLVItem]:
|
||||||
"""Return the dataset in dict format."""
|
"""Return the dataset in dict format."""
|
||||||
return tlv_parser.parse_tlv(self.tlv)
|
return tlv_parser.parse_tlv(self.tlv)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extended_pan_id(self) -> str | None:
|
def extended_pan_id(self) -> str | None:
|
||||||
"""Return extended PAN ID as a hex string."""
|
"""Return extended PAN ID as a hex string."""
|
||||||
return self.dataset.get(tlv_parser.MeshcopTLVType.EXTPANID)
|
if (ext_pan_id := self.dataset.get(MeshcopTLVType.EXTPANID)) is None:
|
||||||
|
return None
|
||||||
|
return str(ext_pan_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def network_name(self) -> str | None:
|
def network_name(self) -> str | None:
|
||||||
"""Return network name as a string."""
|
"""Return network name as a string."""
|
||||||
return self.dataset.get(tlv_parser.MeshcopTLVType.NETWORKNAME)
|
if (name := self.dataset.get(MeshcopTLVType.NETWORKNAME)) is None:
|
||||||
|
return None
|
||||||
|
return cast(tlv_parser.NetworkName, name).name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pan_id(self) -> str | None:
|
def pan_id(self) -> str | None:
|
||||||
"""Return PAN ID as a hex string."""
|
"""Return PAN ID as a hex string."""
|
||||||
return self.dataset.get(tlv_parser.MeshcopTLVType.PANID)
|
return str(self.dataset.get(MeshcopTLVType.PANID))
|
||||||
|
|
||||||
def to_json(self) -> dict[str, Any]:
|
def to_json(self) -> dict[str, Any]:
|
||||||
"""Return a JSON serializable representation for storage."""
|
"""Return a JSON serializable representation for storage."""
|
||||||
|
|
|
@ -148,7 +148,8 @@ async def async_get_config_entry_diagnostics(
|
||||||
"unexpected_routers": set(),
|
"unexpected_routers": set(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if mlp := record.dataset.get(MeshcopTLVType.MESHLOCALPREFIX):
|
if mlp_item := record.dataset.get(MeshcopTLVType.MESHLOCALPREFIX):
|
||||||
|
mlp = str(mlp_item)
|
||||||
network["prefixes"].add(f"{mlp[0:4]}:{mlp[4:8]}:{mlp[8:12]}:{mlp[12:16]}")
|
network["prefixes"].add(f"{mlp[0:4]}:{mlp[4:8]}:{mlp[8:12]}:{mlp[12:16]}")
|
||||||
|
|
||||||
# Find all routes currently act that might be thread related, so we can match them to
|
# Find all routes currently act that might be thread related, so we can match them to
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["python-otbr-api==2.0.0", "pyroute2==0.7.5"],
|
"requirements": ["python-otbr-api==2.1.0", "pyroute2==0.7.5"],
|
||||||
"zeroconf": ["_meshcop._udp.local."]
|
"zeroconf": ["_meshcop._udp.local."]
|
||||||
}
|
}
|
||||||
|
|
|
@ -2109,7 +2109,7 @@ python-opensky==0.0.7
|
||||||
|
|
||||||
# homeassistant.components.otbr
|
# homeassistant.components.otbr
|
||||||
# homeassistant.components.thread
|
# homeassistant.components.thread
|
||||||
python-otbr-api==2.0.0
|
python-otbr-api==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.picnic
|
# homeassistant.components.picnic
|
||||||
python-picnic-api==1.1.0
|
python-picnic-api==1.1.0
|
||||||
|
|
|
@ -1532,7 +1532,7 @@ python-nest==4.2.0
|
||||||
|
|
||||||
# homeassistant.components.otbr
|
# homeassistant.components.otbr
|
||||||
# homeassistant.components.thread
|
# homeassistant.components.thread
|
||||||
python-otbr-api==2.0.0
|
python-otbr-api==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.picnic
|
# homeassistant.components.picnic
|
||||||
python-picnic-api==1.1.0
|
python-picnic-api==1.1.0
|
||||||
|
|
|
@ -24,12 +24,6 @@ from . import (
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
DATASET_BAD_CHANNEL = bytes.fromhex(
|
|
||||||
"0E080000000000010000000035060004001FFFE00208F642646DA209B1C00708FDF57B5A"
|
|
||||||
"0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102"
|
|
||||||
"25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8"
|
|
||||||
)
|
|
||||||
|
|
||||||
DATASET_NO_CHANNEL = bytes.fromhex(
|
DATASET_NO_CHANNEL = bytes.fromhex(
|
||||||
"0E08000000000001000035060004001FFFE00208F642646DA209B1C00708FDF57B5A"
|
"0E08000000000001000035060004001FFFE00208F642646DA209B1C00708FDF57B5A"
|
||||||
"0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102"
|
"0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102"
|
||||||
|
@ -103,9 +97,7 @@ async def test_import_share_radio_channel_collision(hass: HomeAssistant) -> None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize("dataset", [DATASET_CH15, DATASET_NO_CHANNEL])
|
||||||
"dataset", [DATASET_BAD_CHANNEL, DATASET_CH15, DATASET_NO_CHANNEL]
|
|
||||||
)
|
|
||||||
async def test_import_share_radio_no_channel_collision(
|
async def test_import_share_radio_no_channel_collision(
|
||||||
hass: HomeAssistant, dataset: bytes
|
hass: HomeAssistant, dataset: bytes
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -121,7 +121,6 @@ async def test_dataset_properties(hass: HomeAssistant) -> None:
|
||||||
{"source": "Google", "tlv": DATASET_1},
|
{"source": "Google", "tlv": DATASET_1},
|
||||||
{"source": "Multipan", "tlv": DATASET_2},
|
{"source": "Multipan", "tlv": DATASET_2},
|
||||||
{"source": "🎅", "tlv": DATASET_3},
|
{"source": "🎅", "tlv": DATASET_3},
|
||||||
{"source": "test1", "tlv": DATASET_1_BAD_CHANNEL},
|
|
||||||
{"source": "test2", "tlv": DATASET_1_NO_CHANNEL},
|
{"source": "test2", "tlv": DATASET_1_NO_CHANNEL},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -136,10 +135,8 @@ async def test_dataset_properties(hass: HomeAssistant) -> None:
|
||||||
dataset_2 = dataset
|
dataset_2 = dataset
|
||||||
if dataset.source == "🎅":
|
if dataset.source == "🎅":
|
||||||
dataset_3 = dataset
|
dataset_3 = dataset
|
||||||
if dataset.source == "test1":
|
|
||||||
dataset_4 = dataset
|
|
||||||
if dataset.source == "test2":
|
if dataset.source == "test2":
|
||||||
dataset_5 = dataset
|
dataset_4 = dataset
|
||||||
|
|
||||||
dataset = store.async_get(dataset_1.id)
|
dataset = store.async_get(dataset_1.id)
|
||||||
assert dataset == dataset_1
|
assert dataset == dataset_1
|
||||||
|
@ -166,10 +163,6 @@ async def test_dataset_properties(hass: HomeAssistant) -> None:
|
||||||
assert dataset == dataset_4
|
assert dataset == dataset_4
|
||||||
assert dataset.channel is None
|
assert dataset.channel is None
|
||||||
|
|
||||||
dataset = store.async_get(dataset_5.id)
|
|
||||||
assert dataset == dataset_5
|
|
||||||
assert dataset.channel is None
|
|
||||||
|
|
||||||
|
|
||||||
async def test_load_datasets(hass: HomeAssistant) -> None:
|
async def test_load_datasets(hass: HomeAssistant) -> None:
|
||||||
"""Make sure that we can load/save data correctly."""
|
"""Make sure that we can load/save data correctly."""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue