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