From 2b241522763f3a7c5e6bc41f11df73fe1e897ebd Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Wed, 29 Jul 2026 02:52:33 +0000 Subject: [PATCH] feat(registry): cover absence-power-saving and motion-detect-wind on AC (#173) Lennox-branded heat pump on the Samsung RAC board family (modelNum TP1X_LNX-AC-RAC-01001_0000) already routes correctly via the existing '-RAC-' token, but its dump has two resources no prior AC fixture carried: /mds/absencepowersaving/vs/0 and /option/motiondetectwind/stateful/vs/0. Bind both as read-only sensors, matching the CURRENT_LIMIT/ANOMALY_LOAD precedent -- nothing in the dump confirms write safety on live HVAC hardware. --- .../registry/by_type/airconditioner.py | 2 + .../registry/capabilities/airconditioner.py | 46 ++ .../localthings/translations/en.json | 21 + .../localthings/translations/nl.json | 21 + ...irconditioner_lnx_rac_heatpump_device.json | 626 ++++++++++++++++++ .../airconditioner_lnx_rac_heatpump.json | 24 + tests/test_airconditioner_capabilities.py | 59 ++ tests/test_golden_regression.py | 21 + 8 files changed, 820 insertions(+) create mode 100644 tests/fixtures/airconditioner_lnx_rac_heatpump_device.json create mode 100644 tests/fixtures/golden/airconditioner_lnx_rac_heatpump.json diff --git a/custom_components/localthings/registry/by_type/airconditioner.py b/custom_components/localthings/registry/by_type/airconditioner.py index 94e4797..4d61fff 100644 --- a/custom_components/localthings/registry/by_type/airconditioner.py +++ b/custom_components/localthings/registry/by_type/airconditioner.py @@ -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, diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index c9c8761..4c145d7 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -677,6 +677,52 @@ ANOMALY_LOAD = Capability( ), ) +# Absence-detection power-saving (issue #173, TP1X_LNX-AC-RAC-01001 -- +# Lennox-branded heat pump on the RAC board family): `status` toggles the +# feature, `switchPowerSaveMode` picks the save intensity out of its own +# supportedSwitchPowerSaveMode list. 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. Same 'don't guess' read-only treatment as CURRENT_LIMIT/ +# ANOMALY_LOAD above -- nothing here confirms write safety on live HVAC +# hardware. +ABSENCE_POWER_SAVING = Capability( + href='/mds/absencepowersaving/vs/0', + poll_tier='cold', + entities=( + BinarySensorDesc(key='absence_power_saving_active', field='status', + icon='mdi:human-greeting-proximity', + entity_category='diagnostic', + value_fn=lambda v: v == 'On'), + 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` toggles it, `modes` picks Direct/Indirect +# airflow out of `supportedModes`. Same read-only treatment. +MOTION_DETECT_WIND = Capability( + href='/option/motiondetectwind/stateful/vs/0', + poll_tier='cold', + entities=( + BinarySensorDesc(key='motion_detect_wind_active', field='status', + icon='mdi:motion-sensor', + entity_category='diagnostic', + value_fn=lambda v: v == 'On'), + 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- diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 6fb19fb..c8f573f 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -34,6 +34,12 @@ "overload_protection_active": { "name": "Overload protection active" }, + "absence_power_saving_active": { + "name": "Absence power saving active" + }, + "motion_detect_wind_active": { + "name": "Motion-detect wind avoidance active" + }, "cycle_active": { "name": "Running" }, @@ -669,6 +675,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" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index af8800b..93e497c 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -34,6 +34,12 @@ "overload_protection_active": { "name": "Overbelastingsbeveiliging actief" }, + "absence_power_saving_active": { + "name": "Energiebesparing bij afwezigheid actief" + }, + "motion_detect_wind_active": { + "name": "Bewegingsdetectie luchtstroomvermijding actief" + }, "cycle_active": { "name": "Actief" }, @@ -669,6 +675,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" }, diff --git a/tests/fixtures/airconditioner_lnx_rac_heatpump_device.json b/tests/fixtures/airconditioner_lnx_rac_heatpump_device.json new file mode 100644 index 0000000..1bdb5a9 --- /dev/null +++ b/tests/fixtures/airconditioner_lnx_rac_heatpump_device.json @@ -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" + } + } + ] +} diff --git a/tests/fixtures/golden/airconditioner_lnx_rac_heatpump.json b/tests/fixtures/golden/airconditioner_lnx_rac_heatpump.json new file mode 100644 index 0000000..7def68d --- /dev/null +++ b/tests/fixtures/golden/airconditioner_lnx_rac_heatpump.json @@ -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" + ] +} diff --git a/tests/test_airconditioner_capabilities.py b/tests/test_airconditioner_capabilities.py index c2dad10..a0d6987 100644 --- a/tests/test_airconditioner_capabilities.py +++ b/tests/test_airconditioner_capabilities.py @@ -467,6 +467,65 @@ 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_new_capabilities_are_read_only(): + """Nothing in the dump confirms write safety on live HVAC hardware for + either feature -- exposed as sensors, not switches/selects.""" + 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'} + for entity in (*airconditioner.ABSENCE_POWER_SAVING.entities, + *airconditioner.MOTION_DETECT_WIND.entities): + assert not hasattr(entity, 'write_fn') or entity.write_fn is None + + # --------------------------------------------------------------------------- # Additive entities layered on the ARTIK051_PRAC family on top of the upstream # registry: beep (Volume_* option), tropical night mode (Sleep_ option), diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index cffbcb3..209a1a2 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -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