Fix washer course enum display
This commit is contained in:
1 parent
93744e80f4
commit
f8849df8a6
12 files changed
+315
-19
No files matched your search
@@ -35,6 +35,54 @@ class TestCourseHelpers:
|
||||
assert laundry.option_value(opts, 'Course') == '1C'
|
||||
assert laundry.option_value(opts, 'Missing') is None
|
||||
|
||||
def test_decodes_device_provided_personal_course_names(self):
|
||||
"""Real /wm/personalcourse/vs/0 entries from the reported washer.
|
||||
|
||||
The first TLV field contains a byte-counted UTF-8 name. It is the only
|
||||
course-name metadata in the diagnostics and is safe to display without
|
||||
assigning an inferred meaning to any standard course code.
|
||||
"""
|
||||
resources = {
|
||||
'/wm/personalcourse/vs/0': {
|
||||
'x.com.samsung.da.courses': [
|
||||
'F1_0106EC868DEC98B7021EED8CACED8BB020EB93B1',
|
||||
'F2_00',
|
||||
'F3_0109EC9A94EAB8B0EBB3B40220ECA084EC9AA9',
|
||||
],
|
||||
},
|
||||
}
|
||||
first_name = bytes.fromhex('EC868DEC98B7').decode('utf-8')
|
||||
second_name = bytes.fromhex('EC9A94EAB8B0EBB3B4').decode('utf-8')
|
||||
assert laundry.personal_course_labels(resources) == {
|
||||
'F1': first_name,
|
||||
'F3': second_name,
|
||||
}
|
||||
|
||||
def test_personal_course_names_reject_malformed_payloads(self):
|
||||
resources = {
|
||||
'/wm/personalcourse/vs/0': {
|
||||
'x.com.samsung.da.courses': [
|
||||
'F1_not-hex',
|
||||
'F2_0106AA',
|
||||
'F3_020141',
|
||||
None,
|
||||
],
|
||||
},
|
||||
}
|
||||
assert laundry.personal_course_labels(resources) == {}
|
||||
|
||||
def test_washer_cycle_fallback_uses_labels_then_safe_unknown_text(self):
|
||||
resources = {
|
||||
'/wm/personalcourse/vs/0': {
|
||||
'x.com.samsung.da.courses': ['F1_0106EC868DEC98B7'],
|
||||
},
|
||||
}
|
||||
expected = bytes.fromhex('EC868DEC98B7').decode('utf-8')
|
||||
assert laundry.washer_cycle_fallback('F1', resources) == expected
|
||||
assert laundry.washer_cycle_fallback('69', resources) == 'Unknown (0x69)'
|
||||
assert laundry.washer_cycle_fallback('6f', resources) == 'Unknown (0x6F)'
|
||||
assert laundry.washer_cycle_fallback('Normal', resources) is None
|
||||
|
||||
|
||||
class TestCourseCodesFromSupportedOptions:
|
||||
"""cycle_options()'s fallback for boards that populate
|
||||
|
||||
@@ -45,3 +45,9 @@ def test_known_camel_case_state_uses_snake_case_translation_key():
|
||||
|
||||
def test_display_passes_through_non_string_values():
|
||||
assert _display(None, _UNTRANSLATED) is None
|
||||
|
||||
|
||||
def test_display_uses_fallback_when_translation_has_no_state_table():
|
||||
assert _display('69', 'cycle', lambda value: f'Unknown (0x{value})') == (
|
||||
'Unknown (0x69)'
|
||||
)
|
||||
@@ -2,6 +2,9 @@
|
||||
(custom_components/localthings/select.py) -- the static tuple, options_field,
|
||||
and callable forms of SelectDesc.options.
|
||||
"""
|
||||
from custom_components.localthings.registry.capabilities.laundry import (
|
||||
cycle_select, washer_cycle_fallback,
|
||||
)
|
||||
from custom_components.localthings.registry.capability import Capability
|
||||
from custom_components.localthings.registry.discovery import BoundEntity
|
||||
from custom_components.localthings.registry.entities import SelectDesc
|
||||
@@ -106,3 +109,78 @@ async def test_unknown_vendor_option_round_trips_to_exact_raw_value():
|
||||
assert entity.options[-1] == 'Future Vendor Mode'
|
||||
await entity.async_select_option('Future Vendor Mode')
|
||||
assert coordinator.writes == ['FutureVendorMode']
|
||||
|
||||
|
||||
async def test_washer_diagnostic_cycle_values_share_one_display_and_write_path():
|
||||
"""Regression for Course_69/EditCourseList_696F... from real hardware."""
|
||||
class _WritableCoordinator(_FakeCoordinator):
|
||||
data = {'cycle': '69'}
|
||||
|
||||
def __init__(self, last_resources):
|
||||
super().__init__(last_resources)
|
||||
self.writes = []
|
||||
|
||||
async def async_send_command(self, bound, value):
|
||||
self.writes.append(value)
|
||||
|
||||
desc = cycle_select(
|
||||
translation_key='washer_cycle',
|
||||
icon='mdi:washing-machine',
|
||||
table_href='/st/washercourse/vs/0',
|
||||
display_fn=washer_cycle_fallback,
|
||||
)
|
||||
capability = Capability(href='/course/vs/0', entities=(desc,))
|
||||
bound = BoundEntity(href='/course/vs/0', capability=capability, desc=desc)
|
||||
resources = {
|
||||
'/course/vs/0': {'x.com.samsung.da.options': ['Course_69']},
|
||||
'/st/washercourse/vs/0': {
|
||||
'x.com.samsung.da.st.courseTable': 'Table_02',
|
||||
},
|
||||
'/wm/editcourse/vs/0': {
|
||||
'x.com.samsung.da.editCourseList': (
|
||||
'EditCourseList_696F757801719688706D6A7376726C6E6B777479F1F3'
|
||||
),
|
||||
},
|
||||
'/wm/personalcourse/vs/0': {
|
||||
'x.com.samsung.da.courses': [
|
||||
'F1_0106EC868DEC98B7',
|
||||
'F3_0109EC9A94EAB8B0EBB3B4',
|
||||
],
|
||||
},
|
||||
}
|
||||
coordinator = _WritableCoordinator(resources)
|
||||
entity = LocalThingsSelect(coordinator, bound)
|
||||
first_name = bytes.fromhex('EC868DEC98B7').decode('utf-8')
|
||||
second_name = bytes.fromhex('EC9A94EAB8B0EBB3B4').decode('utf-8')
|
||||
|
||||
# Known catalog states stay as HA translation keys; the frontend renders
|
||||
# this confirmed Table_02 mapping as "AI Wash".
|
||||
assert entity.current_option == '69'
|
||||
assert entity.options == [
|
||||
'69',
|
||||
'6f',
|
||||
'75',
|
||||
'78',
|
||||
'01',
|
||||
'71',
|
||||
'96',
|
||||
'88',
|
||||
'70',
|
||||
'6d',
|
||||
'6a',
|
||||
'73',
|
||||
'76',
|
||||
'72',
|
||||
'6c',
|
||||
'6e',
|
||||
'6b',
|
||||
'77',
|
||||
'74',
|
||||
'79',
|
||||
first_name,
|
||||
second_name,
|
||||
]
|
||||
|
||||
await entity.async_select_option('6f')
|
||||
await entity.async_select_option(first_name)
|
||||
assert coordinator.writes == ['6F', 'F1']
|
||||
@@ -125,6 +125,33 @@ def test_no_catalog_carries_unresolved_core_references():
|
||||
assert unresolved == [], language
|
||||
|
||||
|
||||
def test_confirmed_korean_table_02_washer_course_names():
|
||||
states = _load('en')['entity']['select']['washer_cycle_table_02']['state']
|
||||
assert {code: states[code] for code in (
|
||||
'69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71',
|
||||
'72', '73', '74', '75', '76', '77', '78', '79', '88',
|
||||
)} == {
|
||||
'69': 'AI Wash',
|
||||
'6a': 'Wool',
|
||||
'6b': 'Denim',
|
||||
'6c': 'Blouses',
|
||||
'6d': 'Delicates',
|
||||
'6e': 'Active Wear',
|
||||
'6f': 'Bedding',
|
||||
'70': 'Towels',
|
||||
'71': 'Quick Wash',
|
||||
'72': 'Shirts',
|
||||
'73': 'Sanitize',
|
||||
'74': 'Drum Clean',
|
||||
'75': 'Outdoor',
|
||||
'76': 'Baby Care',
|
||||
'77': 'Cottons',
|
||||
'78': 'Rinse + Spin',
|
||||
'79': 'Spin Only',
|
||||
'88': 'Pet Care',
|
||||
}
|
||||
|
||||
|
||||
# The hood fan is its device's primary feature: fan.py sets _attr_name = None
|
||||
# so it presents as the device itself, and never reads a catalog name. Same
|
||||
# for the ARTIK051 air-purifier's airflow_fan (issue #56) -- ordered speed
|
||||
@@ -233,4 +260,3 @@ def test_every_kimchi_zone_supportmode_code_has_a_state_label():
|
||||
if code.lower() not in state_labels:
|
||||
missing.append((path.name, href, code))
|
||||
assert missing == []
|
||||
|
||||
@@ -82,6 +82,16 @@ class TestWasherCourse:
|
||||
rep = {'x.com.samsung.da.options': ['DeviceType_0167', 'Course_1C', 'GMT_04']}
|
||||
assert desc.rep_fn(rep) == '1C'
|
||||
|
||||
def test_reported_table_02_course_codes_are_translated(self):
|
||||
"""The owner confirmed this washer's newer Table_02 course family."""
|
||||
from custom_components.localthings.catalog import translated_states
|
||||
|
||||
confirmed = {
|
||||
'69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71',
|
||||
'72', '73', '74', '75', '76', '77', '78', '79', '88',
|
||||
}
|
||||
assert confirmed <= translated_states('select', 'washer_cycle_table_02')
|
||||
|
||||
def test_missing_course_option_returns_none(self):
|
||||
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == 'cycle')
|
||||
assert desc.rep_fn({'x.com.samsung.da.options': ['GMT_04']}) is None
|
||||
@@ -89,6 +99,7 @@ class TestWasherCourse:
|
||||
def test_cycle_desc_uses_cycle_options_callable(self):
|
||||
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == 'cycle')
|
||||
assert desc.options is laundry.cycle_options
|
||||
assert desc.display_fn is laundry.washer_cycle_fallback
|
||||
|
||||
def test_exists_only_when_edit_course_list_is_live(self):
|
||||
"""No hardcoded course table is kept -- the selector only appears
|
||||
|
||||
Reference in new issue
Block a user