Merge pull request #184 from mbillow/claude/issue-triage-429jtb

Issue triage: absence/motion AC sensors (#173), microwave reminders (#181), Python 3.13 pin
This commit is contained in:
Marc Billow
2026-07-28 22:37:55 -05:00
committed by GitHub
13 changed files with 989 additions and 0 deletions
@@ -27,6 +27,8 @@ REGISTRY = DeviceRegistry(
airconditioner.MUTE_ONCE,
airconditioner.CURRENT_LIMIT,
airconditioner.ANOMALY_LOAD,
airconditioner.ABSENCE_POWER_SAVING,
airconditioner.MOTION_DETECT_WIND,
airconditioner.CURRENT_TEMPERATURE,
airconditioner.CURRENT_TEMPERATURE_VS,
airconditioner.HUMIDITY,
@@ -677,6 +677,65 @@ ANOMALY_LOAD = Capability(
),
)
# Absence-detection power-saving (issue #173, TP1X_LNX-AC-RAC-01001 --
# Lennox-branded heat pump on the RAC board family): `status` is a bare
# On/Off boolean with no vendor prefix, the same shape already shipped
# writable elsewhere in this file (MUTE_ONCE, AUTO_CLEAN, AIR_PURIFY,
# DISPLAY_LIGHT) despite none of those having a live-confirmed write either
# -- worst case a wrong token no-ops, same risk profile as that family, so
# it's a switch rather than a sensor. `switchPowerSaveMode` picks the save
# intensity out of its own supportedSwitchPowerSaveMode list, but *what*
# writing it actually does to a running compressor isn't knowable from the
# dump -- same 'don't guess' read-only treatment as CURRENT_LIMIT/
# ANOMALY_LOAD's mode fields. A third field, `motionState`, also carries a
# supportedMotionState list but its role (a live sensor readout vs. a
# sensitivity setting) isn't distinguishable from the dump, so it's left
# unmodeled entirely.
ABSENCE_POWER_SAVING = Capability(
href='/mds/absencepowersaving/vs/0',
poll_tier='cold',
entities=(
SwitchDesc(key='absence_power_saving_active', field='status',
icon='mdi:human-greeting-proximity',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['mds', 'absencepowersaving', 'vs', '0'],
{'status': 'On' if p == 'On' else 'Off'})),
SensorDesc(key='absence_power_saving_mode', field='switchPowerSaveMode',
device_class='enum',
options=('eco', 'normal', 'comfort'),
translation_key='absence_power_saving_mode',
icon='mdi:leaf', entity_category='diagnostic',
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
),
)
# Avoid-direct-wind-on-motion, a sibling AI feature to ABSENCE_POWER_SAVING
# above on the same dump: `status` is the same bare On/Off shape, promoted to
# a switch for the same reason. `modes` (Direct/Indirect airflow out of
# `supportedModes`) stays read-only -- same reasoning as
# absence_power_saving_mode above.
MOTION_DETECT_WIND = Capability(
href='/option/motiondetectwind/stateful/vs/0',
poll_tier='cold',
entities=(
SwitchDesc(key='motion_detect_wind_active', field='status',
icon='mdi:motion-sensor',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['option', 'motiondetectwind', 'stateful', 'vs', '0'],
{'status': 'On' if p == 'On' else 'Off'})),
SensorDesc(key='motion_detect_wind_mode', field='modes',
device_class='enum',
options=('direct', 'indirect'),
translation_key='motion_detect_wind_mode',
icon='mdi:weather-windy', entity_category='diagnostic',
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
),
)
# The climate entity already surfaces current_temperature as a card
# attribute, but that's not enough for history graphs/automations/
# statistics -- issue #75 asked for a standalone sensor. Same OCF-standard-
@@ -27,6 +27,10 @@ different from an oven, and defined fresh here:
(a brightness level, not literally 'On') -- the switch now treats any
non-Off/non-None value as "on" for reads, and writes back 'High'/'Off'
(the two confirmed tokens) rather than the never-confirmed 'On'.
* Filter reminder / end signal reminder: bare 'FilterRemind'/'RemindBeep'
option-array tokens (issue #181), both with On and Off observed live
(issue #152's ME7500D fixtures) -- gated with exists_fn like Lamp since
the MW7300B combi dump has neither.
Note: cooking-mode writes are unproven here, same caveat as oven.py's
OVEN_MODE -- exposed as a SelectDesc for fidelity, first real-world write
@@ -145,6 +149,14 @@ def _lamp_exists(rep, resources):
return option_value(rep.get('x.com.samsung.da.options'), 'Lamp') is not None
def _filter_remind_exists(rep, resources):
return option_value(rep.get('x.com.samsung.da.options'), 'FilterRemind') is not None
def _remind_beep_exists(rep, resources):
return option_value(rep.get('x.com.samsung.da.options'), 'RemindBeep') is not None
def _lamp_write(p, rep, href=None):
if p not in ('On', 'Off'):
return None
@@ -159,6 +171,26 @@ def _lamp_write(p, rep, href=None):
}
def _filter_remind_write(p, rep, href=None):
if p not in ('On', 'Off'):
return None
if not rep.get('x.com.samsung.da.options'):
return None
return ['mode', 'vs', '0'], {
'x.com.samsung.da.options': option_write('FilterRemind', p),
}
def _remind_beep_write(p, rep, href=None):
if p not in ('On', 'Off'):
return None
if not rep.get('x.com.samsung.da.options'):
return None
return ['mode', 'vs', '0'], {
'x.com.samsung.da.options': option_write('RemindBeep', p),
}
# ---------------------------------------------------------------------------
# Capabilities
# ---------------------------------------------------------------------------
@@ -213,5 +245,22 @@ MICROWAVE_MODE = Capability(
exists_fn=_lamp_exists,
value_fn=lambda opts: option_value(opts, 'Lamp') not in (None, 'Off'),
write_fn=_lamp_write),
# issue #181: Filter Reminder / End Signal Reminder toggles,
# confirmed present (both On and Off observed across dumps -- see
# issue #152's ME7500D fixtures) but only on boards that carry the
# FilterRemind_*/RemindBeep_* tokens; gated off elsewhere (e.g. the
# MW7300B combi dump has neither) rather than assumed universal.
SwitchDesc(key='filter_remind', field='x.com.samsung.da.options',
icon='mdi:air-filter',
entity_category='config',
exists_fn=_filter_remind_exists,
value_fn=lambda opts: option_value(opts, 'FilterRemind') == 'On',
write_fn=_filter_remind_write),
SwitchDesc(key='remind_beep', field='x.com.samsung.da.options',
icon='mdi:bell-ring',
entity_category='config',
exists_fn=_remind_beep_exists,
value_fn=lambda opts: option_value(opts, 'RemindBeep') == 'On',
write_fn=_remind_beep_write),
),
)
@@ -669,6 +669,21 @@
"powersaving": "Power saving"
}
},
"absence_power_saving_mode": {
"name": "Absence power saving mode",
"state": {
"eco": "Eco",
"normal": "Normal",
"comfort": "Comfort"
}
},
"motion_detect_wind_mode": {
"name": "Motion-detect wind mode",
"state": {
"direct": "Direct",
"indirect": "Indirect"
}
},
"current_temp_c": {
"name": "Temperature"
},
@@ -898,6 +913,12 @@
"auto_clean": {
"name": "Auto clean"
},
"absence_power_saving_active": {
"name": "Absence power saving active"
},
"motion_detect_wind_active": {
"name": "Motion-detect wind avoidance active"
},
"beep": {
"name": "Beep"
},
@@ -964,6 +985,9 @@
"favorite_coffee_enabled": {
"name": "Favorite coffee"
},
"filter_remind": {
"name": "Filter reminder"
},
"fridge_sound": {
"name": "Sound"
},
@@ -1006,6 +1030,9 @@
"rapid_fridge": {
"name": "Rapid fridge"
},
"remind_beep": {
"name": "End signal reminder"
},
"sabbath_mode": {
"name": "Sabbath mode"
},
@@ -669,6 +669,21 @@
"powersaving": "Energiebesparing"
}
},
"absence_power_saving_mode": {
"name": "Modus energiebesparing bij afwezigheid",
"state": {
"eco": "Eco",
"normal": "Normaal",
"comfort": "Comfort"
}
},
"motion_detect_wind_mode": {
"name": "Modus luchtstroomvermijding",
"state": {
"direct": "Direct",
"indirect": "Indirect"
}
},
"current_temp_c": {
"name": "Temperatuur"
},
@@ -898,6 +913,12 @@
"auto_clean": {
"name": "Automatisch reinigen"
},
"absence_power_saving_active": {
"name": "Energiebesparing bij afwezigheid actief"
},
"motion_detect_wind_active": {
"name": "Bewegingsdetectie luchtstroomvermijding actief"
},
"beep": {
"name": "Piep"
},
@@ -964,6 +985,9 @@
"favorite_coffee_enabled": {
"name": "Favoriete koffie"
},
"filter_remind": {
"name": "Filterherinnering"
},
"fridge_sound": {
"name": "Geluid"
},
@@ -1006,6 +1030,9 @@
"rapid_fridge": {
"name": "Snelkoelen"
},
"remind_beep": {
"name": "Eindsignaal herinnering"
},
"sabbath_mode": {
"name": "Sabbatmodus"
},
+3
View File
@@ -1,2 +1,5 @@
[project]
requires-python = ">=3.13"
[tool.pytest.ini_options]
asyncio_mode = "auto"
@@ -0,0 +1,626 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/personality/presence/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "",
"x.com.samsung.da.deviceId": "**REDACTED**",
"x.com.samsung.da.value": ""
}
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "false",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "100",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterDesiredUsage": "500",
"x.com.samsung.da.filterStatus": "wash",
"x.com.samsung.da.filterCapacity": "500",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"replaceable",
"washable"
]
}
},
{
"href": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "1"
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Quiet",
"Smart",
"Speed",
"MotionIndirect",
"MotionDirect",
"Nano",
"NanoSleep"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "On",
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.supportedStatus": [
"Start",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"Off"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "3",
"x.com.samsung.da.supportedModes": [
"0",
"1",
"2",
"3",
"4"
],
"x.com.samsung.da.modesName": [
"Auto",
"Low",
"Mid",
"High",
"Turbo"
]
}
},
{
"href": "/wind/direction/vs/0",
"rep": {
"x.com.samsung.da.modes": "Fix",
"x.com.samsung.da.supportedModes": [
"Fix",
"Up_And_Low",
"Left_And_Right",
"All"
]
}
},
{
"href": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-28T19:55:08",
"x.com.samsung.da.state": "Deleted"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "FilterAlarm",
"x.com.samsung.da.triggeredTime": "2026-07-28T19:55:08",
"x.com.samsung.da.state": "Created"
}
]
}
},
{
"href": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Temperature",
"x.com.samsung.da.desired": "70.0",
"x.com.samsung.da.current": "69.0",
"x.com.samsung.da.maximum": "86",
"x.com.samsung.da.minimum": "47",
"x.com.samsung.da.increment": "1.0",
"x.com.samsung.da.unit": "Fahrenheit"
}
]
}
},
{
"href": "/temperature/current/0",
"rep": {
"range": [
47,
86
],
"units": "F",
"temperature": 69.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
47,
86
],
"units": "F",
"temperature": 70.0
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "0.000000",
"x.com.samsung.da.cumulativePower": "631483",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W"
}
},
{
"href": "/energy/consumption/0",
"rep": {
"power": 0.0
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Auto",
"Cool",
"Dry",
"Fan",
"Heat"
],
"x.com.samsung.da.modes": [
"Heat"
],
"x.com.samsung.da.options": [
"Sleep_0",
"ArtificialWorking_Off",
"ComfortAICooling_Off",
"AiTempChanged_Off",
"AiTemp_990",
"OutdoorTemp_78",
"CoolCapa_35",
"WarmCapa_35",
"Volume_Mute",
"StopAutoClean_Set",
"Autoclean_On",
"DiagnosisAI_Off",
"ProgressDiagnosisAI_0",
"ResultDiagnosisAI_Normal",
"KeyInputPermit_On",
"ModePermit_NoLimit",
"SmartCoolClean_Off",
"ProgressSmartClean_0",
"FreezeAlarmSetting_Off",
"DesiredFreezeAlarm_240",
"WashAlarm_Off",
"OptionCode_56636",
"ExtendOptionCode_246669",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"DurationOn_0",
"WelcomeCoolingState_Off"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off",
"operationNumber": "48"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/sensors/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_LNX-AC-RAC-01001_0000|10269441|60010523001811014E0048220090A000",
"x.com.samsung.da.description": "TP1X_LNX-AC-RAC-01001_0000",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJV",
"x.com.samsung.da.diagSetupid": "AR2",
"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": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02646A260327",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "102694A24062500",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "102472A24042300,102579A10000200",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "-03:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.airconOptionList": [
"SingleCommand_1",
"DR",
"HOMECARE_WIZARD_V2",
"PRODUCT_GLOBAL",
"AI_RAC_GLOBAL_HEATPUMP_3.0",
"AI_3.0",
"Auto_To_AI",
"AI_Heat"
]
}
},
{
"href": "/humidity/0",
"rep": {
"humidity": 0
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "0",
"x.com.samsung.da.fivepercentHumidity": "45"
}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 0,
"start": "1970-01-01T00:00:00Z",
"duration": 0,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.duration": "00:00:00",
"x.com.samsung.da.drlcStartTime": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "000001D6035C016102490C0F0000",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/remotedatacontrol/vs/0",
"rep": {
"x.com.samsung.da.status": "Off",
"x.com.samsung.da.connectionStatus": "Disconnected"
}
},
{
"href": "/remotetemperature/vs/0",
"rep": {
"x.com.samsung.da.temperature": "",
"x.com.samsung.da.unit": "",
"x.com.samsung.da.error": ""
}
},
{
"href": "/remotedeviceinfo/vs/0",
"rep": {
"x.com.samsung.da.didList": ""
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "noHistory",
"otnList": [
{
"type": "WIFI",
"modelId": "ARA-WW-TP1-24-ARXX00",
"versions": [
"11260327"
],
"visVersion": "260327"
},
{
"type": "Micom",
"modelId": "045210269441FFFFFFFF",
"versions": [
"24062500",
"FFFFFFFF"
],
"visVersion": "240625"
},
{
"type": "Micom",
"modelId": "04521024724110257941",
"versions": [
"24042300",
"10000200"
],
"visVersion": "240423"
}
]
}
},
{
"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"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "America/Halifax",
"offset": "-03:00",
"DST": "ON"
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"href": "/aisleep/vs/0",
"rep": {
"x.com.samsung.da.displayNightMode": "Off",
"x.com.samsung.da.elapsedTime": "0",
"x.com.samsung.da.requestFeedback": "Off",
"x.com.samsung.da.resultFeedback": "0",
"x.com.samsung.da.statusFeedback": "Idle",
"x.com.samsung.da.sleepTime": "14002200"
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "AEFFFFFFFF3D564156FFFF2F56FFFF00001F001F00010000001F0000009C00FFFF1E00",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/light/vs/0",
"rep": {
"mode": "On",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/mds/absencepowersaving/vs/0",
"rep": {
"status": "Off",
"modes": "Cooling_Heating",
"switchPowerSaveMode": "Normal",
"motionState": "Normal",
"supportedModes": [
"Cooling_Heating",
"Standby"
],
"supportedSwitchPowerSaveMode": [
"Eco",
"Normal",
"Comfort"
],
"supportedMotionState": [
"Normal",
"MotionNano",
"MotionSoftOff"
]
}
},
{
"href": "/mds/absencestate/vs/0",
"rep": {
"status": "Off",
"maxDetectCount": [
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254",
"254"
],
"supportedTime": [
"0",
"30",
"60"
]
}
},
{
"href": "/option/motiondetectwind/stateful/vs/0",
"rep": {
"status": "Off",
"modes": "Indirect",
"supportedModes": [
"Direct",
"Indirect"
]
}
},
{
"href": "/welcome/temperature/vs/0",
"rep": {
"operatingStatus": "None",
"requestId": "0000"
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "KG_IoT"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
@@ -0,0 +1,24 @@
{
"state_keys": [
"absence_power_saving_active",
"absence_power_saving_mode",
"air_filter_status",
"air_filter_usage",
"air_filter_usage_hours",
"alarm_code",
"auto_clean",
"beep",
"climate",
"current_temperature_c",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"motion_detect_wind_active",
"motion_detect_wind_mode",
"mute_once",
"power_watts",
"tropical_night_mode"
]
}
+2
View File
@@ -10,6 +10,7 @@
"door_open",
"energy_kwh",
"fan",
"filter_remind",
"finish_time",
"firmware_update",
"lamp",
@@ -17,6 +18,7 @@
"operation_time_minutes",
"power_level",
"progress_percentage",
"remind_beep",
"sound"
]
}
@@ -10,6 +10,7 @@
"door_open",
"energy_kwh",
"fan",
"filter_remind",
"finish_time",
"firmware_update",
"lamp",
@@ -17,6 +18,7 @@
"operation_time_minutes",
"power_level",
"progress_percentage",
"remind_beep",
"sound"
]
}
+81
View File
@@ -467,6 +467,87 @@ def test_fac_bora_subdevices_and_runningmode_are_ignored_not_guessed():
assert '/runn/vs/0' in airconditioner._AC_IGNORED
# ---------------------------------------------------------------------------
# TP1X_LNX-AC-RAC-01001_0000 -- Lennox-branded heat pump on the Samsung RAC
# board family (issue #173). Routes via the existing '-RAC-' modelNum token,
# same registry as the plain RAC family. Adds two AI-feature resources not
# seen on prior AC dumps: /mds/absencepowersaving/vs/0 (absence-detection
# power saving) and /option/motiondetectwind/stateful/vs/0 (avoid-direct-
# wind-on-motion) -- both exposed read-only, same 'don't guess' precedent as
# CURRENT_LIMIT/ANOMALY_LOAD.
# ---------------------------------------------------------------------------
def _ac_lnx_rac_heatpump():
resources = _load_device('airconditioner_lnx_rac_heatpump')
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 test_lnx_rac_heatpump_resolves_to_airconditioner_registry():
reg, _ = _ac_lnx_rac_heatpump()
assert reg is not None and reg.name == 'airconditioner'
def test_lnx_rac_heatpump_no_unbound_hrefs():
reg, resources = _ac_lnx_rac_heatpump()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_lnx_rac_heatpump_absence_power_saving_state():
reg, resources = _ac_lnx_rac_heatpump()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
state = flatten(bound, resources)
assert state['absence_power_saving_active'] is False
assert state['absence_power_saving_mode'] == 'normal'
def test_lnx_rac_heatpump_motion_detect_wind_state():
reg, resources = _ac_lnx_rac_heatpump()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
state = flatten(bound, resources)
assert state['motion_detect_wind_active'] is False
assert state['motion_detect_wind_mode'] == 'indirect'
def test_lnx_rac_heatpump_enable_switches_are_writable():
"""status is a bare On/Off boolean, same shape already shipped writable
elsewhere in this file (MUTE_ONCE, AUTO_CLEAN) -- worst case a wrong
token no-ops. The paired mode selects stay read-only sensors."""
absence_keys = {e.key for e in airconditioner.ABSENCE_POWER_SAVING.entities}
motion_keys = {e.key for e in airconditioner.MOTION_DETECT_WIND.entities}
assert absence_keys == {'absence_power_saving_active', 'absence_power_saving_mode'}
assert motion_keys == {'motion_detect_wind_active', 'motion_detect_wind_mode'}
absence_mode = next(e for e in airconditioner.ABSENCE_POWER_SAVING.entities
if e.key == 'absence_power_saving_mode')
motion_mode = next(e for e in airconditioner.MOTION_DETECT_WIND.entities
if e.key == 'motion_detect_wind_mode')
assert not hasattr(absence_mode, 'write_fn') or absence_mode.write_fn is None
assert not hasattr(motion_mode, 'write_fn') or motion_mode.write_fn is None
def test_lnx_rac_heatpump_absence_power_saving_write_target():
write = next(e for e in airconditioner.ABSENCE_POWER_SAVING.entities
if e.key == 'absence_power_saving_active').write_fn
assert write('On', {}) == (
['mds', 'absencepowersaving', 'vs', '0'], {'status': 'On'})
assert write('Off', {}) == (
['mds', 'absencepowersaving', 'vs', '0'], {'status': 'Off'})
def test_lnx_rac_heatpump_motion_detect_wind_write_target():
write = next(e for e in airconditioner.MOTION_DETECT_WIND.entities
if e.key == 'motion_detect_wind_active').write_fn
assert write('On', {}) == (
['option', 'motiondetectwind', 'stateful', 'vs', '0'], {'status': 'On'})
assert write('Off', {}) == (
['option', 'motiondetectwind', 'stateful', 'vs', '0'], {'status': 'Off'})
# ---------------------------------------------------------------------------
# Additive entities layered on the ARTIK051_PRAC family on top of the upstream
# registry: beep (Volume_* option), tropical night mode (Sleep_<N> option),
+21
View File
@@ -439,6 +439,27 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_windfree():
)
def test_registry_reproduces_golden_state_keys_for_airconditioner_lnx_rac_heatpump():
"""Lennox-branded heat pump on the Samsung RAC board family (issue #173,
modelNum TP1X_LNX-AC-RAC-01001_0000) -- routes via the existing '-RAC-'
token, same registry as the plain RAC family. Adds two AI-feature
resources not seen on prior AC dumps: /mds/absencepowersaving/vs/0
(absence-detection power saving) and /option/motiondetectwind/stateful/vs/0
(avoid-direct-wind-on-motion), both exposed read-only per the 'don't
guess' rule."""
from tests.conftest import _load_device
resources = _load_device('airconditioner_lnx_rac_heatpump')
golden = json.loads(
(GOLDEN / 'airconditioner_lnx_rac_heatpump.json').read_text()
)
state_keys = _new_state_keys('airconditioner_lnx_rac_heatpump', 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_range():
"""Range/cooktop-oven combo (model TP1X_DA-KS-RANGE-0102X, issue #44) --
reports no oneUiVersion; resolved via the '-RANGE-' modelNum token
+66
View File
@@ -217,3 +217,69 @@ def test_lamp_reads_any_non_off_level_as_true():
just a literal 'On'."""
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
assert desc.value_fn(['Lamp_High']) is True
# ---------------------------------------------------------------------------
# MICROWAVE_MODE — filter_remind/remind_beep options-array writes (issue #181)
# ---------------------------------------------------------------------------
def test_filter_remind_gated_absent_when_no_option():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
rep = {'x.com.samsung.da.options': ['DeviceType_MW7300B-/EU1', 'Sound_Off']}
assert desc.exists_fn(rep, {}) is False
def test_filter_remind_gated_present_when_option_reported():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
rep = {'x.com.samsung.da.options': ['FilterRemind_Off']}
assert desc.exists_fn(rep, {}) is True
def test_filter_remind_reads_on_off():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
assert desc.value_fn(['FilterRemind_On']) is True
assert desc.value_fn(['FilterRemind_Off']) is False
def test_filter_remind_write_is_single_token():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
rep = {'x.com.samsung.da.options': ['FilterRemind_Off']}
path, body = desc.write_fn('On', rep)
assert path == ['mode', 'vs', '0']
assert body == {'x.com.samsung.da.options': ['FilterRemind_On']}
def test_filter_remind_write_requires_existing_options():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
assert desc.write_fn('On', {}) is None
def test_remind_beep_gated_absent_when_no_option():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
rep = {'x.com.samsung.da.options': ['DeviceType_MW7300B-/EU1', 'Sound_Off']}
assert desc.exists_fn(rep, {}) is False
def test_remind_beep_gated_present_when_option_reported():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
rep = {'x.com.samsung.da.options': ['RemindBeep_On']}
assert desc.exists_fn(rep, {}) is True
def test_remind_beep_reads_on_off():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
assert desc.value_fn(['RemindBeep_On']) is True
assert desc.value_fn(['RemindBeep_Off']) is False
def test_remind_beep_write_is_single_token():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
rep = {'x.com.samsung.da.options': ['RemindBeep_On']}
path, body = desc.write_fn('Off', rep)
assert path == ['mode', 'vs', '0']
assert body == {'x.com.samsung.da.options': ['RemindBeep_Off']}
def test_remind_beep_write_requires_existing_options():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
assert desc.write_fn('On', {}) is None