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:
@@ -16,6 +16,7 @@ REGISTRY = DeviceRegistry(
|
||||
*common.POWER,
|
||||
air_dresser.AIR_DRESSER_SETTINGS,
|
||||
air_dresser.AIR_DRESSER_COURSE,
|
||||
air_dresser.AIR_DRESSER_SANITIZE,
|
||||
laundry.JOB_BEGINNING_STATUS,
|
||||
operational.OPERATIONAL_STATE,
|
||||
dishwasher.DIAGNOSIS,
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
"""Capabilities specific to the AirDresser family (Samsung DA_DF_A51-class,
|
||||
issue #162).
|
||||
"""Capabilities specific to the AirDresser family (Samsung DA_DF-class,
|
||||
issues #162/#157).
|
||||
|
||||
This board reports no oneUiVersion and no /information/vs/0 token any
|
||||
existing family routes on, so it gets its own device type -- but most of
|
||||
the resources it exposes are already handled by the shared laundry
|
||||
This board family reports no oneUiVersion and no /information/vs/0 token
|
||||
any existing family routes on, so it gets its own device type -- but most
|
||||
of the resources it exposes are already handled by the shared laundry
|
||||
machinery:
|
||||
|
||||
/washer/vs/0 -> AIR_DRESSER_SETTINGS (wrinkle_prevent only -- a
|
||||
dedicated capability rather than reusing
|
||||
dryer.DRYER_SETTINGS wholesale: this board's dump
|
||||
never populates dryLevel/dryTime/dryerType at all,
|
||||
and those are permanently-empty-not-just-absent
|
||||
dryer.DRYER_SETTINGS wholesale: this board never
|
||||
populates dryLevel/dryTime/dryerType at all, and
|
||||
those are permanently-empty-not-just-absent
|
||||
dryer-only fields on an AirDresser, not merely unset
|
||||
ones, so binding them here would ship three sensors
|
||||
that can never read anything on this device type)
|
||||
/course/vs/0 -> AIR_DRESSER_COURSE (cycle select). This board has no
|
||||
/wm/editcourse/vs/0 at all, so cycle_options() falls
|
||||
through entirely to its supportedOptions fallback --
|
||||
confirmed against the issue #162 dump: header nibble
|
||||
'0' + 10 self-indexed one-byte records
|
||||
(01/02/04/03/05/1A/1B/1C/07/08), all distinct, current
|
||||
selection 'Course_01' among them. Course names aren't
|
||||
identified yet (no code->name mapping was reported),
|
||||
so they render as their raw codes until named in
|
||||
translations, same as dryer.py's unidentified codes.
|
||||
/course/vs/0 -> AIR_DRESSER_COURSE (cycle select), table id from
|
||||
/st/airdressercourse/vs/0 (issue #157's
|
||||
DA_DF_TP2_20_COMMON reports "Table_00"; issue #162's
|
||||
DA_DF_A51_20_COMMON has no table resource at all and
|
||||
falls back to the name-only 'cycle' key, same as an
|
||||
unrecognized table on washer/dryer). Options come
|
||||
from laundry.cycle_options -- #157's board populates
|
||||
/wm/editcourse/vs/0's editCourseList directly; #162's
|
||||
has no editcourse resource at all and falls through
|
||||
to cycle_options' supportedOptions decode instead.
|
||||
Course names aren't identified yet for either table
|
||||
(no code->name mapping was reported), so they render
|
||||
as their raw codes until named in translations, same
|
||||
as dryer.py's unidentified codes.
|
||||
/diagnosis/vs/0 -> reuses dishwasher.DIAGNOSIS
|
||||
/airdresseroption/sanitize/vs/0 -> AIR_DRESSER_SANITIZE (issue #157 only;
|
||||
#162's board doesn't report this resource at all)
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import SwitchDesc
|
||||
@@ -37,6 +43,12 @@ def _wrinkle_write(p, rep, href=None):
|
||||
return ['washer', 'vs', '0'], {'x.com.samsung.da.wrinklePrevent': p}
|
||||
|
||||
|
||||
def _sanitize_write(p, rep, href=None):
|
||||
if p not in ('On', 'Off'):
|
||||
return None
|
||||
return ['airdresseroption', 'sanitize', 'vs', '0'], {'x.com.samsung.da.sanitize': p}
|
||||
|
||||
|
||||
AIR_DRESSER_SETTINGS = Capability(
|
||||
href='/washer/vs/0',
|
||||
poll_tier='warm',
|
||||
@@ -51,6 +63,18 @@ AIR_DRESSER_SETTINGS = Capability(
|
||||
AIR_DRESSER_COURSE = Capability(
|
||||
href='/course/vs/0',
|
||||
entities=(
|
||||
cycle_select(translation_key='air_dresser_cycle', icon='mdi:tshirt-crew'),
|
||||
cycle_select(translation_key='air_dresser_cycle', icon='mdi:tshirt-crew',
|
||||
table_href='/st/airdressercourse/vs/0'),
|
||||
),
|
||||
)
|
||||
|
||||
AIR_DRESSER_SANITIZE = Capability(
|
||||
href='/airdresseroption/sanitize/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SwitchDesc(key='sanitize', field='x.com.samsung.da.sanitize',
|
||||
icon='mdi:weather-sunny',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=_sanitize_write),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -107,6 +107,11 @@ IGNORED: list[Capability] = [
|
||||
# exposed by dryer.DRYER_COURSE at /course/vs/0
|
||||
# (x.com.samsung.da.st.dryerMode is "Table_03_Course_<same hex code>").
|
||||
Capability(href='/st/dryercourse/vs/0'),
|
||||
# AirDresser counterpart of the above (issue #157): read only for its
|
||||
# courseTable id (air_dresser.AIR_DRESSER_COURSE's table_href), no
|
||||
# entity of its own -- same "no entity, just the table id" role as
|
||||
# /st/washercourse/vs/0 and /st/dryercourse/vs/0.
|
||||
Capability(href='/st/airdressercourse/vs/0'),
|
||||
# Empty on every washer dump seen so far.
|
||||
Capability(href='/wm/welcomemsg/vs/0'),
|
||||
# User-saved custom course slots (F1-FA). No controllable/observable
|
||||
|
||||
@@ -193,9 +193,6 @@
|
||||
"ai_energy_level": {
|
||||
"name": "AI Energy Mode level"
|
||||
},
|
||||
"air_dresser_cycle": {
|
||||
"name": "Cycle"
|
||||
},
|
||||
"beverage_zone_mode": {
|
||||
"name": "Beverage zone mode",
|
||||
"state": {
|
||||
|
||||
@@ -193,9 +193,6 @@
|
||||
"ai_energy_level": {
|
||||
"name": "Niveau AI Energy Mode"
|
||||
},
|
||||
"air_dresser_cycle": {
|
||||
"name": "Cyclus"
|
||||
},
|
||||
"beverage_zone_mode": {
|
||||
"name": "Modus drankenzone",
|
||||
"state": {
|
||||
|
||||
+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