for_device_by_model() had grown to 21 sequential `if key is None` branches and 102 comment lines against 59 lines of code -- 33 of the repo's 243 commits have touched this file. Most of that bulk came from one wrong primitive: substring matching on a delimited string. Samsung spells the same board family with either delimiter, so '_RAC_' and '-RAC-' each needed their own rule, and 'ARTIK051_DONGLE_REF' (issues #77, #83) matched no '_TOKEN_' spelling at all because REF lands at the end of the pipe-prefix with no trailing underscore -- which is what _model_num_segments() existed to work around. Which field a rule searched (modelNum, or modelNum + description) was historical accident. Collisions like WAC vs WA were resolved by one `if` physically preceding another, invisible in the code and explained at length in prose. Tokenize on any non-alphanumeric run, upper-case, and look the tokens up in a flat table. Every delimiter spelling collapses to one entry, both fields go through the same matcher in a documented order (modelNum, then description, then the fuzzy consumer prefix), and specificity is a property of the table rather than of line ordering. Two behaviours are preserved deliberately: - modelNum is matched before description, which is what keeps the legacy gas cooktop correct: it reports 'ARTIK051_GB_CT_001' (CT) alongside 'ARTIK051_GLOBAL_COOKTOP' (COOKTOP, which otherwise means induction). It is the only known device whose two fields disagree. - _consumer_model_key still splits on '_' only. Widening it to '-' would read the dishwasher's 'ADW-WW-RTL-24-AILITE' board segment as a bare 'WW' washer. Verified identical: all 49 device fixtures resolve to the same registry before and after, and every existing for_device_by_model test case passes unchanged. The table also picks up two families that previously depended on oneUiVersion alone (TP1X_DA-AC-AIR air purifiers, ADW dishwashers), so they now survive firmware that omits it. TestBoardTokenAmbiguity guards the one property the flat lookup needs -- that no real model string contains two tokens naming different device types -- across the whole fixture corpus, so a newly added dump exercises it automatically. The skill gains a section on routing: what each detection stage is for, the rules for adding a token (name the specific type, never the board family; never add a delimiter spelling; two-letter tokens are a last resort), when to reach for the consumer prefix or a resource signature instead, and the measured stake -- an unrouted device loses roughly half its entities.
59 lines
1.6 KiB
Python
59 lines
1.6 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
FIXTURES = Path(__file__).resolve().parent / 'fixtures'
|
|
|
|
|
|
def _resources_from_dump(dump: dict) -> dict[str, dict]:
|
|
from custom_components.localthings.registry.batch import parse_device0_batch
|
|
return parse_device0_batch(dump['device0'])
|
|
|
|
|
|
def _load_device(name: str) -> dict[str, dict]:
|
|
data = json.loads((FIXTURES / f'{name}_device.json').read_text())
|
|
return _resources_from_dump(data)
|
|
|
|
|
|
def _load_resources(ip: str) -> dict[str, dict]:
|
|
"""Legacy IP-based loader — maps known IPs to named fixtures."""
|
|
_ip_to_name = {
|
|
'10.0.0.129': 'dishwasher',
|
|
'10.0.0.254': 'refrigerator',
|
|
}
|
|
name = _ip_to_name.get(ip)
|
|
if name is None:
|
|
raise ValueError(f"No fixture for IP {ip!r} — add a scrubbed fixture to tests/fixtures/")
|
|
return _load_device(name)
|
|
|
|
|
|
@pytest.fixture
|
|
def dishwasher_resources() -> dict[str, dict]:
|
|
return _load_device('dishwasher')
|
|
|
|
|
|
@pytest.fixture
|
|
def fridge_resources() -> dict[str, dict]:
|
|
return _load_device('refrigerator')
|
|
|
|
|
|
@pytest.fixture
|
|
def washer_resources() -> dict[str, dict]:
|
|
return _load_device('washer')
|
|
|
|
|
|
@pytest.fixture
|
|
def all_device_fixtures() -> dict[str, dict[str, dict]]:
|
|
"""Every scrubbed device dump, keyed by fixture name.
|
|
|
|
For invariants that must hold across the whole corpus rather than for one
|
|
device -- so a newly added dump exercises them automatically.
|
|
"""
|
|
return {
|
|
path.name[:-len('_device.json')]: _resources_from_dump(
|
|
json.loads(path.read_text())
|
|
)
|
|
for path in sorted(FIXTURES.glob('*_device.json'))
|
|
}
|