Add diagnostics platform, config-flow gap warning, issue template

Completes the device-capability-diagnostics spec:

- diagnostics.py: the standard HA diagnostics hook, returning device type,
  one_ui_version, unbound hrefs, and the redacted raw resource tree, plus
  integration and smartthings-local version numbers. This is what the
  Repairs issue (added in the previous commit) points users at, and what
  they attach to a device-support issue.
- config_flow.py: _probe_and_validate now also reports oneUiVersion and
  whether the device type is recognized. Recognized types are unaffected;
  an unrecognized type shows a new confirm_unknown_type step explaining
  that only common capabilities will be available before creating the
  entry, so expectations are set at setup time rather than only after the
  fact via Repairs.
- .github/ISSUE_TEMPLATE/device-support.yml: structured template for
  filing a capability gap, linked from both the Repairs issue and the
  config-flow confirmation step.
- README: short section pointing at this whole mechanism.
This commit is contained in:
Marc Billow committed 2026-07-08 00:10:30 -05:00
1 parent ca754cbb01
commit d2699f0f5b
9 files changed
+221 -12

No files matched your search

+44 -11
View File
@@ -125,6 +125,7 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
import cbor2
from smartthings_local.protocol.dtls_session import DtlsCoapSession
from .registry.batch import parse_device0_batch
from .registry.by_type import for_device
_LOGGER.debug("Fetching Samsung cloud UUID from %s", _SAMSUNG_CLOUD_HOST)
try:
@@ -170,11 +171,21 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
)
if not serial:
serial = f"{host}:{port}"
one_ui_version = (
resources
.get('/otninformation/vs/0', {})
.get('swVersionInfo', {})
.get('oneUiVersion', '')
)
return {
"port": port,
"serial": serial,
"leaf_cert_pem": fullchain_pem,
"leaf_key_pem": leaf_key_pem,
"one_ui_version": one_ui_version,
"device_type_recognized": bool(
one_ui_version and for_device(one_ui_version) is not None
),
}
except CannotConnect:
raise
@@ -197,6 +208,20 @@ class LocalThingsConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._host: str = ""
self._ca_cert_pem: str = ""
self._ca_key_pem: str = ""
self._pending_info: dict | None = None
def _create_entry(self, info: dict) -> FlowResult:
return self.async_create_entry(
title=f"Samsung Appliance ({self._host})",
data={
CONF_HOST: self._host,
CONF_PORT: info["port"],
CONF_CA_CERT_PEM: self._ca_cert_pem,
CONF_CA_KEY_PEM: self._ca_key_pem,
CONF_LEAF_CERT_PEM: info["leaf_cert_pem"],
CONF_LEAF_KEY_PEM: info["leaf_key_pem"],
},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -232,17 +257,10 @@ class LocalThingsConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
else:
await self.async_set_unique_id(f"localthings_{info['serial']}")
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"Samsung Appliance ({self._host})",
data={
CONF_HOST: self._host,
CONF_PORT: info["port"],
CONF_CA_CERT_PEM: self._ca_cert_pem,
CONF_CA_KEY_PEM: self._ca_key_pem,
CONF_LEAF_CERT_PEM: info["leaf_cert_pem"],
CONF_LEAF_KEY_PEM: info["leaf_key_pem"],
},
)
if info["device_type_recognized"]:
return self._create_entry(info)
self._pending_info = info
return await self.async_step_confirm_unknown_type()
if has_creds:
schema = vol.Schema({vol.Required(CONF_HOST): _TEXT})
@@ -261,3 +279,18 @@ class LocalThingsConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
description_placeholders={"reuse_note": reuse_note},
)
async def async_step_confirm_unknown_type(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Shown only when the probe already knows the device type is unrecognized."""
if user_input is not None:
return self._create_entry(self._pending_info)
return self.async_show_form(
step_id="confirm_unknown_type",
data_schema=vol.Schema({}),
description_placeholders={
"one_ui_version": self._pending_info["one_ui_version"] or "(none reported)",
},
)
@@ -0,0 +1,36 @@
"""Diagnostics support for Local Things.
Downloadable from Settings > Devices & Services > this integration's
device > the menu > Download diagnostics. This is what the Repairs issue
(raised in coordinator.py when capability coverage is incomplete) points
users at: a redacted snapshot of the device's raw /device/0 state, plus
enough version/coverage metadata to reproduce and diagnose the gap.
"""
from __future__ import annotations
from importlib.metadata import version as pkg_version
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.loader import async_get_integration
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
from .registry.redact import redact_resources
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][entry.entry_id]
integration = await async_get_integration(hass, DOMAIN)
return {
"device_type": coordinator.device_type_name or "unknown",
"one_ui_version": coordinator.one_ui_version,
"unbound_hrefs": sorted(coordinator._unbound_hrefs),
"resources": redact_resources(coordinator.last_resources),
"integration_version": integration.version,
"smartthings_local_version": pkg_version("smartthings-local"),
}
@@ -63,6 +63,10 @@
"ca_cert_pem": "The PEM-encoded AC14K_M CA certificate chain. Paste the full contents of your fullchain PEM including all BEGIN/END CERTIFICATE blocks.",
"ca_key_pem": "The PEM-encoded AC14K_M private key. Paste the full contents including the BEGIN/END PRIVATE KEY header and footer."
}
},
"confirm_unknown_type": {
"title": "Appliance type not recognized",
"description": "This appliance reported oneUiVersion \"{one_ui_version}\", which isn't a recognized type. It'll still be added, but only with common capabilities (power, alarms, etc. where present) rather than the full set for its family. You can help add full support afterward by downloading diagnostics for this device (Settings > Devices & Services > this device > the menu > Download diagnostics) and filing them in a new issue. Submit to add it anyway."
}
},
"error": {
@@ -63,6 +63,10 @@
"ca_cert_pem": "The PEM-encoded AC14K_M CA certificate chain. Paste the full contents of your fullchain PEM including all BEGIN/END CERTIFICATE blocks.",
"ca_key_pem": "The PEM-encoded AC14K_M private key. Paste the full contents including the BEGIN/END PRIVATE KEY header and footer."
}
},
"confirm_unknown_type": {
"title": "Appliance type not recognized",
"description": "This appliance reported oneUiVersion \"{one_ui_version}\", which isn't a recognized type. It'll still be added, but only with common capabilities (power, alarms, etc. where present) rather than the full set for its family. You can help add full support afterward by downloading diagnostics for this device (Settings > Devices & Services > this device > the menu > Download diagnostics) and filing them in a new issue. Submit to add it anyway."
}
},
"error": {
@@ -73,5 +77,11 @@
"abort": {
"already_configured": "This device is already configured."
}
},
"issues": {
"device_gap": {
"title": "Incomplete capability coverage for {device_name}",
"description": "This device is missing full capability coverage. Either its appliance type wasn't recognized, or some of the resources it exposes aren't modeled yet. It'll keep working with whatever is already supported. You can help expand support by going to Settings > Devices & Services > {device_name} > the menu (top right) > Download diagnostics, then filing it with the linked issue template."
}
}
}