diff --git a/samsung_appliance/registry/capabilities/__init__.py b/samsung_appliance/registry/capabilities/__init__.py index 3a5db2f..154dc34 100644 --- a/samsung_appliance/registry/capabilities/__init__.py +++ b/samsung_appliance/registry/capabilities/__init__.py @@ -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)] diff --git a/samsung_appliance/registry/capabilities/common.py b/samsung_appliance/registry/capabilities/common.py index a05c97b..945487b 100644 --- a/samsung_appliance/registry/capabilities/common.py +++ b/samsung_appliance/registry/capabilities/common.py @@ -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'), ), ) diff --git a/samsung_appliance/registry/capabilities/laundry.py b/samsung_appliance/registry/capabilities/laundry.py new file mode 100644 index 0000000..68a301f --- /dev/null +++ b/samsung_appliance/registry/capabilities/laundry.py @@ -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), + ), +) diff --git a/samsung_appliance/registry/capabilities/operational.py b/samsung_appliance/registry/capabilities/operational.py index b81276a..1320e86 100644 --- a/samsung_appliance/registry/capabilities/operational.py +++ b/samsung_appliance/registry/capabilities/operational.py @@ -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, diff --git a/samsung_appliance/registry/registry.py b/samsung_appliance/registry/registry.py new file mode 100644 index 0000000..af5a5b1 --- /dev/null +++ b/samsung_appliance/registry/registry.py @@ -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} diff --git a/tests/test_laundry_writes.py b/tests/test_laundry_writes.py new file mode 100644 index 0000000..5da9d9f --- /dev/null +++ b/tests/test_laundry_writes.py @@ -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