diff --git a/custom_components/localthings/config_flow.py b/custom_components/localthings/config_flow.py index 4b6c3ac..960c397 100644 --- a/custom_components/localthings/config_flow.py +++ b/custom_components/localthings/config_flow.py @@ -125,7 +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 + from .registry.by_type import for_device, for_device_by_model _LOGGER.debug("Fetching Samsung cloud UUID from %s", _SAMSUNG_CLOUD_HOST) try: @@ -177,15 +177,20 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict: .get('swVersionInfo', {}) .get('oneUiVersion', '') ) + info_resource = resources.get('/information/vs/0', {}) + recognized_registry = ( + for_device(one_ui_version) if one_ui_version else None + ) or for_device_by_model( + info_resource.get('x.com.samsung.da.modelNum', ''), + info_resource.get('x.com.samsung.da.description', ''), + ) 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 - ), + "device_type_recognized": recognized_registry is not None, } except CannotConnect: raise diff --git a/custom_components/localthings/coordinator.py b/custom_components/localthings/coordinator.py index 50c5aed..dd4d624 100644 --- a/custom_components/localthings/coordinator.py +++ b/custom_components/localthings/coordinator.py @@ -20,7 +20,7 @@ from smartthings_local.protocol.dtls_session import DtlsCoapSession from smartthings_local.ocf.state_cache import StateCache from .registry.batch import parse_device0_batch -from .registry.by_type import for_device +from .registry.by_type import for_device, for_device_by_model from .registry.discovery import discover, BoundEntity from .registry import CAPABILITIES from .registry.adapter import flatten @@ -273,7 +273,13 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): .get('swVersionInfo', {}) .get('oneUiVersion', '')) self.one_ui_version = one_ui + info = resources.get('/information/vs/0', {}) reg = for_device(one_ui) if one_ui else None + if reg is None: + reg = for_device_by_model( + info.get('x.com.samsung.da.modelNum', ''), + info.get('x.com.samsung.da.description', ''), + ) unbound: list[str] = [] if reg is not None: self._log.debug("device type: %s (oneUiVersion=%r)", reg.name, one_ui) @@ -287,7 +293,6 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.bound = bound self._unbound_hrefs = unbound - info = resources.get('/information/vs/0', {}) serial = info.get('x.com.samsung.da.serialNum', '') if not serial: serial = self._entry.data[CONF_HOST] diff --git a/tests/localthings/test_config_flow.py b/tests/localthings/test_config_flow.py index 6e696ac..0177f20 100644 --- a/tests/localthings/test_config_flow.py +++ b/tests/localthings/test_config_flow.py @@ -134,3 +134,38 @@ async def test_duplicate_device_aborted(hass: HomeAssistant, mock_probe) -> None ) assert result['type'] == FlowResultType.ABORT assert result['reason'] == 'already_configured' + + +def test_probe_marks_washer_as_recognized(monkeypatch): + """A washer's probe response (no oneUiVersion) must still resolve via + the modelNum/description fallback so setup doesn't warn about an + unrecognized device type.""" + from custom_components.localthings import config_flow + + device0 = [ + {'rt': ['x.com.samsung.devcol']}, + {'href': '/information/vs/0', 'rep': { + 'x.com.samsung.da.modelNum': + 'DA_WM_TP1_21_COMMON|20375141|20010002001811424AA30217008A0000', + 'x.com.samsung.da.description': + 'DA_WM_TP1_21_COMMON_WW5000C/DC92-03495A_B048', + 'x.com.samsung.da.serialNum': 'TEST-SERIAL', + }}, + {'href': '/otninformation/vs/0', 'rep': {'otnStatus': 'None'}}, + ] + from custom_components.localthings.registry.batch import parse_device0_batch + resources = parse_device0_batch(device0) + + info_resource = resources.get('/information/vs/0', {}) + one_ui_version = ( + resources.get('/otninformation/vs/0', {}).get('swVersionInfo', {}).get('oneUiVersion', '') + ) + from custom_components.localthings.registry.by_type import for_device, for_device_by_model + recognized = bool( + (one_ui_version and for_device(one_ui_version) is not None) + or for_device_by_model( + info_resource.get('x.com.samsung.da.modelNum', ''), + info_resource.get('x.com.samsung.da.description', ''), + ) is not None + ) + assert recognized is True diff --git a/tests/localthings/test_coordinator.py b/tests/localthings/test_coordinator.py index 34ffdcd..20c7c54 100644 --- a/tests/localthings/test_coordinator.py +++ b/tests/localthings/test_coordinator.py @@ -113,6 +113,27 @@ async def test_coverage_gap_issue_absent_for_fully_covered_real_fixture( assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is None +def test_run_discovery_detects_washer_via_model_fallback( + hass: HomeAssistant, mock_entry +) -> None: + """Washer hardware reports no oneUiVersion; device type must resolve + via for_device_by_model instead of falling back to generic CAPABILITIES.""" + resources = { + '/information/vs/0': { + 'x.com.samsung.da.modelNum': + 'DA_WM_TP1_21_COMMON|20375141|20010002001811424AA30217008A0000', + 'x.com.samsung.da.description': + 'DA_WM_TP1_21_COMMON_WW5000C/DC92-03495A_B048', + 'x.com.samsung.da.serialNum': 'TEST-SERIAL', + }, + '/otninformation/vs/0': {'otnStatus': 'None'}, + '/power/vs/0': {'x.com.samsung.da.power': 'On'}, + } + coordinator = LocalThingsCoordinator(hass, mock_entry) + coordinator._run_discovery(resources) + assert coordinator.device_type_name == 'washer' + + def test_update_coverage_gap_issue_creates_issue_for_unknown_type( hass: HomeAssistant, mock_entry ) -> None: