Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Z-Wave JS integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import defaultdict
7 from contextlib import suppress
8 import logging
9 from typing import Any
10 
11 from awesomeversion import AwesomeVersion
12 import voluptuous as vol
13 from zwave_js_server.client import Client as ZwaveClient
14 from zwave_js_server.const import CommandClass, RemoveNodeReason
15 from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion
16 from zwave_js_server.model.driver import Driver
17 from zwave_js_server.model.node import Node as ZwaveNode
18 from zwave_js_server.model.notification import (
19  EntryControlNotification,
20  MultilevelSwitchNotification,
21  NotificationNotification,
22  PowerLevelNotification,
23 )
24 from zwave_js_server.model.value import Value, ValueNotification
25 
26 from homeassistant.components.hassio import AddonError, AddonManager, AddonState
28 from homeassistant.config_entries import ConfigEntry
29 from homeassistant.const import (
30  ATTR_DEVICE_ID,
31  ATTR_DOMAIN,
32  ATTR_ENTITY_ID,
33  CONF_URL,
34  EVENT_HOMEASSISTANT_STOP,
35  EVENT_LOGGING_CHANGED,
36  Platform,
37 )
38 from homeassistant.core import Event, HomeAssistant, callback
39 from homeassistant.exceptions import ConfigEntryNotReady
40 from homeassistant.helpers import (
41  config_validation as cv,
42  device_registry as dr,
43  entity_registry as er,
44 )
45 from homeassistant.helpers.aiohttp_client import async_get_clientsession
46 from homeassistant.helpers.dispatcher import async_dispatcher_send
48  IssueSeverity,
49  async_create_issue,
50  async_delete_issue,
51 )
52 from homeassistant.helpers.typing import UNDEFINED, ConfigType
53 
54 from .addon import get_addon_manager
55 from .api import async_register_api
56 from .const import (
57  ATTR_ACKNOWLEDGED_FRAMES,
58  ATTR_COMMAND_CLASS,
59  ATTR_COMMAND_CLASS_NAME,
60  ATTR_DATA_TYPE,
61  ATTR_DATA_TYPE_LABEL,
62  ATTR_DIRECTION,
63  ATTR_ENDPOINT,
64  ATTR_EVENT,
65  ATTR_EVENT_DATA,
66  ATTR_EVENT_LABEL,
67  ATTR_EVENT_TYPE,
68  ATTR_EVENT_TYPE_LABEL,
69  ATTR_HOME_ID,
70  ATTR_LABEL,
71  ATTR_NODE_ID,
72  ATTR_PARAMETERS,
73  ATTR_PROPERTY,
74  ATTR_PROPERTY_KEY,
75  ATTR_PROPERTY_KEY_NAME,
76  ATTR_PROPERTY_NAME,
77  ATTR_STATUS,
78  ATTR_TEST_NODE_ID,
79  ATTR_TYPE,
80  ATTR_VALUE,
81  ATTR_VALUE_RAW,
82  CONF_ADDON_DEVICE,
83  CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY,
84  CONF_ADDON_LR_S2_AUTHENTICATED_KEY,
85  CONF_ADDON_NETWORK_KEY,
86  CONF_ADDON_S0_LEGACY_KEY,
87  CONF_ADDON_S2_ACCESS_CONTROL_KEY,
88  CONF_ADDON_S2_AUTHENTICATED_KEY,
89  CONF_ADDON_S2_UNAUTHENTICATED_KEY,
90  CONF_DATA_COLLECTION_OPTED_IN,
91  CONF_INSTALLER_MODE,
92  CONF_INTEGRATION_CREATED_ADDON,
93  CONF_LR_S2_ACCESS_CONTROL_KEY,
94  CONF_LR_S2_AUTHENTICATED_KEY,
95  CONF_NETWORK_KEY,
96  CONF_S0_LEGACY_KEY,
97  CONF_S2_ACCESS_CONTROL_KEY,
98  CONF_S2_AUTHENTICATED_KEY,
99  CONF_S2_UNAUTHENTICATED_KEY,
100  CONF_USB_PATH,
101  CONF_USE_ADDON,
102  DATA_CLIENT,
103  DOMAIN,
104  EVENT_DEVICE_ADDED_TO_REGISTRY,
105  EVENT_VALUE_UPDATED,
106  LIB_LOGGER,
107  LOGGER,
108  LR_ADDON_VERSION,
109  USER_AGENT,
110  ZWAVE_JS_NOTIFICATION_EVENT,
111  ZWAVE_JS_VALUE_NOTIFICATION_EVENT,
112  ZWAVE_JS_VALUE_UPDATED_EVENT,
113 )
114 from .discovery import (
115  ZwaveDiscoveryInfo,
116  async_discover_node_values,
117  async_discover_single_value,
118 )
119 from .helpers import (
120  async_disable_server_logging_if_needed,
121  async_enable_server_logging_if_needed,
122  async_enable_statistics,
123  get_device_id,
124  get_device_id_ext,
125  get_network_identifier_for_notification,
126  get_unique_id,
127  get_valueless_base_unique_id,
128 )
129 from .migrate import async_migrate_discovered_value
130 from .services import ZWaveServices
131 
132 CONNECT_TIMEOUT = 10
133 DATA_CLIENT_LISTEN_TASK = "client_listen_task"
134 DATA_DRIVER_EVENTS = "driver_events"
135 DATA_START_CLIENT_TASK = "start_client_task"
136 
137 CONFIG_SCHEMA = vol.Schema(
138  {
139  DOMAIN: vol.Schema(
140  {
141  vol.Optional(CONF_INSTALLER_MODE, default=False): cv.boolean,
142  }
143  )
144  },
145  extra=vol.ALLOW_EXTRA,
146 )
147 
148 
149 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
150  """Set up the Z-Wave JS component."""
151  hass.data[DOMAIN] = config.get(DOMAIN, {})
152  for entry in hass.config_entries.async_entries(DOMAIN):
153  if not isinstance(entry.unique_id, str):
154  hass.config_entries.async_update_entry(
155  entry, unique_id=str(entry.unique_id)
156  )
157 
158  dev_reg = dr.async_get(hass)
159  ent_reg = er.async_get(hass)
160  services = ZWaveServices(hass, ent_reg, dev_reg)
161  services.async_register()
162 
163  return True
164 
165 
166 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
167  """Set up Z-Wave JS from a config entry."""
168  if use_addon := entry.data.get(CONF_USE_ADDON):
169  await async_ensure_addon_running(hass, entry)
170 
171  client = ZwaveClient(
172  entry.data[CONF_URL],
174  additional_user_agent_components=USER_AGENT,
175  )
176 
177  # connect and throw error if connection failed
178  try:
179  async with asyncio.timeout(CONNECT_TIMEOUT):
180  await client.connect()
181  except InvalidServerVersion as err:
182  if use_addon:
183  addon_manager = _get_addon_manager(hass)
184  addon_manager.async_schedule_update_addon(catch_error=True)
185  else:
187  hass,
188  DOMAIN,
189  "invalid_server_version",
190  is_fixable=False,
191  severity=IssueSeverity.ERROR,
192  translation_key="invalid_server_version",
193  )
194  raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
195  except (TimeoutError, BaseZwaveJSServerError) as err:
196  raise ConfigEntryNotReady(f"Failed to connect: {err}") from err
197 
198  async_delete_issue(hass, DOMAIN, "invalid_server_version")
199  LOGGER.info("Connected to Zwave JS Server")
200 
201  # Set up websocket API
202  async_register_api(hass)
203  entry.runtime_data = {}
204 
205  # Create a task to allow the config entry to be unloaded before the driver is ready.
206  # Unloading the config entry is needed if the client listen task errors.
207  start_client_task = hass.async_create_task(start_client(hass, entry, client))
208  entry.runtime_data[DATA_START_CLIENT_TASK] = start_client_task
209 
210  return True
211 
212 
213 async def start_client(
214  hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient
215 ) -> None:
216  """Start listening with the client."""
217  entry.runtime_data[DATA_CLIENT] = client
218  driver_events = entry.runtime_data[DATA_DRIVER_EVENTS] = DriverEvents(hass, entry)
219 
220  async def handle_ha_shutdown(event: Event) -> None:
221  """Handle HA shutdown."""
222  await disconnect_client(hass, entry)
223 
224  listen_task = asyncio.create_task(
225  client_listen(hass, entry, client, driver_events.ready)
226  )
227  entry.runtime_data[DATA_CLIENT_LISTEN_TASK] = listen_task
228  entry.async_on_unload(
229  hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown)
230  )
231 
232  try:
233  await driver_events.ready.wait()
234  except asyncio.CancelledError:
235  LOGGER.debug("Cancelling start client")
236  return
237 
238  LOGGER.info("Connection to Zwave JS Server initialized")
239 
240  assert client.driver
242  hass, f"{DOMAIN}_{client.driver.controller.home_id}_connected_to_server"
243  )
244 
245  await driver_events.setup(client.driver)
246 
247 
249  """Represent driver events."""
250 
251  driver: Driver
252 
253  def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
254  """Set up the driver events instance."""
255  self.config_entryconfig_entry = entry
256  self.dev_regdev_reg = dr.async_get(hass)
257  self.hasshass = hass
258  self.platform_setup_tasks: dict[str, asyncio.Task] = {}
259  self.readyready = asyncio.Event()
260  # Make sure to not pass self to ControllerEvents until all attributes are set.
261  self.controller_eventscontroller_events = ControllerEvents(hass, self)
262 
263  async def setup(self, driver: Driver) -> None:
264  """Set up devices using the ready driver."""
265  self.driverdriver = driver
266  controller = driver.controller
267 
268  # If opt in preference hasn't been specified yet, we do nothing, otherwise
269  # we apply the preference
270  if opted_in := self.config_entryconfig_entry.data.get(CONF_DATA_COLLECTION_OPTED_IN):
271  await async_enable_statistics(driver)
272  elif opted_in is False:
273  await driver.async_disable_statistics()
274 
275  async def handle_logging_changed(_: Event | None = None) -> None:
276  """Handle logging changed event."""
277  if LIB_LOGGER.isEnabledFor(logging.DEBUG):
279  self.hasshass, self.config_entryconfig_entry, driver
280  )
281  else:
283  self.hasshass, self.config_entryconfig_entry, driver
284  )
285 
286  # Set up server logging on setup if needed
287  await handle_logging_changed()
288 
289  self.config_entryconfig_entry.async_on_unload(
290  self.hasshass.bus.async_listen(EVENT_LOGGING_CHANGED, handle_logging_changed)
291  )
292 
293  # Check for nodes that no longer exist and remove them
294  stored_devices = dr.async_entries_for_config_entry(
295  self.dev_regdev_reg, self.config_entryconfig_entry.entry_id
296  )
297  known_devices = [
298  self.dev_regdev_reg.async_get_device(identifiers={get_device_id(driver, node)})
299  for node in controller.nodes.values()
300  ]
301 
302  # Devices that are in the device registry that are not known by the controller
303  # can be removed
304  for device in stored_devices:
305  if device not in known_devices:
306  self.dev_regdev_reg.async_remove_device(device.id)
307 
308  # run discovery on controller node
309  if controller.own_node:
310  await self.controller_eventscontroller_events.async_on_node_added(controller.own_node)
311 
312  # run discovery on all other ready nodes
313  await asyncio.gather(
314  *(
315  self.controller_eventscontroller_events.async_on_node_added(node)
316  for node in controller.nodes.values()
317  if node != controller.own_node
318  )
319  )
320 
321  # listen for new nodes being added to the mesh
322  self.config_entryconfig_entry.async_on_unload(
323  controller.on(
324  "node added",
325  lambda event: self.hasshass.async_create_task(
326  self.controller_eventscontroller_events.async_on_node_added(event["node"]),
327  eager_start=False,
328  ),
329  )
330  )
331  # listen for nodes being removed from the mesh
332  # NOTE: This will not remove nodes that were removed when HA was not running
333  self.config_entryconfig_entry.async_on_unload(
334  controller.on("node removed", self.controller_eventscontroller_events.async_on_node_removed)
335  )
336 
337  # listen for identify events for the controller
338  self.config_entryconfig_entry.async_on_unload(
339  controller.on("identify", self.controller_eventscontroller_events.async_on_identify)
340  )
341 
342  async def async_setup_platform(self, platform: Platform) -> None:
343  """Set up platform if needed."""
344  if platform not in self.platform_setup_tasks:
345  self.platform_setup_tasks[platform] = self.hasshass.async_create_task(
346  self.hasshass.config_entries.async_forward_entry_setups(
347  self.config_entryconfig_entry, [platform]
348  )
349  )
350  await self.platform_setup_tasks[platform]
351 
352 
354  """Represent controller events.
355 
356  Handle the following events:
357  - node added
358  - node removed
359  """
360 
361  def __init__(self, hass: HomeAssistant, driver_events: DriverEvents) -> None:
362  """Set up the controller events instance."""
363  self.hasshass = hass
364  self.config_entryconfig_entry = driver_events.config_entry
365  self.discovered_value_ids: dict[str, set[str]] = defaultdict(set)
366  self.driver_eventsdriver_events = driver_events
367  self.dev_regdev_reg = driver_events.dev_reg
368  self.registered_unique_ids: dict[str, dict[Platform, set[str]]] = defaultdict(
369  lambda: defaultdict(set)
370  )
371  self.node_eventsnode_events = NodeEvents(hass, self)
372 
373  @callback
374  def remove_device(self, device: dr.DeviceEntry) -> None:
375  """Remove device from registry."""
376  # note: removal of entity registry entry is handled by core
377  self.dev_regdev_reg.async_remove_device(device.id)
378  self.registered_unique_ids.pop(device.id, None)
379  self.discovered_value_ids.pop(device.id, None)
380 
381  async def async_on_node_added(self, node: ZwaveNode) -> None:
382  """Handle node added event."""
383  # Every node including the controller will have at least one sensor
384  await self.driver_eventsdriver_events.async_setup_platform(Platform.SENSOR)
385 
386  # Remove stale entities that may exist from a previous interview when an
387  # interview is started.
388  base_unique_id = get_valueless_base_unique_id(self.driver_eventsdriver_events.driver, node)
389  self.config_entryconfig_entry.async_on_unload(
390  node.on(
391  "interview started",
392  lambda _: async_dispatcher_send(
393  self.hasshass,
394  f"{DOMAIN}_{base_unique_id}_remove_entity_on_interview_started",
395  ),
396  )
397  )
398 
399  if node.is_controller_node:
400  # Create a controller status sensor for each device
402  self.hasshass,
403  f"{DOMAIN}_{self.config_entry.entry_id}_add_controller_status_sensor",
404  )
405  else:
406  # Create a node status sensor for each device
408  self.hasshass,
409  f"{DOMAIN}_{self.config_entry.entry_id}_add_node_status_sensor",
410  node,
411  )
412 
413  # Create a ping button for each device
414  await self.driver_eventsdriver_events.async_setup_platform(Platform.BUTTON)
416  self.hasshass,
417  f"{DOMAIN}_{self.config_entry.entry_id}_add_ping_button_entity",
418  node,
419  )
420 
421  # Create statistics sensors for each device
423  self.hasshass,
424  f"{DOMAIN}_{self.config_entry.entry_id}_add_statistics_sensors",
425  node,
426  )
427 
428  LOGGER.debug("Node added: %s", node.node_id)
429 
430  # Listen for ready node events, both new and re-interview.
431  self.config_entryconfig_entry.async_on_unload(
432  node.on(
433  "ready",
434  lambda event: self.hasshass.async_create_task(
435  self.node_eventsnode_events.async_on_node_ready(event["node"]),
436  eager_start=False,
437  ),
438  )
439  )
440 
441  # we only want to run discovery when the node has reached ready state,
442  # otherwise we'll have all kinds of missing info issues.
443  if node.ready:
444  await self.node_eventsnode_events.async_on_node_ready(node)
445  return
446 
447  # we do submit the node to device registry so user has
448  # some visual feedback that something is (in the process of) being added
449  self.register_node_in_dev_regregister_node_in_dev_reg(node)
450 
451  @callback
452  def async_on_node_removed(self, event: dict) -> None:
453  """Handle node removed event."""
454  node: ZwaveNode = event["node"]
455  reason: RemoveNodeReason = event["reason"]
456  # grab device in device registry attached to this node
457  dev_id = get_device_id(self.driver_eventsdriver_events.driver, node)
458  device = self.dev_regdev_reg.async_get_device(identifiers={dev_id})
459  # We assert because we know the device exists
460  assert device
461  if reason in (RemoveNodeReason.REPLACED, RemoveNodeReason.PROXY_REPLACED):
462  self.discovered_value_ids.pop(device.id, None)
463 
465  self.hasshass,
466  (
467  f"{DOMAIN}_"
468  f"{get_valueless_base_unique_id(self.driver_events.driver, node)}_"
469  "remove_entity"
470  ),
471  )
472  # We don't want to remove the device so we can keep the user customizations
473  return
474 
475  if reason == RemoveNodeReason.RESET:
476  device_name = device.name_by_user or device.name or f"Node {node.node_id}"
478  self.hasshass, self.config_entryconfig_entry, self.driver_eventsdriver_events.driver.controller
479  )
480  notification_msg = (
481  f"`{device_name}` has been factory reset "
482  "and removed from the Z-Wave network"
483  )
484  if identifier:
485  # Remove trailing comma if it's there
486  if identifier[-1] == ",":
487  identifier = identifier[:-1]
488  notification_msg = f"{notification_msg} {identifier}."
489  else:
490  notification_msg = f"{notification_msg}."
491  async_create(
492  self.hasshass,
493  notification_msg,
494  "Device Was Factory Reset!",
495  f"{DOMAIN}.node_reset_and_removed.{dev_id[1]}",
496  )
497 
498  self.remove_deviceremove_device(device)
499 
500  @callback
501  def async_on_identify(self, event: dict) -> None:
502  """Handle identify event."""
503  # Get node device
504  node: ZwaveNode = event["node"]
505  dev_id = get_device_id(self.driver_eventsdriver_events.driver, node)
506  device = self.dev_regdev_reg.async_get_device(identifiers={dev_id})
507  assert device
508  device_name = device.name_by_user or device.name or f"Node {node.node_id}"
509  # In case the user has multiple networks, we should give them more information
510  # about the network for the controller being identified.
512  self.hasshass, self.config_entryconfig_entry, self.driver_eventsdriver_events.driver.controller
513  )
514  async_create(
515  self.hasshass,
516  (
517  f"`{device_name}` has just requested the controller for your Z-Wave "
518  f"network {identifier} to identify itself. No action is needed from "
519  "you other than to note the source of the request, and you can safely "
520  "dismiss this notification when ready."
521  ),
522  "New Z-Wave Identify Controller Request",
523  f"{DOMAIN}.identify_controller.{dev_id[1]}",
524  )
525 
526  @callback
527  def register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry:
528  """Register node in dev reg."""
529  driver = self.driver_eventsdriver_events.driver
530  device_id = get_device_id(driver, node)
531  device_id_ext = get_device_id_ext(driver, node)
532  node_id_device = self.dev_regdev_reg.async_get_device(identifiers={device_id})
533  via_device_id = None
534  controller = driver.controller
535  # Get the controller node device ID if this node is not the controller
536  if controller.own_node and controller.own_node != node:
537  via_device_id = get_device_id(driver, controller.own_node)
538 
539  if device_id_ext:
540  # If there is a device with this node ID but with a different hardware
541  # signature, remove the node ID based identifier from it. The hardware
542  # signature can be different for one of two reasons: 1) in the ideal
543  # scenario, the node was replaced with a different node that's a different
544  # device entirely, or 2) the device erroneously advertised the wrong
545  # hardware identifiers (this is known to happen due to poor RF conditions).
546  # While we would like to remove the old device automatically for case 1, we
547  # have no way to distinguish between these reasons so we leave it up to the
548  # user to remove the old device manually.
549  if (
550  node_id_device
551  and len(node_id_device.identifiers) == 2
552  and device_id_ext not in node_id_device.identifiers
553  ):
554  new_identifiers = node_id_device.identifiers.copy()
555  new_identifiers.remove(device_id)
556  self.dev_regdev_reg.async_update_device(
557  node_id_device.id, new_identifiers=new_identifiers
558  )
559  # If there is an orphaned device that already exists with this hardware
560  # based identifier, add the node ID based identifier to the orphaned
561  # device.
562  if (
563  hardware_device := self.dev_regdev_reg.async_get_device(
564  identifiers={device_id_ext}
565  )
566  ) and len(hardware_device.identifiers) == 1:
567  new_identifiers = hardware_device.identifiers.copy()
568  new_identifiers.add(device_id)
569  self.dev_regdev_reg.async_update_device(
570  hardware_device.id, new_identifiers=new_identifiers
571  )
572  ids = {device_id, device_id_ext}
573  else:
574  ids = {device_id}
575 
576  device = self.dev_regdev_reg.async_get_or_create(
577  config_entry_id=self.config_entryconfig_entry.entry_id,
578  identifiers=ids,
579  sw_version=node.firmware_version,
580  name=node.name or node.device_config.description or f"Node {node.node_id}",
581  model=node.device_config.label,
582  manufacturer=node.device_config.manufacturer,
583  suggested_area=node.location if node.location else UNDEFINED,
584  via_device=via_device_id,
585  )
586 
587  async_dispatcher_send(self.hasshass, EVENT_DEVICE_ADDED_TO_REGISTRY, device)
588 
589  return device
590 
591 
593  """Represent node events.
594 
595  Handle the following events:
596  - ready
597  - value added
598  - value updated
599  - metadata updated
600  - value notification
601  - notification
602  """
603 
604  def __init__(
605  self, hass: HomeAssistant, controller_events: ControllerEvents
606  ) -> None:
607  """Set up the node events instance."""
608  self.config_entryconfig_entry = controller_events.config_entry
609  self.controller_eventscontroller_events = controller_events
610  self.dev_regdev_reg = controller_events.dev_reg
611  self.ent_regent_reg = er.async_get(hass)
612  self.hasshass = hass
613 
614  async def async_on_node_ready(self, node: ZwaveNode) -> None:
615  """Handle node ready event."""
616  LOGGER.debug("Processing node %s", node)
617  # register (or update) node in device registry
618  device = self.controller_eventscontroller_events.register_node_in_dev_reg(node)
619 
620  # Remove any old value ids if this is a reinterview.
621  self.controller_eventscontroller_events.discovered_value_ids.pop(device.id, None)
622 
623  value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {}
624 
625  # run discovery on all node values and create/update entities
626  await asyncio.gather(
627  *(
628  self.async_handle_discovery_infoasync_handle_discovery_info(
629  device, disc_info, value_updates_disc_info
630  )
631  for disc_info in async_discover_node_values(
632  node, device, self.controller_eventscontroller_events.discovered_value_ids
633  )
634  )
635  )
636 
637  # add listeners to handle new values that get added later
638  for event in ("value added", EVENT_VALUE_UPDATED, "metadata updated"):
639  self.config_entryconfig_entry.async_on_unload(
640  node.on(
641  event,
642  lambda event: self.hasshass.async_create_task(
643  self.async_on_value_addedasync_on_value_added(
644  value_updates_disc_info, event["value"]
645  )
646  ),
647  )
648  )
649 
650  # add listener for stateless node value notification events
651  self.config_entryconfig_entry.async_on_unload(
652  node.on(
653  "value notification",
654  lambda event: self.async_on_value_notificationasync_on_value_notification(
655  event["value_notification"]
656  ),
657  )
658  )
659 
660  # add listener for stateless node notification events
661  self.config_entryconfig_entry.async_on_unload(
662  node.on("notification", self.async_on_notificationasync_on_notification)
663  )
664 
665  # Create a firmware update entity for each non-controller device that
666  # supports firmware updates
667  if not node.is_controller_node and any(
668  cc.id == CommandClass.FIRMWARE_UPDATE_MD.value
669  for cc in node.command_classes
670  ):
671  await self.controller_eventscontroller_events.driver_events.async_setup_platform(
672  Platform.UPDATE
673  )
675  self.hasshass,
676  f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity",
677  node,
678  )
679 
680  # After ensuring the node is set up in HA, we should check if the node's
681  # device config has changed, and if so, issue a repair registry entry for a
682  # possible reinterview
683  if not node.is_controller_node and await node.async_has_device_config_changed():
684  device_name = device.name_by_user or device.name or "Unnamed device"
686  self.hasshass,
687  DOMAIN,
688  f"device_config_file_changed.{device.id}",
689  data={"device_id": device.id, "device_name": device_name},
690  is_fixable=True,
691  is_persistent=False,
692  translation_key="device_config_file_changed",
693  translation_placeholders={"device_name": device_name},
694  severity=IssueSeverity.WARNING,
695  )
696 
698  self,
699  device: dr.DeviceEntry,
700  disc_info: ZwaveDiscoveryInfo,
701  value_updates_disc_info: dict[str, ZwaveDiscoveryInfo],
702  ) -> None:
703  """Handle discovery info and all dependent tasks."""
704  # This migration logic was added in 2021.3 to handle a breaking change to
705  # the value_id format. Some time in the future, this call (as well as the
706  # helper functions) can be removed.
708  self.hasshass,
709  self.ent_regent_reg,
710  self.controller_eventscontroller_events.registered_unique_ids[device.id][disc_info.platform],
711  device,
712  self.controller_eventscontroller_events.driver_events.driver,
713  disc_info,
714  )
715 
716  platform = disc_info.platform
717  await self.controller_eventscontroller_events.driver_events.async_setup_platform(platform)
718 
719  LOGGER.debug("Discovered entity: %s", disc_info)
721  self.hasshass,
722  f"{DOMAIN}_{self.config_entry.entry_id}_add_{platform}",
723  disc_info,
724  )
725 
726  # If we don't need to watch for updates return early
727  if not disc_info.assumed_state:
728  return
729  value_updates_disc_info[disc_info.primary_value.value_id] = disc_info
730  # If this is not the first time we found a value we want to watch for updates,
731  # return early because we only need one listener for all values.
732  if len(value_updates_disc_info) != 1:
733  return
734  # add listener for value updated events
735  self.config_entryconfig_entry.async_on_unload(
736  disc_info.node.on(
737  EVENT_VALUE_UPDATED,
738  lambda event: self.async_on_value_updated_fire_eventasync_on_value_updated_fire_event(
739  value_updates_disc_info, event["value"]
740  ),
741  )
742  )
743 
745  self, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value
746  ) -> None:
747  """Fire value updated event."""
748  # If node isn't ready or a device for this node doesn't already exist, we can
749  # let the node ready event handler perform discovery. If a value has already
750  # been processed, we don't need to do it again
751  device_id = get_device_id(
752  self.controller_eventscontroller_events.driver_events.driver, value.node
753  )
754  if (
755  not value.node.ready
756  or not (device := self.dev_regdev_reg.async_get_device(identifiers={device_id}))
757  or value.value_id in self.controller_eventscontroller_events.discovered_value_ids[device.id]
758  ):
759  return
760 
761  LOGGER.debug("Processing node %s added value %s", value.node, value)
762  await asyncio.gather(
763  *(
764  self.async_handle_discovery_infoasync_handle_discovery_info(
765  device, disc_info, value_updates_disc_info
766  )
767  for disc_info in async_discover_single_value(
768  value, device, self.controller_eventscontroller_events.discovered_value_ids
769  )
770  )
771  )
772 
773  @callback
774  def async_on_value_notification(self, notification: ValueNotification) -> None:
775  """Relay stateless value notification events from Z-Wave nodes to hass."""
776  driver = self.controller_eventscontroller_events.driver_events.driver
777  device = self.dev_regdev_reg.async_get_device(
778  identifiers={get_device_id(driver, notification.node)}
779  )
780  # We assert because we know the device exists
781  assert device
782  raw_value = value = notification.value
783  if notification.metadata.states:
784  value = notification.metadata.states.get(str(value), value)
785  self.hasshass.bus.async_fire(
786  ZWAVE_JS_VALUE_NOTIFICATION_EVENT,
787  {
788  ATTR_DOMAIN: DOMAIN,
789  ATTR_NODE_ID: notification.node.node_id,
790  ATTR_HOME_ID: driver.controller.home_id,
791  ATTR_ENDPOINT: notification.endpoint,
792  ATTR_DEVICE_ID: device.id,
793  ATTR_COMMAND_CLASS: notification.command_class,
794  ATTR_COMMAND_CLASS_NAME: notification.command_class_name,
795  ATTR_LABEL: notification.metadata.label,
796  ATTR_PROPERTY: notification.property_,
797  ATTR_PROPERTY_NAME: notification.property_name,
798  ATTR_PROPERTY_KEY: notification.property_key,
799  ATTR_PROPERTY_KEY_NAME: notification.property_key_name,
800  ATTR_VALUE: value,
801  ATTR_VALUE_RAW: raw_value,
802  },
803  )
804 
805  @callback
806  def async_on_notification(self, event: dict[str, Any]) -> None:
807  """Relay stateless notification events from Z-Wave nodes to hass."""
808  if "notification" not in event:
809  LOGGER.info("Unknown notification: %s", event)
810  return
811 
812  driver = self.controller_eventscontroller_events.driver_events.driver
813  notification: (
814  EntryControlNotification
815  | NotificationNotification
816  | PowerLevelNotification
817  | MultilevelSwitchNotification
818  ) = event["notification"]
819  device = self.dev_regdev_reg.async_get_device(
820  identifiers={get_device_id(driver, notification.node)}
821  )
822  # We assert because we know the device exists
823  assert device
824  event_data = {
825  ATTR_DOMAIN: DOMAIN,
826  ATTR_NODE_ID: notification.node.node_id,
827  ATTR_HOME_ID: driver.controller.home_id,
828  ATTR_ENDPOINT: notification.endpoint_idx,
829  ATTR_DEVICE_ID: device.id,
830  ATTR_COMMAND_CLASS: notification.command_class,
831  }
832 
833  if isinstance(notification, EntryControlNotification):
834  event_data.update(
835  {
836  ATTR_COMMAND_CLASS_NAME: "Entry Control",
837  ATTR_EVENT_TYPE: notification.event_type,
838  ATTR_EVENT_TYPE_LABEL: notification.event_type_label,
839  ATTR_DATA_TYPE: notification.data_type,
840  ATTR_DATA_TYPE_LABEL: notification.data_type_label,
841  ATTR_EVENT_DATA: notification.event_data,
842  }
843  )
844  elif isinstance(notification, NotificationNotification):
845  event_data.update(
846  {
847  ATTR_COMMAND_CLASS_NAME: "Notification",
848  ATTR_LABEL: notification.label,
849  ATTR_TYPE: notification.type_,
850  ATTR_EVENT: notification.event,
851  ATTR_EVENT_LABEL: notification.event_label,
852  ATTR_PARAMETERS: notification.parameters,
853  }
854  )
855  elif isinstance(notification, PowerLevelNotification):
856  event_data.update(
857  {
858  ATTR_COMMAND_CLASS_NAME: "Powerlevel",
859  ATTR_TEST_NODE_ID: notification.test_node_id,
860  ATTR_STATUS: notification.status,
861  ATTR_ACKNOWLEDGED_FRAMES: notification.acknowledged_frames,
862  }
863  )
864  elif isinstance(notification, MultilevelSwitchNotification):
865  event_data.update(
866  {
867  ATTR_COMMAND_CLASS_NAME: "Multilevel Switch",
868  ATTR_EVENT_TYPE: notification.event_type,
869  ATTR_EVENT_TYPE_LABEL: notification.event_type_label,
870  ATTR_DIRECTION: notification.direction,
871  }
872  )
873  else:
874  raise TypeError(f"Unhandled notification type: {notification}")
875 
876  self.hasshass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data)
877 
878  @callback
880  self, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value
881  ) -> None:
882  """Fire value updated event."""
883  # Get the discovery info for the value that was updated. If there is
884  # no discovery info for this value, we don't need to fire an event
885  if value.value_id not in value_updates_disc_info:
886  return
887 
888  driver = self.controller_eventscontroller_events.driver_events.driver
889  disc_info = value_updates_disc_info[value.value_id]
890 
891  device = self.dev_regdev_reg.async_get_device(
892  identifiers={get_device_id(driver, value.node)}
893  )
894  # We assert because we know the device exists
895  assert device
896 
897  unique_id = get_unique_id(driver, disc_info.primary_value.value_id)
898  entity_id = self.ent_regent_reg.async_get_entity_id(
899  disc_info.platform, DOMAIN, unique_id
900  )
901 
902  raw_value = value_ = value.value
903  if value.metadata.states:
904  value_ = value.metadata.states.get(str(value_), value_)
905 
906  self.hasshass.bus.async_fire(
907  ZWAVE_JS_VALUE_UPDATED_EVENT,
908  {
909  ATTR_NODE_ID: value.node.node_id,
910  ATTR_HOME_ID: driver.controller.home_id,
911  ATTR_DEVICE_ID: device.id,
912  ATTR_ENTITY_ID: entity_id,
913  ATTR_COMMAND_CLASS: value.command_class,
914  ATTR_COMMAND_CLASS_NAME: value.command_class_name,
915  ATTR_ENDPOINT: value.endpoint,
916  ATTR_PROPERTY: value.property_,
917  ATTR_PROPERTY_NAME: value.property_name,
918  ATTR_PROPERTY_KEY: value.property_key,
919  ATTR_PROPERTY_KEY_NAME: value.property_key_name,
920  ATTR_VALUE: value_,
921  ATTR_VALUE_RAW: raw_value,
922  },
923  )
924 
925 
926 async def client_listen(
927  hass: HomeAssistant,
928  entry: ConfigEntry,
929  client: ZwaveClient,
930  driver_ready: asyncio.Event,
931 ) -> None:
932  """Listen with the client."""
933  should_reload = True
934  try:
935  await client.listen(driver_ready)
936  except asyncio.CancelledError:
937  should_reload = False
938  except BaseZwaveJSServerError as err:
939  LOGGER.error("Failed to listen: %s", err)
940  except Exception as err: # noqa: BLE001
941  # We need to guard against unknown exceptions to not crash this task.
942  LOGGER.exception("Unexpected exception: %s", err)
943 
944  # The entry needs to be reloaded since a new driver state
945  # will be acquired on reconnect.
946  # All model instances will be replaced when the new state is acquired.
947  if should_reload:
948  LOGGER.info("Disconnected from server. Reloading integration")
949  hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
950 
951 
952 async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None:
953  """Disconnect client."""
954  client: ZwaveClient = entry.runtime_data[DATA_CLIENT]
955  listen_task: asyncio.Task = entry.runtime_data[DATA_CLIENT_LISTEN_TASK]
956  start_client_task: asyncio.Task = entry.runtime_data[DATA_START_CLIENT_TASK]
957  driver_events: DriverEvents = entry.runtime_data[DATA_DRIVER_EVENTS]
958  listen_task.cancel()
959  start_client_task.cancel()
960  platform_setup_tasks = driver_events.platform_setup_tasks.values()
961  for task in platform_setup_tasks:
962  task.cancel()
963 
964  tasks = (listen_task, start_client_task, *platform_setup_tasks)
965  await asyncio.gather(*tasks, return_exceptions=True)
966  for task in tasks:
967  with suppress(asyncio.CancelledError):
968  await task
969 
970  if client.connected:
971  await client.disconnect()
972  LOGGER.info("Disconnected from Zwave JS Server")
973 
974 
975 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
976  """Unload a config entry."""
977  client: ZwaveClient = entry.runtime_data[DATA_CLIENT]
978  driver_events: DriverEvents = entry.runtime_data[DATA_DRIVER_EVENTS]
979  platforms = [
980  platform
981  for platform, task in driver_events.platform_setup_tasks.items()
982  if not task.cancel()
983  ]
984  unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms)
985 
986  if client.connected and client.driver:
987  await async_disable_server_logging_if_needed(hass, entry, client.driver)
988  if DATA_CLIENT_LISTEN_TASK in entry.runtime_data:
989  await disconnect_client(hass, entry)
990 
991  if entry.data.get(CONF_USE_ADDON) and entry.disabled_by:
992  addon_manager: AddonManager = get_addon_manager(hass)
993  LOGGER.debug("Stopping Z-Wave JS add-on")
994  try:
995  await addon_manager.async_stop_addon()
996  except AddonError as err:
997  LOGGER.error("Failed to stop the Z-Wave JS add-on: %s", err)
998  return False
999 
1000  return unload_ok
1001 
1002 
1003 async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
1004  """Remove a config entry."""
1005  if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON):
1006  return
1007 
1008  addon_manager: AddonManager = get_addon_manager(hass)
1009  try:
1010  await addon_manager.async_stop_addon()
1011  except AddonError as err:
1012  LOGGER.error(err)
1013  return
1014  try:
1015  await addon_manager.async_create_backup()
1016  except AddonError as err:
1017  LOGGER.error(err)
1018  return
1019  try:
1020  await addon_manager.async_uninstall_addon()
1021  except AddonError as err:
1022  LOGGER.error(err)
1023 
1024 
1026  hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
1027 ) -> bool:
1028  """Remove a config entry from a device."""
1029  client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT]
1030 
1031  # Driver may not be ready yet so we can't allow users to remove a device since
1032  # we need to check if the device is still known to the controller
1033  if (driver := client.driver) is None:
1034  LOGGER.error("Driver for %s is not ready", config_entry.title)
1035  return False
1036 
1037  # If a node is found on the controller that matches the hardware based identifier
1038  # on the device, prevent the device from being removed.
1039  if next(
1040  (
1041  node
1042  for node in driver.controller.nodes.values()
1043  if get_device_id_ext(driver, node) in device_entry.identifiers
1044  ),
1045  None,
1046  ):
1047  return False
1048 
1049  controller_events: ControllerEvents = config_entry.runtime_data[
1050  DATA_DRIVER_EVENTS
1051  ].controller_events
1052  controller_events.registered_unique_ids.pop(device_entry.id, None)
1053  controller_events.discovered_value_ids.pop(device_entry.id, None)
1054  return True
1055 
1056 
1057 async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None:
1058  """Ensure that Z-Wave JS add-on is installed and running."""
1059  addon_manager = _get_addon_manager(hass)
1060  try:
1061  addon_info = await addon_manager.async_get_addon_info()
1062  except AddonError as err:
1063  raise ConfigEntryNotReady(err) from err
1064 
1065  usb_path: str = entry.data[CONF_USB_PATH]
1066  # s0_legacy_key was saved as network_key before s2 was added.
1067  s0_legacy_key: str = entry.data.get(CONF_S0_LEGACY_KEY, "")
1068  if not s0_legacy_key:
1069  s0_legacy_key = entry.data.get(CONF_NETWORK_KEY, "")
1070  s2_access_control_key: str = entry.data.get(CONF_S2_ACCESS_CONTROL_KEY, "")
1071  s2_authenticated_key: str = entry.data.get(CONF_S2_AUTHENTICATED_KEY, "")
1072  s2_unauthenticated_key: str = entry.data.get(CONF_S2_UNAUTHENTICATED_KEY, "")
1073  lr_s2_access_control_key: str = entry.data.get(CONF_LR_S2_ACCESS_CONTROL_KEY, "")
1074  lr_s2_authenticated_key: str = entry.data.get(CONF_LR_S2_AUTHENTICATED_KEY, "")
1075  addon_state = addon_info.state
1076  addon_config = {
1077  CONF_ADDON_DEVICE: usb_path,
1078  CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key,
1079  CONF_ADDON_S2_ACCESS_CONTROL_KEY: s2_access_control_key,
1080  CONF_ADDON_S2_AUTHENTICATED_KEY: s2_authenticated_key,
1081  CONF_ADDON_S2_UNAUTHENTICATED_KEY: s2_unauthenticated_key,
1082  }
1083  if addon_info.version and AwesomeVersion(addon_info.version) >= LR_ADDON_VERSION:
1084  addon_config[CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY] = lr_s2_access_control_key
1085  addon_config[CONF_ADDON_LR_S2_AUTHENTICATED_KEY] = lr_s2_authenticated_key
1086 
1087  if addon_state == AddonState.NOT_INSTALLED:
1088  addon_manager.async_schedule_install_setup_addon(
1089  addon_config,
1090  catch_error=True,
1091  )
1092  raise ConfigEntryNotReady
1093 
1094  if addon_state == AddonState.NOT_RUNNING:
1095  addon_manager.async_schedule_setup_addon(
1096  addon_config,
1097  catch_error=True,
1098  )
1099  raise ConfigEntryNotReady
1100 
1101  addon_options = addon_info.options
1102  addon_device = addon_options[CONF_ADDON_DEVICE]
1103  # s0_legacy_key was saved as network_key before s2 was added.
1104  addon_s0_legacy_key = addon_options.get(CONF_ADDON_S0_LEGACY_KEY, "")
1105  if not addon_s0_legacy_key:
1106  addon_s0_legacy_key = addon_options.get(CONF_ADDON_NETWORK_KEY, "")
1107  addon_s2_access_control_key = addon_options.get(
1108  CONF_ADDON_S2_ACCESS_CONTROL_KEY, ""
1109  )
1110  addon_s2_authenticated_key = addon_options.get(CONF_ADDON_S2_AUTHENTICATED_KEY, "")
1111  addon_s2_unauthenticated_key = addon_options.get(
1112  CONF_ADDON_S2_UNAUTHENTICATED_KEY, ""
1113  )
1114  updates = {}
1115  if usb_path != addon_device:
1116  updates[CONF_USB_PATH] = addon_device
1117  if s0_legacy_key != addon_s0_legacy_key:
1118  updates[CONF_S0_LEGACY_KEY] = addon_s0_legacy_key
1119  if s2_access_control_key != addon_s2_access_control_key:
1120  updates[CONF_S2_ACCESS_CONTROL_KEY] = addon_s2_access_control_key
1121  if s2_authenticated_key != addon_s2_authenticated_key:
1122  updates[CONF_S2_AUTHENTICATED_KEY] = addon_s2_authenticated_key
1123  if s2_unauthenticated_key != addon_s2_unauthenticated_key:
1124  updates[CONF_S2_UNAUTHENTICATED_KEY] = addon_s2_unauthenticated_key
1125 
1126  if addon_info.version and AwesomeVersion(addon_info.version) >= AwesomeVersion(
1127  LR_ADDON_VERSION
1128  ):
1129  addon_lr_s2_access_control_key = addon_options.get(
1130  CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, ""
1131  )
1132  addon_lr_s2_authenticated_key = addon_options.get(
1133  CONF_ADDON_LR_S2_AUTHENTICATED_KEY, ""
1134  )
1135  if lr_s2_access_control_key != addon_lr_s2_access_control_key:
1136  updates[CONF_LR_S2_ACCESS_CONTROL_KEY] = addon_lr_s2_access_control_key
1137  if lr_s2_authenticated_key != addon_lr_s2_authenticated_key:
1138  updates[CONF_LR_S2_AUTHENTICATED_KEY] = addon_lr_s2_authenticated_key
1139 
1140  if updates:
1141  hass.config_entries.async_update_entry(entry, data={**entry.data, **updates})
1142 
1143 
1144 @callback
1145 def _get_addon_manager(hass: HomeAssistant) -> AddonManager:
1146  """Ensure that Z-Wave JS add-on is updated and running."""
1147  addon_manager: AddonManager = get_addon_manager(hass)
1148  if addon_manager.task_in_progress():
1149  raise ConfigEntryNotReady
1150  return addon_manager
None __init__(self, HomeAssistant hass, DriverEvents driver_events)
Definition: __init__.py:361
None remove_device(self, dr.DeviceEntry device)
Definition: __init__.py:374
dr.DeviceEntry register_node_in_dev_reg(self, ZwaveNode node)
Definition: __init__.py:527
None async_on_node_added(self, ZwaveNode node)
Definition: __init__.py:381
None __init__(self, HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:253
None async_setup_platform(self, Platform platform)
Definition: __init__.py:342
None async_handle_discovery_info(self, dr.DeviceEntry device, ZwaveDiscoveryInfo disc_info, dict[str, ZwaveDiscoveryInfo] value_updates_disc_info)
Definition: __init__.py:702
None async_on_value_added(self, dict[str, ZwaveDiscoveryInfo] value_updates_disc_info, Value value)
Definition: __init__.py:746
None __init__(self, HomeAssistant hass, ControllerEvents controller_events)
Definition: __init__.py:606
None async_on_node_ready(self, ZwaveNode node)
Definition: __init__.py:614
None async_on_notification(self, dict[str, Any] event)
Definition: __init__.py:806
None async_on_value_updated_fire_event(self, dict[str, ZwaveDiscoveryInfo] value_updates_disc_info, Value value)
Definition: __init__.py:881
None async_on_value_notification(self, ValueNotification notification)
Definition: __init__.py:774
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
None async_update_device(HomeAssistant hass, ConfigEntry entry, str adapter, AdapterDetails details)
Definition: __init__.py:294
AddonManager get_addon_manager(HomeAssistant hass)
Definition: addon.py:16
None async_register_api(HomeAssistant hass)
Definition: api.py:30
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
None async_delete_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:85
None async_create(HomeAssistant hass, str message, str|None title=None, str|None notification_id=None)
Definition: __init__.py:102
DeviceTuple get_device_id(rfxtrxmod.RFXtrxDevice device, int|None data_bits=None)
Definition: __init__.py:434
tuple[str, str]|None get_device_id_ext(Driver driver, ZwaveNode node)
Definition: helpers.py:217
str get_valueless_base_unique_id(Driver driver, ZwaveNode node)
Definition: helpers.py:202
None async_enable_statistics(Driver driver)
Definition: helpers.py:134
None async_enable_server_logging_if_needed(HomeAssistant hass, ConfigEntry entry, Driver driver)
Definition: helpers.py:141
str get_network_identifier_for_notification(HomeAssistant hass, ConfigEntry config_entry, Controller controller)
Definition: helpers.py:524
None async_disable_server_logging_if_needed(HomeAssistant hass, ConfigEntry entry, Driver driver)
Definition: helpers.py:175
None async_migrate_discovered_value(HomeAssistant hass, er.EntityRegistry ent_reg, set[str] registered_unique_ids, dr.DeviceEntry device, Driver driver, ZwaveDiscoveryInfo disc_info)
Definition: migrate.py:144
None async_ensure_addon_running(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:1057
None client_listen(HomeAssistant hass, ConfigEntry entry, ZwaveClient client, asyncio.Event driver_ready)
Definition: __init__.py:931
bool async_remove_config_entry_device(HomeAssistant hass, ConfigEntry config_entry, dr.DeviceEntry device_entry)
Definition: __init__.py:1027
None async_remove_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:1003
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:166
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:975
None start_client(HomeAssistant hass, ConfigEntry entry, ZwaveClient client)
Definition: __init__.py:215
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:149
AddonManager _get_addon_manager(HomeAssistant hass)
Definition: __init__.py:1145
None disconnect_client(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:952
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193