From 2852a21c4ac310c7d1861dcafb5f452c7830e6d8 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Sat, 11 Jul 2026 12:49:08 -0500 Subject: [PATCH] feat(washer): add model-number-based device-type detection fallback --- .../localthings/registry/by_type/__init__.py | 35 ++++++++++- tests/test_by_type.py | 63 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/custom_components/localthings/registry/by_type/__init__.py b/custom_components/localthings/registry/by_type/__init__.py index c6049f1..207c0e1 100644 --- a/custom_components/localthings/registry/by_type/__init__.py +++ b/custom_components/localthings/registry/by_type/__init__.py @@ -4,7 +4,7 @@ from typing import Optional from ._base import DeviceRegistry from . import dishwasher, dryer, oven, refrigerator, washer -__all__ = ['DeviceRegistry', '_type_key', 'for_device'] +__all__ = ['DeviceRegistry', '_type_key', 'for_device', 'for_device_by_model'] _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = { @@ -56,3 +56,36 @@ def for_device(one_ui_version: str) -> Optional[DeviceRegistry]: if key.endswith(f'_{rkey}'): return reg return None + + +# Consumer-model prefix (first two letters of the '_'-delimited token in +# `description` right before any '/board-info' suffix) -> registry key. +# NOT derived from `modelNum` -- washer and dryer share the same 'DA_WM_' +# internal board-family prefix there, and dishwasher's modelNum contains +# the substring 'WW', so a modelNum-only rule misroutes both. +_CONSUMER_PREFIX_TO_KEY: dict[str, str] = { + 'WW': 'washer', + 'WD': 'washer', + 'DV': 'dryer', + 'DW': 'dishwasher', +} + + +def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegistry]: + """Fallback device-type detection for hardware that never reports + oneUiVersion (confirmed for washers -- their /otninformation/vs/0 has + no swVersionInfo key at all). + + Args: + model_num: x.com.samsung.da.modelNum from /information/vs/0. + description: x.com.samsung.da.description from /information/vs/0. + + Returns: + DeviceRegistry if the consumer-model code or modelNum resolves to a + known type, None otherwise. + """ + token = (description or '').split('/', 1)[0].rsplit('_', 1)[-1] + key = _CONSUMER_PREFIX_TO_KEY.get(token[:2].upper()) + if key is None and '_REF_' in (model_num or ''): + key = 'refrigerator' + return _REGISTRY_BY_KEY.get(key) if key else None diff --git a/tests/test_by_type.py b/tests/test_by_type.py index 6dfebf0..8c2ea64 100644 --- a/tests/test_by_type.py +++ b/tests/test_by_type.py @@ -99,3 +99,66 @@ class TestWasherRegistry: '/diagnosis/vs/0', '/otninformation/vs/0', ): assert href in registry.capabilities, f"{href} missing from washer registry" + + +class TestForDeviceByModel: + """Fallback device-type detection for hardware without oneUiVersion.""" + + def test_washer_ww_prefix(self): + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'DA_WM_TP1_21_COMMON|20375141|20010002001811424AA30217008A0000', + 'DA_WM_TP1_21_COMMON_WW5000C/DC92-03495A_B048', + ) + assert reg is not None + assert reg.name == 'washer' + + def test_washer_wd_prefix(self): + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'DA_WM_TP1_21_COMMON|20375141|20010002001811424AA30217008A0000', + 'DA_WM_TP1_21_COMMON_WD7000B/DC92-03724A_004D', + ) + assert reg is not None + assert reg.name == 'washer' + + def test_dryer_not_misdetected_as_washer(self): + """Dryer shares the DA_WM_ board prefix with washer -- must not + be misrouted despite the shared prefix.""" + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'DA_WM_TP2_20_COMMON_DV5000T', 'DA_WM_TP2_20_COMMON_DV5000T', + ) + assert reg is not None + assert reg.name == 'dryer' + + def test_dishwasher_not_misdetected_as_washer(self): + """Dishwasher's modelNum contains the substring 'WW' -- must not + be misrouted by a naive substring match.""" + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'ADW-WW-RTL-24-AILITE|90000541|400002010019130059C1000500E10000', + 'ADW-WW-RTL-24-AILITE_DW9000F/DD91-00002A_0002', + ) + assert reg is not None + assert reg.name == 'dishwasher' + + def test_refrigerator_via_modelnum_ref_token(self): + """Refrigerator's description has no consumer-model suffix; falls + back to the '_REF_' token in modelNum.""" + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'TP1X_REF_21K|00176141|00000850031813294103010041030000', + 'TP1X_REF_21K', + ) + assert reg is not None + assert reg.name == 'refrigerator' + + def test_unknown_model_returns_none(self): + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model('SOME-UNKNOWN-BOARD', 'SOME-UNKNOWN-BOARD') + assert reg is None + + def test_empty_inputs_return_none(self): + from custom_components.localthings.registry.by_type import for_device_by_model + assert for_device_by_model('', '') is None