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:
@@ -136,8 +136,20 @@ def _board_family_key(value: str, cut_at: str) -> Optional[str]:
|
||||
a flat lookup, not a priority list. Adding an entry that could co-occur
|
||||
with another (a family token, or one short enough to collide by accident)
|
||||
would break that property; see this table's comment.
|
||||
|
||||
One documented exception (issue #196): AILITE water-purifier boards
|
||||
spell their modelNum '...-REF-WATERPURIFIER-...', where 'REF' names the
|
||||
shared cooling-subsystem board, not the refrigerator device type --
|
||||
'WATERPURIFIER' is the actual, more specific type here. Rather than drop
|
||||
or rename either entry (both are correct on their own for the model
|
||||
strings that exist today), this one known co-occurrence resolves to
|
||||
'water_purifier'; TestBoardTokenAmbiguity's blanket check carries a
|
||||
matching carve-out for this exact pair.
|
||||
"""
|
||||
for token in _board_tokens(value, cut_at):
|
||||
tokens = _board_tokens(value, cut_at)
|
||||
if 'REF' in tokens and 'WATERPURIFIER' in tokens:
|
||||
return 'water_purifier'
|
||||
for token in tokens:
|
||||
key = _BOARD_TOKEN_TO_KEY.get(token)
|
||||
if key is not None:
|
||||
return key
|
||||
|
||||
@@ -15,6 +15,11 @@ REGISTRY = DeviceRegistry(
|
||||
water_purifier.FAVORITE_HOTWATER,
|
||||
water_purifier.COFFEE,
|
||||
water_purifier.LOCK,
|
||||
water_purifier.CUP_STATE,
|
||||
water_purifier.SOUND_MODE,
|
||||
water_purifier.SOUND_OUTPUT,
|
||||
water_purifier.SOUND_VOLUME,
|
||||
water_purifier.STATISTIC_POUR,
|
||||
*water_purifier.COVERAGE,
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Capabilities for the Samsung water-purifier family (TP2X_WATERPURIFIER-class,
|
||||
issue #90, model TP2X_WATERPURIFIER_20K).
|
||||
issue #90, model TP2X_WATERPURIFIER_20K; also AILITE_DA-REF-WATERPURIFIER-class,
|
||||
issue #196, model RWP70F15ANW/AILITE_WATERPURIFIER_25K).
|
||||
|
||||
Resources verified against the issue #90 diagnostics dump.
|
||||
Resources verified against the issue #90 and #196 diagnostics dumps.
|
||||
"""
|
||||
from ..batch import is_stub_rep
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||
from .common import int_or_none, parse_iso_utc as _parse_iso_utc
|
||||
@@ -21,10 +23,23 @@ DISPENSE = Capability(
|
||||
# Only a handful of discrete temperatures are selectable (not a
|
||||
# continuous range) -- a select over the live-reported set, not a
|
||||
# number with invented bounds.
|
||||
#
|
||||
# Newer boards (issue #196, RWP70F15ANW) don't populate
|
||||
# supportedHotTemperatures at all -- they report a hotwaterRange
|
||||
# (min/max) and a hotwaterLevel (step count?) instead, with no
|
||||
# confirmed write contract for values off the old preset list. With
|
||||
# an empty options_field result, HA's current_option still returns
|
||||
# the live tempDesiredHotWater, which isn't in the (empty) options
|
||||
# list and renders as "unknown" -- the exact symptom reported. Gate
|
||||
# the entity off entirely when the board doesn't report a supported
|
||||
# list, rather than guess at hotwaterRange/hotwaterLevel's meaning.
|
||||
SelectDesc(key='hot_water_temperature', field='x.com.samsung.da.tempDesiredHotWater',
|
||||
icon='mdi:thermometer',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.supportedHotTemperatures',
|
||||
exists_fn=lambda rep, resources: (
|
||||
is_stub_rep(rep)
|
||||
or 'x.com.samsung.da.supportedHotTemperatures' in rep),
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['setting', 'waterpurifier', 'vs', '0'],
|
||||
{'x.com.samsung.da.tempDesiredHotWater': p})),
|
||||
@@ -190,6 +205,86 @@ COFFEE = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
# Cup-detection status (issue #196, RWP70F15ANW). Only "UnReady" observed;
|
||||
# the full state domain isn't confirmed, so this stays a plain diagnostic
|
||||
# sensor rather than an enum with an invented state table.
|
||||
CUP_STATE = Capability(
|
||||
href='/cup/state/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='cup_state', field='water.cup.state',
|
||||
icon='mdi:cup-outline', entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
|
||||
# Sound mode/output/volume (issue #196). Shapes echo laundry.py/
|
||||
# air_purifier.py's same-named hrefs, but this board's own values differ
|
||||
# from both (supportedModes here is voice/fixedTone/mute, not laundry's
|
||||
# voice/tone/mute nor air_purifier's mute/buzzer) -- reusing either would
|
||||
# reject a live-supported value, so these read the device's own supported
|
||||
# list/range like air_purifier's versions do.
|
||||
SOUND_MODE = Capability(
|
||||
href='/settings/sound/mode/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SelectDesc(key='sound_mode', translation_key='water_purifier_sound_mode',
|
||||
field='mode',
|
||||
icon='mdi:volume-high',
|
||||
entity_category='config',
|
||||
options_field='supportedModes',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['settings', 'sound', 'mode', 'vs', '0'], {'mode': p})),
|
||||
),
|
||||
)
|
||||
|
||||
SOUND_OUTPUT = Capability(
|
||||
href='/settings/sound/output/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='sound_output', field='deviceType',
|
||||
icon='mdi:volume-high', entity_category='diagnostic'),
|
||||
# No confirmed write contract (no sibling field advertising this as
|
||||
# user-settable) -- surfaced read-only per the 'don't guess' rule.
|
||||
BinarySensorDesc(key='alarm_in_mute', field='alarmInMute',
|
||||
icon='mdi:volume-mute',
|
||||
entity_category='diagnostic',
|
||||
value_fn=lambda v: str(v).lower() == 'true'),
|
||||
),
|
||||
)
|
||||
|
||||
SOUND_VOLUME = Capability(
|
||||
href='/settings/sound/volume/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
NumberDesc(key='sound_volume', field='level',
|
||||
icon='mdi:volume-medium',
|
||||
entity_category='config',
|
||||
native_min_fn=lambda rep: int_or_none(rep.get('minLevel')) or 0,
|
||||
native_max_fn=lambda rep: int_or_none(rep.get('maxLevel')) or 0,
|
||||
step_fn=lambda rep: int_or_none(rep.get('resolution')) or 1,
|
||||
value_fn=int_or_none,
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['settings', 'sound', 'volume', 'vs', '0'],
|
||||
{'level': str(int(p))})),
|
||||
),
|
||||
)
|
||||
|
||||
# Last-pour statistics (issue #196). last.capacity's unit isn't confirmed
|
||||
# (no sibling unit field on this resource, unlike DISPENSE.dispense_capacity
|
||||
# which at least has an -- albeit suspect -- capacityUnit) so it's left
|
||||
# unitless rather than assumed to be mL.
|
||||
STATISTIC_POUR = Capability(
|
||||
href='/statistic/pour/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='last_pour_type', field='last.type',
|
||||
icon='mdi:cup-water', entity_category='diagnostic'),
|
||||
SensorDesc(key='last_pour_capacity', field='last.capacity',
|
||||
icon='mdi:cup-water', entity_category='diagnostic',
|
||||
value_fn=int_or_none),
|
||||
),
|
||||
)
|
||||
|
||||
LOCK = Capability(
|
||||
href='/status/lock/vs/0',
|
||||
poll_tier='warm',
|
||||
|
||||
@@ -73,6 +73,9 @@
|
||||
"pouring": {
|
||||
"name": "Pouring"
|
||||
},
|
||||
"alarm_in_mute": {
|
||||
"name": "Alarm in mute"
|
||||
},
|
||||
"power_state": {
|
||||
"name": "Power state"
|
||||
},
|
||||
@@ -490,6 +493,14 @@
|
||||
"buzzer": "Buzzer"
|
||||
}
|
||||
},
|
||||
"water_purifier_sound_mode": {
|
||||
"name": "Sound mode",
|
||||
"state": {
|
||||
"voice": "Voice",
|
||||
"fixedtone": "Fixed tone",
|
||||
"mute": "Mute"
|
||||
}
|
||||
},
|
||||
"spin_speed": {
|
||||
"name": "Spin speed",
|
||||
"state": {
|
||||
@@ -876,6 +887,15 @@
|
||||
"waterpurifier_status": {
|
||||
"name": "Status"
|
||||
},
|
||||
"cup_state": {
|
||||
"name": "Cup state"
|
||||
},
|
||||
"last_pour_type": {
|
||||
"name": "Last pour type"
|
||||
},
|
||||
"last_pour_capacity": {
|
||||
"name": "Last pour capacity"
|
||||
},
|
||||
"machine_state": {
|
||||
"name": "Machine state",
|
||||
"state": {
|
||||
|
||||
@@ -73,6 +73,9 @@
|
||||
"pouring": {
|
||||
"name": "Schenken"
|
||||
},
|
||||
"alarm_in_mute": {
|
||||
"name": "Alarm bij dempen"
|
||||
},
|
||||
"power_state": {
|
||||
"name": "Voedingsstatus"
|
||||
},
|
||||
@@ -490,6 +493,14 @@
|
||||
"buzzer": "Zoemer"
|
||||
}
|
||||
},
|
||||
"water_purifier_sound_mode": {
|
||||
"name": "Geluidsmodus",
|
||||
"state": {
|
||||
"voice": "Spraak",
|
||||
"fixedtone": "Vaste toon",
|
||||
"mute": "Dempen"
|
||||
}
|
||||
},
|
||||
"spin_speed": {
|
||||
"name": "Centrifugesnelheid",
|
||||
"state": {
|
||||
@@ -876,6 +887,15 @@
|
||||
"waterpurifier_status": {
|
||||
"name": "Status"
|
||||
},
|
||||
"cup_state": {
|
||||
"name": "Kopstatus"
|
||||
},
|
||||
"last_pour_type": {
|
||||
"name": "Laatste schenktype"
|
||||
},
|
||||
"last_pour_capacity": {
|
||||
"name": "Laatste schenkhoeveelheid"
|
||||
},
|
||||
"machine_state": {
|
||||
"name": "Machinestatus",
|
||||
"state": {
|
||||
|
||||
Reference in New Issue
Block a user