Home Assistant Unofficial Reference 2024.12.1
hub.py
Go to the documentation of this file.
1 """Representation of a deCONZ gateway."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from typing import TYPE_CHECKING, cast
7 
8 from pydeconz import DeconzSession
9 from pydeconz.interfaces import sensors
10 from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler
11 from pydeconz.interfaces.groups import GroupHandler
12 from pydeconz.models.event import EventType
13 
14 from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
15 from homeassistant.core import Event, HomeAssistant, callback
16 from homeassistant.helpers import device_registry as dr, entity_registry as er
17 from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
18 from homeassistant.helpers.dispatcher import async_dispatcher_send
19 
20 from ..const import (
21  CONF_MASTER_GATEWAY,
22  DOMAIN as DECONZ_DOMAIN,
23  HASSIO_CONFIGURATION_URL,
24  PLATFORMS,
25 )
26 from .config import DeconzConfig
27 
28 if TYPE_CHECKING:
29  from ..deconz_event import (
30  DeconzAlarmEvent,
31  DeconzEvent,
32  DeconzPresenceEvent,
33  DeconzRelativeRotaryEvent,
34  )
35 
36 SENSORS = (
37  sensors.SensorResourceManager,
38  sensors.AirPurifierHandler,
39  sensors.AirQualityHandler,
40  sensors.AlarmHandler,
41  sensors.AncillaryControlHandler,
42  sensors.BatteryHandler,
43  sensors.CarbonMonoxideHandler,
44  sensors.ConsumptionHandler,
45  sensors.DaylightHandler,
46  sensors.DoorLockHandler,
47  sensors.FireHandler,
48  sensors.GenericFlagHandler,
49  sensors.GenericStatusHandler,
50  sensors.HumidityHandler,
51  sensors.LightLevelHandler,
52  sensors.OpenCloseHandler,
53  sensors.PowerHandler,
54  sensors.PresenceHandler,
55  sensors.PressureHandler,
56  sensors.RelativeRotaryHandler,
57  sensors.SwitchHandler,
58  sensors.TemperatureHandler,
59  sensors.ThermostatHandler,
60  sensors.TimeHandler,
61  sensors.VibrationHandler,
62  sensors.WaterHandler,
63 )
64 
65 
66 class DeconzHub:
67  """Manages a single deCONZ gateway."""
68 
69  def __init__(
70  self, hass: HomeAssistant, config_entry: ConfigEntry, api: DeconzSession
71  ) -> None:
72  """Initialize the system."""
73  self.hasshass = hass
74  self.configconfig = DeconzConfig.from_config_entry(config_entry)
75  self.config_entryconfig_entry = config_entry
76  self.apiapi = api
77 
78  api.connection_status_callback = self.async_connection_status_callbackasync_connection_status_callback
79 
80  self.availableavailable = True
81  self.ignore_state_updatesignore_state_updates = False
82 
83  self.signal_reachablesignal_reachable = f"deconz-reachable-{config_entry.entry_id}"
84 
85  self.deconz_idsdeconz_ids: dict[str, str] = {}
86  self.entities: dict[str, set[str]] = {}
87  self.events: list[
88  DeconzAlarmEvent
89  | DeconzEvent
90  | DeconzPresenceEvent
91  | DeconzRelativeRotaryEvent
92  ] = []
93  self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set()
94  self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set()
95  self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set()
96 
97  @callback
98  @staticmethod
99  def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> DeconzHub:
100  """Return hub with a matching config entry ID."""
101  return cast(DeconzHub, hass.data[DECONZ_DOMAIN][config_entry.entry_id])
102 
103  @property
104  def bridgeid(self) -> str:
105  """Return the unique identifier of the gateway."""
106  return cast(str, self.config_entryconfig_entry.unique_id)
107 
108  @property
109  def master(self) -> bool:
110  """Gateway which is used with deCONZ services without defining id."""
111  return cast(bool, self.config_entryconfig_entry.options[CONF_MASTER_GATEWAY])
112 
113  @callback
115  self,
116  add_device_callback: Callable[[EventType, str], None],
117  deconz_device_interface: APIHandler | GroupedAPIHandler,
118  always_ignore_clip_sensors: bool = False,
119  ) -> None:
120  """Wrap add_device_callback to check allow_new_devices option."""
121 
122  initializing = True
123 
124  def async_add_device(_: EventType, device_id: str) -> None:
125  """Add device or add it to ignored_devices set.
126 
127  If ignore_state_updates is True means device_refresh service is used.
128  Device_refresh is expected to load new devices.
129  """
130  if (
131  not initializing
132  and not self.configconfig.allow_new_devices
133  and not self.ignore_state_updatesignore_state_updates
134  ):
135  self.ignored_devices.add((async_add_device, device_id))
136  return
137 
138  if isinstance(deconz_device_interface, GroupHandler):
139  self.deconz_groups.add((async_add_device, device_id))
140  if not self.configconfig.allow_deconz_groups:
141  return
142 
143  if isinstance(deconz_device_interface, SENSORS):
144  device = deconz_device_interface[device_id]
145  if device.type.startswith("CLIP") and not always_ignore_clip_sensors:
146  self.clip_sensors.add((async_add_device, device_id))
147  if not self.configconfig.allow_clip_sensor:
148  return
149 
150  add_device_callback(EventType.ADDED, device_id)
151 
152  self.config_entryconfig_entry.async_on_unload(
153  deconz_device_interface.subscribe(
154  async_add_device,
155  EventType.ADDED,
156  )
157  )
158 
159  for device_id in sorted(deconz_device_interface, key=int):
160  async_add_device(EventType.ADDED, device_id)
161 
162  initializing = False
163 
164  @callback
165  def load_ignored_devices(self) -> None:
166  """Load previously ignored devices."""
167  for add_entities, device_id in self.ignored_devices:
168  add_entities(EventType.ADDED, device_id)
169  self.ignored_devices.clear()
170 
171  # Callbacks
172 
173  @callback
174  def async_connection_status_callback(self, available: bool) -> None:
175  """Handle signals of gateway connection status."""
176  self.availableavailable = available
177  self.ignore_state_updatesignore_state_updates = False
178  async_dispatcher_send(self.hasshass, self.signal_reachablesignal_reachable)
179 
180  async def async_update_device_registry(self) -> None:
181  """Update device registry."""
182  if self.apiapi.config.mac is None:
183  return
184 
185  device_registry = dr.async_get(self.hasshass)
186 
187  # Host device
188  device_registry.async_get_or_create(
189  config_entry_id=self.config_entryconfig_entry.entry_id,
190  connections={(CONNECTION_NETWORK_MAC, self.apiapi.config.mac)},
191  )
192 
193  # Gateway service
194  configuration_url = f"http://{self.config.host}:{self.config.port}"
195  if self.config_entryconfig_entry.source == SOURCE_HASSIO:
196  configuration_url = HASSIO_CONFIGURATION_URL
197  device_registry.async_get_or_create(
198  config_entry_id=self.config_entryconfig_entry.entry_id,
199  configuration_url=configuration_url,
200  entry_type=dr.DeviceEntryType.SERVICE,
201  identifiers={(DECONZ_DOMAIN, self.apiapi.config.bridge_id)},
202  manufacturer="Dresden Elektronik",
203  model=self.apiapi.config.model_id,
204  name=self.apiapi.config.name,
205  sw_version=self.apiapi.config.software_version,
206  via_device=(CONNECTION_NETWORK_MAC, self.apiapi.config.mac),
207  )
208 
209  @staticmethod
211  hass: HomeAssistant, config_entry: ConfigEntry
212  ) -> None:
213  """Handle signals of config entry being updated.
214 
215  This is a static method because a class method (bound method),
216  cannot be used with weak references.
217  Causes for this is either discovery updating host address or
218  config entry options changing.
219  """
220  if config_entry.entry_id not in hass.data[DECONZ_DOMAIN]:
221  # A race condition can occur if multiple config entries are
222  # unloaded in parallel
223  return
224  hub = DeconzHub.get_hub(hass, config_entry)
225  previous_config = hub.config
226  hub.config = DeconzConfig.from_config_entry(config_entry)
227  if previous_config.host != hub.config.host:
228  hub.api.close()
229  hub.api.host = hub.config.host
230  hub.api.start()
231  return
232 
233  await hub.options_updated(previous_config)
234 
235  async def options_updated(self, previous_config: DeconzConfig) -> None:
236  """Manage entities affected by config entry options."""
237  deconz_ids = []
238 
239  # Allow CLIP sensors
240 
241  if self.configconfig.allow_clip_sensor != previous_config.allow_clip_sensor:
242  if self.configconfig.allow_clip_sensor:
243  for add_device, device_id in self.clip_sensors:
244  add_device(EventType.ADDED, device_id)
245  else:
246  deconz_ids += [
247  sensor.deconz_id
248  for sensor in self.apiapi.sensors.values()
249  if sensor.type.startswith("CLIP")
250  ]
251 
252  # Allow Groups
253 
254  if self.configconfig.allow_deconz_groups != previous_config.allow_deconz_groups:
255  if self.configconfig.allow_deconz_groups:
256  for add_device, device_id in self.deconz_groups:
257  add_device(EventType.ADDED, device_id)
258  else:
259  deconz_ids += [group.deconz_id for group in self.apiapi.groups.values()]
260 
261  # Allow adding new devices
262 
263  if self.configconfig.allow_new_devices != previous_config.allow_new_devices:
264  if self.configconfig.allow_new_devices:
265  self.load_ignored_devicesload_ignored_devices()
266 
267  # Remove entities based on above categories
268 
269  entity_registry = er.async_get(self.hasshass)
270 
271  # Copy the ids since calling async_remove will modify the dict
272  # and will cause a runtime error because the dict size changes
273  # during iteration
274  for entity_id, deconz_id in self.deconz_idsdeconz_ids.copy().items():
275  if deconz_id in deconz_ids and entity_registry.async_is_registered(
276  entity_id
277  ):
278  # Removing an entity from the entity registry will also remove them
279  # from Home Assistant
280  entity_registry.async_remove(entity_id)
281 
282  @callback
283  def shutdown(self, event: Event) -> None:
284  """Wrap the call to deconz.close.
285 
286  Used as an argument to EventBus.async_listen_once.
287  """
288  self.apiapi.close()
289 
290  async def async_reset(self) -> bool:
291  """Reset this gateway to default state."""
292  self.apiapi.connection_status_callback = None
293  self.apiapi.close()
294 
295  await self.hasshass.config_entries.async_unload_platforms(
296  self.config_entryconfig_entry, PLATFORMS
297  )
298 
299  self.deconz_idsdeconz_ids = {}
300  return True
None __init__(self, HomeAssistant hass, ConfigEntry config_entry, DeconzSession api)
Definition: hub.py:71
None register_platform_add_device_callback(self, Callable[[EventType, str], None] add_device_callback, APIHandler|GroupedAPIHandler deconz_device_interface, bool always_ignore_clip_sensors=False)
Definition: hub.py:119
None options_updated(self, DeconzConfig previous_config)
Definition: hub.py:235
None async_connection_status_callback(self, bool available)
Definition: hub.py:174
DeconzHub get_hub(HomeAssistant hass, ConfigEntry config_entry)
Definition: hub.py:99
None async_config_entry_updated(HomeAssistant hass, ConfigEntry config_entry)
Definition: hub.py:212
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
bool add(self, _T matcher)
Definition: match.py:185
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193