diff --git a/custom_components/localthings/config_flow.py b/custom_components/localthings/config_flow.py index c65fcec..d2a3608 100644 --- a/custom_components/localthings/config_flow.py +++ b/custom_components/localthings/config_flow.py @@ -215,8 +215,23 @@ def _is_placeholder_serial(serial: str) -> bool: 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). + + Issue #189: the DA_WM_A51_20_COMMON (ARTIK051) laundry board family + reports a flash-unset sentinel instead -- every character the same + repeated hex digit (a washer and a dryer, two different physical + units, both reported the literal serialNum 'FFFFFFFFFFFFFFF') -- which + the 'nothing' check above doesn't catch either, so the second unit's + config flow aborted as already configured. """ - return serial.strip().lower().startswith('nothing') + s = serial.strip() + if s.lower().startswith('nothing'): + return True + upper = s.upper() + return ( + len(upper) >= 8 + and len(set(upper)) == 1 + and upper[0] in '0123456789ABCDEF' + ) def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict: diff --git a/custom_components/localthings/coordinator.py b/custom_components/localthings/coordinator.py index e895904..f17b1d8 100644 --- a/custom_components/localthings/coordinator.py +++ b/custom_components/localthings/coordinator.py @@ -92,12 +92,30 @@ def _is_placeholder_serial(serial: str) -> bool: 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. + and the second one's entities get dropped (issue #83). + + Issue #189: the DA_WM_A51_20_COMMON (ARTIK051) laundry board family + reports a flash-unset sentinel instead -- every character the same + repeated hex digit (a washer and a dryer, two different physical + units, both reported the literal serialNum 'FFFFFFFFFFFFFFF') -- which + the 'nothing' check above doesn't catch either, so two such units + collided on the config-entry unique_id and the second couldn't be + added at all. + + 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') + s = serial.strip() + if s.lower().startswith('nothing'): + return True + upper = s.upper() + return ( + len(upper) >= 8 + and len(set(upper)) == 1 + and upper[0] in '0123456789ABCDEF' + ) class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): diff --git a/tests/localthings/test_config_flow.py b/tests/localthings/test_config_flow.py index 8d40bd3..492614d 100644 --- a/tests/localthings/test_config_flow.py +++ b/tests/localthings/test_config_flow.py @@ -539,3 +539,18 @@ 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 + + +def test_is_placeholder_serial_catches_all_same_hex_digit(): + """Issue #189: the DA_WM_A51_20_COMMON (ARTIK051) laundry board family + reports a flash-unset sentinel instead of 'Nothing(SVC)' -- every + character the same repeated hex digit. A washer and a dryer, two + different physical units, both reported the literal serialNum + 'FFFFFFFFFFFFFFF', colliding on the config-entry unique_id.""" + from custom_components.localthings.config_flow import _is_placeholder_serial + assert _is_placeholder_serial('FFFFFFFFFFFFFFF') is True + assert _is_placeholder_serial('ffffffffffffffff') is True + assert _is_placeholder_serial('00000000') is True + # Too short to be the flash-unset sentinel -- a real serial could + # plausibly repeat one hex digit seven times by chance. + assert _is_placeholder_serial('FFFFFFF') is False diff --git a/tests/localthings/test_coordinator.py b/tests/localthings/test_coordinator.py index e9e6f45..3b89d5e 100644 --- a/tests/localthings/test_coordinator.py +++ b/tests/localthings/test_coordinator.py @@ -15,7 +15,7 @@ from custom_components.localthings.const import ( SUMMARY_INTERVAL_S, ) from custom_components.localthings.coordinator import ( - LocalThingsCoordinator, _local_source_port, + LocalThingsCoordinator, _is_placeholder_serial, _local_source_port, ) from custom_components.localthings.registry.capabilities.common import ( remote_control_enabled, @@ -163,6 +163,44 @@ def test_run_discovery_falls_back_to_host_for_placeholder_serial( assert coordinator.device_serial == mock_entry.data[CONF_HOST] +def test_run_discovery_falls_back_to_host_for_all_f_placeholder_serial( + hass: HomeAssistant, mock_entry +) -> None: + """Issue #189: the DA_WM_A51_20_COMMON (ARTIK051) laundry board family + reports a flash-unset sentinel instead of 'Nothing(SVC)' -- every + character the same repeated hex digit. A washer and a dryer, two + different physical units, both reported the literal serialNum + 'FFFFFFFFFFFFFFF', so without this fallback they'd collide on + device_serial exactly like the #83 case above.""" + resources = { + '/information/vs/0': { + 'x.com.samsung.da.modelNum': + 'DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000', + 'x.com.samsung.da.description': + 'DA_WM_A51_20_COMMON_DVE50A8800/DC92-02835A_0080', + 'x.com.samsung.da.serialNum': 'FFFFFFFFFFFFFFF', + }, + '/otninformation/vs/0': {}, + } + coordinator = LocalThingsCoordinator(hass, mock_entry) + coordinator._run_discovery(resources) + assert coordinator.device_serial == mock_entry.data[CONF_HOST] + + +def test_is_placeholder_serial_catches_all_same_hex_digit(): + assert _is_placeholder_serial('FFFFFFFFFFFFFFF') is True + assert _is_placeholder_serial('ffffffffffffffff') is True + assert _is_placeholder_serial('00000000') is True + + +def test_is_placeholder_serial_accepts_real_serials_and_short_runs(): + assert _is_placeholder_serial('0A1B2C3D4E5F') is False + assert _is_placeholder_serial('') is False + # Too short to be the flash-unset sentinel -- a real serial could + # plausibly repeat one hex digit seven times by chance. + assert _is_placeholder_serial('FFFFFFF') is False + + def test_run_discovery_detects_cooktop_via_resource_signature( hass: HomeAssistant, mock_entry ) -> None: