From 39dcf6a7b83472cc68a152a816f5a6a531c1293f Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Fri, 24 Jul 2026 04:20:42 +0000 Subject: [PATCH] Drop unexplained Blooming_* diagnostic, confirm remaining air purifier fields (issue #56) Per the five running-state diagnostics dumps (Auto/Sleep/Low/Medium/High) gathered in the issue thread: - Blooming_* has no corresponding SmartThings app setting, so it's dropped entirely rather than kept as an unexplained diagnostic. - Comode_* reads 'Off' on all five, ruling out the original guess that it was the fan-speed selector -- still exposed read-only, purpose unconfirmed. - OptionCode_60282 and the missing humidity sensor are confirmed correct as already modeled. - /airflow's speed doesn't map monotonically to the five settings and the dumps were all captured within one ~30s poll cycle of each other, so it stays read-only pending a cleaner, time-spaced capture. FilterProgress is untouched here: an earlier pass on this issue read the thread as confirming 100 means "fresh" and renamed the sensor to filter_life to match, but that reading was backwards -- the reporter clarified 100 means fully used and needs replacing, which is what filter_progress (the already-shipped name) already implies. That rename was caught before merging and is not part of this change. --- .../registry/capabilities/air_purifier.py | 77 ++++++++++--------- tests/fixtures/golden/air_purifier.json | 1 - tests/test_air_purifier_capabilities.py | 25 ++++-- 3 files changed, 58 insertions(+), 45 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/air_purifier.py b/custom_components/localthings/registry/capabilities/air_purifier.py index 2150a51..ec27121 100644 --- a/custom_components/localthings/registry/capabilities/air_purifier.py +++ b/custom_components/localthings/registry/capabilities/air_purifier.py @@ -1,6 +1,5 @@ """Capabilities for the Samsung ARTIK051_TVTL-class air purifier family -(model AX60R5080WD/SE, issue #56 -- verified against two independent -diagnostics dumps of the same internal model). +(model AX60R5080WD/SE, issue #56). Power, kids-lock, remote-control, alarms, and the energy meter are the shared common.py capabilities (this family exposes the standard /power/0+/power/vs/0 @@ -8,32 +7,39 @@ pair and /alarms/vs/0, /energy/consumption/vs/0). /diagnosis/vs/0 reuses dishwasher.DIAGNOSIS -- identical field/write contract (x.com.samsung.da.diagnosisStart, 'Ready' on both dumps). -Two things are deliberately left as raw, unwritable diagnostic sensors rather -than modeled as real controls, per the "don't guess" rule: +/mode/vs/0's x.com.samsung.da.options array packs multiple independent +'_' flags into one list -- the same packed-list/RMW contract +laundry.py's option_value/replace_in_options already model for +/course/vs/0's options[] (reused directly below, just against this family's +own href). Per issue #56's follow-up (five diagnostics dumps captured with +the physical unit set to Auto/Sleep/Low/Medium/High): + Light_On / Light_Off -- a plain on/off flag; MODE below models it as a + real switch, RMW-replacing just that one entry. + Comode_Off -- read 'Off' on *every* one of the five dumps, + including High/Low/Medium/Auto -- confirms this + is NOT the fan-speed selector (ruling out the + original guess); exposed read-only since its + actual purpose is still unconfirmed. + OptionCode_60282 -- confirmed opaque/not user-facing in the + SmartThings app; not modeled (same treatment as + range_hood's OptionCode_* token on the same + href). + Blooming_* -- confirmed to have no corresponding SmartThings + app setting; dropped entirely rather than kept + as an unexplained diagnostic (it did track 1:1 + with Sleep mode across the five dumps -- 0 in + Sleep, 6 otherwise -- so it's plausibly an + automatic side effect of sleep mode, e.g. a + display-dimming level, but that's still a guess). - /airflow/0, /airflow/vs/0 -- OCF-standard + vendor pair for fan speed/ - direction, both zeroed/'Off' on every dump seen (device was off in both). - No supportedSpeed/supportedModes list is present anywhere in either dump - to confirm the valid range, so a write-capable fan/select isn't safe to - build yet -- see the issue #56 request for a running-state dump. - - /mode/vs/0's x.com.samsung.da.options array packs multiple independent - '_' flags into one list -- the same packed-list/RMW - contract laundry.py's option_value/replace_in_options already model for - /course/vs/0's options[] (reused directly below, just against this - family's own href). Of the tokens seen: - Light_On / Light_Off -- read as a plain on/off flag; MODE below - models it as a real switch, RMW- - replacing just that one list entry. - Comode_Off -- never seen non-'Off' on these dumps; - likely the fan operating mode the issue - describes (Auto/Sleep/1/2/3), but - unconfirmed -- exposed read-only. - Blooming_0 / Blooming_6 -- meaning unconfirmed; exposed read-only. - OptionCode_60282 -- opaque, unchanged across both dumps; - not modeled (same treatment as - range_hood's OptionCode_* token on the - same href). +/airflow/0 and /airflow/vs/0's `speed` still isn't modeled as a real +fan-speed control: across the same five dumps it read 0 for both Auto *and* +High, and 3 for Low/Medium *and* Sleep -- not a monotonic mapping to any +selectable level, and the dumps were all captured within about three minutes +of each other (only one poll cycle apart at this integration's 30s summary +interval), so the values may not have settled after each change before the +diagnostics snapshot was taken. Exposed read-only pending a confirmed, +stable capture -- see the issue #56 discussion for what's needed. """ from ..capability import Capability from ..entities import BinarySensorDesc, SensorDesc, SwitchDesc @@ -68,10 +74,12 @@ def _consumable_state(items, name): return None -# FilterProgress is a raw 0-100 percentage in both dumps (100 and 62); which -# end of that scale means "replace me" isn't confirmed from the dump alone, -# so the entity is named after the raw field rather than asserting a -# direction (see issue #56 follow-up questions). +# FilterProgress is a 0-100 percentage counting up as the filter wears -- +# confirmed via issue #56: the SmartThings app shows "Filter needs changing" +# once this reaches 100, so 100 means fully used, not "brand new." Named +# after the raw field (matching the AC/range_hood filterUsage convention, +# which counts the same direction) rather than "filter life," which would +# imply the opposite direction. FILTER = Capability( href='/consumable/vs/0', poll_tier='cold', @@ -142,15 +150,12 @@ MODE = Capability( rep_fn=bool_option_value('Light'), exists_fn=bool_option_exists('Light'), write_fn=_light_write), - # Read-only pending issue #56 follow-up -- see module docstring. + # Read-only -- confirmed NOT the fan-speed selector (see module + # docstring), actual purpose still unconfirmed. SensorDesc(key='operating_mode', name='Operating mode', icon='mdi:fan', entity_category='diagnostic', rep_fn=lambda rep: option_value(rep.get('x.com.samsung.da.options'), 'Comode'), exists_fn=bool_option_exists('Comode')), - SensorDesc(key='blooming_level', name='Blooming level', icon='mdi:flower', - entity_category='diagnostic', - rep_fn=lambda rep: option_value(rep.get('x.com.samsung.da.options'), 'Blooming'), - exists_fn=bool_option_exists('Blooming')), ), ) diff --git a/tests/fixtures/golden/air_purifier.json b/tests/fixtures/golden/air_purifier.json index 5b6fa07..91ce610 100644 --- a/tests/fixtures/golden/air_purifier.json +++ b/tests/fixtures/golden/air_purifier.json @@ -1,7 +1,6 @@ { "state_keys": [ "alarm_code", - "blooming_level", "clean_level", "device_active", "diagnosis_status", diff --git a/tests/test_air_purifier_capabilities.py b/tests/test_air_purifier_capabilities.py index fb39425..0132aed 100644 --- a/tests/test_air_purifier_capabilities.py +++ b/tests/test_air_purifier_capabilities.py @@ -42,7 +42,7 @@ def test_expected_entities_present(): 'power_switch', 'alarm_code', 'dust', 'fine_dust', 'super_fine_dust', 'odor', 'clean_level', 'filter_progress', 'device_active', 'diagnosis_status', 'fan_speed_level', 'fan_direction', - 'display_light', 'operating_mode', 'blooming_level', + 'display_light', 'operating_mode', ): assert key in state, key @@ -59,6 +59,8 @@ def test_air_quality_sensor_values(): def test_filter_progress_reads_named_consumable_item(): + """FilterProgress is confirmed (issue #56) to count up as the filter + wears -- 100 means fully used and needs replacing, not "brand new".""" assert _state()['filter_progress'] == 100 @@ -87,18 +89,25 @@ def test_light_switch_write_contract(): ) -def test_mode_tokens_are_read_only_diagnostics(): - """Comode_/Blooming_ tokens surface as raw diagnostic sensors rather than - a select/control -- their valid value ranges aren't confirmed yet (see - the air_purifier.py module docstring and the issue #56 follow-up).""" +def test_operating_mode_is_a_read_only_diagnostic(): + """Comode_* surfaces as a raw diagnostic sensor rather than a select/ + control -- issue #56's five running-state dumps confirmed it reads 'Off' + regardless of the device's actual fan setting, ruling out the original + guess that it was the fan-speed selector; its real purpose is still + unconfirmed (see the air_purifier.py module docstring).""" operating_mode = next(e for e in air_purifier.MODE.entities if e.key == 'operating_mode') - blooming = next(e for e in air_purifier.MODE.entities if e.key == 'blooming_level') - rep = {'x.com.samsung.da.options': ['Comode_Off', 'Blooming_6']} + rep = {'x.com.samsung.da.options': ['Comode_Off']} assert operating_mode.rep_fn(rep) == 'Off' - assert blooming.rep_fn(rep) == '6' assert not hasattr(operating_mode, 'write_fn') +def test_blooming_not_modeled(): + """Confirmed (issue #56) to have no corresponding SmartThings app + setting -- dropped entirely rather than kept as an unexplained + diagnostic.""" + assert not any(e.key == 'blooming_level' for e in air_purifier.MODE.entities) + + def test_airflow_vs_fallback_only_binds_without_generic(): """/airflow/vs/0 is a match_fn fallback -- it must not bind when the OCF-standard /airflow/0 is also present (both are on every dump seen)."""