Catch the 'Nothing(SVC)' placeholder serial in both fallback sites (issue #83)
The ARTIK051_DONGLE_REF firmware family reports the literal string "Nothing(SVC)" as serialNum on every unit -- non-empty, so the existing `if not serial` checks in config_flow.py's _probe_and_validate and coordinator.py's _run_discovery don't catch it. Two such appliances on one install (a fridge and a freezer, each its own dongle) then collide: config_flow gives both the same unique_id and rejects the second as "already configured" (bug 2), and even once that's worked around, device_serial feeds every entity's unique_id too, so the second appliance's entities get silently dropped with "does not generate unique IDs" log lines (bug 4). Add _is_placeholder_serial (duplicated in both modules rather than imported, to avoid pulling config_flow into the runtime coordinator's import graph or vice versa for a two-line check) and treat it the same as an empty serial: fall back to host/port. Type detection (bug 1) and the door sensor's field-name gap (bug 3), also reported in this issue, are already fixed via the claude/device-support-issue-77-freezer branch, which hit the same ARTIK051_DONGLE_REF family from a different report -- not duplicated here.
This commit is contained in:
@@ -197,6 +197,18 @@ def _find_live_ports(host: str, ports: list[int], timeout: float) -> list[int]:
|
||||
return _order_candidates(live)
|
||||
|
||||
|
||||
def _is_placeholder_serial(serial: str) -> bool:
|
||||
"""True for a non-empty serialNum that isn't actually a real identity.
|
||||
|
||||
The ARTIK051_DONGLE_REF firmware family reports the literal string
|
||||
'Nothing(SVC)' for every unit -- non-empty, so the plain `if not
|
||||
serial` check here (and the equivalent one in coordinator.py's
|
||||
`_run_discovery`) doesn't catch it, and two such units get the same
|
||||
config-entry unique_id / entity unique_ids and collide (issue #83).
|
||||
"""
|
||||
return serial.strip().lower().startswith('nothing')
|
||||
|
||||
|
||||
def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
|
||||
"""Fetch UUID, mint leaf cert, probe each port. Returns config entry data dict."""
|
||||
import cbor2
|
||||
@@ -261,7 +273,7 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
|
||||
.get('/information/vs/0', {})
|
||||
.get('x.com.samsung.da.serialNum', '')
|
||||
)
|
||||
if not serial:
|
||||
if not serial or _is_placeholder_serial(serial):
|
||||
serial = f"{host}:{port}"
|
||||
one_ui_version = (
|
||||
resources
|
||||
|
||||
@@ -49,6 +49,22 @@ class _NoOpDescriptor:
|
||||
_RECOVERY_RETRY_S = 600.0 # re-attempt observe mode this often while polling
|
||||
|
||||
|
||||
def _is_placeholder_serial(serial: str) -> bool:
|
||||
"""True for a non-empty serialNum that isn't actually a real identity.
|
||||
|
||||
The ARTIK051_DONGLE_REF firmware family reports the literal string
|
||||
'Nothing(SVC)' for every unit -- non-empty, so the plain `if not
|
||||
serial` check below doesn't catch it, and `device_serial` feeds both
|
||||
the HA device-registry identifier and every entity's unique_id
|
||||
(entity.py), so two such units on the same install silently collide
|
||||
and the second one's entities get dropped (issue #83). Mirrors the
|
||||
identical helper in config_flow.py's `_probe_and_validate` -- kept
|
||||
separate rather than imported to avoid pulling the config-flow module
|
||||
into the runtime coordinator's import graph for a two-line check.
|
||||
"""
|
||||
return serial.strip().lower().startswith('nothing')
|
||||
|
||||
|
||||
class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Manages one Samsung appliance: session, discovery, polling."""
|
||||
|
||||
@@ -312,7 +328,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._unbound_hrefs = unbound
|
||||
|
||||
serial = info.get('x.com.samsung.da.serialNum', '')
|
||||
if not serial:
|
||||
if not serial or _is_placeholder_serial(serial):
|
||||
serial = self._entry.data[CONF_HOST]
|
||||
self.device_serial = serial
|
||||
|
||||
|
||||
@@ -508,3 +508,19 @@ async def test_options_flow_finish_preserves_existing_options(
|
||||
|
||||
assert result['type'] == FlowResultType.CREATE_ENTRY
|
||||
assert entry.options[CONF_BYPASS_REMOTE_CONTROL] is True
|
||||
|
||||
|
||||
def test_is_placeholder_serial_catches_nothing_svc():
|
||||
"""Issue #83: the ARTIK051_DONGLE_REF firmware family reports the
|
||||
literal string 'Nothing(SVC)' as serialNum on every unit -- non-empty,
|
||||
so it must be caught by name, not by the plain `if not serial` check."""
|
||||
from custom_components.localthings.config_flow import _is_placeholder_serial
|
||||
assert _is_placeholder_serial('Nothing(SVC)') is True
|
||||
assert _is_placeholder_serial('nothing(svc)') is True
|
||||
assert _is_placeholder_serial(' Nothing(SVC) ') is True
|
||||
|
||||
|
||||
def test_is_placeholder_serial_accepts_real_serials():
|
||||
from custom_components.localthings.config_flow import _is_placeholder_serial
|
||||
assert _is_placeholder_serial('0A1B2C3D4E5F') is False
|
||||
assert _is_placeholder_serial('') is False
|
||||
|
||||
@@ -136,6 +136,29 @@ def test_run_discovery_detects_washer_via_model_fallback(
|
||||
assert coordinator.device_type_name == 'washer'
|
||||
|
||||
|
||||
def test_run_discovery_falls_back_to_host_for_placeholder_serial(
|
||||
hass: HomeAssistant, mock_entry
|
||||
) -> None:
|
||||
"""Issue #83: the ARTIK051_DONGLE_REF firmware family reports the
|
||||
literal string 'Nothing(SVC)' as serialNum on every unit. Left as-is,
|
||||
two such units get the same device_serial (which feeds both the HA
|
||||
device-registry identifier and every entity's unique_id), so the
|
||||
second one's entities silently collide and get dropped. It must be
|
||||
treated the same as an empty serial and fall back to the host."""
|
||||
resources = {
|
||||
'/information/vs/0': {
|
||||
'x.com.samsung.da.modelNum':
|
||||
'ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000',
|
||||
'x.com.samsung.da.description': 'ARTIK_REF_17K',
|
||||
'x.com.samsung.da.serialNum': 'Nothing(SVC)',
|
||||
},
|
||||
'/otninformation/vs/0': {},
|
||||
}
|
||||
coordinator = LocalThingsCoordinator(hass, mock_entry)
|
||||
coordinator._run_discovery(resources)
|
||||
assert coordinator.device_serial == mock_entry.data[CONF_HOST]
|
||||
|
||||
|
||||
def test_run_discovery_detects_cooktop_via_resource_signature(
|
||||
hass: HomeAssistant, mock_entry
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user