diff --git a/custom_components/localthings/registry/by_type/__init__.py b/custom_components/localthings/registry/by_type/__init__.py index a8ae11f..2dcd79b 100644 --- a/custom_components/localthings/registry/by_type/__init__.py +++ b/custom_components/localthings/registry/by_type/__init__.py @@ -96,4 +96,9 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi # a modelNum carrying the '_PRAC_' (Package Room Air Conditioner) token. if key is None and '_PRAC_' in (model_num or ''): key = 'airconditioner' + # Older/simpler RAC boards (e.g. TP2X_RAC_20K, issue #37) use the plain + # '_RAC_' token instead -- distinct from '_PRAC_' above (no overlap: the + # 'P' sits between the underscore and 'RAC' in that token). + if key is None and '_RAC_' in (model_num or ''): + key = 'airconditioner' return _REGISTRY_BY_KEY.get(key) if key else None diff --git a/custom_components/localthings/registry/by_type/airconditioner.py b/custom_components/localthings/registry/by_type/airconditioner.py index a7cbf36..0d5f050 100644 --- a/custom_components/localthings/registry/by_type/airconditioner.py +++ b/custom_components/localthings/registry/by_type/airconditioner.py @@ -19,12 +19,15 @@ REGISTRY = DeviceRegistry( common.ALARMS, common.ENERGY_METER, fridge.FIRMWARE_UPDATE, + fridge.SELF_CHECK, dishwasher.DIAGNOSIS, airconditioner.CLIMATE, airconditioner.AIR_PURIFY, airconditioner.AUTO_CLEAN, airconditioner.AIR_FILTER, airconditioner.DISPLAY_LIGHT, + airconditioner.MUTE_ONCE, + airconditioner.CURRENT_LIMIT, *airconditioner.COVERAGE, ]), ) diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index 0a43e3c..bfcb374 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -15,7 +15,7 @@ different schema (see capabilities/__init__.py). They live only in the AC by_type registry. """ from ..capability import Capability -from ..entities import ClimateDesc, SensorDesc, SwitchDesc +from ..entities import BinarySensorDesc, ClimateDesc, SensorDesc, SwitchDesc # --------------------------------------------------------------------------- # Canonical AC resource hrefs. The climate entity (climate.py) binds the @@ -162,6 +162,43 @@ DISPLAY_LIGHT = Capability( ), ) +# Confirmed against issue #38's dump (TP1X_DA-AC-RAC-01001_0000): a single +# boolean field, no vendor prefix, mirroring the On/Off convention used +# throughout the rest of this API. +MUTE_ONCE = Capability( + href='/option/muteonce/vs/0', + poll_tier='warm', + entities=( + SwitchDesc(key='mute_once', field='muteonce', + name='Mute once', icon='mdi:volume-mute', + entity_category='config', + value_fn=lambda v: v == 'On', + write_fn=lambda p, rep, href=None: ( + ['option', 'muteonce', 'vs', '0'], + {'muteonce': 'On' if p == 'On' else 'Off'})), + ), +) + +# Circuit-breaker current-limit setting (issue #38, TP1X board): `operation` +# toggles the limiter and `modes` picks a level out of `supportedModes` +# (seen as '3'..'9'). No vendor field-name prefix and no unit/label in the +# dump to confirm what the levels mean (amps vs. an abstract tier) -- +# exposed read-only per the 'don't guess' rule rather than risking an +# unverified write to live HVAC hardware. +CURRENT_LIMIT = Capability( + href='/electriccurrent/vs/0', + poll_tier='cold', + entities=( + BinarySensorDesc(key='current_limit_enabled', field='operation', + name='Current limit enabled', icon='mdi:current-ac', + entity_category='diagnostic', + value_fn=lambda v: v == 'On'), + SensorDesc(key='current_limit_level', field='modes', + name='Current limit level', icon='mdi:current-ac', + entity_category='diagnostic'), + ), +) + # --------------------------------------------------------------------------- # AC-scoped coverage: the CLIMATE_CONSUMED_HREFS above (read by the climate # entity) plus vendor duplicates / all-zero-ambiguous / plumbing resources. @@ -178,11 +215,12 @@ _AC_IGNORED = [ '/humidity/vs/0', # Presence-personalization plumbing (empty item list here). '/personality/presence/vs/0', - # --- TP1X-class (oneUiVersion "7.0 Air conditioner") housekeeping / opaque - # blobs. These carry no user-actionable state or no documented write - # contract, so per the 'don't guess' rule they are ignored rather than - # modeled. (Temperature for this class lives at /temperatures/vs/0, now - # consumed by the climate entity -- see CLIMATE_CONSUMED_HREFS.) + # --- TP1X/TP2X-class housekeeping / opaque blobs. These carry no + # user-actionable state or no documented write contract, so per the + # 'don't guess' rule they are ignored rather than modeled. + # /option/muteonce/vs/0 and /selfcheck/vs/0 are deliberately NOT here -- + # see MUTE_ONCE and fridge.SELF_CHECK above/in the by_type registry, + # both of which have a confirmed, cleanly modelable contract. '/airlevelcheck/vs/0', # periodic air-quality sensing scheduler plumbing '/aisleep/vs/0', # AI-sleep feedback state (no actionable control) '/availablecontrolsets/vs/0', # opaque hex-encoded control-set bitmap @@ -190,11 +228,9 @@ _AC_IGNORED = [ '/keepnormalstate/vs/0', # internal keep-normal flag '/mds/absencemonitoring/vs/0', # motion-detection sensor plumbing (empty here) '/mds/absencestate/vs/0', # motion-detection state (empty here) - '/option/muteonce/vs/0', # one-shot buzzer mute (no persistent state) '/remotedatacontrol/vs/0', # remote data-control session status '/remotetemperature/vs/0', # external temp-sensor feed (unset on this unit) '/reserverulesets/vs/0', # opaque hex-encoded schedule reservation blob - '/selfcheck/vs/0', # self-diagnosis action (start/cancel, no simple state) '/welcome/temperature/vs/0', # welcome-cooling plumbing ] diff --git a/tests/fixtures/airconditioner_tp1x_rac_device.json b/tests/fixtures/airconditioner_tp1x_rac_device.json new file mode 100644 index 0000000..1eff68c --- /dev/null +++ b/tests/fixtures/airconditioner_tp1x_rac_device.json @@ -0,0 +1,591 @@ +{ + "meta": { + "model": "TP1X_DA-AC-RAC-01001_0000", + "device_type": "airconditioner", + "source": "issue #38 diagnostics (scrubbed)", + "note": "Newer TP1X RAC board (oneUiVersion '7.0 Air conditioner') with a fuller resource set than the ARTIK051/TP2X boards: display light, self-check, mute-once, and a circuit-breaker current-limit setting." + }, + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "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": "/alarms/vs/0", + "rep": { + "rt": [ + "x.com.samsung.da.alarms" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ], + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.alarmType": "Device", + "x.com.samsung.da.code": "ErrorCode_OFF", + "x.com.samsung.da.triggeredTime": "2026-07-22T12:58:00", + "x.com.samsung.da.state": "Deleted" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.alarmType": "Device", + "x.com.samsung.da.code": "FilterAlarm_OFF", + "x.com.samsung.da.triggeredTime": "2026-07-22T12:58:00", + "x.com.samsung.da.state": "Deleted" + } + ] + } + }, + { + "href": "/availablecontrolsets/vs/0", + "rep": { + "x.com.samsung.da.sets": "AEB700A0012C01610248040F0000", + "x.com.samsung.da.id": "RAC", + "x.com.samsung.da.version": "1.0" + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "0000000000", + "x.com.samsung.da.airconOptionList": [ + "PRODUCT_GLOBAL", + "AI_GLOBAL_HEATPUMP_4.0", + "AI_3.0", + "Auto_To_AI", + "AI_Heat", + "ENERGY_2.0", + "HOMECARE_WIZARD_V2", + "DR", + "SingleCommand_1" + ] + } + }, + { + "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": "/da/softreset/vs/0", + "rep": { + "x.com.samsung.da.softwarereset": "false" + } + }, + { + "href": "/dginformation/vs/0", + "rep": { + "enrolmentstatus": "Unknown", + "devicestate": "Unknown", + "lockstatus": "Unknown", + "nextduedate": "", + "workingminutes": 0, + "paymentinfo": { + "emiplan": "Unknown", + "currency": "Unknown", + "totalemi": 0, + "totalemipaid": 0 + } + } + }, + { + "href": "/drlc/vs/0", + "rep": { + "x.com.samsung.da.start": "2026-07-22T10:59:14Z", + "x.com.samsung.da.override": "Off", + "x.com.samsung.da.durationminutes": "1148", + "x.com.samsung.da.drlcLevel": "1", + "x.com.samsung.da.realSaving": "Off" + } + }, + { + "href": "/electriccurrent/vs/0", + "rep": { + "operation": "Off", + "modes": "3", + "supportedModes": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9" + ] + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.cumulativeDate": "1784725078", + "x.com.samsung.da.cumulativeSavedPower": "-1", + "x.com.samsung.da.cumulativePower": "160922", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.cumulativePowerType": "total" + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+03:00", + "x.com.samsung.supprtedtype": 1 + } + }, + { + "href": "/filter/airdustfilter/vs/0", + "rep": { + "x.com.samsung.da.filterUsageResolution": "1", + "x.com.samsung.da.filterCapacity": "500", + "x.com.samsung.da.filterCapacityUnit": "Hour", + "x.com.samsung.da.filterResetType": [ + "replaceable", + "washable" + ], + "x.com.samsung.da.filterStatus": "normal", + "x.com.samsung.da.filterUsage": "30", + "x.com.samsung.da.filterDesiredUsage": "500", + "x.com.samsung.da.supportedFilterDesiredUsage": [ + "180", + "300", + "500", + "700" + ] + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.otnDUID": "**REDACTED**", + "x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01001_0000|10267841|60010532001911000E0041220090A000", + "x.com.samsung.da.description": "TP1X_DA-AC-RAC-01001_0000", + "x.com.samsung.da.serialNum": "**REDACTED**", + "x.com.samsung.da.serialNumOption": "**REDACTED**", + "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": "02762A260401", + "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.newVersionAvailable": "0", + "x.com.samsung.da.number": "02678A24092500,FFFFFFFFFFFFFF" + }, + { + "x.com.samsung.da.id": "2", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "0", + "x.com.samsung.da.number": "02669A24092600,02636A10001200" + }, + { + "x.com.samsung.da.id": "3", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "0", + "x.com.samsung.da.number": "02672A10001000,FFFFFFFFFFFFFF" + } + ], + "x.com.samsung.da.diagDumpType": "file", + "x.com.samsung.da.diagEndPoint": "SSM", + "x.com.samsung.da.diagLogType": [ + "errCode", + "dump" + ], + "x.com.samsung.da.diagMnid": "0AJT", + "x.com.samsung.da.diagSetupid": "AR2", + "x.com.samsung.da.diagProtocolType": "BLE_OCF", + "x.com.samsung.da.diagMinVersion": "3.0", + "x.com.samsung.da.diagTsId": "DA01" + } + }, + { + "href": "/keepnormalstate/vs/0", + "rep": { + "x.com.samsung.da.keepnormal": 1 + } + }, + { + "href": "/light/vs/0", + "rep": { + "mode": "On", + "supportedModes": [ + "On", + "Off" + ] + } + }, + { + "href": "/mds/absencemonitoring/vs/0", + "rep": {} + }, + { + "href": "/mds/absencestate/vs/0", + "rep": {} + }, + { + "href": "/mode/convenient/vs/0", + "rep": { + "x.com.samsung.da.modes": "Off", + "x.com.samsung.da.supportedModes": [ + "Off", + "Sleep", + "Quiet", + "Smart", + "Speed" + ] + } + }, + { + "href": "/mode/vs/0", + "rep": { + "rt": [ + "x.com.samsung.da.mode" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ], + "x.com.samsung.da.modes": [ + "Cool" + ], + "workingMode": "Cool", + "x.com.samsung.da.supportedModes": [ + "Auto", + "Cool", + "Dry", + "Fan", + "Heat" + ], + "x.com.samsung.da.options": [ + "OptionCode_54328", + "Sleep_0", + "ArtificialWorking_Off", + "ComfortAICooling_Off", + "AiTempChanged_Off", + "AiTemp_240", + "Volume_100", + "StopAutoClean_Idle", + "DiagnosisAI_Off", + "ProgressDiagnosisAI_0", + "ResultDiagnosisAI_Normal", + "ExtendOptionCode_230024", + "UpdateAllow_NotAllowed", + "DurationOn_120", + "OutdoorTemp_92", + "CoolCapa_50", + "WarmCapa_60", + "Autoclean_Off", + "WelcomeCoolingState_Off", + "WelcomeHeatingState_Off", + "OutdoorConnection_Connected" + ] + } + }, + { + "href": "/option/airpurify/vs/0", + "rep": { + "rt": [ + "x.com.samsung.da.option.airpurify" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ], + "x.com.samsung.da.modes": "Off", + "x.com.samsung.da.supportedModes": [ + "On", + "Off" + ] + } + }, + { + "href": "/option/autoclean/vs/0", + "rep": { + "x.com.samsung.da.status": "Stop", + "x.com.samsung.da.supportedStatus": [ + "Start", + "Stop" + ], + "x.com.samsung.da.progress": "0", + "x.com.samsung.da.settingStatus": "Off", + "x.com.samsung.da.supportedSettingStatus": [ + "On", + "Off" + ] + } + }, + { + "href": "/option/muteonce/vs/0", + "rep": { + "muteonce": "Off" + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "", + "x.com.samsung.da.newVersionAvailable": "false", + "otnStatus": "None", + "flashingProgress": "0", + "otnCompleteDate": "noHistory", + "scheduledTime": "None", + "swVersionInfo": { + "platform": "Tizen Lite", + "oneUiVersion": "7.0 Air conditioner", + "osVersion": "4.0" + }, + "otnList": [ + { + "type": "WIFI", + "modelId": "ARA-KR-TP1-25-ARXX00", + "versions": [ + "11260401" + ], + "visVersion": "260401" + }, + { + "type": "Micom", + "modelId": "845210267841FFFFFFFF", + "versions": [ + "24092500", + "FFFFFFFF" + ], + "visVersion": "240925" + }, + { + "type": "Micom", + "modelId": "045210267841FFFFFFFF", + "versions": [ + "24092500", + "FFFFFFFF" + ], + "visVersion": "240925" + }, + { + "type": "Micom", + "modelId": "04521026694110263641", + "versions": [ + "24092600", + "10001200" + ], + "visVersion": "240926" + }, + { + "type": "Micom", + "modelId": "FGCAN3", + "versions": [ + "", + "" + ], + "visVersion": "999999" + }, + { + "type": "Micom", + "modelId": "845210266941FFFFFFFF", + "versions": [ + "24092600", + "FFFFFFFF" + ], + "visVersion": "240926" + } + ] + } + }, + { + "href": "/personality/presence/vs/0", + "rep": {} + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "On", + "operationNumber": "9" + } + }, + { + "href": "/quickcontrol/info/vs/0", + "rep": { + "supportedVersion": "1.0" + } + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.longnotisubscription": "false", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "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": "/reserverulesets/vs/0", + "rep": { + "x.com.samsung.da.sets": "AEFFFFFFFF101E121EFFFF101EFFFF000001001F00010000001F0000000000FFFF1E00", + "x.com.samsung.da.id": "RAC", + "x.com.samsung.da.version": "1.0" + } + }, + { + "href": "/selfcheck/vs/0", + "rep": { + "x.com.samsung.da.start": "Cancel", + "x.com.samsung.da.status": "Ready", + "x.com.samsung.da.progress": "0", + "x.com.samsung.da.result": "NA", + "x.com.samsung.da.supportedActions": [ + "Start", + "Cancel" + ], + "x.com.samsung.da.error": [ + "DA_ERROR_NONE" + ] + } + }, + { + "href": "/temperature/control/vs/0", + "rep": { + "x.com.samsung.da.increment": "1" + } + }, + { + "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.current": "26.0", + "x.com.samsung.da.desired": "22.0", + "x.com.samsung.da.minimum": "16", + "x.com.samsung.da.maximum": "30", + "x.com.samsung.da.increment": "1", + "x.com.samsung.da.unit": "Celsius" + } + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Asia/Amman", + "offset": "+03:00", + "DST": "OFF" + } + }, + { + "href": "/welcome/temperature/vs/0", + "rep": { + "operatingStatus": "None", + "requestId": "0000" + } + }, + { + "href": "/wind/direction/vs/0", + "rep": { + "x.com.samsung.da.modes": "All", + "x.com.samsung.da.supportedModes": [ + "Fix", + "Up_And_Low", + "Left_And_Right", + "All" + ] + } + }, + { + "href": "/wind/strength/vs/0", + "rep": { + "x.com.samsung.da.modes": "1", + "x.com.samsung.da.supportedModes": [ + "0", + "1", + "2", + "3", + "4" + ], + "x.com.samsung.da.modesName": [ + "Auto", + "Low", + "Mid", + "High", + "Turbo" + ] + } + }, + { + "href": "/wirelessinfo/vs/0", + "rep": { + "macaddressWiFi": "**REDACTED**", + "macaddressBLE": "**REDACTED**", + "connectedApSsid": "**REDACTED**" + } + } + ] +} diff --git a/tests/fixtures/airconditioner_tp2x_rac_20k_device.json b/tests/fixtures/airconditioner_tp2x_rac_20k_device.json new file mode 100644 index 0000000..1f4baaf --- /dev/null +++ b/tests/fixtures/airconditioner_tp2x_rac_20k_device.json @@ -0,0 +1,380 @@ +{ + "meta": { + "model": "TP2X_RAC_20K", + "device_type": "airconditioner", + "source": "issue #37 diagnostics (scrubbed)", + "note": "Older/simpler RAC board reporting no oneUiVersion -- resolved via the '_RAC_' modelNum token fallback in for_device_by_model (unlike issue #38's TP1X board, which self-reports oneUiVersion)." + }, + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "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-22T12:42:46", + "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_OFF", + "x.com.samsung.da.triggeredTime": "2026-07-22T12:42:46", + "x.com.samsung.da.state": "Deleted" + } + ] + } + }, + { + "href": "/availablecontrolsets/vs/0", + "rep": { + "x.com.samsung.da.sets": "000000A0012C01610249040F0000", + "x.com.samsung.da.id": "RAC", + "x.com.samsung.da.version": "1.0" + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "0000000000", + "x.com.samsung.da.airconOptionList": [ + "SingleCommand_1", + "HOMECARE_WIZARD_V2", + "PRODUCT_GLOBAL", + "AI_RAC_GLOBAL_HEATPUMP_3.0", + "AI_3.0", + "Auto_To_AI", + "AI_Heat" + ] + } + }, + { + "href": "/energy/consumption/0", + "rep": { + "power": 0.0 + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.instantaneousPower": "0.000000", + "x.com.samsung.da.cumulativePower": "717060", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.cumulativePowerType": "total", + "x.com.samsung.da.instantaneousPowerUnit": "W" + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+03:00", + "x.com.samsung.supprtedtype": 1 + } + }, + { + "href": "/filter/airdustfilter/vs/0", + "rep": { + "x.com.samsung.da.filterUsage": "39", + "x.com.samsung.da.filterUsageResolution": "1", + "x.com.samsung.da.filterDesiredUsage": "500", + "x.com.samsung.da.filterStatus": "normal", + "x.com.samsung.da.filterCapacity": "500", + "x.com.samsung.da.filterCapacityUnit": "Hour", + "x.com.samsung.da.filterResetType": [ + "replaceable", + "washable" + ] + } + }, + { + "href": "/humidity/vs/0", + "rep": { + "x.com.samsung.da.humidity": "0", + "x.com.samsung.da.fivepercentHumidity": "43" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "TP2X_RAC_20K|10229641|60010523001511014600003200800000", + "x.com.samsung.da.description": "TP2X_RAC_20K", + "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": "010", + "x.com.samsung.da.diagMinVersion": "1.0", + "x.com.samsung.da.serialNumOption": "**REDACTED**", + "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": "02273A260324", + "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": "102296A23012700", + "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.newVersionAvailable": "0" + } + ] + } + }, + { + "href": "/keepnormalstate/vs/0", + "rep": { + "x.com.samsung.da.keepnormal": 0 + } + }, + { + "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": "/mode/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "Auto", + "Cool", + "Dry", + "Wind", + "Heat", + "HOMECARE_WIZARD_V2" + ], + "x.com.samsung.da.modes": [ + "Cool" + ], + "x.com.samsung.da.options": [ + "Sleep_16", + "ArtificialWorking_Off", + "ComfortAICooling_Off", + "MotionState_Normal", + "AiTempChanged_Off", + "AiTemp_240", + "OutdoorTemp_85", + "CoolCapa_35", + "WarmCapa_35", + "Light_Off", + "Volume_100", + "StopAutoClean_Idle", + "Autoclean_Off", + "DiagnosisAI_Off", + "ProgressDiagnosisAI_1", + "ResultDiagnosisAI_Normal", + "MotionMode_PowerSave", + "OptionCode_56376", + "ExtendOptionCode_230157", + "RacInfo_None", + "UpdateAllow_NotAllowed", + "DurationOn_50", + "WelcomeCoolingState_Off" + ] + } + }, + { + "href": "/option/autoclean/vs/0", + "rep": { + "x.com.samsung.da.status": "Stop", + "x.com.samsung.da.settingStatus": "Off", + "x.com.samsung.da.progress": "0", + "x.com.samsung.da.supportedStatus": [ + "Start", + "Stop" + ], + "x.com.samsung.da.supportedSettingStatus": [ + "On", + "Off" + ] + } + }, + { + "href": "/option/muteonce/vs/0", + "rep": { + "muteonce": "Off" + } + }, + { + "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": "TP2X_RAC_20K", + "versions": [ + "11260324" + ], + "visVersion": "260324" + }, + { + "type": "Micom", + "modelId": "045210229641FFFFFFFF", + "versions": [ + "23012700", + "FFFFFFFF" + ], + "visVersion": "230127" + }, + { + "type": "Micom", + "modelId": "04521022954110229941", + "versions": [ + "21092700", + "10000300" + ], + "visVersion": "210927" + } + ] + } + }, + { + "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": "/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.longnotisubscription": "false", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "href": "/sensors/vs/0", + "rep": {} + }, + { + "href": "/temperature/control/vs/0", + "rep": { + "x.com.samsung.da.increment": "1" + } + }, + { + "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": "22.0", + "x.com.samsung.da.current": "26.0", + "x.com.samsung.da.maximum": "30", + "x.com.samsung.da.minimum": "16", + "x.com.samsung.da.increment": "1.0", + "x.com.samsung.da.unit": "Celsius" + } + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Asia/Amman", + "offset": "+03:00", + "DST": "OFF" + } + }, + { + "href": "/wind/direction/vs/0", + "rep": { + "x.com.samsung.da.modes": "Left_And_Right", + "x.com.samsung.da.supportedModes": [ + "Fix", + "All", + "Up_And_Low", + "Left_And_Right" + ] + } + }, + { + "href": "/wind/strength/vs/0", + "rep": { + "x.com.samsung.da.modes": "2", + "x.com.samsung.da.supportedModes": [ + "0", + "1", + "2", + "3", + "4" + ], + "x.com.samsung.da.modesName": [ + "Auto", + "Low", + "Mid", + "High", + "Turbo" + ] + } + } + ] +} diff --git a/tests/fixtures/golden/airconditioner_tp1x_rac.json b/tests/fixtures/golden/airconditioner_tp1x_rac.json new file mode 100644 index 0000000..b7ce7c3 --- /dev/null +++ b/tests/fixtures/golden/airconditioner_tp1x_rac.json @@ -0,0 +1,20 @@ +{ + "state_keys": [ + "air_filter_status", + "air_filter_usage", + "air_purify", + "alarm_code", + "auto_clean", + "climate", + "current_limit_enabled", + "current_limit_level", + "display_light", + "energy_kwh", + "energy_saved_kwh", + "firmware_update", + "mute_once", + "selfcheck_error", + "selfcheck_result", + "selfcheck_status" + ] +} diff --git a/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json b/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json new file mode 100644 index 0000000..4b50703 --- /dev/null +++ b/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json @@ -0,0 +1,13 @@ +{ + "state_keys": [ + "air_filter_status", + "air_filter_usage", + "alarm_code", + "auto_clean", + "climate", + "energy_kwh", + "firmware_update", + "mute_once", + "power_watts" + ] +} diff --git a/tests/test_airconditioner_capabilities.py b/tests/test_airconditioner_capabilities.py index 328fbc7..676f026 100644 --- a/tests/test_airconditioner_capabilities.py +++ b/tests/test_airconditioner_capabilities.py @@ -23,6 +23,22 @@ def _ac(): return reg, resources +def _resolve(name): + """Mirror the coordinator's detection order: oneUiVersion first, modelNum + fallback second (needed for issue #37's board, which reports neither + oneUiVersion nor a '_PRAC_' modelNum token).""" + resources = _load_device(name) + otn = resources.get('/otninformation/vs/0', {}) + one_ui = otn.get('swVersionInfo', {}).get('oneUiVersion', '') + info = resources['/information/vs/0'] + reg = for_device(one_ui) if one_ui else None + if reg is None: + reg = for_device_by_model( + info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'], + ) + return reg, resources + + def _bound(): reg, resources = _ac() return discover(resources, reg.capabilities, reg.pattern_capabilities), resources @@ -113,7 +129,7 @@ def test_climate_consumed_hrefs_declared_as_coverage(): # newer model class than the ARTIK051_PRAC dump above. It has no OCF-standard # /temperature/current+desired pair (temperature lives on the vendor # /temperatures/vs/0 items[] resource), exposes a /light/vs/0 display light, and -# carries 13 extra vendor housekeeping hrefs. Issue #17 for this class. +# carries extra vendor housekeeping hrefs. Issue #17 for this class (PR #36). # --------------------------------------------------------------------------- def _ac_tp1x(): @@ -129,7 +145,7 @@ def test_tp1x_resolves_to_airconditioner_registry(): def test_tp1x_no_unbound_hrefs(): """Every resource in the TP1X dump binds or is covered -- including - /temperatures/vs/0, /light/vs/0 and the 13 housekeeping hrefs absent from + /temperatures/vs/0, /light/vs/0 and the housekeeping hrefs absent from the ARTIK051 dump. Clears the coverage-gap repair.""" reg, resources = _ac_tp1x() unbound = [] @@ -159,3 +175,61 @@ def test_tp1x_climate_entity_is_bound(): bound = discover(resources, reg.capabilities, reg.pattern_capabilities) climate = [b for b in bound if isinstance(b.desc, ClimateDesc)] assert len(climate) == 1 and climate[0].href == '/mode/vs/0' + + +def test_tp2x_rac_20k_model_resolves_via_model_fallback(): + """TP2X_RAC_20K (issue #37) reports no oneUiVersion and no '_PRAC_' token + -- resolved via the '_RAC_' modelNum fallback added for this device.""" + reg, _ = _resolve('airconditioner_tp2x_rac_20k') + assert reg is not None and reg.name == 'airconditioner' + + +def test_tp2x_rac_20k_no_unbound_hrefs(): + reg, resources = _resolve('airconditioner_tp2x_rac_20k') + unbound = [] + discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append) + assert unbound == [] + + +def test_tp1x_rac_model_resolves_via_one_ui_version(): + """TP1X_DA-AC-RAC-01001_0000 (issue #38) self-reports oneUiVersion + '7.0 Air conditioner' -- resolved via for_device(), not the modelNum + fallback.""" + reg, _ = _resolve('airconditioner_tp1x_rac') + assert reg is not None and reg.name == 'airconditioner' + + +def test_tp1x_rac_no_unbound_hrefs(): + reg, resources = _resolve('airconditioner_tp1x_rac') + unbound = [] + discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append) + assert unbound == [] + + +def test_tp1x_rac_expected_state_keys_present(): + reg, resources = _resolve('airconditioner_tp1x_rac') + bound = discover(resources, reg.capabilities, reg.pattern_capabilities) + state = flatten(bound, resources) + for key in ('climate', 'display_light', 'mute_once', 'selfcheck_status', + 'selfcheck_result', 'current_limit_enabled', 'current_limit_level'): + assert key in state, key + + +def test_mute_once_write_target(): + write = airconditioner.MUTE_ONCE.entities[0].write_fn + assert write('On', {}) == (['option', 'muteonce', 'vs', '0'], {'muteonce': 'On'}) + assert write('Off', {}) == (['option', 'muteonce', 'vs', '0'], {'muteonce': 'Off'}) + + +def test_display_light_write_target(): + write = airconditioner.DISPLAY_LIGHT.entities[0].write_fn + assert write('On', {}) == (['light', 'vs', '0'], {'mode': 'On'}) + assert write('Off', {}) == (['light', 'vs', '0'], {'mode': 'Off'}) + + +def test_current_limit_is_read_only(): + """Meaning/write contract for the current-limit levels isn't confirmed + from the dump alone -- exposed as read-only diagnostic sensors rather + than a guessed writable control.""" + for desc in airconditioner.CURRENT_LIMIT.entities: + assert getattr(desc, 'write_fn', None) is None diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index 85c1eb1..0dc0066 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -156,8 +156,8 @@ def test_registry_reproduces_golden_state_keys_for_tp2x_ref_20k(): def test_registry_reproduces_golden_state_keys_for_ac_tp1x_da_ac_rac_01011(): """Newer AC firmware (Tizen Lite, oneUiVersion "7.0 Air conditioner"; model TP1X_DA-AC-RAC-01011) reports temperature via the vendor /temperatures/vs/0 - items[] resource and adds a /light/vs/0 display light, with 13 extra vendor - housekeeping hrefs -- issue #17 for this model class.""" + items[] resource and adds a /light/vs/0 display light, with extra vendor + housekeeping hrefs -- issue #17 for this model class (PR #36).""" from tests.conftest import _load_device resources = _load_device('airconditioner_tp1x_da_ac_rac_01011') golden = json.loads( @@ -171,6 +171,34 @@ def test_registry_reproduces_golden_state_keys_for_ac_tp1x_da_ac_rac_01011(): ) +def test_registry_reproduces_golden_state_keys_for_tp2x_rac_20k(): + """TP2X_RAC_20K (issue #37) -- reports no oneUiVersion; resolved via the + '_RAC_' modelNum token fallback in for_device_by_model.""" + from tests.conftest import _load_device + resources = _load_device('airconditioner_tp2x_rac_20k') + golden = json.loads((GOLDEN / 'airconditioner_tp2x_rac_20k.json').read_text()) + state_keys = _new_state_keys('airconditioner_tp2x_rac_20k', 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_tp1x_rac(): + """TP1X_DA-AC-RAC-01001_0000 (issue #38) -- fuller RAC board with display + light, self-check, mute-once, and a current-limit setting.""" + from tests.conftest import _load_device + resources = _load_device('airconditioner_tp1x_rac') + golden = json.loads((GOLDEN / 'airconditioner_tp1x_rac.json').read_text()) + state_keys = _new_state_keys('airconditioner_tp1x_rac', 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 = {