Files
ha-nvidia-shield/custom_components/nvidia_shield/diagnostics.py
T

102 lines
3.5 KiB
Python

"""Entry diagnostics for Nvidia Shield (P4-2b, t21 F1 platform-module move).
HA discovers ``async_get_config_entry_diagnostics`` through the integration's
``diagnostics`` platform module — a placement inside media_player.py never
serves in a real install (t20 finding F1).
The payload: redacted entry config (data + options), the manager's StateStore
snapshot via the public ``store_snapshot`` accessor, and both transports'
health. Cert paths are kept readable; key material / pairing secrets never
exist in stored state, and any key carrying a secret-shaped name is redacted
defensively.
"""
import time
from dataclasses import asdict, is_dataclass
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .connection import ShieldConnectionManager
_REDACT_KEY_MARKERS = ("pairing", "pin", "secret", "password", "token", "key_material")
_REDACTED = "**REDACTED**"
# G14: the documented explicit redaction field list — the audit calls out
# config fields like the hosts; pairing/key-shaped fields are masked by
# async_redact_data and the marker scan adds a second layer.
_TO_REDACT = {
"pairing_code",
"adb_pairing_code",
"protocol_pairing_code",
"serial_number",
"uuid",
"adb_host",
"protocol_host",
"host",
}
def _redact(obj):
"""Marker-based defensive pass (kept behind the async_redact_data helper)."""
"""Redact sensitive keys from a JSON-ish diagnostics payload."""
if isinstance(obj, dict):
return {
key: (
_REDACTED
if any(m in str(key).lower() for m in _REDACT_KEY_MARKERS)
else _redact(value)
)
for key, value in obj.items()
}
if isinstance(obj, list):
return [_redact(item) for item in obj]
return obj
def _store_snapshot(manager) -> dict[str, dict]:
"""TTL-stamped store snapshot; dataclass values expand to dicts."""
snapshot = {}
for name, (stamp, value) in manager.store.values_stamped().items():
if is_dataclass(value) and not isinstance(value, type):
value = asdict(value)
snapshot[name] = {"age": round(time.monotonic() - stamp, 3), "value": value}
return snapshot
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict:
"""Entry diagnostics: redacted config + store snapshot + transport health."""
manager = getattr(entry, "runtime_data", None)
payload: dict = {
"entry": {
"data": dict(getattr(entry, "data", {}) or {}),
"options": dict(getattr(entry, "options", {}) or {}),
},
"store": {},
"transport": {},
}
if isinstance(manager, ShieldConnectionManager):
payload["store"] = manager.store_snapshot()
payload["transport"] = {
"adb": {"connected": manager.connected, "awake": manager.awake},
"protocol": {
"active": manager.protocol_active,
"available": manager.is_available,
"is_on": manager.is_on,
},
}
# G14 (t30): the documented helper is the primary redaction path with
# an explicit field list; the marker scan above runs defensively after.
try:
from homeassistant.util.diagnostic import async_redact_data
except ImportError:
async_redact_data = None
if async_redact_data is not None:
payload = async_redact_data(payload, _TO_REDACT)
return _redact(payload)
__all__ = ["async_get_config_entry_diagnostics"]