feat: add laundry-family capabilities; dishwasher golden green

This commit is contained in:
Marc Billow
2026-06-30 16:32:37 -05:00
parent d720b29a67
commit 23b4cdc315
6 changed files with 155 additions and 3 deletions
@@ -1,4 +1,4 @@
from . import common, operational
from . import common, laundry, operational
from ..capability import Capability
@@ -6,5 +6,5 @@ def _is_capability(v):
return isinstance(v, Capability)
ALL = [v for mod in (common, operational)
ALL = [v for mod in (common, operational, laundry)
for v in vars(mod).values() if _is_capability(v)]
@@ -48,6 +48,9 @@ KIDS_LOCK = Capability(
BinarySensorDesc(key='child_lock', field='x.com.samsung.da.kidsLock',
name='Child lock', device_class='lock',
value_fn=lambda v: v != 'Ready'),
BinarySensorDesc(key='child_lock_binary', field='x.com.samsung.da.kidsLock',
name='Child lock (binary)', device_class='lock',
value_fn=lambda v: v != 'Ready'),
),
)
@@ -58,6 +61,10 @@ REMOTE_CONTROL = Capability(
field='x.com.samsung.da.remoteControlEnabled',
name='Remote control', device_class='connectivity',
value_fn=lambda v: str(v).lower() == 'true'),
BinarySensorDesc(key='remote_control_binary',
field='x.com.samsung.da.remoteControlEnabled',
name='Remote control (binary)', device_class='connectivity',
value_fn=lambda v: str(v).lower() == 'true'),
),
)
@@ -68,6 +75,11 @@ POWER = Capability(
name='Power',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep: (['/power/vs/0'], {'x.com.samsung.da.power': 'On' if p else 'Off'})),
SensorDesc(key='power_state', field='x.com.samsung.da.power',
name='Power state'),
BinarySensorDesc(key='power_state_binary', field='x.com.samsung.da.power',
name='Power state (binary)',
value_fn=lambda v: v == 'On'),
),
)
@@ -0,0 +1,52 @@
"""Capabilities specific to the laundry-family appliances (dishwasher, dryer).
Resources verified against the DW9000F-class dump at 10.0.0.129.
Note: door-LED keys use NO `x.com.samsung.da.` prefix — `setBrightness` and
`setNightLight` — preserved exactly as they appear in the OCF resource rep.
"""
from ..capability import Capability
from ..entities import SelectDesc, SwitchDesc
_LED_LEVELS = ('Low', 'High')
_SOUND_MODES = ('voice', 'tone', 'mute')
def _led_brightness_write(p, rep):
if p not in _LED_LEVELS:
return None
return ['doorled', 'light', 'vs', '0'], {'setBrightness': p}
def _led_night_write(p, rep):
if p not in ('On', 'Off'):
return None
return ['doorled', 'light', 'vs', '0'], {'setNightLight': p}
def _sound_mode_write(p, rep):
if p not in _SOUND_MODES:
return None
return ['settings', 'sound', 'mode', 'vs', '0'], {'mode': p}
DOOR_LED = Capability(
href='/doorled/light/vs/0',
entities=(
SelectDesc(key='led_brightness', field='setBrightness',
name='Door LED brightness', icon='mdi:brightness-6',
options=_LED_LEVELS, write_fn=_led_brightness_write),
SwitchDesc(key='led_night_light', field='setNightLight',
name='Door LED night light', icon='mdi:weather-night',
value_fn=lambda v: v == 'On',
write_fn=_led_night_write),
),
)
SOUND_MODE = Capability(
href='/settings/sound/mode/vs/0',
entities=(
SelectDesc(key='sound_mode', field='mode',
name='Sound mode', icon='mdi:volume-high',
options=_SOUND_MODES, write_fn=_sound_mode_write),
),
)
@@ -6,7 +6,7 @@ remaining-time extrapolation. Shared by dryer/dishwasher/oven/washer families.
import time
from ..capability import Capability
from ..entities import SensorDesc
from ..entities import ButtonDesc, SensorDesc
_SAMSUNG_STATE_TO_OCF = {
'Ready': 'idle', 'Run': 'active', 'Running': 'active',
@@ -87,6 +87,23 @@ OPERATIONAL_STATE = Capability(
name='Remaining minutes', unit='min', device_class='duration',
state_class='measurement',
value_fn=lambda r: _rem_minutes(r)),
SensorDesc(key='delay_start_time', field='x.com.samsung.da.delayStartTime',
name='Delay start time', icon='mdi:timer-pause'),
ButtonDesc(key='start', field='', name='Start cycle', payload='Run',
icon='mdi:play',
write_fn=lambda p, rep: (
['operational', 'state', 'vs', '0'],
{'x.com.samsung.da.state': p})),
ButtonDesc(key='pause', field='', name='Pause cycle', payload='Pause',
icon='mdi:pause',
write_fn=lambda p, rep: (
['operational', 'state', 'vs', '0'],
{'x.com.samsung.da.state': p})),
ButtonDesc(key='stop', field='', name='Stop cycle', payload='Ready',
icon='mdi:stop',
write_fn=lambda p, rep: (
['operational', 'state', 'vs', '0'],
{'x.com.samsung.da.state': p})),
),
active_when=_active_when,
on_observation=_on_observation,
+13
View File
@@ -0,0 +1,13 @@
"""Global CAPABILITIES registry: href -> Capability.
Built from the full list of Capability objects in the capabilities package.
Consumed by discover() at connection time to bind device resources to entities.
Note: each href must appear in at most one Capability across all modules.
If two capabilities share an href, the later one in the scan order wins —
this is a bug, not a feature. Add all entities for a given href to the same
Capability object instead.
"""
from .capabilities import ALL
CAPABILITIES: dict = {cap.href: cap for cap in ALL}
+58
View File
@@ -0,0 +1,58 @@
"""Write-handler unit tests for laundry-family capabilities."""
from samsung_appliance.registry.capabilities import laundry
from samsung_appliance.registry.capabilities.operational import OPERATIONAL_STATE
def test_start_button_writes_run():
btn = next(e for e in OPERATIONAL_STATE.entities if e.key == 'start')
assert btn.write_fn('Run', {}) == (
['operational', 'state', 'vs', '0'],
{'x.com.samsung.da.state': 'Run'},
)
def test_pause_button_writes_pause():
btn = next(e for e in OPERATIONAL_STATE.entities if e.key == 'pause')
assert btn.write_fn('Pause', {}) == (
['operational', 'state', 'vs', '0'],
{'x.com.samsung.da.state': 'Pause'},
)
def test_stop_button_writes_ready():
btn = next(e for e in OPERATIONAL_STATE.entities if e.key == 'stop')
assert btn.write_fn('Ready', {}) == (
['operational', 'state', 'vs', '0'],
{'x.com.samsung.da.state': 'Ready'},
)
def test_sound_mode_write_valid():
desc = next(e for e in laundry.SOUND_MODE.entities if e.key == 'sound_mode')
assert desc.write_fn('mute', {}) == (
['settings', 'sound', 'mode', 'vs', '0'],
{'mode': 'mute'},
)
assert desc.write_fn('invalid', {}) is None
def test_led_brightness_write_valid():
desc = next(e for e in laundry.DOOR_LED.entities if e.key == 'led_brightness')
assert desc.write_fn('Low', {}) == (
['doorled', 'light', 'vs', '0'],
{'setBrightness': 'Low'},
)
assert desc.write_fn('Medium', {}) is None
def test_led_night_light_write_valid():
desc = next(e for e in laundry.DOOR_LED.entities if e.key == 'led_night_light')
assert desc.write_fn('On', {}) == (
['doorled', 'light', 'vs', '0'],
{'setNightLight': 'On'},
)
assert desc.write_fn('Off', {}) == (
['doorled', 'light', 'vs', '0'],
{'setNightLight': 'Off'},
)
assert desc.write_fn('invalid', {}) is None