Home Assistant Unofficial Reference 2024.12.1
api.py
Go to the documentation of this file.
1 """Handle websocket api for Matter."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Coroutine
6 from functools import wraps
7 from typing import Any, Concatenate
8 
9 from matter_server.client.models.node import MatterNode
10 from matter_server.common.errors import MatterError
11 from matter_server.common.helpers.util import dataclass_to_dict
12 import voluptuous as vol
13 
14 from homeassistant.components import websocket_api
15 from homeassistant.components.websocket_api import ActiveConnection
16 from homeassistant.core import HomeAssistant, callback
17 
18 from .adapter import MatterAdapter
19 from .helpers import MissingNode, get_matter, node_from_ha_device_id
20 
21 ID = "id"
22 TYPE = "type"
23 DEVICE_ID = "device_id"
24 
25 
26 ERROR_NODE_NOT_FOUND = "node_not_found"
27 
28 
29 @callback
30 def async_register_api(hass: HomeAssistant) -> None:
31  """Register all of our api endpoints."""
32  websocket_api.async_register_command(hass, websocket_commission)
33  websocket_api.async_register_command(hass, websocket_commission_on_network)
34  websocket_api.async_register_command(hass, websocket_set_thread_dataset)
35  websocket_api.async_register_command(hass, websocket_set_wifi_credentials)
36  websocket_api.async_register_command(hass, websocket_node_diagnostics)
37  websocket_api.async_register_command(hass, websocket_ping_node)
38  websocket_api.async_register_command(hass, websocket_open_commissioning_window)
39  websocket_api.async_register_command(hass, websocket_remove_matter_fabric)
40  websocket_api.async_register_command(hass, websocket_interview_node)
41 
42 
44  func: Callable[
45  [HomeAssistant, ActiveConnection, dict[str, Any], MatterAdapter, MatterNode],
46  Coroutine[Any, Any, None],
47  ],
48 ) -> Callable[
49  [HomeAssistant, ActiveConnection, dict[str, Any], MatterAdapter],
50  Coroutine[Any, Any, None],
51 ]:
52  """Decorate async function to get node."""
53 
54  @wraps(func)
55  async def async_get_node_func(
56  hass: HomeAssistant,
57  connection: ActiveConnection,
58  msg: dict[str, Any],
59  matter: MatterAdapter,
60  ) -> None:
61  """Provide user specific data and store to function."""
62  node = node_from_ha_device_id(hass, msg[DEVICE_ID])
63  if not node:
64  raise MissingNode(
65  f"Could not resolve Matter node from device id {msg[DEVICE_ID]}"
66  )
67  await func(hass, connection, msg, matter, node)
68 
69  return async_get_node_func
70 
71 
73  func: Callable[
74  [HomeAssistant, ActiveConnection, dict[str, Any], MatterAdapter],
75  Coroutine[Any, Any, None],
76  ],
77 ) -> Callable[
78  [HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None]
79 ]:
80  """Decorate function to get the MatterAdapter."""
81 
82  @wraps(func)
83  async def _get_matter(
84  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
85  ) -> None:
86  """Provide the Matter client to the function."""
87  matter = get_matter(hass)
88 
89  await func(hass, connection, msg, matter)
90 
91  return _get_matter
92 
93 
94 def async_handle_failed_command[**_P](
95  func: Callable[
96  Concatenate[HomeAssistant, ActiveConnection, dict[str, Any], _P],
97  Coroutine[Any, Any, None],
98  ],
99 ) -> Callable[
100  Concatenate[HomeAssistant, ActiveConnection, dict[str, Any], _P],
101  Coroutine[Any, Any, None],
102 ]:
103  """Decorate function to handle MatterError and send relevant error."""
104 
105  @wraps(func)
106  async def async_handle_failed_command_func(
107  hass: HomeAssistant,
108  connection: ActiveConnection,
109  msg: dict[str, Any],
110  *args: _P.args,
111  **kwargs: _P.kwargs,
112  ) -> None:
113  """Handle MatterError within function and send relevant error."""
114  try:
115  await func(hass, connection, msg, *args, **kwargs)
116  except MatterError as err:
117  connection.send_error(msg[ID], str(err.error_code), err.args[0])
118  except MissingNode as err:
119  connection.send_error(msg[ID], ERROR_NODE_NOT_FOUND, err.args[0])
120 
121  return async_handle_failed_command_func
122 
123 
124 @websocket_api.require_admin
125 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/commission",
126  vol.Required("code"): str,
127  vol.Optional("network_only"): bool,
128  }
129 )
130 @websocket_api.async_response
131 @async_handle_failed_command
132 @async_get_matter_adapter
133 async def websocket_commission(
134  hass: HomeAssistant,
135  connection: ActiveConnection,
136  msg: dict[str, Any],
137  matter: MatterAdapter,
138 ) -> None:
139  """Add a device to the network and commission the device."""
140  await matter.matter_client.commission_with_code(
141  msg["code"], network_only=msg.get("network_only", True)
142  )
143  connection.send_result(msg[ID])
144 
145 
146 @websocket_api.require_admin
147 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/commission_on_network",
148  vol.Required("pin"): int,
149  vol.Optional("ip_addr"): str,
150  }
151 )
152 @websocket_api.async_response
153 @async_handle_failed_command
154 @async_get_matter_adapter
156  hass: HomeAssistant,
157  connection: ActiveConnection,
158  msg: dict[str, Any],
159  matter: MatterAdapter,
160 ) -> None:
161  """Commission a device already on the network."""
162  await matter.matter_client.commission_on_network(
163  msg["pin"], ip_addr=msg.get("ip_addr")
164  )
165  connection.send_result(msg[ID])
166 
167 
168 @websocket_api.require_admin
169 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/set_thread",
170  vol.Required("thread_operation_dataset"): str,
171  }
172 )
173 @websocket_api.async_response
174 @async_handle_failed_command
175 @async_get_matter_adapter
177  hass: HomeAssistant,
178  connection: ActiveConnection,
179  msg: dict[str, Any],
180  matter: MatterAdapter,
181 ) -> None:
182  """Set thread dataset."""
183  await matter.matter_client.set_thread_operational_dataset(
184  msg["thread_operation_dataset"]
185  )
186  connection.send_result(msg[ID])
187 
188 
189 @websocket_api.require_admin
190 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/set_wifi_credentials",
191  vol.Required("network_name"): str,
192  vol.Required("password"): str,
193  }
194 )
195 @websocket_api.async_response
196 @async_handle_failed_command
197 @async_get_matter_adapter
199  hass: HomeAssistant,
200  connection: ActiveConnection,
201  msg: dict[str, Any],
202  matter: MatterAdapter,
203 ) -> None:
204  """Set WiFi credentials for a device."""
205  await matter.matter_client.set_wifi_credentials(
206  ssid=msg["network_name"], credentials=msg["password"]
207  )
208  connection.send_result(msg[ID])
209 
210 
211 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/node_diagnostics",
212  vol.Required(DEVICE_ID): str,
213  }
214 )
215 @websocket_api.async_response
216 @async_handle_failed_command
217 @async_get_matter_adapter
218 @async_get_node
220  hass: HomeAssistant,
221  connection: ActiveConnection,
222  msg: dict[str, Any],
223  matter: MatterAdapter,
224  node: MatterNode,
225 ) -> None:
226  """Gather diagnostics for the given node."""
227  result = await matter.matter_client.node_diagnostics(node_id=node.node_id)
228  connection.send_result(msg[ID], dataclass_to_dict(result))
229 
230 
231 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/ping_node",
232  vol.Required(DEVICE_ID): str,
233  }
234 )
235 @websocket_api.async_response
236 @async_handle_failed_command
237 @async_get_matter_adapter
238 @async_get_node
239 async def websocket_ping_node(
240  hass: HomeAssistant,
241  connection: ActiveConnection,
242  msg: dict[str, Any],
243  matter: MatterAdapter,
244  node: MatterNode,
245 ) -> None:
246  """Ping node on the currently known IP-adress(es)."""
247  result = await matter.matter_client.ping_node(node_id=node.node_id)
248  connection.send_result(msg[ID], result)
249 
250 
251 @websocket_api.require_admin
252 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/open_commissioning_window",
253  vol.Required(DEVICE_ID): str,
254  }
255 )
256 @websocket_api.async_response
257 @async_handle_failed_command
258 @async_get_matter_adapter
259 @async_get_node
261  hass: HomeAssistant,
262  connection: ActiveConnection,
263  msg: dict[str, Any],
264  matter: MatterAdapter,
265  node: MatterNode,
266 ) -> None:
267  """Open a commissioning window to commission a device present on this controller to another."""
268  result = await matter.matter_client.open_commissioning_window(node_id=node.node_id)
269  connection.send_result(msg[ID], dataclass_to_dict(result))
270 
271 
272 @websocket_api.require_admin
273 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/remove_matter_fabric",
274  vol.Required(DEVICE_ID): str,
275  vol.Required("fabric_index"): int,
276  }
277 )
278 @websocket_api.async_response
279 @async_handle_failed_command
280 @async_get_matter_adapter
281 @async_get_node
283  hass: HomeAssistant,
284  connection: ActiveConnection,
285  msg: dict[str, Any],
286  matter: MatterAdapter,
287  node: MatterNode,
288 ) -> None:
289  """Remove Matter fabric from a device."""
290  await matter.matter_client.remove_matter_fabric(
291  node_id=node.node_id, fabric_index=msg["fabric_index"]
292  )
293  connection.send_result(msg[ID])
294 
295 
296 @websocket_api.require_admin
297 @websocket_api.websocket_command( { vol.Required(TYPE): "matter/interview_node",
298  vol.Required(DEVICE_ID): str,
299  }
300 )
301 @websocket_api.async_response
302 @async_handle_failed_command
303 @async_get_matter_adapter
304 @async_get_node
305 async def websocket_interview_node(
306  hass: HomeAssistant,
307  connection: ActiveConnection,
308  msg: dict[str, Any],
309  matter: MatterAdapter,
310  node: MatterNode,
311 ) -> None:
312  """Interview a node."""
313  await matter.matter_client.interview_node(node_id=node.node_id)
314  connection.send_result(msg[ID])
315 
None websocket_open_commissioning_window(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter, MatterNode node)
Definition: api.py:280
None websocket_set_thread_dataset(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter)
Definition: api.py:187
None websocket_node_diagnostics(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter, MatterNode node)
Definition: api.py:235
Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None]] async_get_matter_adapter(Callable[[HomeAssistant, ActiveConnection, dict[str, Any], MatterAdapter], Coroutine[Any, Any, None],] func)
Definition: api.py:79
None websocket_remove_matter_fabric(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter, MatterNode node)
Definition: api.py:304
None websocket_set_wifi_credentials(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter)
Definition: api.py:211
None websocket_ping_node(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter, MatterNode node)
Definition: api.py:257
None async_register_api(HomeAssistant hass)
Definition: api.py:30
None websocket_interview_node(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter, MatterNode node)
Definition: api.py:329
None websocket_commission(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter)
Definition: api.py:140
Callable[[HomeAssistant, ActiveConnection, dict[str, Any], MatterAdapter], Coroutine[Any, Any, None],] async_get_node(Callable[[HomeAssistant, ActiveConnection, dict[str, Any], MatterAdapter, MatterNode], Coroutine[Any, Any, None],] func)
Definition: api.py:51
None websocket_commission_on_network(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg, MatterAdapter matter)
Definition: api.py:164
MatterAdapter get_matter(HomeAssistant hass)
Definition: helpers.py:35
MatterNode|None node_from_ha_device_id(HomeAssistant hass, str ha_device_id)
Definition: helpers.py:76