1
1
"""Passive BLE monitor integration."""
2
2
import asyncio
3
+ import copy
4
+ from Cryptodome .Cipher import AES
5
+ import json
3
6
import logging
4
7
import queue
5
8
import struct
6
9
from threading import Thread
7
10
import voluptuous as vol
8
- from homeassistant . helpers import config_validation as cv
9
- from homeassistant .helpers import discovery
11
+
12
+ from homeassistant .config_entries import SOURCE_IMPORT , ConfigEntry
10
13
from homeassistant .const import (
11
14
CONF_DEVICES ,
12
15
CONF_DISCOVERY ,
13
16
CONF_MAC ,
14
17
CONF_NAME ,
15
18
CONF_TEMPERATURE_UNIT ,
16
19
EVENT_HOMEASSISTANT_STOP ,
20
+ TEMP_CELSIUS ,
21
+ TEMP_FAHRENHEIT ,
22
+ )
23
+ from homeassistant .core import HomeAssistant
24
+ from homeassistant .helpers import config_validation as cv
25
+ from homeassistant .helpers .entity_registry import (
26
+ async_entries_for_device ,
17
27
)
18
-
19
- from Cryptodome .Cipher import AES
20
28
21
29
# It was decided to temporarily include this file in the integration bundle
22
30
# until the issue with checking the adapter's capabilities is resolved in the official aioblescan repo
30
38
DEFAULT_LOG_SPIKES ,
31
39
DEFAULT_USE_MEDIAN ,
32
40
DEFAULT_ACTIVE_SCAN ,
33
- DEFAULT_HCI_INTERFACE ,
34
41
DEFAULT_BATT_ENTITIES ,
35
42
DEFAULT_REPORT_UNKNOWN ,
36
43
DEFAULT_DISCOVERY ,
37
44
DEFAULT_RESTORE_STATE ,
45
+ DEFAULT_HCI_INTERFACE ,
38
46
CONF_ROUNDING ,
39
47
CONF_DECIMALS ,
40
48
CONF_PERIOD ,
46
54
CONF_REPORT_UNKNOWN ,
47
55
CONF_RESTORE_STATE ,
48
56
CONF_ENCRYPTION_KEY ,
57
+ CONFIG_IS_FLOW ,
49
58
DOMAIN ,
59
+ MAC_REGEX ,
60
+ AES128KEY_REGEX ,
50
61
XIAOMI_TYPE_DICT ,
62
+ SERVICE_CLEANUP_ENTRIES ,
51
63
)
52
64
53
65
_LOGGER = logging .getLogger (__name__ )
60
72
ILL_STRUCT = struct .Struct ("<I" )
61
73
FMDH_STRUCT = struct .Struct ("<H" )
62
74
63
- # regex constants for configuration schema
64
- MAC_REGEX = "(?i)^(?:[0-9A-F]{2}[:]){5}(?:[0-9A-F]{2})$"
65
- AES128KEY_REGEX = "(?i)^[A-F0-9]{32}$"
75
+ PLATFORMS = ["binary_sensor" , "sensor" ]
76
+
77
+ CONFIG_YAML = {}
78
+ UPDATE_UNLISTENER = None
66
79
67
80
DEVICE_SCHEMA = vol .Schema (
68
81
{
103
116
extra = vol .ALLOW_EXTRA ,
104
117
)
105
118
119
+ SERVICE_CLEANUP_ENTRIES_SCHEMA = vol .Schema ({})
106
120
107
- def setup (hass , config ):
121
+
122
+ async def async_setup (hass : HomeAssistant , config ):
108
123
"""Set up integration."""
109
- blemonitor = BLEmonitor (config [DOMAIN ])
110
- hass .bus .listen (EVENT_HOMEASSISTANT_STOP , blemonitor .shutdown_handler )
124
+
125
+ async def service_cleanup_entries (service_call ):
126
+ # service = service_call.service
127
+ service_data = service_call .data
128
+
129
+ await async_cleanup_entries_service (hass , service_data )
130
+
131
+ hass .services .async_register (
132
+ DOMAIN ,
133
+ SERVICE_CLEANUP_ENTRIES ,
134
+ service_cleanup_entries ,
135
+ schema = SERVICE_CLEANUP_ENTRIES_SCHEMA ,
136
+ )
137
+
138
+ if DOMAIN not in config :
139
+ return True
140
+
141
+ if DOMAIN in hass .data :
142
+ # One instance only
143
+ return False
144
+
145
+ # Save and set default for the YAML config
146
+ global CONFIG_YAML
147
+ CONFIG_YAML = json .loads (json .dumps (config [DOMAIN ]))
148
+ CONFIG_YAML [CONFIG_IS_FLOW ] = False
149
+
150
+ _LOGGER .debug ("Initializing BLE Monitor integration (YAML): %s" , CONFIG_YAML )
151
+
152
+ hass .async_add_job (
153
+ hass .config_entries .flow .async_init (
154
+ DOMAIN , context = {"source" : SOURCE_IMPORT }, data = copy .deepcopy (CONFIG_YAML )
155
+ )
156
+ )
157
+
158
+ return True
159
+
160
+
161
+ async def async_setup_entry (hass : HomeAssistant , config_entry : ConfigEntry ):
162
+ """Set up BLE Monitor from a config entry."""
163
+ _LOGGER .debug ("Initializing BLE Monitor entry (config entry): %s" , config_entry )
164
+
165
+ # Prevent unload to be triggered each time we update the config entry
166
+ global UPDATE_UNLISTENER
167
+ if UPDATE_UNLISTENER :
168
+ UPDATE_UNLISTENER ()
169
+
170
+ if not config_entry .unique_id :
171
+ hass .config_entries .async_update_entry (config_entry , unique_id = config_entry .title )
172
+
173
+ _LOGGER .debug ("async_setup_entry: domain %s" , CONFIG_YAML )
174
+
175
+ config = {}
176
+
177
+ if not CONFIG_YAML :
178
+ for key , value in config_entry .data .items ():
179
+ config [key ] = value
180
+
181
+ for key , value in config_entry .options .items ():
182
+ config [key ] = value
183
+
184
+ config [CONFIG_IS_FLOW ] = True
185
+ if CONF_DEVICES not in config :
186
+ config [CONF_DEVICES ] = []
187
+ else :
188
+ for key , value in CONFIG_YAML .items ():
189
+ config [key ] = value
190
+ if CONF_HCI_INTERFACE in CONFIG_YAML :
191
+ hci_list = []
192
+ if isinstance (CONFIG_YAML [CONF_HCI_INTERFACE ], list ):
193
+ for hci in CONFIG_YAML [CONF_HCI_INTERFACE ]:
194
+ hci_list .append (str (hci ))
195
+ else :
196
+ hci_list .append (str (CONFIG_YAML [CONF_HCI_INTERFACE ]))
197
+ config [CONF_HCI_INTERFACE ] = hci_list
198
+
199
+ hass .config_entries .async_update_entry (config_entry , data = {}, options = config )
200
+
201
+ _LOGGER .debug ("async_setup_entry: %s" , config )
202
+
203
+ UPDATE_UNLISTENER = config_entry .add_update_listener (_async_update_listener )
204
+
205
+ if CONF_HCI_INTERFACE not in config :
206
+ config [CONF_HCI_INTERFACE ] = [DEFAULT_HCI_INTERFACE ]
207
+ else :
208
+ hci_list = config_entry .options .get (CONF_HCI_INTERFACE )
209
+ for i , hci in enumerate (hci_list ):
210
+ hci_list [i ] = int (hci )
211
+ config [CONF_HCI_INTERFACE ] = hci_list
212
+ _LOGGER .debug ("HCI interface is %s" , config [CONF_HCI_INTERFACE ])
213
+
214
+ blemonitor = BLEmonitor (config )
215
+ hass .bus .async_listen (EVENT_HOMEASSISTANT_STOP , blemonitor .shutdown_handler )
111
216
blemonitor .start ()
112
- hass .data [DOMAIN ] = blemonitor
113
- discovery .load_platform (hass , "sensor" , DOMAIN , {}, config )
114
- discovery .load_platform (hass , "binary_sensor" , DOMAIN , {}, config )
217
+
218
+ hass .data [DOMAIN ] = {}
219
+ hass .data [DOMAIN ]["blemonitor" ] = blemonitor
220
+ hass .data [DOMAIN ]["config_entry_id" ] = config_entry .entry_id
221
+
222
+ for component in PLATFORMS :
223
+ hass .async_create_task (
224
+ hass .config_entries .async_forward_entry_setup (config_entry , component )
225
+ )
226
+
115
227
return True
116
228
117
229
230
+ async def async_unload_entry (hass : HomeAssistant , entry : ConfigEntry ):
231
+ """Unload a config entry."""
232
+ _LOGGER .debug ("async_unload_entry: %s" , entry )
233
+
234
+ unload_ok = all (
235
+ await asyncio .gather (
236
+ * [
237
+ hass .config_entries .async_forward_entry_unload (entry , component )
238
+ for component in PLATFORMS
239
+ ]
240
+ )
241
+ )
242
+ blemonitor : BLEmonitor = hass .data [DOMAIN ]["blemonitor" ]
243
+ if blemonitor :
244
+ blemonitor .stop ()
245
+
246
+ return unload_ok
247
+
248
+
249
+ async def _async_update_listener (hass : HomeAssistant , entry : ConfigEntry ) -> None :
250
+ """Handle options update."""
251
+ await hass .config_entries .async_reload (entry .entry_id )
252
+
253
+
254
+ async def async_cleanup_entries_service (hass : HomeAssistant , data ):
255
+ """Remove orphaned entries from device and entity registries."""
256
+ _LOGGER .debug ("async_cleanup_entries_service" )
257
+
258
+ entity_registry = await hass .helpers .entity_registry .async_get_registry ()
259
+ device_registry = await hass .helpers .device_registry .async_get_registry ()
260
+ config_entry_id = hass .data [DOMAIN ]["config_entry_id" ]
261
+
262
+ # entity_entries = async_entries_for_config_entry(
263
+ # entity_registry, config_entry_id
264
+ # )
265
+
266
+ # entities_to_be_removed = []
267
+ devices_to_be_removed = [
268
+ entry .id
269
+ for entry in device_registry .devices .values ()
270
+ if config_entry_id in entry .config_entries
271
+ ]
272
+
273
+ # for entry in entity_entries:
274
+
275
+ # # Don't remove available entities
276
+ # if entry.unique_id in gateway.entities[entry.domain]:
277
+
278
+ # # Don't remove devices with available entities
279
+ # if entry.device_id in devices_to_be_removed:
280
+ # devices_to_be_removed.remove(entry.device_id)
281
+ # continue
282
+ # # Remove entities that are not available
283
+ # entities_to_be_removed.append(entry.entity_id)
284
+
285
+ # # Remove unavailable entities
286
+ # for entity_id in entities_to_be_removed:
287
+ # entity_registry.async_remove(entity_id)
288
+
289
+ # Remove devices that don't belong to any entity
290
+ for device_id in devices_to_be_removed :
291
+ if len (async_entries_for_device (entity_registry , device_id )) == 0 :
292
+ device_registry .async_remove_device (device_id )
293
+ _LOGGER .debug ("device %s will be deleted" , device_id )
294
+
295
+
118
296
class BLEmonitor :
119
297
"""BLE scanner."""
120
298
@@ -125,6 +303,8 @@ def __init__(self, config):
125
303
"measuring" : queue .SimpleQueue (),
126
304
}
127
305
self .config = config
306
+ if config [CONF_REPORT_UNKNOWN ] is True :
307
+ _LOGGER .info ("Attention! Option report_unknown is enabled, be ready for a huge output..." )
128
308
self .dumpthread = None
129
309
130
310
def shutdown_handler (self , event ):
@@ -241,17 +421,17 @@ def reverse_mac(rmac):
241
421
self .report_unknown = False
242
422
if self .config [CONF_REPORT_UNKNOWN ]:
243
423
self .report_unknown = True
244
- _LOGGER .info (
424
+ _LOGGER .debug (
245
425
"Attention! Option report_unknown is enabled, be ready for a huge output..."
246
426
)
247
427
# prepare device:key lists to speedup parser
248
428
if self .config [CONF_DEVICES ]:
249
429
for device in self .config [CONF_DEVICES ]:
250
- if "encryption_key" in device :
430
+ if CONF_ENCRYPTION_KEY in device and device [ CONF_ENCRYPTION_KEY ] :
251
431
p_mac = bytes .fromhex (
252
432
reverse_mac (device ["mac" ].replace (":" , "" )).lower ()
253
433
)
254
- p_key = bytes .fromhex (device ["encryption_key" ].lower ())
434
+ p_key = bytes .fromhex (device [CONF_ENCRYPTION_KEY ].lower ())
255
435
self .aeskeys [p_mac ] = p_key
256
436
else :
257
437
continue
0 commit comments