Home Assistant Unofficial Reference 2024.12.1
websocket_api.py
Go to the documentation of this file.
1 """Websocket API for OTBR."""
2 
3 from collections.abc import Callable, Coroutine
4 from functools import wraps
5 from typing import TYPE_CHECKING, Any, cast
6 
7 import python_otbr_api
8 from python_otbr_api import PENDING_DATASET_DELAY_TIMER, tlv_parser
9 from python_otbr_api.tlv_parser import MeshcopTLVType
10 import voluptuous as vol
11 
12 from homeassistant.components import websocket_api
14  is_multiprotocol_url,
15 )
16 from homeassistant.components.thread import async_add_dataset, async_get_dataset
17 from homeassistant.core import HomeAssistant, callback
18 from homeassistant.exceptions import HomeAssistantError
19 
20 from .const import DEFAULT_CHANNEL, DOMAIN
21 from .util import (
22  OTBRData,
23  compose_default_network_name,
24  generate_random_pan_id,
25  get_allowed_channel,
26  update_issues,
27 )
28 
29 if TYPE_CHECKING:
30  from . import OTBRConfigEntry
31 
32 
33 @callback
34 def async_setup(hass: HomeAssistant) -> None:
35  """Set up the OTBR Websocket API."""
36  websocket_api.async_register_command(hass, websocket_info)
37  websocket_api.async_register_command(hass, websocket_create_network)
38  websocket_api.async_register_command(hass, websocket_set_channel)
39  websocket_api.async_register_command(hass, websocket_set_network)
40 
41 
42 @websocket_api.websocket_command( { "type": "otbr/info", } )
43 @websocket_api.require_admin
44 @websocket_api.async_response
45 async def websocket_info(
46  hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
47 ) -> None:
48  """Get OTBR info."""
49  config_entries: list[OTBRConfigEntry]
50  config_entries = hass.config_entries.async_loaded_entries(DOMAIN)
51 
52  if not config_entries:
53  connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded")
54  return
55 
56  response: dict[str, dict[str, Any]] = {}
57 
58  for config_entry in config_entries:
59  data = config_entry.runtime_data
60  try:
61  border_agent_id = await data.get_border_agent_id()
62  dataset = await data.get_active_dataset()
63  dataset_tlvs = await data.get_active_dataset_tlvs()
64  extended_address = (await data.get_extended_address()).hex()
65  except HomeAssistantError as exc:
66  connection.send_error(msg["id"], "otbr_info_failed", str(exc))
67  return
68 
69  # The border agent ID is checked when the OTBR config entry is setup,
70  # we can assert it's not None
71  assert border_agent_id is not None
72 
73  extended_pan_id = (
74  dataset.extended_pan_id.lower()
75  if dataset and dataset.extended_pan_id
76  else None
77  )
78  response[extended_address] = {
79  "active_dataset_tlvs": dataset_tlvs.hex() if dataset_tlvs else None,
80  "border_agent_id": border_agent_id.hex(),
81  "channel": dataset.channel if dataset else None,
82  "extended_address": extended_address,
83  "extended_pan_id": extended_pan_id,
84  "url": data.url,
85  }
86 
87  connection.send_result(msg["id"], response)
88 
89 
91  orig_func: Callable[
92  [HomeAssistant, websocket_api.ActiveConnection, dict, OTBRData],
93  Coroutine[Any, Any, None],
94  ],
95 ) -> Callable[
96  [HomeAssistant, websocket_api.ActiveConnection, dict], Coroutine[Any, Any, None]
97 ]:
98  """Decorate function to get OTBR data."""
99 
100  @wraps(orig_func)
101  async def async_check_extended_address_func(
102  hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
103  ) -> None:
104  """Fetch OTBR data and pass to orig_func."""
105  config_entries: list[OTBRConfigEntry]
106  config_entries = hass.config_entries.async_loaded_entries(DOMAIN)
107 
108  if not config_entries:
109  connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded")
110  return
111 
112  for config_entry in config_entries:
113  data = config_entry.runtime_data
114  try:
115  extended_address = await data.get_extended_address()
116  except HomeAssistantError as exc:
117  connection.send_error(
118  msg["id"], "get_extended_address_failed", str(exc)
119  )
120  return
121  if extended_address.hex() != msg["extended_address"]:
122  continue
123 
124  await orig_func(hass, connection, msg, data)
125  return
126 
127  connection.send_error(msg["id"], "unknown_router", "")
128 
129  return async_check_extended_address_func
130 
131 
132 @websocket_api.websocket_command( { "type": "otbr/create_network", vol.Required("extended_address"): str,
133  }
134 )
135 @websocket_api.require_admin
136 @websocket_api.async_response
137 @async_get_otbr_data
138 async def websocket_create_network(
139  hass: HomeAssistant,
140  connection: websocket_api.ActiveConnection,
141  msg: dict,
142  data: OTBRData,
143 ) -> None:
144  """Create a new Thread network."""
145  channel = await get_allowed_channel(hass, data.url) or DEFAULT_CHANNEL
146 
147  try:
148  await data.set_enabled(False)
149  except HomeAssistantError as exc:
150  connection.send_error(msg["id"], "set_enabled_failed", str(exc))
151  return
152 
153  try:
154  await data.factory_reset(hass)
155  except HomeAssistantError as exc:
156  connection.send_error(msg["id"], "factory_reset_failed", str(exc))
157  return
158 
159  pan_id = generate_random_pan_id()
160  try:
161  await data.create_active_dataset(
162  python_otbr_api.ActiveDataSet(
163  channel=channel,
164  network_name=compose_default_network_name(pan_id),
165  pan_id=pan_id,
166  )
167  )
168  except HomeAssistantError as exc:
169  connection.send_error(msg["id"], "create_active_dataset_failed", str(exc))
170  return
171 
172  try:
173  await data.set_enabled(True)
174  except HomeAssistantError as exc:
175  connection.send_error(msg["id"], "set_enabled_failed", str(exc))
176  return
177 
178  try:
179  dataset_tlvs = await data.get_active_dataset_tlvs()
180  except HomeAssistantError as exc:
181  connection.send_error(msg["id"], "get_active_dataset_tlvs_failed", str(exc))
182  return
183  if not dataset_tlvs:
184  connection.send_error(msg["id"], "get_active_dataset_tlvs_empty", "")
185  return
186 
187  await async_add_dataset(hass, DOMAIN, dataset_tlvs.hex())
188 
189  # Update repair issues
190  await update_issues(hass, data, dataset_tlvs)
191 
192  connection.send_result(msg["id"])
193 
194 
195 @websocket_api.websocket_command( { "type": "otbr/set_network", vol.Required("extended_address"): str,
196  vol.Required("dataset_id"): str,
197  }
198 )
199 @websocket_api.require_admin
200 @websocket_api.async_response
201 @async_get_otbr_data
202 async def websocket_set_network(
203  hass: HomeAssistant,
204  connection: websocket_api.ActiveConnection,
205  msg: dict,
206  data: OTBRData,
207 ) -> None:
208  """Set the Thread network to be used by the OTBR."""
209  dataset_tlv = await async_get_dataset(hass, msg["dataset_id"])
210 
211  if not dataset_tlv:
212  connection.send_error(msg["id"], "unknown_dataset", "Unknown dataset")
213  return
214  dataset = tlv_parser.parse_tlv(dataset_tlv)
215  if channel := dataset.get(MeshcopTLVType.CHANNEL):
216  thread_dataset_channel = cast(tlv_parser.Channel, channel).channel
217 
218  allowed_channel = await get_allowed_channel(hass, data.url)
219 
220  if allowed_channel and thread_dataset_channel != allowed_channel:
221  connection.send_error(
222  msg["id"],
223  "channel_conflict",
224  f"Can't connect to network on channel {thread_dataset_channel}, ZHA is "
225  f"using channel {allowed_channel}",
226  )
227  return
228 
229  try:
230  await data.set_enabled(False)
231  except HomeAssistantError as exc:
232  connection.send_error(msg["id"], "set_enabled_failed", str(exc))
233  return
234 
235  try:
236  await data.set_active_dataset_tlvs(bytes.fromhex(dataset_tlv))
237  except HomeAssistantError as exc:
238  connection.send_error(msg["id"], "set_active_dataset_tlvs_failed", str(exc))
239  return
240 
241  try:
242  await data.set_enabled(True)
243  except HomeAssistantError as exc:
244  connection.send_error(msg["id"], "set_enabled_failed", str(exc))
245  return
246 
247  # Update repair issues
248  await update_issues(hass, data, bytes.fromhex(dataset_tlv))
249 
250  connection.send_result(msg["id"])
251 
252 
253 @websocket_api.websocket_command( { "type": "otbr/set_channel", vol.Required("extended_address"): str,
254  vol.Required("channel"): int,
255  }
256 )
257 @websocket_api.require_admin
258 @websocket_api.async_response
259 @async_get_otbr_data
260 async def websocket_set_channel(
261  hass: HomeAssistant,
262  connection: websocket_api.ActiveConnection,
263  msg: dict,
264  data: OTBRData,
265 ) -> None:
266  """Set current channel."""
267  if is_multiprotocol_url(data.url):
268  connection.send_error(
269  msg["id"],
270  "multiprotocol_enabled",
271  "Channel change not allowed when in multiprotocol mode",
272  )
273  return
274 
275  channel: int = msg["channel"]
276  delay: float = PENDING_DATASET_DELAY_TIMER / 1000
277 
278  try:
279  await data.set_channel(channel)
280  except HomeAssistantError as exc:
281  connection.send_error(msg["id"], "set_channel_failed", str(exc))
282  return
283 
284  connection.send_result(msg["id"], {"delay": delay})
285 
int|None get_allowed_channel(HomeAssistant hass, str otbr_url)
Definition: util.py:167
str compose_default_network_name(int pan_id)
Definition: util.py:59
None update_issues(HomeAssistant hass, OTBRData otbrdata, bytes dataset_tlvs)
Definition: util.py:270
None websocket_create_network(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, OTBRData data)
None websocket_set_channel(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, OTBRData data)
Callable[[HomeAssistant, websocket_api.ActiveConnection, dict], Coroutine[Any, Any, None]] async_get_otbr_data(Callable[[HomeAssistant, websocket_api.ActiveConnection, dict, OTBRData], Coroutine[Any, Any, None],] orig_func)
None websocket_info(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
None websocket_set_network(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, OTBRData data)
None async_add_dataset(HomeAssistant hass, str source, str tlv, *str|None preferred_border_agent_id=None, str|None preferred_extended_address=None)
str|None async_get_dataset(HomeAssistant hass, str dataset_id)