Files
localthings/tests/test_select_options.py
T
Marc Billow 9264b912f9 feat: full dryer support + consistent laundry capability surface
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.
2026-07-21 01:15:12 +00:00

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 == []