1 """Utility functions for the Open Thread Border Router integration."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Coroutine
7 from functools
import wraps
10 from typing
import TYPE_CHECKING, Any, Concatenate, cast
13 import python_otbr_api
14 from python_otbr_api
import PENDING_DATASET_DELAY_TIMER, tlv_parser
15 from python_otbr_api.pskc
import compute_pskc
16 from python_otbr_api.tlv_parser
import MeshcopTLVType
19 MultiprotocolAddonManager,
20 get_multiprotocol_addon_manager,
22 multi_pan_addon_using_device,
30 from .const
import DOMAIN
33 from .
import OTBRConfigEntry
35 _LOGGER = logging.getLogger(__name__)
37 INFO_URL_SKY_CONNECT = (
38 "https://skyconnect.home-assistant.io/multiprotocol-channel-missmatch"
40 INFO_URL_YELLOW =
"https://yellow.home-assistant.io/multiprotocol-channel-missmatch"
42 INSECURE_NETWORK_KEYS = (
44 bytes.fromhex(
"00112233445566778899AABBCCDDEEFF"),
47 INSECURE_PASSPHRASES = (
56 """Raised from python_otbr_api.GetBorderAgentIdNotSupportedError."""
60 """Generate a default network name."""
61 return f
"ha-thread-{pan_id:04x}"
65 """Generate a random PAN ID."""
67 return random.randint(0, 0xFFFE)
70 def _handle_otbr_error[**_P, _R](
71 func: Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]],
72 ) -> Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]]:
73 """Handle OTBR errors."""
76 async
def _func(self: OTBRData, *args: _P.args, **kwargs: _P.kwargs) -> _R:
78 return await func(self, *args, **kwargs)
79 except (python_otbr_api.OTBRError, aiohttp.ClientError, TimeoutError)
as exc:
85 @dataclasses.dataclass
87 """Container for OTBR data."""
90 api: python_otbr_api.OTBR
95 """Reset the router."""
98 except python_otbr_api.FactoryResetNotSupportedError:
100 "OTBR does not support factory reset, attempting to delete dataset"
102 await self.delete_active_dataset()
105 hass.config_entries.async_get_entry(self.entry_id),
106 await self.get_border_agent_id(),
110 async
def get_border_agent_id(self) -> bytes:
111 """Get the border agent ID or None if not supported by the router."""
113 return await self.api.get_border_agent_id()
114 except python_otbr_api.GetBorderAgentIdNotSupportedError
as exc:
115 raise GetBorderAgentIdNotSupported
from exc
118 async
def set_enabled(self, enabled: bool) ->
None:
119 """Enable or disable the router."""
120 return await self.api.set_enabled(enabled)
123 async
def get_active_dataset(self) -> python_otbr_api.ActiveDataSet | None:
124 """Get current active operational dataset, or None."""
125 return await self.api.get_active_dataset()
128 async
def get_active_dataset_tlvs(self) -> bytes | None:
129 """Get current active operational dataset in TLVS format, or None."""
130 return await self.api.get_active_dataset_tlvs()
133 async
def get_pending_dataset_tlvs(self) -> bytes | None:
134 """Get current pending operational dataset in TLVS format, or None."""
135 return await self.api.get_pending_dataset_tlvs()
138 async
def create_active_dataset(
139 self, dataset: python_otbr_api.ActiveDataSet
141 """Create an active operational dataset."""
142 return await self.api.create_active_dataset(dataset)
145 async
def delete_active_dataset(self) -> None:
146 """Delete the active operational dataset."""
147 return await self.api.delete_active_dataset()
150 async
def set_active_dataset_tlvs(self, dataset: bytes) ->
None:
151 """Set current active operational dataset in TLVS format."""
152 await self.api.set_active_dataset_tlvs(dataset)
155 async
def set_channel(
156 self, channel: int, delay: float = PENDING_DATASET_DELAY_TIMER / 1000
158 """Set current channel."""
159 await self.api.set_channel(channel, delay=
int(delay * 1000))
162 async
def get_extended_address(self) -> bytes:
163 """Get extended address (EUI-64)."""
164 return await self.api.get_extended_address()
168 """Return the allowed channel, or None if there's no restriction."""
176 return multipan_manager.async_get_channel()
180 hass: HomeAssistant, otbrdata: OTBRData, dataset_tlvs: bytes
182 """Warn user if OTBR and ZHA attempt to use different channels."""
185 ir.async_delete_issue(
188 f
"otbr_zha_channel_collision_{otbrdata.entry_id}",
195 dataset = tlv_parser.parse_tlv(dataset_tlvs.hex())
197 if (channel_s := dataset.get(MeshcopTLVType.CHANNEL))
is None:
200 channel = cast(tlv_parser.Channel, channel_s).channel
202 if channel == allowed_channel:
207 learn_more_url = INFO_URL_YELLOW
if yellow
else INFO_URL_SKY_CONNECT
209 ir.async_create_issue(
212 f
"otbr_zha_channel_collision_{otbrdata.entry_id}",
215 learn_more_url=learn_more_url,
216 severity=ir.IssueSeverity.WARNING,
217 translation_key=
"otbr_zha_channel_collision",
218 translation_placeholders={
219 "otbr_channel":
str(channel),
220 "zha_channel":
str(allowed_channel),
226 hass: HomeAssistant, otbrdata: OTBRData, dataset_tlvs: bytes
228 """Warn user if insecure default network settings are used."""
229 dataset = tlv_parser.parse_tlv(dataset_tlvs.hex())
233 network_key := dataset.get(MeshcopTLVType.NETWORKKEY)
234 )
is not None and network_key.data
in INSECURE_NETWORK_KEYS:
238 and MeshcopTLVType.EXTPANID
in dataset
239 and MeshcopTLVType.NETWORKNAME
in dataset
240 and MeshcopTLVType.PSKC
in dataset
242 ext_pan_id = dataset[MeshcopTLVType.EXTPANID]
243 network_name = cast(tlv_parser.NetworkName, dataset[MeshcopTLVType.NETWORKNAME])
244 pskc = dataset[MeshcopTLVType.PSKC].data
245 for passphrase
in INSECURE_PASSPHRASES:
246 if pskc == compute_pskc(ext_pan_id.data, network_name.name, passphrase):
251 ir.async_create_issue(
254 f
"insecure_thread_network_{otbrdata.entry_id}",
257 severity=ir.IssueSeverity.WARNING,
258 translation_key=
"insecure_thread_network",
261 ir.async_delete_issue(
264 f
"insecure_thread_network_{otbrdata.entry_id}",
269 hass: HomeAssistant, otbrdata: OTBRData, dataset_tlvs: bytes
271 """Raise or clear repair issues related to network settings."""
277 hass: HomeAssistant, entry: OTBRConfigEntry |
None, border_agent_id: bytes
279 """Update the config entry's unique_id if not matching."""
280 border_agent_id_hex = border_agent_id.hex()
281 if entry
and entry.source == SOURCE_USER
and entry.unique_id != border_agent_id_hex:
283 "Updating unique_id of entry %s from %s to %s",
288 hass.config_entries.async_update_entry(entry, unique_id=border_agent_id_hex)
None factory_reset(self, HomeAssistant hass)
MultiprotocolAddonManager get_multiprotocol_addon_manager(HomeAssistant hass)
bool multi_pan_addon_using_device(HomeAssistant hass, str device_path)
bool is_multiprotocol_url(str url)
None _warn_on_channel_collision(HomeAssistant hass, OTBRData otbrdata, bytes dataset_tlvs)
int|None get_allowed_channel(HomeAssistant hass, str otbr_url)
int generate_random_pan_id()
None update_unique_id(HomeAssistant hass, OTBRConfigEntry|None entry, bytes border_agent_id)
str compose_default_network_name(int pan_id)
None _warn_on_default_network_settings(HomeAssistant hass, OTBRData otbrdata, bytes dataset_tlvs)
None update_issues(HomeAssistant hass, OTBRData otbrdata, bytes dataset_tlvs)
None delete_issue(HomeAssistant hass, str domain, str issue_id)