feat(washer): add washer-specific capabilities
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Capabilities specific to washer appliances (Samsung DA_WM_TP1-class
|
||||
front-load washers).
|
||||
|
||||
Resources verified against two live WW90DG6U25LEU4 dumps (Table_02 course
|
||||
family). Washers never report `oneUiVersion` -- see
|
||||
`registry/by_type/__init__.py`'s `for_device_by_model()` for the fallback
|
||||
detection this device type requires.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import SelectDesc, SensorDesc
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /washer/vs/0 -- wash temperature, spin speed, rinse cycle count
|
||||
#
|
||||
# Despite the shared href, this is unrelated to dryer.DRYER_SETTINGS (also
|
||||
# bound to '/washer/vs/0') -- an artifact of Samsung reusing the same OCF
|
||||
# path for different device families. Only one of the two ever binds for a
|
||||
# given device, since dryer and washer are separate by_type registries.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
WASHER_SETTINGS = Capability(
|
||||
href='/washer/vs/0',
|
||||
entities=(
|
||||
SelectDesc(key='wash_temperature', field='x.com.samsung.da.waterTemperature',
|
||||
name='Wash temperature', icon='mdi:thermometer-water',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.supportedWaterTemperature',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['washer', 'vs', '0'], {'x.com.samsung.da.waterTemperature': p})),
|
||||
SelectDesc(key='spin_speed', field='x.com.samsung.da.spinLevel',
|
||||
name='Spin speed', icon='mdi:sync',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.supportedSpinLevel',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['washer', 'vs', '0'], {'x.com.samsung.da.spinLevel': p})),
|
||||
SelectDesc(key='rinse_cycles', field='x.com.samsung.da.rinseCycles',
|
||||
name='Rinse cycles', icon='mdi:water-sync',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.supportedRinseCycles',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['washer', 'vs', '0'], {'x.com.samsung.da.rinseCycles': p})),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /course/vs/0 -- selected course, read-only.
|
||||
#
|
||||
# Unlike dishwasher.CYCLE_OPTIONS, we have no verified Course_XX -> name
|
||||
# table for washer hardware (only two codes observed: 1C, 65) and no
|
||||
# supported-course list field to validate a write against. Exposing this
|
||||
# as a plain sensor of the raw hex code is honest about what we actually
|
||||
# know; add write support once a real course-name table exists.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _option_value(options, prefix):
|
||||
for o in (options or []):
|
||||
if isinstance(o, str) and o.startswith(prefix + '_'):
|
||||
return o.split('_', 1)[1]
|
||||
return None
|
||||
|
||||
|
||||
WASHER_COURSE = Capability(
|
||||
href='/course/vs/0',
|
||||
entities=(
|
||||
SensorDesc(key='cycle', name='Cycle', icon='mdi:washing-machine',
|
||||
rep_fn=lambda rep: _option_value(
|
||||
rep.get('x.com.samsung.da.options'), 'Course')),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /buzzersound/vs/0 -- buzzer volume and (on some units) a separate finish
|
||||
# chime. Fields have no 'x.com.samsung.da.' prefix in this resource, unlike
|
||||
# most other washer hrefs.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BUZZER_SOUND = Capability(
|
||||
href='/buzzersound/vs/0',
|
||||
entities=(
|
||||
SelectDesc(key='buzzer_sound', field='setBuzzerSound',
|
||||
name='Buzzer sound', icon='mdi:volume-high',
|
||||
entity_category='config',
|
||||
options_field='supportedBuzzerSound',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['buzzersound', 'vs', '0'], {'setBuzzerSound': p})),
|
||||
SelectDesc(key='finish_sound', field='setFinishSound',
|
||||
name='Finish sound', icon='mdi:bell-ring',
|
||||
entity_category='config',
|
||||
exists_fn=lambda rep: 'supportedFinishSound' in rep,
|
||||
options_field='supportedFinishSound',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['buzzersound', 'vs', '0'], {'setFinishSound': p})),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /wm/jobbeginingstatus/vs/0 -- same href as dryer.JOB_BEGINNING_STATUS, but
|
||||
# a different field name (currentStatus, not jobBeginingStatus).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
WASHER_JOB_BEGINNING_STATUS = Capability(
|
||||
href='/wm/jobbeginingstatus/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='job_beginning_status',
|
||||
field='x.com.samsung.da.currentStatus',
|
||||
name='Job beginning status',
|
||||
entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Tests for washer-specific capabilities."""
|
||||
from custom_components.localthings.registry.capabilities import washer
|
||||
|
||||
|
||||
def _rep_by_href(cap, href):
|
||||
assert cap.href == href
|
||||
return cap
|
||||
|
||||
|
||||
class TestWasherSettings:
|
||||
def test_href(self):
|
||||
assert washer.WASHER_SETTINGS.href == '/washer/vs/0'
|
||||
|
||||
def test_wash_temperature_read(self):
|
||||
desc = next(e for e in washer.WASHER_SETTINGS.entities if e.key == 'wash_temperature')
|
||||
assert desc.field == 'x.com.samsung.da.waterTemperature'
|
||||
assert desc.options_field == 'x.com.samsung.da.supportedWaterTemperature'
|
||||
|
||||
def test_wash_temperature_write(self):
|
||||
desc = next(e for e in washer.WASHER_SETTINGS.entities if e.key == 'wash_temperature')
|
||||
path, body = desc.write_fn('60', {})
|
||||
assert path == ['washer', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.waterTemperature': '60'}
|
||||
|
||||
def test_spin_speed_write(self):
|
||||
desc = next(e for e in washer.WASHER_SETTINGS.entities if e.key == 'spin_speed')
|
||||
path, body = desc.write_fn('1400', {})
|
||||
assert path == ['washer', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.spinLevel': '1400'}
|
||||
|
||||
def test_rinse_cycles_write(self):
|
||||
desc = next(e for e in washer.WASHER_SETTINGS.entities if e.key == 'rinse_cycles')
|
||||
path, body = desc.write_fn('3', {})
|
||||
assert path == ['washer', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.rinseCycles': '3'}
|
||||
|
||||
|
||||
class TestWasherCourse:
|
||||
def test_href(self):
|
||||
assert washer.WASHER_COURSE.href == '/course/vs/0'
|
||||
|
||||
def test_reads_course_code_from_options_array(self):
|
||||
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == 'cycle')
|
||||
rep = {'x.com.samsung.da.options': ['DeviceType_0167', 'Course_1C', 'GMT_04']}
|
||||
assert desc.rep_fn(rep) == '1C'
|
||||
|
||||
def test_missing_course_option_returns_none(self):
|
||||
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == 'cycle')
|
||||
assert desc.rep_fn({'x.com.samsung.da.options': ['GMT_04']}) is None
|
||||
|
||||
def test_no_write_fn_yet(self):
|
||||
"""Read-only until a verified course-name table exists."""
|
||||
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == 'cycle')
|
||||
assert getattr(desc, 'write_fn', None) is None
|
||||
|
||||
|
||||
class TestBuzzerSound:
|
||||
def test_href(self):
|
||||
assert washer.BUZZER_SOUND.href == '/buzzersound/vs/0'
|
||||
|
||||
def test_buzzer_sound_write(self):
|
||||
desc = next(e for e in washer.BUZZER_SOUND.entities if e.key == 'buzzer_sound')
|
||||
path, body = desc.write_fn('Volume_High', {})
|
||||
assert path == ['buzzersound', 'vs', '0']
|
||||
assert body == {'setBuzzerSound': 'Volume_High'}
|
||||
|
||||
def test_finish_sound_exists_only_when_supported(self):
|
||||
desc = next(e for e in washer.BUZZER_SOUND.entities if e.key == 'finish_sound')
|
||||
assert desc.exists_fn({'setBuzzerSound': 'On'}) is False
|
||||
assert desc.exists_fn({'supportedFinishSound': ['FinishSound_1']}) is True
|
||||
|
||||
def test_finish_sound_write(self):
|
||||
desc = next(e for e in washer.BUZZER_SOUND.entities if e.key == 'finish_sound')
|
||||
path, body = desc.write_fn('FinishSound_2', {})
|
||||
assert path == ['buzzersound', 'vs', '0']
|
||||
assert body == {'setFinishSound': 'FinishSound_2'}
|
||||
|
||||
|
||||
class TestJobBeginningStatus:
|
||||
def test_href_and_field(self):
|
||||
assert washer.WASHER_JOB_BEGINNING_STATUS.href == '/wm/jobbeginingstatus/vs/0'
|
||||
desc = washer.WASHER_JOB_BEGINNING_STATUS.entities[0]
|
||||
assert desc.field == 'x.com.samsung.da.currentStatus'
|
||||
Reference in New Issue
Block a user