Skip to content

Commit 985be6f

Browse files
authored
Merge branch 'config_flow' into master
2 parents ee7f913 + 56f3d04 commit 985be6f

13 files changed

+989
-215
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

README.md

+107-99
Large diffs are not rendered by default.

custom_components/ble_monitor/__init__.py

+197-17
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
"""Passive BLE monitor integration."""
22
import asyncio
3+
import copy
4+
from Cryptodome.Cipher import AES
5+
import json
36
import logging
47
import queue
58
import struct
69
from threading import Thread
710
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
1013
from homeassistant.const import (
1114
CONF_DEVICES,
1215
CONF_DISCOVERY,
1316
CONF_MAC,
1417
CONF_NAME,
1518
CONF_TEMPERATURE_UNIT,
1619
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,
1727
)
18-
19-
from Cryptodome.Cipher import AES
2028

2129
# It was decided to temporarily include this file in the integration bundle
2230
# until the issue with checking the adapter's capabilities is resolved in the official aioblescan repo
@@ -30,11 +38,11 @@
3038
DEFAULT_LOG_SPIKES,
3139
DEFAULT_USE_MEDIAN,
3240
DEFAULT_ACTIVE_SCAN,
33-
DEFAULT_HCI_INTERFACE,
3441
DEFAULT_BATT_ENTITIES,
3542
DEFAULT_REPORT_UNKNOWN,
3643
DEFAULT_DISCOVERY,
3744
DEFAULT_RESTORE_STATE,
45+
DEFAULT_HCI_INTERFACE,
3846
CONF_ROUNDING,
3947
CONF_DECIMALS,
4048
CONF_PERIOD,
@@ -46,8 +54,12 @@
4654
CONF_REPORT_UNKNOWN,
4755
CONF_RESTORE_STATE,
4856
CONF_ENCRYPTION_KEY,
57+
CONFIG_IS_FLOW,
4958
DOMAIN,
59+
MAC_REGEX,
60+
AES128KEY_REGEX,
5061
XIAOMI_TYPE_DICT,
62+
SERVICE_CLEANUP_ENTRIES,
5163
)
5264

5365
_LOGGER = logging.getLogger(__name__)
@@ -60,9 +72,10 @@
6072
ILL_STRUCT = struct.Struct("<I")
6173
FMDH_STRUCT = struct.Struct("<H")
6274

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
6679

6780
DEVICE_SCHEMA = vol.Schema(
6881
{
@@ -103,18 +116,183 @@
103116
extra=vol.ALLOW_EXTRA,
104117
)
105118

119+
SERVICE_CLEANUP_ENTRIES_SCHEMA = vol.Schema({})
106120

107-
def setup(hass, config):
121+
122+
async def async_setup(hass: HomeAssistant, config):
108123
"""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)
111216
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+
115227
return True
116228

117229

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+
118296
class BLEmonitor:
119297
"""BLE scanner."""
120298

@@ -125,6 +303,8 @@ def __init__(self, config):
125303
"measuring": queue.SimpleQueue(),
126304
}
127305
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...")
128308
self.dumpthread = None
129309

130310
def shutdown_handler(self, event):
@@ -241,17 +421,17 @@ def reverse_mac(rmac):
241421
self.report_unknown = False
242422
if self.config[CONF_REPORT_UNKNOWN]:
243423
self.report_unknown = True
244-
_LOGGER.info(
424+
_LOGGER.debug(
245425
"Attention! Option report_unknown is enabled, be ready for a huge output..."
246426
)
247427
# prepare device:key lists to speedup parser
248428
if self.config[CONF_DEVICES]:
249429
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]:
251431
p_mac = bytes.fromhex(
252432
reverse_mac(device["mac"].replace(":", "")).lower()
253433
)
254-
p_key = bytes.fromhex(device["encryption_key"].lower())
434+
p_key = bytes.fromhex(device[CONF_ENCRYPTION_KEY].lower())
255435
self.aeskeys[p_mac] = p_key
256436
else:
257437
continue

custom_components/ble_monitor/binary_sensor.py

+29-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Passive BLE monitor sensor platform."""
1+
"""Passive BLE monitor binary sensor platform."""
22
from datetime import timedelta
33
import logging
44
import queue
@@ -11,34 +11,39 @@
1111
BinarySensorEntity,
1212
)
1313
from homeassistant.const import (
14+
CONF_DEVICES,
1415
ATTR_BATTERY_LEVEL,
1516
STATE_OFF,
1617
STATE_ON,
1718
)
1819
from homeassistant.helpers.restore_state import RestoreEntity
1920
import homeassistant.util.dt as dt_util
2021

21-
from . import (
22-
CONF_DEVICES,
22+
from .const import (
2323
CONF_PERIOD,
2424
CONF_BATT_ENTITIES,
2525
CONF_RESTORE_STATE,
26-
)
27-
from .const import (
28-
DOMAIN,
26+
MANUFACTURER_DICT,
2927
MMTS_DICT,
28+
DOMAIN,
3029
)
3130

3231
_LOGGER = logging.getLogger(__name__)
3332

3433

35-
def setup_platform(hass, conf, add_entities, discovery_info=None):
36-
"""Set up the binary_sensor platform."""
37-
_LOGGER.debug("Binary sensor platform setup")
38-
blemonitor = hass.data[DOMAIN]
34+
async def async_setup_platform(hass, conf, add_entities, discovery_info=None):
35+
"""setup from setup_entry"""
36+
return True
37+
38+
39+
async def async_setup_entry(hass, config_entry, add_entities):
40+
"""Set up the binary sensor platform."""
41+
_LOGGER.debug("Starting binary sensor entry startup")
42+
43+
blemonitor = hass.data[DOMAIN]["blemonitor"]
3944
bleupdater = BLEupdaterBinary(blemonitor, add_entities)
4045
bleupdater.start()
41-
_LOGGER.debug("Binary sensor platform setup finished")
46+
_LOGGER.debug("Binary sensor entry setup finished")
4247
# Return successful setup
4348
return True
4449

@@ -172,6 +177,7 @@ def __init__(self, config, mac, devtype):
172177
self._state = None
173178
self._unique_id = ""
174179
self._device_type = devtype
180+
self._device_manufacturer = MANUFACTURER_DICT[devtype]
175181
self._device_state_attributes = {}
176182
self._device_state_attributes["sensor type"] = devtype
177183
self._device_state_attributes["mac address"] = (
@@ -242,6 +248,18 @@ def device_class(self):
242248
"""Return the device class."""
243249
return self._device_class
244250

251+
@property
252+
def device_info(self):
253+
return {
254+
"identifiers": {
255+
# Unique identifiers within a specific domain
256+
(DOMAIN, self._device_state_attributes["mac address"])
257+
},
258+
"name": self.get_sensorname(),
259+
"model": self._device_type,
260+
"manufacturer": self._device_manufacturer,
261+
}
262+
245263
@property
246264
def force_update(self):
247265
"""Force update."""

0 commit comments

Comments
 (0)