Skip to content

Commit 57b5846

Browse files
committed
feat: Add full bt-manager data to diagnostics, with redactions
1 parent 010fa5c commit 57b5846

File tree

2 files changed

+91
-61
lines changed

2 files changed

+91
-61
lines changed

custom_components/bermuda/coordinator.py

+83-61
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ def __init__(
131131
CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL
132132
)
133133

134+
# match/replacement pairs for redacting addresses
135+
self.redactions: dict[str, str] = {}
136+
# Any remaining MAC addresses will be replaced with this. We define it here
137+
# so we can compile it once.
138+
self._redact_generic_re = re.compile(
139+
r"(?P<start>[0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}:){4}(?P<end>[0-9A-Fa-f]{2})"
140+
)
141+
self._redact_generic_sub = r"\g<start>:xx:xx:xx:xx:\g<end>"
142+
134143
self.stamp_last_prune: float = 0 # When we last pruned device list
135144

136145
super().__init__(
@@ -1182,71 +1191,84 @@ async def service_dump_devices(
11821191
# lowercase all the addresses for matching
11831192
addresses = list(map(str.lower, addresses))
11841193

1185-
# set up redaction lookups. To make troubleshooting easier, we assign
1186-
# labels and identifiers to each unique MAC/address.
1187-
redactions = {}
1188-
if redact:
1189-
i = 0
1190-
for address in self.scanner_list:
1191-
i += 1
1192-
redactions[address.upper()] = (
1193-
f"{address[:2]}::SCANNER_{i}::{address[-2:]}"
1194-
)
1195-
i = 0
1196-
for address in self.options.get(CONF_DEVICES, []):
1197-
if address.upper() not in redactions:
1198-
i += 1
1199-
if address.count("_") == 2:
1200-
redactions[address] = (
1201-
f"{address[:4]}::CFG_iBea_{i}::{address[32:]}"
1202-
)
1203-
elif len(address) == 17:
1204-
redactions[address] = (
1205-
f"{address[:2]}::CFG_MAC_{i}::{address[-2:]}"
1206-
)
1207-
else:
1208-
# Don't know what it is, but not a mac.
1209-
redactions[address] = f"CFG_OTHER_{1}_{address}"
1210-
1211-
# don't reset i, just continue on for all the other devices.
1212-
for address, device in self.devices.items():
1213-
if address.upper() not in redactions:
1214-
# Only add if they are not already there.
1215-
i += 1
1216-
if device.address_type == ADDR_TYPE_PRIVATE_BLE_DEVICE:
1217-
redactions[address] = f"{address[:2]}::IRK_DEV_{i}"
1218-
elif address.count("_") == 2:
1219-
redactions[address] = (
1220-
f"{address[:4]}::OTHER_iBea_{i}::{address[32:]}"
1221-
)
1222-
elif len(address) == 17: # a MAC
1223-
redactions[address] = (
1224-
f"{address[:2]}::OTHER_MAC_{i}::{address[-2:]}"
1225-
)
1226-
else:
1227-
# Don't know what it is.
1228-
redactions[address] = f"OTHER_{1}_{address}"
1229-
12301194
# Build the dict of devices
12311195
for address, device in self.devices.items():
12321196
if len(addresses) == 0 or address.lower() in addresses:
12331197
out[address] = device.to_dict()
12341198

1235-
def redact_data(data, redactions):
1236-
if isinstance(data, str):
1237-
for find, fix in redactions.items():
1238-
data = re.sub(find, fix, data, flags=re.I)
1239-
return data
1240-
elif isinstance(data, dict):
1241-
return {
1242-
redact_data(k, redactions): redact_data(v, redactions)
1243-
for k, v in data.items()
1244-
}
1245-
elif isinstance(data, list):
1246-
return [redact_data(v, redactions) for v in data]
1247-
else:
1248-
return data
1249-
12501199
if redact:
1251-
out = cast(ServiceResponse, redact_data(out, redactions))
1200+
self.redaction_list_update()
1201+
out = cast(ServiceResponse, self.redact_data(out))
12521202
return out
1203+
1204+
def redaction_list_update(self):
1205+
"""Freshen or create the list of match/replace pairs that we use to
1206+
redact MAC addresses. This gives a set of helpful address replacements
1207+
that still allows identifying device entries without disclosing MAC
1208+
addresses."""
1209+
i = len(self.redactions) # not entirely accurate but we don't care.
1210+
1211+
# SCANNERS
1212+
for address in self.scanner_list:
1213+
if address.upper() not in self.redactions:
1214+
i += 1
1215+
self.redactions[address.upper()] = (
1216+
f"{address[:2]}::SCANNER_{i}::{address[-2:]}"
1217+
)
1218+
# CONFIGURED DEVICES
1219+
for address in self.options.get(CONF_DEVICES, []):
1220+
if address.upper() not in self.redactions:
1221+
i += 1
1222+
if address.count("_") == 2:
1223+
self.redactions[address] = (
1224+
f"{address[:4]}::CFG_iBea_{i}::{address[32:]}"
1225+
)
1226+
elif len(address) == 17:
1227+
self.redactions[address] = (
1228+
f"{address[:2]}::CFG_MAC_{i}::{address[-2:]}"
1229+
)
1230+
else:
1231+
# Don't know what it is, but not a mac.
1232+
self.redactions[address] = f"CFG_OTHER_{1}_{address}"
1233+
# EVERYTHING ELSE
1234+
for address, device in self.devices.items():
1235+
if address.upper() not in self.redactions:
1236+
# Only add if they are not already there.
1237+
i += 1
1238+
if device.address_type == ADDR_TYPE_PRIVATE_BLE_DEVICE:
1239+
self.redactions[address] = f"{address[:2]}::IRK_DEV_{i}"
1240+
elif address.count("_") == 2:
1241+
self.redactions[address] = (
1242+
f"{address[:4]}::OTHER_iBea_{i}::{address[32:]}"
1243+
)
1244+
elif len(address) == 17: # a MAC
1245+
self.redactions[address] = (
1246+
f"{address[:2]}::OTHER_MAC_{i}::{address[-2:]}"
1247+
)
1248+
else:
1249+
# Don't know what it is.
1250+
self.redactions[address] = f"OTHER_{1}_{address}"
1251+
1252+
def redact_data(self, data):
1253+
"""Wash any collection of data of any MAC addresses.
1254+
1255+
Uses the redaction list of substitutions if already created, then
1256+
washes any remaining mac-like addresses. This routine is recursive,
1257+
so if you're changing it bear that in mind!"""
1258+
if len(self.redactions) == 0:
1259+
# Initialise the list of addresses if not already done.
1260+
self.redaction_list_update()
1261+
if isinstance(data, str):
1262+
# the end of the recursive wormhole, do the actual work:
1263+
for find, fix in self.redactions.items():
1264+
data = re.sub(find, fix, data, flags=re.I)
1265+
# redactions done, now replace any remaining MAC addresses
1266+
# We are only looking for xx:xx:xx... format.
1267+
data = self._redact_generic_re.sub(self._redact_generic_sub, data)
1268+
return data
1269+
elif isinstance(data, dict):
1270+
return {self.redact_data(k): self.redact_data(v) for k, v in data.items()}
1271+
elif isinstance(data, list):
1272+
return [self.redact_data(v) for v in data]
1273+
else:
1274+
return data

custom_components/bermuda/diagnostics.py

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from typing import Any
66

7+
from homeassistant.components.bluetooth.api import _get_manager
78
from homeassistant.config_entries import ConfigEntry
89
from homeassistant.core import HomeAssistant
910
from homeassistant.core import ServiceCall
@@ -18,12 +19,19 @@ async def async_get_config_entry_diagnostics(
1819
"""Return diagnostics for a config entry."""
1920
coordinator: BermudaDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
2021

22+
# We can call this with our own config_entry because the diags step doesn't
23+
# actually use it.
24+
25+
bt_manager = _get_manager(hass)
26+
bt_diags = await bt_manager.async_diagnostics()
27+
2128
# Param structure for service call
2229
call = ServiceCall(DOMAIN, "dump_devices", {"redact": True})
2330

2431
data: dict[str, Any] = {
2532
"active_devices": f"{coordinator.count_active_devices()}/{len(coordinator.devices)}",
2633
"active_scanners": f"{coordinator.count_active_scanners()}/{len(coordinator.scanner_list)}",
2734
"devices": await coordinator.service_dump_devices(call),
35+
"bt_manager": coordinator.redact_data(bt_diags),
2836
}
2937
return data

0 commit comments

Comments
 (0)