Home Assistant Unofficial Reference 2024.12.1
websocket.py
Go to the documentation of this file.
1 """LCN Websocket API."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Awaitable, Callable
6 from functools import wraps
7 from typing import TYPE_CHECKING, Any, Final
8 
9 import lcn_frontend as lcn_panel
10 import voluptuous as vol
11 
12 from homeassistant.components import panel_custom, websocket_api
13 from homeassistant.components.http import StaticPathConfig
14 from homeassistant.components.websocket_api import AsyncWebSocketCommandHandler
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import (
17  CONF_ADDRESS,
18  CONF_DEVICES,
19  CONF_DOMAIN,
20  CONF_ENTITIES,
21  CONF_NAME,
22  CONF_RESOURCE,
23 )
24 from homeassistant.core import HomeAssistant
25 from homeassistant.helpers import device_registry as dr, entity_registry as er
27 
28 from .const import (
29  ADD_ENTITIES_CALLBACKS,
30  CONF_DOMAIN_DATA,
31  CONF_HARDWARE_SERIAL,
32  CONF_HARDWARE_TYPE,
33  CONF_SOFTWARE_SERIAL,
34  CONNECTION,
35  DOMAIN,
36 )
37 from .helpers import (
38  DeviceConnectionType,
39  async_update_device_config,
40  generate_unique_id,
41  get_device_config,
42  get_device_connection,
43  get_resource,
44  purge_device_registry,
45  purge_entity_registry,
46  register_lcn_address_devices,
47 )
48 from .schemas import (
49  ADDRESS_SCHEMA,
50  DOMAIN_DATA_BINARY_SENSOR,
51  DOMAIN_DATA_CLIMATE,
52  DOMAIN_DATA_COVER,
53  DOMAIN_DATA_LIGHT,
54  DOMAIN_DATA_SCENE,
55  DOMAIN_DATA_SENSOR,
56  DOMAIN_DATA_SWITCH,
57 )
58 
59 if TYPE_CHECKING:
60  from homeassistant.components.websocket_api import ActiveConnection
61 
62 type AsyncLcnWebSocketCommandHandler = Callable[
63  [HomeAssistant, ActiveConnection, dict[str, Any], ConfigEntry], Awaitable[None]
64 ]
65 
66 URL_BASE: Final = "/lcn_static"
67 
68 
69 async def register_panel_and_ws_api(hass: HomeAssistant) -> None:
70  """Register the LCN Panel and Websocket API."""
71  websocket_api.async_register_command(hass, websocket_get_device_configs)
72  websocket_api.async_register_command(hass, websocket_get_entity_configs)
73  websocket_api.async_register_command(hass, websocket_scan_devices)
74  websocket_api.async_register_command(hass, websocket_add_device)
75  websocket_api.async_register_command(hass, websocket_delete_device)
76  websocket_api.async_register_command(hass, websocket_add_entity)
77  websocket_api.async_register_command(hass, websocket_delete_entity)
78 
79  if DOMAIN not in hass.data.get("frontend_panels", {}):
80  await hass.http.async_register_static_paths(
81  [
83  URL_BASE,
84  path=lcn_panel.locate_dir(),
85  cache_headers=lcn_panel.is_prod_build,
86  )
87  ]
88  )
89  await panel_custom.async_register_panel(
90  hass=hass,
91  frontend_url_path=DOMAIN,
92  webcomponent_name=lcn_panel.webcomponent_name,
93  config_panel_domain=DOMAIN,
94  module_url=f"{URL_BASE}/{lcn_panel.entrypoint_js}",
95  embed_iframe=True,
96  require_admin=True,
97  )
98 
99 
101  func: AsyncLcnWebSocketCommandHandler,
102 ) -> AsyncWebSocketCommandHandler:
103  """Websocket decorator to ensure the config_entry exists and return it."""
104 
105  @wraps(func)
106  async def get_entry(
107  hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
108  ) -> None:
109  """Get config_entry."""
110  if not (config_entry := hass.config_entries.async_get_entry(msg["entry_id"])):
111  connection.send_result(msg["id"], False)
112  else:
113  await func(hass, connection, msg, config_entry)
114 
115  return get_entry
116 
117 
118 @websocket_api.require_admin
119 @websocket_api.websocket_command( {vol.Required("type"): "lcn/devices", vol.Required("entry_id"): cv.string}
120 )
121 @websocket_api.async_response
122 @get_config_entry
124  hass: HomeAssistant,
125  connection: websocket_api.ActiveConnection,
126  msg: dict,
127  config_entry: ConfigEntry,
128 ) -> None:
129  """Get device configs."""
130  connection.send_result(msg["id"], config_entry.data[CONF_DEVICES])
131 
132 
133 @websocket_api.require_admin
134 @websocket_api.websocket_command( { vol.Required("type"): "lcn/entities",
135  vol.Required("entry_id"): cv.string,
136  vol.Optional(CONF_ADDRESS): ADDRESS_SCHEMA,
137  }
138 )
139 @websocket_api.async_response
140 @get_config_entry
142  hass: HomeAssistant,
143  connection: websocket_api.ActiveConnection,
144  msg: dict,
145  config_entry: ConfigEntry,
146 ) -> None:
147  """Get entities configs."""
148  if CONF_ADDRESS in msg:
149  entity_configs = [
150  entity_config
151  for entity_config in config_entry.data[CONF_ENTITIES]
152  if tuple(entity_config[CONF_ADDRESS]) == msg[CONF_ADDRESS]
153  ]
154  else:
155  entity_configs = config_entry.data[CONF_ENTITIES]
156 
157  result_entity_configs = [
158  {**entity_config, CONF_NAME: entity.name or entity.original_name}
159  for entity_config in entity_configs[:]
160  if (entity := get_entity_entry(hass, entity_config, config_entry)) is not None
161  ]
162 
163  connection.send_result(msg["id"], result_entity_configs)
164 
165 
166 @websocket_api.require_admin
167 @websocket_api.websocket_command( {vol.Required("type"): "lcn/devices/scan", vol.Required("entry_id"): cv.string}
168 )
169 @websocket_api.async_response
170 @get_config_entry
171 async def websocket_scan_devices(
172  hass: HomeAssistant,
173  connection: websocket_api.ActiveConnection,
174  msg: dict,
175  config_entry: ConfigEntry,
176 ) -> None:
177  """Scan for new devices."""
178  host_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION]
179  await host_connection.scan_modules()
180 
181  for device_connection in host_connection.address_conns.values():
182  if not device_connection.is_group:
184  hass, device_connection, config_entry
185  )
186 
187  # create/update devices in device registry
188  register_lcn_address_devices(hass, config_entry)
189 
190  connection.send_result(msg["id"], config_entry.data[CONF_DEVICES])
191 
192 
193 @websocket_api.require_admin
194 @websocket_api.websocket_command( { vol.Required("type"): "lcn/devices/add",
195  vol.Required("entry_id"): cv.string,
196  vol.Required(CONF_ADDRESS): ADDRESS_SCHEMA,
197  }
198 )
199 @websocket_api.async_response
200 @get_config_entry
201 async def websocket_add_device(
202  hass: HomeAssistant,
203  connection: websocket_api.ActiveConnection,
204  msg: dict,
205  config_entry: ConfigEntry,
206 ) -> None:
207  """Add a device."""
208  if get_device_config(msg[CONF_ADDRESS], config_entry):
209  connection.send_result(
210  msg["id"], False
211  ) # device_config already in config_entry
212  return
213 
214  device_config = {
215  CONF_ADDRESS: msg[CONF_ADDRESS],
216  CONF_NAME: "",
217  CONF_HARDWARE_SERIAL: -1,
218  CONF_SOFTWARE_SERIAL: -1,
219  CONF_HARDWARE_TYPE: -1,
220  }
221 
222  # update device info from LCN
223  device_connection = get_device_connection(hass, msg[CONF_ADDRESS], config_entry)
224  await async_update_device_config(device_connection, device_config)
225 
226  # add device_config to config_entry
227  device_configs = [*config_entry.data[CONF_DEVICES], device_config]
228  data = {**config_entry.data, CONF_DEVICES: device_configs}
229  hass.config_entries.async_update_entry(config_entry, data=data)
230 
231  # create/update devices in device registry
232  register_lcn_address_devices(hass, config_entry)
233 
234  connection.send_result(msg["id"], True)
235 
236 
237 @websocket_api.require_admin
238 @websocket_api.websocket_command( { vol.Required("type"): "lcn/devices/delete",
239  vol.Required("entry_id"): cv.string,
240  vol.Required(CONF_ADDRESS): ADDRESS_SCHEMA,
241  }
242 )
243 @websocket_api.async_response
244 @get_config_entry
245 async def websocket_delete_device(
246  hass: HomeAssistant,
247  connection: websocket_api.ActiveConnection,
248  msg: dict,
249  config_entry: ConfigEntry,
250 ) -> None:
251  """Delete a device."""
252  device_config = get_device_config(msg[CONF_ADDRESS], config_entry)
253 
254  device_registry = dr.async_get(hass)
255  identifiers = {
256  (DOMAIN, generate_unique_id(config_entry.entry_id, msg[CONF_ADDRESS]))
257  }
258  device = device_registry.async_get_device(identifiers, set())
259 
260  if not (device and device_config):
261  connection.send_result(msg["id"], False)
262  return
263 
264  # remove module/group device from config_entry data
265  device_configs = [
266  dc for dc in config_entry.data[CONF_DEVICES] if dc != device_config
267  ]
268  data = {**config_entry.data, CONF_DEVICES: device_configs}
269  hass.config_entries.async_update_entry(config_entry, data=data)
270 
271  # remove all child devices (and entities) from config_entry data
272  for entity_config in data[CONF_ENTITIES][:]:
273  if tuple(entity_config[CONF_ADDRESS]) == msg[CONF_ADDRESS]:
274  data[CONF_ENTITIES].remove(entity_config)
275 
276  hass.config_entries.async_update_entry(config_entry, data=data)
277 
278  # cleanup registries
279  purge_entity_registry(hass, config_entry.entry_id, data)
280  purge_device_registry(hass, config_entry.entry_id, data)
281 
282  # return the device config, not all devices !!!
283  connection.send_result(msg["id"])
284 
285 
286 @websocket_api.require_admin
287 @websocket_api.websocket_command( { vol.Required("type"): "lcn/entities/add",
288  vol.Required("entry_id"): cv.string,
289  vol.Required(CONF_ADDRESS): ADDRESS_SCHEMA,
290  vol.Required(CONF_NAME): cv.string,
291  vol.Required(CONF_DOMAIN): cv.string,
292  vol.Required(CONF_DOMAIN_DATA): vol.Any(
293  DOMAIN_DATA_BINARY_SENSOR,
294  DOMAIN_DATA_SENSOR,
295  DOMAIN_DATA_SWITCH,
296  DOMAIN_DATA_LIGHT,
297  DOMAIN_DATA_CLIMATE,
298  DOMAIN_DATA_COVER,
299  DOMAIN_DATA_SCENE,
300  ),
301  }
302 )
303 @websocket_api.async_response
304 @get_config_entry
305 async def websocket_add_entity(
306  hass: HomeAssistant,
307  connection: websocket_api.ActiveConnection,
308  msg: dict,
309  config_entry: ConfigEntry,
310 ) -> None:
311  """Add an entity."""
312  if not (device_config := get_device_config(msg[CONF_ADDRESS], config_entry)):
313  connection.send_result(msg["id"], False)
314  return
315 
316  domain_name = msg[CONF_DOMAIN]
317  domain_data = msg[CONF_DOMAIN_DATA]
318  resource = get_resource(domain_name, domain_data).lower()
319  unique_id = generate_unique_id(
320  config_entry.entry_id,
321  device_config[CONF_ADDRESS],
322  resource,
323  )
324 
325  entity_registry = er.async_get(hass)
326  if entity_registry.async_get_entity_id(msg[CONF_DOMAIN], DOMAIN, unique_id):
327  connection.send_result(msg["id"], False)
328  return
329 
330  entity_config = {
331  CONF_ADDRESS: msg[CONF_ADDRESS],
332  CONF_NAME: msg[CONF_NAME],
333  CONF_RESOURCE: resource,
334  CONF_DOMAIN: domain_name,
335  CONF_DOMAIN_DATA: domain_data,
336  }
337 
338  # Create new entity and add to corresponding component
339  add_entities = hass.data[DOMAIN][msg["entry_id"]][ADD_ENTITIES_CALLBACKS][
340  msg[CONF_DOMAIN]
341  ]
342  add_entities([entity_config])
343 
344  # Add entity config to config_entry
345  entity_configs = [*config_entry.data[CONF_ENTITIES], entity_config]
346  data = {**config_entry.data, CONF_ENTITIES: entity_configs}
347 
348  # schedule config_entry for save
349  hass.config_entries.async_update_entry(config_entry, data=data)
350 
351  connection.send_result(msg["id"], True)
352 
353 
354 @websocket_api.require_admin
355 @websocket_api.websocket_command( { vol.Required("type"): "lcn/entities/delete",
356  vol.Required("entry_id"): cv.string,
357  vol.Required(CONF_ADDRESS): ADDRESS_SCHEMA,
358  vol.Required(CONF_DOMAIN): cv.string,
359  vol.Required(CONF_RESOURCE): cv.string,
360  }
361 )
362 @websocket_api.async_response
363 @get_config_entry
364 async def websocket_delete_entity(
365  hass: HomeAssistant,
366  connection: websocket_api.ActiveConnection,
367  msg: dict,
368  config_entry: ConfigEntry,
369 ) -> None:
370  """Delete an entity."""
371  entity_config = next(
372  (
373  entity_config
374  for entity_config in config_entry.data[CONF_ENTITIES]
375  if (
376  tuple(entity_config[CONF_ADDRESS]) == msg[CONF_ADDRESS]
377  and entity_config[CONF_DOMAIN] == msg[CONF_DOMAIN]
378  and entity_config[CONF_RESOURCE] == msg[CONF_RESOURCE]
379  )
380  ),
381  None,
382  )
383 
384  if entity_config is None:
385  connection.send_result(msg["id"], False)
386  return
387 
388  entity_configs = [
389  ec for ec in config_entry.data[CONF_ENTITIES] if ec != entity_config
390  ]
391  data = {**config_entry.data, CONF_ENTITIES: entity_configs}
392 
393  hass.config_entries.async_update_entry(config_entry, data=data)
394 
395  # cleanup registries
396  purge_entity_registry(hass, config_entry.entry_id, data)
397  purge_device_registry(hass, config_entry.entry_id, data)
398 
399  connection.send_result(msg["id"])
400 
401 
403  hass: HomeAssistant,
404  device_connection: DeviceConnectionType,
405  config_entry: ConfigEntry,
406 ) -> None:
407  """Create or update device in config_entry according to given device_connection."""
408  address = (
409  device_connection.seg_id,
410  device_connection.addr_id,
411  device_connection.is_group,
412  )
413 
414  device_configs = [*config_entry.data[CONF_DEVICES]]
415  data = {**config_entry.data, CONF_DEVICES: device_configs}
416  for device_config in data[CONF_DEVICES]:
417  if tuple(device_config[CONF_ADDRESS]) == address:
418  break # device already in config_entry
419  else:
420  # create new device_entry
421  device_config = {
422  CONF_ADDRESS: address,
423  CONF_NAME: "",
424  CONF_HARDWARE_SERIAL: -1,
425  CONF_SOFTWARE_SERIAL: -1,
426  CONF_HARDWARE_TYPE: -1,
427  }
428  data[CONF_DEVICES].append(device_config)
429 
430  # update device_entry
431  await async_update_device_config(device_connection, device_config)
432 
433  hass.config_entries.async_update_entry(config_entry, data=data)
434 
435 
436 def get_entity_entry(
437  hass: HomeAssistant, entity_config: dict, config_entry: ConfigEntry
438 ) -> er.RegistryEntry | None:
439  """Get entity RegistryEntry from entity_config."""
440  entity_registry = er.async_get(hass)
441  domain_name = entity_config[CONF_DOMAIN]
442  domain_data = entity_config[CONF_DOMAIN_DATA]
443  resource = get_resource(domain_name, domain_data).lower()
444  unique_id = generate_unique_id(
445  config_entry.entry_id,
446  entity_config[CONF_ADDRESS],
447  resource,
448  )
449  if (
450  entity_id := entity_registry.async_get_entity_id(domain_name, DOMAIN, unique_id)
451  ) is None:
452  return None
453  return entity_registry.async_get(entity_id)
454 
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
bool remove(self, _T matcher)
Definition: match.py:214
config_entries.ConfigEntry|None get_entry(HomeAssistant hass, websocket_api.ActiveConnection connection, str entry_id, int msg_id)
str generate_unique_id(list[int] dev_id, int channel)
Definition: switch.py:35
ConfigType|None get_device_config(AddressType address, ConfigEntry config_entry)
Definition: helpers.py:315
None purge_device_registry(HomeAssistant hass, str entry_id, ConfigType imported_entry_data)
Definition: helpers.py:168
None async_update_device_config(DeviceConnectionType device_connection, ConfigType device_config)
Definition: helpers.py:264
None register_lcn_address_devices(HomeAssistant hass, ConfigEntry config_entry)
Definition: helpers.py:224
None purge_entity_registry(HomeAssistant hass, str entry_id, ConfigType imported_entry_data)
Definition: helpers.py:139
DeviceConnectionType get_device_connection(HomeAssistant hass, AddressType address, ConfigEntry config_entry)
Definition: helpers.py:76
str get_resource(str domain_name, ConfigType domain_data)
Definition: helpers.py:83
None websocket_get_entity_configs(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, ConfigEntry config_entry)
Definition: websocket.py:149
None websocket_add_device(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, ConfigEntry config_entry)
Definition: websocket.py:212
AsyncWebSocketCommandHandler get_config_entry(AsyncLcnWebSocketCommandHandler func)
Definition: websocket.py:102
None websocket_delete_device(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, ConfigEntry config_entry)
Definition: websocket.py:258
None websocket_add_entity(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, ConfigEntry config_entry)
Definition: websocket.py:320
None register_panel_and_ws_api(HomeAssistant hass)
Definition: websocket.py:69
None async_create_or_update_device_in_config_entry(HomeAssistant hass, DeviceConnectionType device_connection, ConfigEntry config_entry)
Definition: websocket.py:418
er.RegistryEntry|None get_entity_entry(HomeAssistant hass, dict entity_config, ConfigEntry config_entry)
Definition: websocket.py:450
None websocket_get_device_configs(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, ConfigEntry config_entry)
Definition: websocket.py:129
None websocket_scan_devices(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, ConfigEntry config_entry)
Definition: websocket.py:180
None websocket_delete_entity(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg, ConfigEntry config_entry)
Definition: websocket.py:381