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.
119 lines
3.9 KiB
Python
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'
|