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
@@ -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
|
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)
|
with another (a family token, or one short enough to collide by accident)
|
||||||
would break that property; see this table's comment.
|
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)
|
key = _BOARD_TOKEN_TO_KEY.get(token)
|
||||||
if key is not None:
|
if key is not None:
|
||||||
return key
|
return key
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ REGISTRY = DeviceRegistry(
|
|||||||
water_purifier.FAVORITE_HOTWATER,
|
water_purifier.FAVORITE_HOTWATER,
|
||||||
water_purifier.COFFEE,
|
water_purifier.COFFEE,
|
||||||
water_purifier.LOCK,
|
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,
|
*water_purifier.COVERAGE,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
"""Capabilities for the Samsung water-purifier family (TP2X_WATERPURIFIER-class,
|
"""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 ..capability import Capability
|
||||||
from ..entities import BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc
|
from ..entities import BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||||
from .common import int_or_none, parse_iso_utc as _parse_iso_utc
|
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
|
# Only a handful of discrete temperatures are selectable (not a
|
||||||
# continuous range) -- a select over the live-reported set, not a
|
# continuous range) -- a select over the live-reported set, not a
|
||||||
# number with invented bounds.
|
# 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',
|
SelectDesc(key='hot_water_temperature', field='x.com.samsung.da.tempDesiredHotWater',
|
||||||
icon='mdi:thermometer',
|
icon='mdi:thermometer',
|
||||||
entity_category='config',
|
entity_category='config',
|
||||||
options_field='x.com.samsung.da.supportedHotTemperatures',
|
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: (
|
write_fn=lambda p, rep, href=None: (
|
||||||
['setting', 'waterpurifier', 'vs', '0'],
|
['setting', 'waterpurifier', 'vs', '0'],
|
||||||
{'x.com.samsung.da.tempDesiredHotWater': p})),
|
{'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(
|
LOCK = Capability(
|
||||||
href='/status/lock/vs/0',
|
href='/status/lock/vs/0',
|
||||||
poll_tier='warm',
|
poll_tier='warm',
|
||||||
|
|||||||
@@ -73,6 +73,9 @@
|
|||||||
"pouring": {
|
"pouring": {
|
||||||
"name": "Pouring"
|
"name": "Pouring"
|
||||||
},
|
},
|
||||||
|
"alarm_in_mute": {
|
||||||
|
"name": "Alarm in mute"
|
||||||
|
},
|
||||||
"power_state": {
|
"power_state": {
|
||||||
"name": "Power state"
|
"name": "Power state"
|
||||||
},
|
},
|
||||||
@@ -490,6 +493,14 @@
|
|||||||
"buzzer": "Buzzer"
|
"buzzer": "Buzzer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"water_purifier_sound_mode": {
|
||||||
|
"name": "Sound mode",
|
||||||
|
"state": {
|
||||||
|
"voice": "Voice",
|
||||||
|
"fixedtone": "Fixed tone",
|
||||||
|
"mute": "Mute"
|
||||||
|
}
|
||||||
|
},
|
||||||
"spin_speed": {
|
"spin_speed": {
|
||||||
"name": "Spin speed",
|
"name": "Spin speed",
|
||||||
"state": {
|
"state": {
|
||||||
@@ -876,6 +887,15 @@
|
|||||||
"waterpurifier_status": {
|
"waterpurifier_status": {
|
||||||
"name": "Status"
|
"name": "Status"
|
||||||
},
|
},
|
||||||
|
"cup_state": {
|
||||||
|
"name": "Cup state"
|
||||||
|
},
|
||||||
|
"last_pour_type": {
|
||||||
|
"name": "Last pour type"
|
||||||
|
},
|
||||||
|
"last_pour_capacity": {
|
||||||
|
"name": "Last pour capacity"
|
||||||
|
},
|
||||||
"machine_state": {
|
"machine_state": {
|
||||||
"name": "Machine state",
|
"name": "Machine state",
|
||||||
"state": {
|
"state": {
|
||||||
|
|||||||
@@ -73,6 +73,9 @@
|
|||||||
"pouring": {
|
"pouring": {
|
||||||
"name": "Schenken"
|
"name": "Schenken"
|
||||||
},
|
},
|
||||||
|
"alarm_in_mute": {
|
||||||
|
"name": "Alarm bij dempen"
|
||||||
|
},
|
||||||
"power_state": {
|
"power_state": {
|
||||||
"name": "Voedingsstatus"
|
"name": "Voedingsstatus"
|
||||||
},
|
},
|
||||||
@@ -490,6 +493,14 @@
|
|||||||
"buzzer": "Zoemer"
|
"buzzer": "Zoemer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"water_purifier_sound_mode": {
|
||||||
|
"name": "Geluidsmodus",
|
||||||
|
"state": {
|
||||||
|
"voice": "Spraak",
|
||||||
|
"fixedtone": "Vaste toon",
|
||||||
|
"mute": "Dempen"
|
||||||
|
}
|
||||||
|
},
|
||||||
"spin_speed": {
|
"spin_speed": {
|
||||||
"name": "Centrifugesnelheid",
|
"name": "Centrifugesnelheid",
|
||||||
"state": {
|
"state": {
|
||||||
@@ -876,6 +887,15 @@
|
|||||||
"waterpurifier_status": {
|
"waterpurifier_status": {
|
||||||
"name": "Status"
|
"name": "Status"
|
||||||
},
|
},
|
||||||
|
"cup_state": {
|
||||||
|
"name": "Kopstatus"
|
||||||
|
},
|
||||||
|
"last_pour_type": {
|
||||||
|
"name": "Laatste schenktype"
|
||||||
|
},
|
||||||
|
"last_pour_capacity": {
|
||||||
|
"name": "Laatste schenkhoeveelheid"
|
||||||
|
},
|
||||||
"machine_state": {
|
"machine_state": {
|
||||||
"name": "Machinestatus",
|
"name": "Machinestatus",
|
||||||
"state": {
|
"state": {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"alarm_code",
|
||||||
|
"alarm_in_mute",
|
||||||
|
"buzz_lock",
|
||||||
|
"coffee_brew_status",
|
||||||
|
"coldwater_lock",
|
||||||
|
"cup_state",
|
||||||
|
"dispense_capacity",
|
||||||
|
"dispense_type",
|
||||||
|
"favorite_capacity",
|
||||||
|
"favorite_capacity_enabled",
|
||||||
|
"favorite_coffee_enabled",
|
||||||
|
"favorite_hotwater_temperature",
|
||||||
|
"filter_clean_remain_time",
|
||||||
|
"filter_door_status",
|
||||||
|
"filter_status",
|
||||||
|
"filter_usage",
|
||||||
|
"firmware_update",
|
||||||
|
"hotwater_lock",
|
||||||
|
"last_pour_capacity",
|
||||||
|
"last_pour_type",
|
||||||
|
"pouring",
|
||||||
|
"selfcheck_error",
|
||||||
|
"selfcheck_result",
|
||||||
|
"selfcheck_status",
|
||||||
|
"sound_mode",
|
||||||
|
"sound_output",
|
||||||
|
"sound_volume",
|
||||||
|
"sterilize_last_time",
|
||||||
|
"sterilize_period",
|
||||||
|
"sterilize_plan_time",
|
||||||
|
"sterilize_run_time",
|
||||||
|
"waterpurifier_status"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
"filter_status",
|
"filter_status",
|
||||||
"filter_usage",
|
"filter_usage",
|
||||||
"firmware_update",
|
"firmware_update",
|
||||||
"hot_water_temperature",
|
|
||||||
"hotwater_lock",
|
"hotwater_lock",
|
||||||
"pouring",
|
"pouring",
|
||||||
"selfcheck_error",
|
"selfcheck_error",
|
||||||
|
|||||||
@@ -0,0 +1,590 @@
|
|||||||
|
{
|
||||||
|
"device0": [
|
||||||
|
{
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.devcol",
|
||||||
|
"oic.wk.col"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.ll",
|
||||||
|
"oic.if.b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/selfcheck/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedActions": [
|
||||||
|
"Start"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.result": "Success",
|
||||||
|
"x.com.samsung.da.error": [
|
||||||
|
"ErrorCode_None"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.status": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/realtimenotiforclient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.timeforshortnoti": "0",
|
||||||
|
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/filter/waterfilter/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.filterUsage": "69",
|
||||||
|
"x.com.samsung.da.filterUsageResolution": "1",
|
||||||
|
"x.com.samsung.da.filterResetType": [
|
||||||
|
"replaceable"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.lastResetDate": "2025-11-19T05:39:17",
|
||||||
|
"x.com.samsung.da.filterStatus": "normal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/alarms/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "AILITE_DA-REF-WATERPURIFIER-01011|70674641|900100000219130081088700001E0000",
|
||||||
|
"x.com.samsung.da.description": "AILITE_WATERPURIFIER_25K",
|
||||||
|
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.diagDumpType": "file",
|
||||||
|
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||||
|
"x.com.samsung.da.diagLogType": [
|
||||||
|
"errCode",
|
||||||
|
"dump"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagMnid": "0AJT",
|
||||||
|
"x.com.samsung.da.diagSetupid": "RX0",
|
||||||
|
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||||
|
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||||
|
"x.com.samsung.da.diagTsId": "DA01",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "WiFi Module",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "251121",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Micom",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "25071603, 2510140E, 22092302, FFFFFFFF, 25061700, FFFFFFFF",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "2",
|
||||||
|
"x.com.samsung.da.description": "Bixby",
|
||||||
|
"x.com.samsung.da.type": "Bixby",
|
||||||
|
"x.com.samsung.da.number": "1.2.2-rc.3",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/runningmode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.runningMode": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/setting/waterpurifier/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedTypes": [
|
||||||
|
"ambientwater",
|
||||||
|
"coldwater",
|
||||||
|
"hotwater"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.desiredType": "coldwater",
|
||||||
|
"x.com.samsung.da.tempDesiredHotWater": "85",
|
||||||
|
"x.com.samsung.da.desiredCapacity": "120",
|
||||||
|
"x.com.samsung.da.hotwaterLevel": "5",
|
||||||
|
"x.com.samsung.da.hotwaterRange": [
|
||||||
|
"40",
|
||||||
|
"90"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.tempUnit": "C",
|
||||||
|
"x.com.samsung.da.pourStatus": "Off",
|
||||||
|
"x.com.samsung.da.capacityUnit": "C",
|
||||||
|
"x.com.samsung.da.capacityResolution": "10",
|
||||||
|
"x.com.samsung.da.desiredCapacityRange": [
|
||||||
|
"50",
|
||||||
|
"2000"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.triggerTime": "1785341896789"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/favorite/capacity/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.switchCapacity": "On",
|
||||||
|
"x.com.samsung.da.defaultCapacity": "120",
|
||||||
|
"x.com.samsung.da.capacityList": [
|
||||||
|
"80",
|
||||||
|
"120",
|
||||||
|
"260",
|
||||||
|
"500",
|
||||||
|
"9999"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.triggerTime": "1785341867176"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/status/waterpurifier/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.filterCleanMaxTime": "14",
|
||||||
|
"x.com.samsung.da.sterilizeMaxTime": "10",
|
||||||
|
"x.com.samsung.da.sterilizeRunTime": "0",
|
||||||
|
"x.com.samsung.da.status": "Ready",
|
||||||
|
"x.com.samsung.da.sterilizePlanTime": "2026-07-31T17:00:00",
|
||||||
|
"x.com.samsung.da.sterilizeLastTime": "2026-07-28T17:08:36",
|
||||||
|
"x.com.samsung.da.sterilizeStartTime": "2026-07-31T17:00:00",
|
||||||
|
"x.com.samsung.da.sterilizePeriod": "3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/status/lock/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.coldwaterLock": "Unlocked",
|
||||||
|
"x.com.samsung.da.device.microphone": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/favorite/hotwater/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.favorite.revision": "0",
|
||||||
|
"x.com.samsung.da.favorite.defaultTemperature": "85",
|
||||||
|
"x.com.samsung.da.favorite.showList": [
|
||||||
|
"40",
|
||||||
|
"50",
|
||||||
|
"75",
|
||||||
|
"85",
|
||||||
|
"90"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.favorite.supportedList": [
|
||||||
|
"40",
|
||||||
|
"75",
|
||||||
|
"85",
|
||||||
|
"90"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.switchHotwater": "Unlocked",
|
||||||
|
"x.com.samsung.da.triggerTime": "1785341867177"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/settings/sound/volume/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"level": "5",
|
||||||
|
"minLevel": "0",
|
||||||
|
"maxLevel": "15",
|
||||||
|
"resolution": "5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/settings/sound/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"mode": "voice",
|
||||||
|
"supportedModes": [
|
||||||
|
"voice",
|
||||||
|
"fixedTone",
|
||||||
|
"mute"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/settings/sound/output/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"deviceType": "speaker",
|
||||||
|
"alarmInMute": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/setting/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.setLanguage": "ko_KR",
|
||||||
|
"x.com.samsung.da.supportedSetLanguage": [
|
||||||
|
"ko_KR"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/automation/waterpurifier/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"automation.supported.modes": [
|
||||||
|
"0"
|
||||||
|
],
|
||||||
|
"automation.supported.options": [
|
||||||
|
"capacity"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/favorite/coffee/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"favorite.activate": "On",
|
||||||
|
"brew.status": "Suspend",
|
||||||
|
"favorite.enable.list": [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/brand/recipe/info/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"revision": "0",
|
||||||
|
"brand.num.max": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/recipe/coffee/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/coffee/custom/recipe/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"publisher": "0",
|
||||||
|
"cups": 1,
|
||||||
|
"favor": "223",
|
||||||
|
"numberOfStages": "2",
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"temperature": 0,
|
||||||
|
"waterAmount": 30,
|
||||||
|
"timeSecond": 20,
|
||||||
|
"turnOver": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"temperature": 0,
|
||||||
|
"waterAmount": 30,
|
||||||
|
"timeSecond": 20,
|
||||||
|
"turnOver": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"publisher.support": [
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/recipe/coffee/deletion/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/cup/state/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"water.cup.state": "UnReady",
|
||||||
|
"water.cup.basis.revision": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/statistic/pour/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"last.type": "coldwater",
|
||||||
|
"last.capacity": "40"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false",
|
||||||
|
"otnStatus": "None",
|
||||||
|
"flashingProgress": "0",
|
||||||
|
"otnCompleteDate": "noHistory",
|
||||||
|
"scheduledTime": "None",
|
||||||
|
"swVersionInfo": {
|
||||||
|
"platform": "Tizen Lite",
|
||||||
|
"oneUiVersion": "7.0 Water purifier",
|
||||||
|
"osVersion": "5.0"
|
||||||
|
},
|
||||||
|
"otnList": [
|
||||||
|
{
|
||||||
|
"type": "WIFI",
|
||||||
|
"modelId": "A-WPWW-AIL-25-COMMON",
|
||||||
|
"versions": [
|
||||||
|
"20251121"
|
||||||
|
],
|
||||||
|
"visVersion": "251121"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "838070674741FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"2510140E",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "251014"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "838070674641FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"25071603",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "250716"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "03807067464170674741",
|
||||||
|
"versions": [
|
||||||
|
"25071603",
|
||||||
|
"2510140E"
|
||||||
|
],
|
||||||
|
"visVersion": "251014"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "038000156141FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"22092302",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "220923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "038070673941FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"25061700",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "250617"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/timezone/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"timezoneid": "Asia/Seoul",
|
||||||
|
"offset": "+09:00",
|
||||||
|
"DST": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/connectionconfig/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"autoReconnectionMinVersion": "1.0",
|
||||||
|
"autoReconnection": "true",
|
||||||
|
"autoReconnectionProtocolType": [
|
||||||
|
"helper_hotspot",
|
||||||
|
"ble_ocf"
|
||||||
|
],
|
||||||
|
"supportedWiFiAuthType": [
|
||||||
|
"OPEN",
|
||||||
|
"WEP",
|
||||||
|
"WPA-PSK",
|
||||||
|
"WPA2-PSK",
|
||||||
|
"SAE"
|
||||||
|
],
|
||||||
|
"supportedWiFiCryptoType": [
|
||||||
|
"TKIP",
|
||||||
|
"AES",
|
||||||
|
"WEP-64",
|
||||||
|
"WEP-128"
|
||||||
|
],
|
||||||
|
"supportedWiFiFreq": [
|
||||||
|
"2.4G",
|
||||||
|
"5G"
|
||||||
|
],
|
||||||
|
"calmConnectionCare": {
|
||||||
|
"version": "1.0",
|
||||||
|
"role": [
|
||||||
|
"things"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/bixbyuservalidate/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.loginId": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.isExpired": "false"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/bixby/status/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.enable": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/bixbyterms/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.accountInfo": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.termsName": "AudioReview",
|
||||||
|
"x.com.samsung.da.termsStatus": "Agreed",
|
||||||
|
"x.com.samsung.da.termsType": "Optional",
|
||||||
|
"x.com.samsung.da.termsURL": "https://d264isyiyrfhr3.cloudfront.net/storage/tos/kor/1.0.1/1773704680415/kor/kor_kor_AudioRecordingReview.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.accountInfo": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.termsName": "Datasharing",
|
||||||
|
"x.com.samsung.da.termsStatus": "Agreed",
|
||||||
|
"x.com.samsung.da.termsType": "Mandatory",
|
||||||
|
"x.com.samsung.da.termsURL": "https://d264isyiyrfhr3.cloudfront.net/storage/tos/kor/1.0.1/1690269284216/kor/kor_kor_DataSharingAgreement_v3.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.accountInfo": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.termsName": "Personalization",
|
||||||
|
"x.com.samsung.da.termsStatus": "Agreed",
|
||||||
|
"x.com.samsung.da.termsType": "Optional",
|
||||||
|
"x.com.samsung.da.termsURL": "https://d264isyiyrfhr3.cloudfront.net/storage/tos/kor/1.0.2/1773644167282/kor/kor_kor_PersonalizationAgreement.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.accountInfo": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.termsName": "PrivacyNotice",
|
||||||
|
"x.com.samsung.da.termsStatus": "Agreed",
|
||||||
|
"x.com.samsung.da.termsType": "Mandatory",
|
||||||
|
"x.com.samsung.da.termsURL": "https://d264isyiyrfhr3.cloudfront.net/storage/tos/kor/1.0.2/1726806774110/kor/kor_kor_pn.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.accountInfo": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.termsName": "PrivacyPolicy",
|
||||||
|
"x.com.samsung.da.termsStatus": "Agreed",
|
||||||
|
"x.com.samsung.da.termsType": "Mandatory",
|
||||||
|
"x.com.samsung.da.termsURL": "https://d264isyiyrfhr3.cloudfront.net/storage/tos/kor/1.7.8/1783668586319/kor/kor_kor_pp.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.accountInfo": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.termsName": "SamsungLocation",
|
||||||
|
"x.com.samsung.da.termsStatus": "Agreed",
|
||||||
|
"x.com.samsung.da.termsType": "Mandatory",
|
||||||
|
"x.com.samsung.da.termsURL": "https://static.bada.com/contents/legal/kor/kor/locationinfortnc.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.accountInfo": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.termsName": "FMMPrivacyPolicy",
|
||||||
|
"x.com.samsung.da.termsStatus": "Agreed",
|
||||||
|
"x.com.samsung.da.termsType": "Mandatory",
|
||||||
|
"x.com.samsung.da.termsURL": "https://account.samsung.com/membership/policy/privacy/service/o12ht525e6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.accountInfo": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.termsName": "BixbyPrivacy",
|
||||||
|
"x.com.samsung.da.termsStatus": "NotOnboarding",
|
||||||
|
"x.com.samsung.da.termsType": "Optional",
|
||||||
|
"x.com.samsung.da.termsURL": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.lastCheckedTime": "1785341867"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/bixby/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.voiceWakeup": "Disable",
|
||||||
|
"x.com.samsung.da.multidevWakeup": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/sec/networkaudio/audio/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"volume": "4",
|
||||||
|
"mute": "false",
|
||||||
|
"maxVolume": "15",
|
||||||
|
"timestamp": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/voice/feature/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"voice.feature": {
|
||||||
|
"ContinuousConversation": {
|
||||||
|
"support": "true",
|
||||||
|
"value": "false",
|
||||||
|
"supportedValues": [
|
||||||
|
"true",
|
||||||
|
"false"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"DiscoverBixby": {
|
||||||
|
"support": "false"
|
||||||
|
},
|
||||||
|
"LLMBixby": {
|
||||||
|
"support": "true"
|
||||||
|
},
|
||||||
|
"SimpleCommand": {
|
||||||
|
"support": "false",
|
||||||
|
"value": "false",
|
||||||
|
"supportedValues": [
|
||||||
|
"true",
|
||||||
|
"false"
|
||||||
|
],
|
||||||
|
"description": [
|
||||||
|
"\ub0c9\uc7a5\uace0 \ubb38 \uc5f4\uc5b4\uc918"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/voice/provisioning/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"voice.provisioning.headers": {
|
||||||
|
"login_id": "**REDACTED**",
|
||||||
|
"voice.provisioning.headers.bixbyLanguage": "ko_KR",
|
||||||
|
"voice.provisioning.headers.bixbyType": "BixbyAgent",
|
||||||
|
"voice.provisioning.headers.bixbyVersion": "1.2.2-rc.3",
|
||||||
|
"voice.provisioning.headers.companioncountry": "KR",
|
||||||
|
"voice.provisioning.headers.csc": "920",
|
||||||
|
"voice.provisioning.headers.csciso": "KR",
|
||||||
|
"voice.provisioning.headers.deviceLanguage": "ko-KR",
|
||||||
|
"voice.provisioning.headers.deviceModelNameAsh": "RWP70F15ANW",
|
||||||
|
"voice.provisioning.headers.deviceModelNamePro": "BIXBY-LITE",
|
||||||
|
"voice.provisioning.headers.deviceSubType": "screenless_waterpurifier_lite",
|
||||||
|
"voice.provisioning.headers.deviceTypeAsh": "appliance",
|
||||||
|
"voice.provisioning.headers.deviceTypePro": "appliance",
|
||||||
|
"voice.provisioning.headers.mcc": "450",
|
||||||
|
"voice.provisioning.headers.osType": "Tizen Lite",
|
||||||
|
"voice.provisioning.headers.osVersion": "Tizen Lite",
|
||||||
|
"voice.provisioning.headers.requestTrackId": "1785341867293",
|
||||||
|
"voice.provisioning.headers.sspServerUrl": "us-auth2.samsungosp.com",
|
||||||
|
"voice.provisioning.headers.storecountry": "KR",
|
||||||
|
"voice.provisioning.headers.userType": "COMMERCIAL"
|
||||||
|
},
|
||||||
|
"voice.provisioning.termsstatechange": {
|
||||||
|
"voice.provisioning.termsstatechange.refresh": "",
|
||||||
|
"voice.provisioning.termsstatechange.state": "Agreed",
|
||||||
|
"voice.provisioning.termsstatechange.terms": ""
|
||||||
|
},
|
||||||
|
"voice.provisioning.deviceinfo": {
|
||||||
|
"voice.provisioning.deviceinfo.accesstoken": "**REDACTED**",
|
||||||
|
"voice.provisioning.deviceinfo.bixbyserver": "bxb3PrdUS",
|
||||||
|
"voice.provisioning.deviceinfo.bixbyserviceid": "US1DAAPPFD31232A4E4B40D6B83630AF58385719",
|
||||||
|
"voice.provisioning.deviceinfo.deviceid": "**REDACTED**",
|
||||||
|
"voice.provisioning.deviceinfo.gdprcountry": "false",
|
||||||
|
"voice.provisioning.deviceinfo.isSupportedCountry": "true",
|
||||||
|
"voice.provisioning.deviceinfo.userid": "**REDACTED**"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+36
-1
@@ -277,6 +277,33 @@ class TestForDeviceByModel:
|
|||||||
assert reg is not None
|
assert reg is not None
|
||||||
assert reg.name == 'water_purifier'
|
assert reg.name == 'water_purifier'
|
||||||
|
|
||||||
|
def test_water_purifier_wins_over_ref_when_both_tokens_present(self):
|
||||||
|
"""Issue #196: an AILITE water purifier (RWP70F15ANW) spells its
|
||||||
|
modelNum '...-REF-WATERPURIFIER-...', which would otherwise match the
|
||||||
|
bare 'REF' board token (refrigerator) before ever reaching
|
||||||
|
'WATERPURIFIER' -- misrouting it to the refrigerator registry, whose
|
||||||
|
resource surface shares almost nothing with a water purifier."""
|
||||||
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||||
|
reg = for_device_by_model(
|
||||||
|
'AILITE_DA-REF-WATERPURIFIER-01011|70674641|'
|
||||||
|
'900100000219130081088700001E0000',
|
||||||
|
'AILITE_WATERPURIFIER_25K',
|
||||||
|
)
|
||||||
|
assert reg is not None
|
||||||
|
assert reg.name == 'water_purifier'
|
||||||
|
|
||||||
|
def test_bare_ref_token_still_resolves_refrigerator(self):
|
||||||
|
"""Regression guard for the #196 carve-out above: a genuine
|
||||||
|
refrigerator modelNum with no 'WATERPURIFIER' token must still
|
||||||
|
resolve to 'refrigerator'."""
|
||||||
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||||
|
reg = for_device_by_model(
|
||||||
|
'TP1X_REF_21K|00175941|00050126001811344100000020090000',
|
||||||
|
'TP1X_REF_21K',
|
||||||
|
)
|
||||||
|
assert reg is not None
|
||||||
|
assert reg.name == 'refrigerator'
|
||||||
|
|
||||||
def test_airconditioner_via_wac_token(self):
|
def test_airconditioner_via_wac_token(self):
|
||||||
"""Issue #87: a Bespoke Window AC (AW06C7155EWAZ) reports no
|
"""Issue #87: a Bespoke Window AC (AW06C7155EWAZ) reports no
|
||||||
oneUiVersion and a modelNum carrying the '_WAC_' (Window Air
|
oneUiVersion and a modelNum carrying the '_WAC_' (Window Air
|
||||||
@@ -487,6 +514,14 @@ class TestBoardTokenAmbiguity:
|
|||||||
safe while no real model string contains two tokens naming different
|
safe while no real model string contains two tokens naming different
|
||||||
device types. Guard that against every dump we have."""
|
device types. Guard that against every dump we have."""
|
||||||
|
|
||||||
|
# Issue #196: AILITE water-purifier boards spell their modelNum
|
||||||
|
# '...-REF-WATERPURIFIER-...' -- 'REF' names the shared cooling-subsystem
|
||||||
|
# board, not a refrigerator. `_board_family_key` carries an explicit
|
||||||
|
# carve-out resolving this one pair to 'water_purifier'; this is the one
|
||||||
|
# documented exception to the "no two board tokens ever co-occur"
|
||||||
|
# invariant the rest of this test enforces.
|
||||||
|
_ALLOWED_CONFLICTS = {frozenset({'refrigerator', 'water_purifier'})}
|
||||||
|
|
||||||
def test_no_fixture_model_string_yields_two_conflicting_keys(self, all_device_fixtures):
|
def test_no_fixture_model_string_yields_two_conflicting_keys(self, all_device_fixtures):
|
||||||
from custom_components.localthings.registry.by_type import (
|
from custom_components.localthings.registry.by_type import (
|
||||||
_BOARD_TOKEN_TO_KEY, _board_tokens,
|
_BOARD_TOKEN_TO_KEY, _board_tokens,
|
||||||
@@ -502,7 +537,7 @@ class TestBoardTokenAmbiguity:
|
|||||||
for t in _board_tokens(field, cut)
|
for t in _board_tokens(field, cut)
|
||||||
if t in _BOARD_TOKEN_TO_KEY
|
if t in _BOARD_TOKEN_TO_KEY
|
||||||
}
|
}
|
||||||
assert len(keys) <= 1, (
|
assert len(keys) <= 1 or frozenset(keys) in self._ALLOWED_CONFLICTS, (
|
||||||
f"{name}: {field!r} matches conflicting board tokens {keys}"
|
f"{name}: {field!r} matches conflicting board tokens {keys}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -138,10 +138,37 @@ def test_registry_reproduces_golden_state_keys_for_water_purifier():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_reproduces_golden_state_keys_for_water_purifier_ailite_25k():
|
||||||
|
"""AILITE_WATERPURIFIER_25K (issue #196, RWP70F15ANW) -- a coffee-capable
|
||||||
|
water purifier on an AILITE_DA-REF-WATERPURIFIER board, whose modelNum's
|
||||||
|
'REF' token would otherwise misroute it to the refrigerator registry (see
|
||||||
|
TestBoardTokenAmbiguity's 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, and a hot_water_temperature select that must gate off
|
||||||
|
(no supportedHotTemperatures reported) rather than surface as 'unknown'."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('water_purifier_ailite_25k')
|
||||||
|
golden = json.loads((GOLDEN / 'water_purifier_ailite_25k.json').read_text())
|
||||||
|
state_keys = _new_state_keys('water_purifier_ailite_25k', resources)
|
||||||
|
assert set(state_keys) == set(golden['state_keys']), (
|
||||||
|
f"state_keys mismatch:\n"
|
||||||
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||||
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_registry_reproduces_golden_state_keys_for_water_purifier_coffee():
|
def test_registry_reproduces_golden_state_keys_for_water_purifier_coffee():
|
||||||
"""TP2X_WATERPURIFIER_20K coffee-capable variant (issue #107) adds
|
"""TP2X_WATERPURIFIER_20K coffee-capable variant (issue #107) adds
|
||||||
/favorite/coffee/vs/0, /favorite/hotwater/vs/0, and three static
|
/favorite/coffee/vs/0, /favorite/hotwater/vs/0, and three static
|
||||||
coffee-recipe resources not present in issue #90's original dump."""
|
coffee-recipe resources not present in issue #90's original dump.
|
||||||
|
|
||||||
|
This fixture's /setting/waterpurifier/vs/0 turns out to report no
|
||||||
|
supportedHotTemperatures either (only hotwaterLevel/hotwaterRange, the
|
||||||
|
same shape issue #196 surfaced) -- so hot_water_temperature dropped out
|
||||||
|
of this golden when the #196 fix (see water_purifier.DISPENSE's
|
||||||
|
exists_fn) landed. This fixture was quietly hitting the same 'unknown'
|
||||||
|
bug all along; the registry-level golden just had no way to show it
|
||||||
|
since flatten()'s state dict doesn't model select option membership."""
|
||||||
from tests.conftest import _load_device
|
from tests.conftest import _load_device
|
||||||
resources = _load_device('water_purifier_coffee')
|
resources = _load_device('water_purifier_coffee')
|
||||||
golden = json.loads((GOLDEN / 'water_purifier_coffee.json').read_text())
|
golden = json.loads((GOLDEN / 'water_purifier_coffee.json').read_text())
|
||||||
|
|||||||
@@ -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',
|
for href in ('/brand/recipe/info/vs/0', '/coffee/custom/recipe/vs/0',
|
||||||
'/recipe/coffee/vs/0', '/recipe/coffee/deletion/vs/0'):
|
'/recipe/coffee/vs/0', '/recipe/coffee/deletion/vs/0'):
|
||||||
assert href in ignored_hrefs, href
|
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