Merge pull request #350 from galaxysj/codex/fix-washer-course-enum-display
Fix AC setup timeout and verify appliance course mappings
This commit is contained in:
@@ -416,8 +416,12 @@ def enumerate_subdevices(
|
|||||||
# /subdevices/vs/0 and /device/<n> 404s; the only trace of the sibling
|
# /subdevices/vs/0 and /device/<n> 404s; the only trace of the sibling
|
||||||
# is a UUID-prefixed link in /oic/res (the x.com.samsung.da.multidevice
|
# is a UUID-prefixed link in /oic/res (the x.com.samsung.da.multidevice
|
||||||
# link). Its own tree answers a full Collection at /<uuid>/device/0,
|
# link). Its own tree answers a full Collection at /<uuid>/device/0,
|
||||||
# exactly Pattern B's transform, so every UUID path prefix seen in
|
# exactly Pattern B's transform, so a UUID prefix attached to that
|
||||||
# /oic/res is treated as a candidate. _probe_prefixed's probed_ids
|
# resource type is treated as a candidate. Other UUID-prefixed links are
|
||||||
|
# not evidence of a sibling: some single-unit AC boards advertise only
|
||||||
|
# per-prefix file-transfer resources, and probing those prefixes against
|
||||||
|
# every master href needlessly burns the setup timeout budget.
|
||||||
|
# _probe_prefixed's probed_ids
|
||||||
# guard (not a set difference against `listed`) is what keeps an id
|
# guard (not a set difference against `listed`) is what keeps an id
|
||||||
# already named by subdeviceIdList from being probed twice, since the
|
# already named by subdeviceIdList from being probed twice, since the
|
||||||
# two sources can disagree on case.
|
# two sources can disagree on case.
|
||||||
@@ -426,7 +430,7 @@ def enumerate_subdevices(
|
|||||||
m.group(1)
|
m.group(1)
|
||||||
for link in _iter_oic_res_hrefs(oic_res_links)
|
for link in _iter_oic_res_hrefs(oic_res_links)
|
||||||
for m in [_UUID_PREFIX_RE.match(link.get("href", ""))]
|
for m in [_UUID_PREFIX_RE.match(link.get("href", ""))]
|
||||||
if m
|
if m and "x.com.samsung.da.multidevice" in (link.get("rt") or ())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
for sub_id in linked:
|
for sub_id in linked:
|
||||||
|
|||||||
@@ -54,6 +54,45 @@ class TestCourseHelpers:
|
|||||||
assert laundry.cycle_options({}) == []
|
assert laundry.cycle_options({}) == []
|
||||||
assert laundry.cycle_options({"/wm/editcourse/vs/0": {}}) == []
|
assert laundry.cycle_options({"/wm/editcourse/vs/0": {}}) == []
|
||||||
|
|
||||||
|
def test_decodes_reported_washer_course_table_including_personal_slots(self):
|
||||||
|
"""Real DA_WM_TP1_21_COMMON diagnostics, with every selectable
|
||||||
|
standard and personal-course code preserved in the device's order.
|
||||||
|
F1/F3 are part of the packed list; their labels come from the
|
||||||
|
companion personal-course resource tested below.
|
||||||
|
"""
|
||||||
|
resources = {
|
||||||
|
"/wm/editcourse/vs/0": {
|
||||||
|
"x.com.samsung.da.editCourseList": (
|
||||||
|
"EditCourseList_696F73757801719688706D6A76726C6E6B777479F1F3"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert laundry.cycle_options(resources) == [
|
||||||
|
"69",
|
||||||
|
"6F",
|
||||||
|
"73",
|
||||||
|
"75",
|
||||||
|
"78",
|
||||||
|
"01",
|
||||||
|
"71",
|
||||||
|
"96",
|
||||||
|
"88",
|
||||||
|
"70",
|
||||||
|
"6D",
|
||||||
|
"6A",
|
||||||
|
"76",
|
||||||
|
"72",
|
||||||
|
"6C",
|
||||||
|
"6E",
|
||||||
|
"6B",
|
||||||
|
"77",
|
||||||
|
"74",
|
||||||
|
"79",
|
||||||
|
"F1",
|
||||||
|
"F3",
|
||||||
|
]
|
||||||
|
|
||||||
def test_option_value(self):
|
def test_option_value(self):
|
||||||
opts = ["DeviceType_0167", "Course_1C", "GMT_04"]
|
opts = ["DeviceType_0167", "Course_1C", "GMT_04"]
|
||||||
assert laundry.option_value(opts, "Course") == "1C"
|
assert laundry.option_value(opts, "Course") == "1C"
|
||||||
@@ -226,6 +265,37 @@ class TestCourseCodesFromSupportedOptions:
|
|||||||
"8F",
|
"8F",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_decodes_reported_dishwasher_course_table_without_editcourse(self):
|
||||||
|
"""Real DA_DW_A51_20_COMMON diagnostics: the board has no
|
||||||
|
/wm/editcourse/vs/0, so every selectable code must come from its
|
||||||
|
packed supportedOptions table, including the newer 82/8A/A7/A8/8C
|
||||||
|
family that previously appeared as raw UI text.
|
||||||
|
"""
|
||||||
|
resources = {
|
||||||
|
"/course/vs/0": {
|
||||||
|
"x.com.samsung.da.options": ["Course_82"],
|
||||||
|
"x.com.samsung.da.supportedOptions": [
|
||||||
|
"482E0026002F00270028AE0026002F0027002A7E0026002F00270028"
|
||||||
|
"0E0006000F0027000A8E0026002F002700288E0006000F00070008C"
|
||||||
|
"E0026002F00270028DE0026002F00270028EE0006000F00270028FE"
|
||||||
|
"0006002F0027002"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert laundry.cycle_options(resources) == [
|
||||||
|
"82",
|
||||||
|
"8A",
|
||||||
|
"A7",
|
||||||
|
"80",
|
||||||
|
"A8",
|
||||||
|
"88",
|
||||||
|
"8C",
|
||||||
|
"8D",
|
||||||
|
"8E",
|
||||||
|
"8F",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestCycleSelect:
|
class TestCycleSelect:
|
||||||
def test_builds_labelled_cycle_select(self):
|
def test_builds_labelled_cycle_select(self):
|
||||||
|
|||||||
@@ -625,7 +625,10 @@ def test_enumerate_prefixed_id_named_by_both_subdevice_id_list_and_oic_res_is_no
|
|||||||
{
|
{
|
||||||
"di": "a",
|
"di": "a",
|
||||||
"links": [
|
"links": [
|
||||||
{"href": f"/{_UUID}/multidevice/vs/0"},
|
{
|
||||||
|
"href": f"/{_UUID}/multidevice/vs/0",
|
||||||
|
"rt": ["x.com.samsung.da.multidevice"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -650,7 +653,10 @@ def test_enumerate_prefixed_id_case_mismatch_between_sources_is_not_duplicated()
|
|||||||
{
|
{
|
||||||
"di": "a",
|
"di": "a",
|
||||||
"links": [
|
"links": [
|
||||||
{"href": f"/{_UUID}/multidevice/vs/0"},
|
{
|
||||||
|
"href": f"/{_UUID}/multidevice/vs/0",
|
||||||
|
"rt": ["x.com.samsung.da.multidevice"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -666,6 +672,37 @@ def test_enumerate_prefixed_id_case_mismatch_between_sources_is_not_duplicated()
|
|||||||
assert subdevices[0].key.lower() == _UUID
|
assert subdevices[0].key.lower() == _UUID
|
||||||
|
|
||||||
|
|
||||||
|
def test_uuid_prefixed_file_links_do_not_trigger_flat_subdevice_probe():
|
||||||
|
"""A single-unit AC can advertise UUID-prefixed file-transfer links.
|
||||||
|
|
||||||
|
They identify a transfer namespace, not a sibling indoor unit. Treating
|
||||||
|
the prefix as Pattern C makes setup probe every master href under a UUID
|
||||||
|
that never answers, consuming the whole enumeration timeout budget.
|
||||||
|
"""
|
||||||
|
oic_res = [
|
||||||
|
{
|
||||||
|
"di": "a",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": f"/{_UUID}/file/transfer/vs/0",
|
||||||
|
"rt": ["x.com.samsung.file.transfer"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": f"/{_UUID}/file/list/vs/0",
|
||||||
|
"rt": ["x.com.samsung.file.list"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
sess = _FakeSession({})
|
||||||
|
probes: dict[str, bool] = {}
|
||||||
|
|
||||||
|
subdevices, _extra = enumerate_subdevices(sess, {}, oic_res, probe_log=probes.__setitem__)
|
||||||
|
|
||||||
|
assert subdevices == []
|
||||||
|
assert not any(href.startswith(f"/{_UUID}/") for href in probes)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# discover_partitioned
|
# discover_partitioned
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -190,6 +190,38 @@ def test_confirmed_washer_table_02_missing_course_names():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_reported_washer_standard_courses_all_have_table_02_labels():
|
||||||
|
"""Every non-personal code in the reported washer's live course list
|
||||||
|
must resolve through the Table_02 catalog instead of appearing as raw
|
||||||
|
text. F1/F3 deliberately come from device-provided personal names.
|
||||||
|
"""
|
||||||
|
states = _load("en")["entity"]["select"]["washer_cycle_table_02"]["state"]
|
||||||
|
reported = {
|
||||||
|
"69",
|
||||||
|
"6f",
|
||||||
|
"73",
|
||||||
|
"75",
|
||||||
|
"78",
|
||||||
|
"01",
|
||||||
|
"71",
|
||||||
|
"96",
|
||||||
|
"88",
|
||||||
|
"70",
|
||||||
|
"6d",
|
||||||
|
"6a",
|
||||||
|
"76",
|
||||||
|
"72",
|
||||||
|
"6c",
|
||||||
|
"6e",
|
||||||
|
"6b",
|
||||||
|
"77",
|
||||||
|
"74",
|
||||||
|
"79",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert reported <= states.keys()
|
||||||
|
|
||||||
|
|
||||||
def test_confirmed_dishwasher_course_names():
|
def test_confirmed_dishwasher_course_names():
|
||||||
states = _load("en")["entity"]["select"]["dishwasher_cycle"]["state"]
|
states = _load("en")["entity"]["select"]["dishwasher_cycle"]["state"]
|
||||||
assert {
|
assert {
|
||||||
|
|||||||
Reference in New Issue
Block a user