feat(diagnostics): capture /oic/p and /oic/d identity
Device-type detection currently parses board part numbers out of /information/vs/0's modelNum. OCF has a standard field for exactly this question -- /oic/d's `rt` -- and read_identity() already fetches the resource, but kept only `n` and threw the rest away. No captured dump has ever included it either: /device/0 batch responses don't carry /oic/d, and diagnostics didn't report it, so there's no evidence on whether real hardware populates it usefully. Keep `rt` as DeviceIdentity.device_types, keep both raw payloads whole (we don't yet know which of their fields identify a type), and surface them in diagnostics so incoming issue reports answer the question. Nothing routes on it yet. /oic/d and /oic/p identify the unit with bare two-letter keys -- 'di' and 'pi' -- as sensitive as the serial number redact.py already covers but far too short to match on: 'di' alone is a substring of 'condition', 'display' and 'dispenser'. Add a whole-key match alongside the substring rules.
This commit is contained in:
6 files changed
+166
-1
No files matched your search
@@ -31,9 +31,19 @@ async def async_get_config_entry_diagnostics(
|
||||
# detector when called inline here. Offload it to the executor.
|
||||
stl_version = await hass.async_add_executor_job(pkg_version, "smartthings-local")
|
||||
|
||||
# /oic/p and /oic/d sit outside the /device/0 batch captured below, so
|
||||
# they'd otherwise never reach an issue report. /oic/d's `rt` is OCF's
|
||||
# standard device-type declaration -- see registry/identity.py.
|
||||
identity = coordinator._identity
|
||||
return {
|
||||
"device_type": coordinator.device_type_name or "unknown",
|
||||
"one_ui_version": coordinator.one_ui_version,
|
||||
"identity": {
|
||||
"manufacturer": identity.manufacturer,
|
||||
"model": identity.model,
|
||||
"device_types": list(identity.device_types),
|
||||
"resources": redact_resources(identity.raw),
|
||||
} if identity is not None else None,
|
||||
"unbound_hrefs": sorted(coordinator._unbound_hrefs),
|
||||
"resources": redact_resources(coordinator.last_resources),
|
||||
"integration_version": integration.version,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Read device identity from standard OCF resources (/oic/p, /oic/d)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import cbor2
|
||||
@@ -13,6 +13,8 @@ class DeviceIdentity:
|
||||
model: str
|
||||
name: str
|
||||
serial: Optional[str]
|
||||
device_types: tuple[str, ...] = ()
|
||||
raw: dict[str, dict] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _get(sess, path) -> dict:
|
||||
@@ -26,6 +28,27 @@ def _get(sess, path) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def _device_types(d: dict) -> tuple[str, ...]:
|
||||
"""/oic/d's `rt` -- the device's own OCF device-type declaration.
|
||||
|
||||
In OCF this is the one standardized "what am I" field: alongside the
|
||||
generic 'oic.wk.d' it carries a concrete type such as 'oic.d.airconditioner'
|
||||
or a Samsung 'x.com.samsung.da.*' equivalent. Nothing routes on it yet --
|
||||
device-type detection currently parses board part numbers out of
|
||||
/information/vs/0's modelNum instead (see registry/by_type/__init__.py) --
|
||||
because no captured dump has ever included it: /device/0 batch responses
|
||||
don't carry /oic/d, and diagnostics didn't report it. It's surfaced in
|
||||
diagnostics so incoming issue reports can tell us whether real hardware
|
||||
populates it usefully enough to route on.
|
||||
"""
|
||||
rt = d.get('rt')
|
||||
if isinstance(rt, str):
|
||||
rt = [rt]
|
||||
if not isinstance(rt, (list, tuple)):
|
||||
return ()
|
||||
return tuple(t for t in rt if isinstance(t, str))
|
||||
|
||||
|
||||
def read_identity(sess, serial: Optional[str]) -> DeviceIdentity:
|
||||
p = _get(sess, ['oic', 'p'])
|
||||
d = _get(sess, ['oic', 'd'])
|
||||
@@ -34,4 +57,9 @@ def read_identity(sess, serial: Optional[str]) -> DeviceIdentity:
|
||||
model=p.get('mnmo') or '',
|
||||
name=d.get('n') or '',
|
||||
serial=serial,
|
||||
device_types=_device_types(d),
|
||||
# Kept whole rather than field-by-field: these resources are outside
|
||||
# the /device/0 dump diagnostics already captures, and we don't yet
|
||||
# know which of their fields will turn out to identify a device type.
|
||||
raw={'/oic/p': p, '/oic/d': d},
|
||||
)
|
||||
@@ -19,9 +19,18 @@ _SENSITIVE_SUBSTRINGS = (
|
||||
'userid', 'deviceid', 'uuid', 'duid', 'password', 'secret',
|
||||
)
|
||||
|
||||
# Matched whole, not as substrings. OCF's /oic/d and /oic/p identify the unit
|
||||
# with bare two-letter keys -- 'di' (device UUID) and 'pi' (platform UUID) --
|
||||
# which are as identifying as the serial number above but far too short to
|
||||
# match on: 'di' alone is a substring of 'condition', 'display', 'dispenser'
|
||||
# and plenty of other perfectly ordinary appliance fields.
|
||||
_SENSITIVE_EXACT = frozenset({'di', 'pi'})
|
||||
|
||||
|
||||
def _is_sensitive_key(key: str) -> bool:
|
||||
lowered = key.lower()
|
||||
if lowered in _SENSITIVE_EXACT:
|
||||
return True
|
||||
return any(s in lowered for s in _SENSITIVE_SUBSTRINGS)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user