Files
localthings/tests/test_dehumidifier_capabilities.py
T
Marc Billow 89429039fb Add device support for Samsung dehumidifiers (issue #88)
The AY18CG7500GED dehumidifier (modelNum TP1X_DA_AC_DHM_01001_0000)
shares the DA_AC_ board family with the room-AC models but carries the
'_DHM_' token instead of '_RAC_'/'_PRAC_'/'_WAC_', so it fell into the
unknown-device-type fallback. Add a new dehumidifier registry, routed
via a '_DHM_' modelNum fallback rule, distinct from airconditioner
since target humidity (not temperature) is the primary control and
there's no climate composite.

Reuses airconditioner.py's AUTO_CLEAN/AIR_FILTER/MUTE_ONCE capabilities
directly (identical resource shapes on the shared board family). Adds
a new humidity sensor + target-humidity number pair and an
operating-mode select. Per the adding-device-support skill's
"never hard-code the one dump's values" rule, the target-humidity
number has no hardcoded min/max (falls back to HA's own 0-100 default
for a percentage field) and reads its step live from the device's own
`increment` field rather than a spec-sheet-derived constant. The
operating-mode select's options come live from supportedModes.

/mode/convenient/vs/0 is left unmodeled: only supportedModes is present
on this dump, with no live current-value field to confirm a read/write
contract.

Confirmed against the issue #88 diagnostics dump with zero unbound
hrefs. Updates the README's supported-appliance-types table for the
new device type.
2026-07-27 01:29:25 +00:00

119 lines
3.9 KiB
Python

"""Tests for Samsung dehumidifier support (issue #88, AY18CG7500GED).
HA-free like the rest of the suite: exercises the registry, discovery/
flatten, and the target-humidity/operating-mode write contracts.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import NumberDesc, SelectDesc
from tests.conftest import _load_device
def _dehumidifier():
resources = _load_device('dehumidifier')
info = resources['/information/vs/0']
reg = for_device_by_model(
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
)
return reg, resources
def _bound():
reg, resources = _dehumidifier()
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
def _state():
bound, resources = _bound()
return flatten(bound, resources)
def _desc(key):
bound, _ = _bound()
return next(b.desc for b in bound if b.desc.key == key)
def test_model_resolves_to_dehumidifier_registry():
reg, _ = _dehumidifier()
assert reg is not None and reg.name == 'dehumidifier'
def test_no_unbound_hrefs():
"""Every resource in the issue #88 dump binds or is covered -- clears
the coverage-gap repair."""
reg, resources = _dehumidifier()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_expected_state_keys_present():
state = _state()
for key in ('humidity', 'target_humidity', 'operating_mode', 'power_switch',
'auto_clean', 'air_filter_status', 'mute_once'):
assert key in state, key
def test_humidity_sensor_reads_current_value():
state = _state()
assert state['humidity'] == 47
def test_target_humidity_number_reads_desired_value():
state = _state()
assert state['target_humidity'] == 50
def test_target_humidity_write_contract():
desc = _desc('target_humidity')
assert isinstance(desc, NumberDesc)
path, body = desc.write_fn('55', {})
assert path == ['humidity', 'vs', '0']
assert body == {'x.com.samsung.da.desiredHumidity': '55'}
def test_target_humidity_step_reads_live_increment():
"""Step comes from the device's own `increment` field rather than a
hardcoded constant -- see the adding-device-support skill's 'never
hard-code the one dump's values' section."""
desc = _desc('target_humidity')
assert desc.step_fn({'increment': '5'}) == 5
assert desc.step_fn({'increment': '10'}) == 10
# No live field: falls back to a sane default rather than raising.
assert desc.step_fn({}) == 1
def test_target_humidity_has_no_hardcoded_bounds():
"""No range field is present in any dump seen so far -- native_min/max
are deliberately left unset (falls back to HA's own 0-100 default for a
percentage field) rather than a bound guessed from a spec sheet."""
desc = _desc('target_humidity')
assert desc.native_min is None
assert desc.native_max is None
assert desc.native_min_fn is None
assert desc.native_max_fn is None
def test_operating_mode_select_options_come_from_live_supported_modes():
"""Options are read live from x.com.samsung.da.supportedModes, not a
hardcoded tuple -- so a future device with a different mode set is
handled automatically."""
desc = _desc('operating_mode')
assert isinstance(desc, SelectDesc)
assert desc.options_field == 'x.com.samsung.da.supportedModes'
assert desc.options == ()
def test_operating_mode_write_contract():
desc = _desc('operating_mode')
path, body = desc.write_fn('Quiet', {})
assert path == ['mode', 'vs', '0']
assert body == {'x.com.samsung.da.modes': ['Quiet']}
def test_operating_mode_reads_first_mode():
state = _state()
assert state['operating_mode'] == 'Smart'