Extend AirDresser support to DA_DF_TP2_20_COMMON (#157)
A different board generation from issue #162's DA_DF_A51_20_COMMON, also carrying the '_DF_' modelNum token and so already routed into the air_dresser registry -- but reporting two resources #162's board doesn't: /st/airdressercourse/vs/0 -- the course table id (Table_00), read the same way washer/dryer read /st/washercourse|dryercourse/vs/0. Wired up as AIR_DRESSER_COURSE's table_href and added to the global ignore list, mirroring that existing pair exactly. /airdresseroption/sanitize/vs/0 -- a genuine on/off setting (not covered by any existing capability), added as AIR_DRESSER_SANITIZE. Introducing table_href means the course select's translation_key is now always the table-lookup callable, so the bare 'air_dresser_cycle' catalog entry added for #162 (only ever used when no table_href was passed) is unreachable in every case and is removed -- both boards fall back to the shared 'cycle' entry until their course tables get named, same as washer/dryer's own precedent for an unrecognized table.
This commit is contained in:
+282
@@ -0,0 +1,282 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/airdresseroption/sanitize/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sanitize": "Off",
|
||||
"x.com.samsung.da.supportedSanitize": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "3171000000",
|
||||
"x.com.samsung.da.countryCode": "KR"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/course/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_015E",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"Course_22",
|
||||
"AiOption_On",
|
||||
"SteamPush_None",
|
||||
"SeamlessControl_Disable",
|
||||
"KidsLockBypass_On",
|
||||
"FilterUsage_384019D5",
|
||||
"WashingTimes_6",
|
||||
"DrumCleanProposal_30",
|
||||
"ProgressTimeSet_AA01A4AC00F0B20690",
|
||||
"WrinklePreventRunning_Off",
|
||||
"SendToDevice_Off",
|
||||
"WeatherPush_Off",
|
||||
"GMT_12",
|
||||
"Alarm_Off",
|
||||
"WrinklePreventSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0FF00F0F0F0F0F0F0F0F0F0F0FF0F0",
|
||||
"DelayEndSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0FF0F0F00F0F0FF0F0F0F0F00F0F",
|
||||
"EnergyLevelSet_0502010102010202030303010401040505050503030502040503030303030405",
|
||||
"UsagesDB_ok",
|
||||
"DrumCleanLog_2023-04-02T08:08:55|2023-10-02T03:38:19|2024-05-26T03:25:41|2024-10-01T01:07:20|2026-06-16T02:23:29"
|
||||
],
|
||||
"x.com.samsung.da.supportedOptions": [
|
||||
"12261062361020E61020961021261020C61021E61020B61021061020A61061461021361061661022462062562062F62062062040F62042761023061021561021A61021B61021C61022A61022B61022C61022D61022E6102076102086102"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cycleinterface/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cycleInterfaceEnabled": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePower": "63600",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.cumulativeDate": "1784404800",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1784372400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "DA_DF_TP2_20_COMMON|20286141|380101010015110F0201000100010000",
|
||||
"x.com.samsung.da.description": "DA_DF_TP2_20_COMMON_DF9500A/DC92-02888A_0002",
|
||||
"x.com.samsung.da.serialNum": "REDACTED",
|
||||
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "A00",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "DA_DF_TP2_20_COMMON|20286141|380101010015110F0201000100010000",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02673A250416(F822)",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Firmware_1_DB",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "21020115,23030652",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Firmware_2_DB",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "19111852,FFFFFFFF"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/0",
|
||||
"rep": {
|
||||
"currentMachineState": "REDACTED",
|
||||
"machineStates": "REDACTED",
|
||||
"jobStates": [
|
||||
"None",
|
||||
"Steaming",
|
||||
"Airwashing",
|
||||
"Drying",
|
||||
"Finish"
|
||||
],
|
||||
"currentJobState": "None",
|
||||
"remainingTime": "00:39:00",
|
||||
"progressPercentage": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.remainingTime": "00:39:00",
|
||||
"x.com.samsung.da.progressPercentage": "1",
|
||||
"x.com.samsung.da.progress": "None",
|
||||
"x.com.samsung.da.delayEndTime": "00:00:00",
|
||||
"x.com.samsung.da.supportedProgress": [
|
||||
"None",
|
||||
"Steaming",
|
||||
"Airwashing",
|
||||
"Drying",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.remoteControlEnabled": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/st/airdressercourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.st.airdresserMode": "Table_00_Course_22",
|
||||
"x.com.samsung.da.st.courseTable": "Table_00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/washer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.wrinklePrevent": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/editcourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.editCourseList": "EditCourseList_22230C10252F0F0E09121E0B0A14131624202730151A1B1C2A2B2C2D2E0708",
|
||||
"x.com.samsung.da.fixedCourseList": "FixedCourseList_22270F"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/jobbeginingstatus/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wm/personalcourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.courses": [
|
||||
"F1_00",
|
||||
"F2_00",
|
||||
"F3_00",
|
||||
"F4_00",
|
||||
"F5_00",
|
||||
"F6_00",
|
||||
"F7_00",
|
||||
"F8_00",
|
||||
"F9_00",
|
||||
"FA_00"
|
||||
],
|
||||
"x.com.samsung.da.maxCourseNum": "10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/setinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.isModelSettingWithoutSC": "false",
|
||||
"x.com.samsung.da.aiCourse": "false",
|
||||
"x.com.samsung.da.isModelSettingPowerOnOff": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/welcomemsg/vs/0",
|
||||
"rep": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"completion_minutes",
|
||||
"cycle",
|
||||
"cycle_active",
|
||||
"delay_start_hours",
|
||||
"diagnosis_status",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"job_beginning_status",
|
||||
"machine_state",
|
||||
"power_switch",
|
||||
"progress",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"sanitize",
|
||||
"wrinkle_prevent"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Tests for the DA_DF_TP2_20_COMMON AirDresser (model DF9500A, issue #157).
|
||||
|
||||
A different board generation than issue #162's DA_DF_A51_20_COMMON, routed
|
||||
into the same air_dresser registry (both carry the '_DF_' modelNum token).
|
||||
Exercises the two things this board does differently: a populated
|
||||
/wm/editcourse/vs/0 (so cycle_options() never needs the supportedOptions
|
||||
fallback #162 relies on) and the new /airdresseroption/sanitize/vs/0
|
||||
capability #162's dump doesn't report at all.
|
||||
"""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import air_dresser, for_device_by_model
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _air_dresser():
|
||||
resources = _load_device('air_dresser_tp2_20')
|
||||
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 _state():
|
||||
reg, resources = _air_dresser()
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def test_resolves_to_air_dresser_registry():
|
||||
reg, _ = _air_dresser()
|
||||
assert reg is not None and reg.name == 'air_dresser'
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
"""Confirms /st/airdressercourse/vs/0 and /airdresseroption/sanitize/vs/0
|
||||
-- the two hrefs this board reports that #162's dump doesn't -- are
|
||||
covered (ignored.py and AIR_DRESSER_SANITIZE respectively)."""
|
||||
reg, resources = _air_dresser()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_course_options_come_from_edit_course_list_not_the_fallback():
|
||||
"""Unlike issue #162's board, this one populates /wm/editcourse/vs/0
|
||||
directly, so cycle_options() should never reach the
|
||||
supportedOptions-decode fallback."""
|
||||
from custom_components.localthings.registry.capabilities.laundry import cycle_options
|
||||
_, resources = _air_dresser()
|
||||
codes = cycle_options(resources)
|
||||
assert codes[:3] == ['22', '23', '0C']
|
||||
assert codes == cycle_options({'/wm/editcourse/vs/0': resources['/wm/editcourse/vs/0']})
|
||||
|
||||
|
||||
def test_course_select_reads_current_selection():
|
||||
state = _state()
|
||||
assert state['cycle'] == '22'
|
||||
|
||||
|
||||
def test_course_translation_key_falls_back_to_cycle_for_unidentified_table():
|
||||
"""Table_00's course codes aren't identified yet, so the select's
|
||||
translation_key resolves to the generic 'cycle' catalog entry rather
|
||||
than a table-specific one that doesn't exist."""
|
||||
_, resources = _air_dresser()
|
||||
desc = air_dresser.REGISTRY.capabilities['/course/vs/0'][0].entities[0]
|
||||
assert desc.translation_key(resources) == 'cycle'
|
||||
|
||||
|
||||
def test_sanitize_present_and_toggles():
|
||||
state = _state()
|
||||
assert state['sanitize'] is False
|
||||
|
||||
desc = next(
|
||||
e for e in air_dresser.REGISTRY.capabilities['/airdresseroption/sanitize/vs/0'][0].entities
|
||||
if e.key == 'sanitize'
|
||||
)
|
||||
path, body = desc.write_fn('On', {})
|
||||
assert path == ['airdresseroption', 'sanitize', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.sanitize': 'On'}
|
||||
assert desc.write_fn('Sparkle', {}) is None
|
||||
@@ -747,6 +747,27 @@ def test_registry_reproduces_golden_state_keys_for_air_dresser():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_air_dresser_tp2_20():
|
||||
"""DA_DF_TP2_20_COMMON AirDresser (model DF9500A, issue #157) -- a
|
||||
different board generation than issue #162's DA_DF_A51_20_COMMON, also
|
||||
routed via the '_DF_' modelNum fallback into the same air_dresser
|
||||
registry. Unlike #162's board, this one populates /wm/editcourse/vs/0's
|
||||
editCourseList directly (no supportedOptions fallback needed) and
|
||||
reports two AirDresser-specific resources #162 doesn't have:
|
||||
/st/airdressercourse/vs/0 (course table id, ignored.py) and
|
||||
/airdresseroption/sanitize/vs/0 (air_dresser.AIR_DRESSER_SANITIZE).
|
||||
Binds cleanly with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('air_dresser_tp2_20')
|
||||
golden = json.loads((GOLDEN / 'air_dresser_tp2_20.json').read_text())
|
||||
state_keys = _new_state_keys('air_dresser_tp2_20', 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_resources_from_batch_preferred_over_flat():
|
||||
from tests.conftest import _resources_from_dump
|
||||
dump = {
|
||||
|
||||
Reference in New Issue
Block a user