fix(water_purifier): route AILITE_DA-REF-WATERPURIFIER boards correctly (#196)
The AILITE water-purifier board (RWP70F15ANW) spells its modelNum
'...-REF-WATERPURIFIER-...', so the board-token scan hit the bare 'REF'
token before ever reaching 'WATERPURIFIER' and misrouted the device to
the refrigerator registry, whose resource surface shares almost nothing
with a water purifier -- hence the incomplete-coverage warning. Add a
documented carve-out for this one token co-occurrence and a matching
TestBoardTokenAmbiguity exception.
Also bind the water-purifier hrefs this board additionally exposes:
cup-detection status, the settings/sound/{mode,output,volume} trio (read
live, since this board's own supportedModes vocabulary differs from both
laundry's and air_purifier's hardcoded/live sets), and last-pour
statistics.
Separately, gate hot_water_temperature off when the device doesn't report
a supportedHotTemperatures list (only a hotwaterRange/hotwaterLevel pair
with no confirmed write contract) -- previously an empty options list
plus a live current value rendered as 'unknown' in HA, the second bug
reported in #196. The existing water_purifier_coffee fixture (#107) turns
out to hit the same shape, so its golden drops the entity too.
Issue #195 (TP1X_REF_21K) needed no change: its diagnostics show zero
unbound hrefs and the model already routes to the refrigerator registry,
matching the maintainer's own comment on the issue.
This commit is contained in:
11 files changed
+940
-6
No files matched your search
@@ -295,3 +295,98 @@ def test_coffee_recipe_hrefs_are_ignored_not_guessed():
|
||||
for href in ('/brand/recipe/info/vs/0', '/coffee/custom/recipe/vs/0',
|
||||
'/recipe/coffee/vs/0', '/recipe/coffee/deletion/vs/0'):
|
||||
assert href in ignored_hrefs, href
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AILITE_DA-REF-WATERPURIFIER board (issue #196, RWP70F15ANW) -- a
|
||||
# coffee-capable variant on a different board family than issue #90/#107's
|
||||
# TP2X_WATERPURIFIER_20K, whose modelNum's 'REF' token would otherwise
|
||||
# misroute it to the refrigerator registry (see test_by_type.py's
|
||||
# TestBoardTokenAmbiguity carve-out). Also the first dump to expose
|
||||
# /cup/state/vs/0, /statistic/pour/vs/0, and the settings/sound/* trio on
|
||||
# this device type.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _water_purifier_ailite():
|
||||
resources = _load_device('water_purifier_ailite_25k')
|
||||
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_ailite():
|
||||
reg, resources = _water_purifier_ailite()
|
||||
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
|
||||
|
||||
|
||||
def _state_ailite():
|
||||
bound, resources = _bound_ailite()
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def _desc_ailite(key):
|
||||
bound, _ = _bound_ailite()
|
||||
return next(b.desc for b in bound if b.desc.key == key)
|
||||
|
||||
|
||||
def test_ailite_model_resolves_to_water_purifier_not_refrigerator():
|
||||
reg, _ = _water_purifier_ailite()
|
||||
assert reg is not None and reg.name == 'water_purifier'
|
||||
|
||||
|
||||
def test_ailite_no_unbound_hrefs():
|
||||
reg, resources = _water_purifier_ailite()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_ailite_expected_state_keys_present():
|
||||
state = _state_ailite()
|
||||
for key in ('cup_state', 'sound_mode', 'sound_output', 'sound_volume',
|
||||
'alarm_in_mute', 'last_pour_type', 'last_pour_capacity'):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_ailite_hot_water_temperature_gated_off_without_supported_list():
|
||||
"""This board reports tempDesiredHotWater but no
|
||||
supportedHotTemperatures (only a hotwaterRange/hotwaterLevel pair whose
|
||||
write contract isn't confirmed) -- the exact shape that used to make
|
||||
HA's select show 'unknown' (issue #196), since current_option isn't in
|
||||
an empty options list. The descriptor must gate off entirely rather than
|
||||
register with empty options."""
|
||||
state = _state_ailite()
|
||||
assert 'hot_water_temperature' not in state
|
||||
|
||||
|
||||
def test_ailite_sound_mode_options_come_from_live_supported_modes():
|
||||
"""This board's supportedModes (voice/fixedTone/mute) differs from both
|
||||
laundry.SOUND_MODE's hardcoded voice/tone/mute and air_purifier.SOUND_MODE's
|
||||
mute/buzzer -- reusing either would reject a value this device actually
|
||||
supports, per the oven._OVEN_MODES lesson from issue #138."""
|
||||
desc = _desc_ailite('sound_mode')
|
||||
assert desc.options_field == 'supportedModes'
|
||||
assert desc.translation_key == 'water_purifier_sound_mode'
|
||||
|
||||
|
||||
def test_ailite_sound_volume_bounds_come_live_not_hardcoded():
|
||||
desc = _desc_ailite('sound_volume')
|
||||
assert isinstance(desc, NumberDesc)
|
||||
assert desc.native_min is None and desc.native_max is None
|
||||
rep = {'minLevel': '0', 'maxLevel': '15', 'resolution': '5'}
|
||||
assert desc.native_min_fn(rep) == 0
|
||||
assert desc.native_max_fn(rep) == 15
|
||||
assert desc.step_fn(rep) == 5
|
||||
|
||||
|
||||
def test_ailite_alarm_in_mute_is_read_only():
|
||||
"""No sibling field advertises alarmInMute as user-settable -- surfaced
|
||||
as a read-only diagnostic per the 'don't guess' rule rather than an
|
||||
invented switch."""
|
||||
from custom_components.localthings.registry.entities import BinarySensorDesc
|
||||
desc = _desc_ailite('alarm_in_mute')
|
||||
assert isinstance(desc, BinarySensorDesc)
|
||||
assert desc.value_fn('true') is True
|
||||
assert desc.value_fn('false') is False
|
||||
Reference in new issue
Block a user