First full dryer dump (issue #14, DV90BB5245AES1) surfaced 5 unbound hrefs and an "incomplete capability coverage" repair. Handle them and, while here, make the washer/dryer/dishwasher families consistent instead of each carrying a bespoke variant of the same controls. Dryer coverage: - /power/0, /kidslock/0, /remotectrl/0: bind via the OCF-native + vendor fallback pairs (prefer the standard OCF resource, fall back to -vs). - /buzzersound/vs/0: new Buzzer sound select. - /course/vs/0: cycle select shared with washer/dishwasher; ignore the /st/dryercourse/vs/0 re-encoding (mirror of /st/washercourse/vs/0). Consistency / de-duplication: - Move generic OCF controls (power/kids-lock/remote-control fallback pairs, energy meter) into common.py; every registry uses them. - Move shared laundry controls (buzzer, job-beginning-status, and the /course/vs/0 cycle-select machinery) into laundry.py; washer and dishwasher stop hand-rolling their own copies. - Energy meter is now sentinel-aware everywhere: the dead '-500' instantaneousPower reading no longer shows a misleading 0 W (fixes it on dryers and dishwashers, matching the earlier washer fix). - Job-beginning-status reads x.com.samsung.da.currentStatus, the field every dump actually carries; the dryer sensor was previously blank. Adds a scrubbed dryer fixture, golden, and capability tests, plus an .claude/skills/adding-device-support skill capturing the dump-reading, OCF-vs-vendor, entity-taxonomy, and coverage workflow. Bumps to 0.6.0.
60 lines
2.1 KiB
Python
60 lines
2.1 KiB
Python
"""Tests for LocalThingsSelect's option-list resolution
|
|
(custom_components/localthings/select.py) -- the static tuple, options_field,
|
|
and callable forms of SelectDesc.options.
|
|
"""
|
|
from custom_components.localthings.registry.capability import Capability
|
|
from custom_components.localthings.registry.discovery import BoundEntity
|
|
from custom_components.localthings.registry.entities import SelectDesc
|
|
from custom_components.localthings.select import LocalThingsSelect
|
|
|
|
|
|
class _FakeCoordinator:
|
|
device_serial = 'TEST-SERIAL'
|
|
|
|
def __init__(self, last_resources):
|
|
self.last_resources = last_resources
|
|
|
|
|
|
def _make_select(desc, href, last_resources):
|
|
capability = Capability(href=href, entities=(desc,))
|
|
bound = BoundEntity(href=href, capability=capability, desc=desc)
|
|
return LocalThingsSelect(_FakeCoordinator(last_resources), bound)
|
|
|
|
|
|
def test_static_options_unaffected():
|
|
desc = SelectDesc(key='x', options=('A', 'B'))
|
|
entity = _make_select(desc, '/x/vs/0', {})
|
|
assert entity.options == ['A', 'B']
|
|
|
|
|
|
def test_options_field_unaffected():
|
|
desc = SelectDesc(key='x', options_field='supported')
|
|
entity = _make_select(desc, '/x/vs/0', {'/x/vs/0': {'supported': ['Lo', 'Hi']}})
|
|
assert entity.options == ['Lo', 'Hi']
|
|
|
|
|
|
def test_callable_options_receives_full_resource_snapshot():
|
|
"""A callable options is handed the coordinator's full href->rep
|
|
snapshot, not just this entity's own href's rep -- needed for course
|
|
lists decoded from a sibling resource (see laundry.cycle_options)."""
|
|
calls = []
|
|
|
|
def _options_fn(resources):
|
|
calls.append(resources)
|
|
return list(resources.get('/other/vs/0', {}).get('codes', []))
|
|
|
|
desc = SelectDesc(key='cycle', translation_key='fake_cycle', options=_options_fn)
|
|
resources = {
|
|
'/x/vs/0': {},
|
|
'/other/vs/0': {'codes': ['1C', '1D']},
|
|
}
|
|
entity = _make_select(desc, '/x/vs/0', resources)
|
|
assert entity.options == ['1c', '1d']
|
|
assert calls == [resources]
|
|
|
|
|
|
def test_callable_options_empty_result():
|
|
desc = SelectDesc(key='cycle', options=lambda resources: [])
|
|
entity = _make_select(desc, '/x/vs/0', {})
|
|
assert entity.options == []
|