Home Assistant Unofficial Reference 2024.12.1
websocket.py
Go to the documentation of this file.
1 """KNX Websocket API."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable
7 from functools import wraps
8 from typing import TYPE_CHECKING, Any, Final, overload
9 
10 import knx_frontend as knx_panel
11 import voluptuous as vol
12 from xknx.telegram import Telegram
13 from xknxproject.exceptions import XknxProjectException
14 
15 from homeassistant.components import panel_custom, websocket_api
16 from homeassistant.components.http import StaticPathConfig
17 from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.helpers import device_registry as dr
20 from homeassistant.helpers.dispatcher import async_dispatcher_connect
21 from homeassistant.helpers.typing import UNDEFINED
22 from homeassistant.util.ulid import ulid_now
23 
24 from .const import DOMAIN, KNX_MODULE_KEY
25 from .storage.config_store import ConfigStoreException
26 from .storage.const import CONF_DATA
27 from .storage.entity_store_schema import (
28  CREATE_ENTITY_BASE_SCHEMA,
29  UPDATE_ENTITY_BASE_SCHEMA,
30 )
31 from .storage.entity_store_validation import (
32  EntityStoreValidationException,
33  EntityStoreValidationSuccess,
34  validate_entity_data,
35 )
36 from .telegrams import SIGNAL_KNX_TELEGRAM, TelegramDict
37 
38 if TYPE_CHECKING:
39  from . import KNXModule
40 
41 URL_BASE: Final = "/knx_static"
42 
43 
44 async def register_panel(hass: HomeAssistant) -> None:
45  """Register the KNX Panel and Websocket API."""
46  websocket_api.async_register_command(hass, ws_info)
47  websocket_api.async_register_command(hass, ws_project_file_process)
48  websocket_api.async_register_command(hass, ws_project_file_remove)
49  websocket_api.async_register_command(hass, ws_group_monitor_info)
50  websocket_api.async_register_command(hass, ws_group_telegrams)
51  websocket_api.async_register_command(hass, ws_subscribe_telegram)
52  websocket_api.async_register_command(hass, ws_get_knx_project)
53  websocket_api.async_register_command(hass, ws_validate_entity)
54  websocket_api.async_register_command(hass, ws_create_entity)
55  websocket_api.async_register_command(hass, ws_update_entity)
56  websocket_api.async_register_command(hass, ws_delete_entity)
57  websocket_api.async_register_command(hass, ws_get_entity_config)
58  websocket_api.async_register_command(hass, ws_get_entity_entries)
59  websocket_api.async_register_command(hass, ws_create_device)
60 
61  if DOMAIN not in hass.data.get("frontend_panels", {}):
62  await hass.http.async_register_static_paths(
63  [
65  URL_BASE,
66  path=knx_panel.locate_dir(),
67  cache_headers=knx_panel.is_prod_build,
68  )
69  ]
70  )
71  await panel_custom.async_register_panel(
72  hass=hass,
73  frontend_url_path=DOMAIN,
74  webcomponent_name=knx_panel.webcomponent_name,
75  sidebar_title=DOMAIN.upper(),
76  sidebar_icon="mdi:bus-electric",
77  module_url=f"{URL_BASE}/{knx_panel.entrypoint_js}",
78  embed_iframe=True,
79  require_admin=True,
80  )
81 
82 
83 type KnxWebSocketCommandHandler = Callable[
84  [HomeAssistant, KNXModule, websocket_api.ActiveConnection, dict[str, Any]], None
85 ]
86 type KnxAsyncWebSocketCommandHandler = Callable[
87  [HomeAssistant, KNXModule, websocket_api.ActiveConnection, dict[str, Any]],
88  Awaitable[None],
89 ]
90 
91 
92 @overload
94  func: KnxAsyncWebSocketCommandHandler,
95 ) -> websocket_api.const.AsyncWebSocketCommandHandler: ...
96 @overload
98  func: KnxWebSocketCommandHandler,
99 ) -> websocket_api.const.WebSocketCommandHandler: ...
100 
101 
103  func: KnxAsyncWebSocketCommandHandler | KnxWebSocketCommandHandler,
104 ) -> (
105  websocket_api.const.AsyncWebSocketCommandHandler
106  | websocket_api.const.WebSocketCommandHandler
107 ):
108  """Websocket decorator to provide a KNXModule instance."""
109 
110  def _send_not_loaded_error(
111  connection: websocket_api.ActiveConnection, msg_id: int
112  ) -> None:
113  connection.send_error(
114  msg_id,
115  websocket_api.const.ERR_HOME_ASSISTANT_ERROR,
116  "KNX integration not loaded.",
117  )
118 
119  if asyncio.iscoroutinefunction(func):
120 
121  @wraps(func)
122  async def with_knx(
123  hass: HomeAssistant,
124  connection: websocket_api.ActiveConnection,
125  msg: dict[str, Any],
126  ) -> None:
127  """Add KNX Module to call function."""
128  try:
129  knx = hass.data[KNX_MODULE_KEY]
130  except KeyError:
131  _send_not_loaded_error(connection, msg["id"])
132  return
133  await func(hass, knx, connection, msg)
134 
135  else:
136 
137  @wraps(func)
138  def with_knx(
139  hass: HomeAssistant,
140  connection: websocket_api.ActiveConnection,
141  msg: dict[str, Any],
142  ) -> None:
143  """Add KNX Module to call function."""
144  try:
145  knx = hass.data[KNX_MODULE_KEY]
146  except KeyError:
147  _send_not_loaded_error(connection, msg["id"])
148  return
149  func(hass, knx, connection, msg)
150 
151  return with_knx
152 
153 
154 @websocket_api.require_admin
155 @websocket_api.websocket_command( { vol.Required("type"): "knx/info",
156  }
157 )
158 @provide_knx
159 @callback
160 def ws_info(
161  hass: HomeAssistant,
162  knx: KNXModule,
163  connection: websocket_api.ActiveConnection,
164  msg: dict,
165 ) -> None:
166  """Handle get info command."""
167  _project_info = None
168  if project_info := knx.project.info:
169  _project_info = {
170  "name": project_info["name"],
171  "last_modified": project_info["last_modified"],
172  "tool_version": project_info["tool_version"],
173  "xknxproject_version": project_info["xknxproject_version"],
174  }
175 
176  connection.send_result(
177  msg["id"],
178  {
179  "version": knx.xknx.version,
180  "connected": knx.xknx.connection_manager.connected.is_set(),
181  "current_address": str(knx.xknx.current_address),
182  "project": _project_info,
183  },
184  )
185 
186 
187 @websocket_api.require_admin
188 @websocket_api.websocket_command( { vol.Required("type"): "knx/get_knx_project",
189  }
190 )
191 @websocket_api.async_response
192 @provide_knx
193 async def ws_get_knx_project(
194  hass: HomeAssistant,
195  knx: KNXModule,
196  connection: websocket_api.ActiveConnection,
197  msg: dict,
198 ) -> None:
199  """Handle get KNX project."""
200  knxproject = await knx.project.get_knxproject()
201  connection.send_result(
202  msg["id"],
203  {
204  "project_loaded": knx.project.loaded,
205  "knxproject": knxproject,
206  },
207  )
208 
209 
210 @websocket_api.require_admin
211 @websocket_api.websocket_command( { vol.Required("type"): "knx/project_file_process",
212  vol.Required("file_id"): str,
213  vol.Required("password"): str,
214  }
215 )
216 @websocket_api.async_response
217 @provide_knx
218 async def ws_project_file_process(
219  hass: HomeAssistant,
220  knx: KNXModule,
221  connection: websocket_api.ActiveConnection,
222  msg: dict,
223 ) -> None:
224  """Handle get info command."""
225  try:
226  await knx.project.process_project_file(
227  xknx=knx.xknx,
228  file_id=msg["file_id"],
229  password=msg["password"],
230  )
231  except (ValueError, XknxProjectException) as err:
232  # ValueError could raise from file_upload integration
233  connection.send_error(
234  msg["id"], websocket_api.ERR_HOME_ASSISTANT_ERROR, str(err)
235  )
236  return
237 
238  connection.send_result(msg["id"])
239 
240 
241 @websocket_api.require_admin
242 @websocket_api.websocket_command( { vol.Required("type"): "knx/project_file_remove",
243  }
244 )
245 @websocket_api.async_response
246 @provide_knx
247 async def ws_project_file_remove(
248  hass: HomeAssistant,
249  knx: KNXModule,
250  connection: websocket_api.ActiveConnection,
251  msg: dict,
252 ) -> None:
253  """Handle get info command."""
254  await knx.project.remove_project_file()
255  connection.send_result(msg["id"])
256 
257 
258 @websocket_api.require_admin
259 @websocket_api.websocket_command( { vol.Required("type"): "knx/group_monitor_info",
260  }
261 )
262 @provide_knx
263 @callback
265  hass: HomeAssistant,
266  knx: KNXModule,
267  connection: websocket_api.ActiveConnection,
268  msg: dict,
269 ) -> None:
270  """Handle get info command of group monitor."""
271  recent_telegrams = [*knx.telegrams.recent_telegrams]
272  connection.send_result(
273  msg["id"],
274  {
275  "project_loaded": knx.project.loaded,
276  "recent_telegrams": recent_telegrams,
277  },
278  )
279 
280 
281 @websocket_api.require_admin
282 @websocket_api.websocket_command( { vol.Required("type"): "knx/group_telegrams",
283  }
284 )
285 @provide_knx
286 @callback
288  hass: HomeAssistant,
289  knx: KNXModule,
290  connection: websocket_api.ActiveConnection,
291  msg: dict,
292 ) -> None:
293  """Handle get group telegrams command."""
294  connection.send_result(
295  msg["id"],
296  knx.telegrams.last_ga_telegrams,
297  )
298 
299 
300 @websocket_api.require_admin
301 @websocket_api.websocket_command( { vol.Required("type"): "knx/subscribe_telegrams",
302  }
303 )
304 @callback
306  hass: HomeAssistant,
307  connection: websocket_api.ActiveConnection,
308  msg: dict,
309 ) -> None:
310  """Subscribe to incoming and outgoing KNX telegrams."""
311 
312  @callback
313  def forward_telegram(_telegram: Telegram, telegram_dict: TelegramDict) -> None:
314  """Forward telegram to websocket subscription."""
315  connection.send_event(
316  msg["id"],
317  telegram_dict,
318  )
319 
320  connection.subscriptions[msg["id"]] = async_dispatcher_connect(
321  hass,
322  signal=SIGNAL_KNX_TELEGRAM,
323  target=forward_telegram,
324  )
325  connection.send_result(msg["id"])
326 
327 
328 @websocket_api.require_admin
329 @websocket_api.websocket_command( { vol.Required("type"): "knx/validate_entity",
330  **CREATE_ENTITY_BASE_SCHEMA,
331  }
332 )
333 @callback
335  hass: HomeAssistant,
336  connection: websocket_api.ActiveConnection,
337  msg: dict,
338 ) -> None:
339  """Validate entity data."""
340  try:
342  except EntityStoreValidationException as exc:
343  connection.send_result(msg["id"], exc.validation_error)
344  return
345  connection.send_result(
346  msg["id"], EntityStoreValidationSuccess(success=True, entity_id=None)
347  )
348 
349 
350 @websocket_api.require_admin
351 @websocket_api.websocket_command( { vol.Required("type"): "knx/create_entity",
352  **CREATE_ENTITY_BASE_SCHEMA,
353  }
354 )
355 @websocket_api.async_response
356 @provide_knx
357 async def ws_create_entity(
358  hass: HomeAssistant,
359  knx: KNXModule,
360  connection: websocket_api.ActiveConnection,
361  msg: dict,
362 ) -> None:
363  """Create entity in entity store and load it."""
364  try:
365  validated_data = validate_entity_data(msg)
366  except EntityStoreValidationException as exc:
367  connection.send_result(msg["id"], exc.validation_error)
368  return
369  try:
370  entity_id = await knx.config_store.create_entity(
371  # use validation result so defaults are applied
372  validated_data[CONF_PLATFORM],
373  validated_data[CONF_DATA],
374  )
375  except ConfigStoreException as err:
376  connection.send_error(
377  msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err)
378  )
379  return
380  connection.send_result(
381  msg["id"], EntityStoreValidationSuccess(success=True, entity_id=entity_id)
382  )
383 
384 
385 @websocket_api.require_admin
386 @websocket_api.websocket_command( { vol.Required("type"): "knx/update_entity",
387  **UPDATE_ENTITY_BASE_SCHEMA,
388  }
389 )
390 @websocket_api.async_response
391 @provide_knx
392 async def ws_update_entity(
393  hass: HomeAssistant,
394  knx: KNXModule,
395  connection: websocket_api.ActiveConnection,
396  msg: dict,
397 ) -> None:
398  """Update entity in entity store and reload it."""
399  try:
400  validated_data = validate_entity_data(msg)
401  except EntityStoreValidationException as exc:
402  connection.send_result(msg["id"], exc.validation_error)
403  return
404  try:
405  await knx.config_store.update_entity(
406  validated_data[CONF_PLATFORM],
407  validated_data[CONF_ENTITY_ID],
408  validated_data[CONF_DATA],
409  )
410  except ConfigStoreException as err:
411  connection.send_error(
412  msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err)
413  )
414  return
415  connection.send_result(
416  msg["id"], EntityStoreValidationSuccess(success=True, entity_id=None)
417  )
418 
419 
420 @websocket_api.require_admin
421 @websocket_api.websocket_command( { vol.Required("type"): "knx/delete_entity",
422  vol.Required(CONF_ENTITY_ID): str,
423  }
424 )
425 @websocket_api.async_response
426 @provide_knx
427 async def ws_delete_entity(
428  hass: HomeAssistant,
429  knx: KNXModule,
430  connection: websocket_api.ActiveConnection,
431  msg: dict,
432 ) -> None:
433  """Delete entity from entity store and remove it."""
434  try:
435  await knx.config_store.delete_entity(msg[CONF_ENTITY_ID])
436  except ConfigStoreException as err:
437  connection.send_error(
438  msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err)
439  )
440  return
441  connection.send_result(msg["id"])
442 
443 
444 @websocket_api.require_admin
445 @websocket_api.websocket_command( { vol.Required("type"): "knx/get_entity_entries",
446  }
447 )
448 @provide_knx
449 @callback
451  hass: HomeAssistant,
452  knx: KNXModule,
453  connection: websocket_api.ActiveConnection,
454  msg: dict,
455 ) -> None:
456  """Get entities configured from entity store."""
457  entity_entries = [
458  entry.extended_dict for entry in knx.config_store.get_entity_entries()
459  ]
460  connection.send_result(msg["id"], entity_entries)
461 
462 
463 @websocket_api.require_admin
464 @websocket_api.websocket_command( { vol.Required("type"): "knx/get_entity_config",
465  vol.Required(CONF_ENTITY_ID): str,
466  }
467 )
468 @provide_knx
469 @callback
471  hass: HomeAssistant,
472  knx: KNXModule,
473  connection: websocket_api.ActiveConnection,
474  msg: dict,
475 ) -> None:
476  """Get entity configuration from entity store."""
477  try:
478  config_info = knx.config_store.get_entity_config(msg[CONF_ENTITY_ID])
479  except ConfigStoreException as err:
480  connection.send_error(
481  msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err)
482  )
483  return
484  connection.send_result(msg["id"], config_info)
485 
486 
487 @websocket_api.require_admin
488 @websocket_api.websocket_command( { vol.Required("type"): "knx/create_device",
489  vol.Required("name"): str,
490  vol.Optional("area_id"): str,
491  }
492 )
493 @provide_knx
494 @callback
495 def ws_create_device(
496  hass: HomeAssistant,
497  knx: KNXModule,
498  connection: websocket_api.ActiveConnection,
499  msg: dict,
500 ) -> None:
501  """Create a new KNX device."""
502  identifier = f"knx_vdev_{ulid_now()}"
503  device_registry = dr.async_get(hass)
504  _device = device_registry.async_get_or_create(
505  config_entry_id=knx.entry.entry_id,
506  manufacturer="KNX",
507  name=msg["name"],
508  identifiers={(DOMAIN, identifier)},
509  )
510  device_registry.async_update_device(
511  _device.id,
512  area_id=msg.get("area_id") or UNDEFINED,
513  configuration_url=f"homeassistant://knx/entities/view?device_id={_device.id}",
514  )
515  connection.send_result(msg["id"], _device.dict_repr)
516 
None ws_get_entity_entries(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:479
None ws_delete_entity(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:454
None ws_get_knx_project(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:202
None ws_project_file_process(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:229
None ws_get_entity_config(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:501
websocket_api.const .AsyncWebSocketCommandHandler provide_knx(KnxAsyncWebSocketCommandHandler func)
Definition: websocket.py:95
None ws_validate_entity(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:354
None ws_update_entity(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:417
None ws_subscribe_telegram(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:323
None ws_group_monitor_info(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:279
None ws_project_file_remove(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:260
None ws_info(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:167
None ws_group_telegrams(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:304
None ws_create_entity(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:380
None register_panel(HomeAssistant hass)
Definition: websocket.py:44
None ws_create_device(HomeAssistant hass, KNXModule knx, websocket_api.ActiveConnection connection, dict msg)
Definition: websocket.py:528
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103