Skip to content

Commit c29dfc8

Browse files
authored
chore: Add additional Home data to diagnostics (#723)
* chore: Add additional Home data to diagnostics Add home API data including products devices, rooms, etc and increase the number of redacted fields. Add a device manager snapshot test to verify which fields are redacted. * chore: Update device snapshots and lint errors * chore: Improve redaction logic to support more complex paths * chore: Fix schema redaction * chore: Use built-in as_dict method for creating diagnostic data * chore: fix diagnostic lint issues
1 parent cd6cbbe commit c29dfc8

File tree

6 files changed

+662
-34
lines changed

6 files changed

+662
-34
lines changed

roborock/devices/device.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,11 @@ def diagnostic_data(self) -> dict[str, Any]:
226226
"""Return diagnostics information about the device."""
227227
extra: dict[str, Any] = {}
228228
if self.v1_properties:
229-
extra["traits"] = redact_device_data(self.v1_properties.as_dict())
230-
return {
231-
"device": redact_device_data(self.device_info.as_dict()),
232-
"product": redact_device_data(self.product.as_dict()),
233-
**extra,
234-
}
229+
extra["traits"] = self.v1_properties.as_dict()
230+
return redact_device_data(
231+
{
232+
"device": self.device_info.as_dict(),
233+
"product": self.product.as_dict(),
234+
**extra,
235+
}
236+
)

roborock/devices/device_manager.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
UserData,
1717
)
1818
from roborock.devices.device import DeviceReadyCallback, RoborockDevice
19-
from roborock.diagnostics import Diagnostics
19+
from roborock.diagnostics import Diagnostics, redact_device_data
2020
from roborock.exceptions import RoborockException
2121
from roborock.map.map_parser import MapParserConfig
2222
from roborock.mqtt.roborock_session import create_lazy_mqtt_session
@@ -76,6 +76,7 @@ def __init__(
7676
self._devices: dict[str, RoborockDevice] = {}
7777
self._mqtt_session = mqtt_session
7878
self._diagnostics = diagnostics
79+
self._home_data: HomeData | None = None
7980

8081
async def discover_devices(self, prefer_cache: bool = True) -> list[RoborockDevice]:
8182
"""Discover all devices for the logged-in user."""
@@ -91,9 +92,9 @@ async def discover_devices(self, prefer_cache: bool = True) -> list[RoborockDevi
9192
raise
9293
_LOGGER.debug("Failed to fetch home data, using cached data: %s", ex)
9394
await self._cache.set(cache_data)
94-
home_data = cache_data.home_data
95+
self._home_data = cache_data.home_data
9596

96-
device_products = home_data.device_products
97+
device_products = self._home_data.device_products
9798
_LOGGER.debug("Discovered %d devices", len(device_products))
9899

99100
# These are connected serially to avoid overwhelming the MQTT broker
@@ -106,7 +107,7 @@ async def discover_devices(self, prefer_cache: bool = True) -> list[RoborockDevi
106107
if duid in self._devices:
107108
continue
108109
try:
109-
new_device = self._device_creator(home_data, device, product)
110+
new_device = self._device_creator(self._home_data, device, product)
110111
except UnsupportedDeviceError:
111112
_LOGGER.info("Skipping unsupported device %s %s", product.summary_info(), device.summary_info())
112113
unsupported_devices_counter.increment(device.pv or "unknown")
@@ -136,7 +137,11 @@ async def close(self) -> None:
136137

137138
def diagnostic_data(self) -> Mapping[str, Any]:
138139
"""Return diagnostics information about the device manager."""
139-
return self._diagnostics.as_dict()
140+
return {
141+
"home_data": redact_device_data(self._home_data.as_dict()) if self._home_data else None,
142+
"devices": [device.diagnostic_data() for device in self._devices.values()],
143+
"diagnostics": self._diagnostics.as_dict(),
144+
}
140145

141146

142147
@dataclass

roborock/diagnostics.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,30 +101,50 @@ def reset(self) -> None:
101101
"imageContent",
102102
"mapData",
103103
"rawApiResponse",
104+
# Home data
105+
"id", # We want to redact home_data.id but keep some other ids, see below
106+
"name",
107+
"productId",
108+
"ipAddress",
109+
"wifiName",
110+
"lat",
111+
"long",
112+
}
113+
KEEP_KEYS = {
114+
# Product information not unique per user
115+
"product.id",
116+
"product.schema.id",
117+
"product.schema.name",
118+
# Room ids are likely unique per user, but don't seem too sensitive and are
119+
# useful for debugging
120+
"rooms.id",
104121
}
105122
DEVICE_UID = "duid"
106123
REDACTED = "**REDACTED**"
107124

108125

109-
def redact_device_data(data: T) -> T | dict[str, Any]:
126+
def redact_device_data(data: T, path: str = "") -> T | dict[str, Any]:
110127
"""Redact sensitive data in a dict."""
111128
if not isinstance(data, (Mapping, list)):
112129
return data
113130

114131
if isinstance(data, list):
115-
return cast(T, [redact_device_data(item) for item in data])
132+
return cast(T, [redact_device_data(item, path) for item in data])
116133

117134
redacted = {**data}
118135

119136
for key, value in redacted.items():
120-
if key in REDACT_KEYS:
137+
curr_path = f"{path}.{key}" if path else key
138+
if key in KEEP_KEYS or curr_path in KEEP_KEYS:
139+
continue
140+
if key in REDACT_KEYS or curr_path in REDACT_KEYS:
121141
redacted[key] = REDACTED
122142
elif key == DEVICE_UID and isinstance(value, str):
123143
redacted[key] = redact_device_uid(value)
124144
elif isinstance(value, dict):
125-
redacted[key] = redact_device_data(value)
145+
redacted[key] = redact_device_data(value, curr_path)
126146
elif isinstance(value, list):
127-
redacted[key] = [redact_device_data(item) for item in value]
147+
redacted[key] = [redact_device_data(item, curr_path) for item in value]
128148

129149
return redacted
130150

0 commit comments

Comments
 (0)