feat(airconditioner): reset the legacy filter counter locally

FilterCleanAlarm_Clear, through the same single-token options merge as every
other setting on /mode/vs/0. Measured on an ARTIK051_KRAC_18K: 2.04 Changed and
FilterTime_95 (9 h 30 min) -> FilterTime_0, still zero on a fresh DTLS session
and on every poll after; none of the other 17 tokens moved and the alarm
entries stayed Deleted.

The counter has had no reset until now, and the descriptor said so: two earlier
rounds against live hardware failed, and the conclusion drawn from them was
that the reset had to be cloud-only. That conclusion was wrong, and the way it
was reached is the interesting part -- it came from diffing every resource the
appliance reports before and after pressing reset in Samsung's app, which
showed only the counter zeroing and the alarm clearing. A trigger token cannot
show up in such a diff, because a trigger is never stored. The appliance's own
app sends this token and skips the write when the counter is already zero.

Both failures stay in the comment, because they say what this is not: writing
FilterTime_0 (the value is not writable -- 5595 -> 5595 after 69 s, 1925 ->
1925 after 65 s, two units, opposite power states), and POSTing the cloud
capability's command name to /actions/vs/0 (real name, wrong transport).

Gated on the FilterTime_ token, so it appears only where there is a counter to
reset; newer boards report filter usage through their own resource and would
need a different mechanism.
This commit is contained in:
perseus177
2026-08-05 01:46:29 +00:00
parent e9e278726a
commit 9ee0329467
4 changed files with 78 additions and 14 deletions
@@ -555,9 +555,9 @@ CLIMATE = Capability(
),
# Filter time in tenths of an hour, counting UP since last filter
# reset; scale and direction confirmed against the Samsung app and
# the /alarms/vs/0 threshold crossing (500h). No reset entity: no
# local write path has been found -- see
# docs/investigations/ac-filter-reset.md for what's been tried.
# the /alarms/vs/0 threshold crossing (500h). Resettable via the
# FilterCleanAlarm_Clear trigger token below -- see
# docs/investigations/ac-filter-reset.md for how that was found.
SensorDesc(
key="filter_time",
rep_fn=_option_token_num("FilterTime", divisor=10),
@@ -567,6 +567,26 @@ CLIMATE = Capability(
state_class="measurement",
icon="mdi:air-filter",
),
# Resets the counter above via the FilterCleanAlarm_Clear token, the
# same single-token options merge as every other /mode/vs/0 setting.
# It's a trigger, not a stored setting -- the board zeroes the
# counter on receipt and never reports this token back, which is why
# it doesn't show up in exists_fn like the others. Confirmed on an
# ARTIK051_KRAC_18K: FilterTime_95 -> FilterTime_0, stable across a
# fresh DTLS session and every poll after (see the investigation doc
# for why this was believed cloud-only until now).
ButtonDesc(
key="filter_time_reset",
field="",
payload="FilterCleanAlarm_Clear",
icon="mdi:restart",
entity_category="config",
exists_fn=_has_option_token("FilterTime"),
write_fn=lambda p, rep, href=None: (
["mode", "vs", "0"],
{"x.com.samsung.da.options": [p]},
),
),
# FilterTime_'s threshold, exposed as a static 4-way radio
# (180/300/500/700h, matching the app) since options[] tokens carry
# no supported-values list to read from, unlike air_filter_threshold
@@ -120,6 +120,9 @@
"auto_clean_stop": {
"name": "Stop auto clean"
},
"filter_time_reset": {
"name": "Reset filter time"
},
"pause": {
"name": "Pause"
},
+30 -11
View File
@@ -1,20 +1,28 @@
# AC filter-time counter reset: not solved
# AC filter-time counter reset: solved
`registry/capabilities/airconditioner.py`'s `filter_time` sensor
(`FilterTime_<N>` option token, tenths of an hour) has no reset entity. This
is where the failed attempts to find one are kept, so the next attempt starts
from the evidence instead of from scratch. Not finding a mechanism is not the
same as it not existing.
(`FilterTime_<N>` option token, tenths of an hour) has a reset entity now
(`filter_time_reset`, issue-tracked as PR #289): a single-token options write
of `FilterCleanAlarm_Clear` to `/mode/vs/0`, the same merge every other
setting on that href uses. Measured on an ARTIK051_KRAC_18K: `FilterTime_95`
(9h30m) → `FilterTime_0`, still zero on a fresh DTLS session and every poll
after; none of the other 17 tokens moved and the `/alarms/vs/0` entries
stayed `Deleted`.
The rest of this file is kept as-is: the failed attempts below are still the
best record of what *doesn't* work on this generation, and the reasoning
that follows them explains why the reset looked cloud-only for as long as it
did — a genuine trap worth knowing about before the next reset-adjacent
mystery on this board family.
## What the reset actually is
Samsung models it as a **command**, not a value write: capability
A **command**, not a value write. Samsung's cloud models it as capability
`custom.dustFilter`, command `resetDustFilter`, no arguments (implemented in
several SmartThings HA forks; not in the core integration). That reframes
every attempt below — nothing changes the counter by writing to it, because
the board zeroes it itself on receiving a command.
several SmartThings HA forks; not in the core integration) — that command
name is real, but see below for why POSTing it directly went nowhere.
## Tried, all against a live unit, all failed
## Tried, all against a live unit, all failed (before the token above was found)
- `FilterTime_0` via the single-token options merge that works for every
other setting on this href — accepted with no error, then discarded. Tried
@@ -50,7 +58,18 @@ on `/rm/state/vs/0` was accepted (2.04 Changed, value held, restored
afterwards), and `FilterAlarmTime_` is written through the very same options
merge and kept. Writes work; this one value just isn't driven that way.
## Where to look next
## The token, and why the dead ends below missed it
`FilterCleanAlarm_Clear` is not derived from anything in this file's earlier
attempts — how it was originally identified isn't recorded here. What is
recorded is why the standard technique (diff the appliance's reported state
before/after triggering the action in Samsung's app) couldn't have found it
on its own: the token is a trigger, never stored and never echoed back in
`x.com.samsung.da.options`, so a before/after diff of stored state shows
only the *effects* (counter zeroing, alarm clearing) and never the token
that caused them.
## Dead ends tried before the token was known (kept for the next unrelated mystery)
- The `/actions/vs/0` action vocabulary from an independent source (a
firmware image, or a capture of what the cloud sends the device).
@@ -261,6 +261,28 @@ def test_auto_clean_stop_stays_off_boards_without_the_token():
assert "auto_clean_progress_legacy" not in _state("airconditioner_tp1x_rac")
def test_filter_time_reset_writes_the_appliance_s_own_trigger_token():
"""FilterCleanAlarm_Clear, measured on hardware: 2.04 Changed and
FilterTime_95 -> FilterTime_0, still zero on a fresh session and every poll
after. The token is a trigger the board acts on rather than a value it
stores -- it never appears in options[] -- so the button is gated on the
counter's own token being present, which is what says this board has a
filter timer at all."""
desc = _desc(_load_device(FIXTURE), "filter_time_reset")
assert desc is not None
assert desc.write_fn(desc.payload, {}) == (
["mode", "vs", "0"],
{"x.com.samsung.da.options": ["FilterCleanAlarm_Clear"]},
)
def test_filter_time_reset_stays_off_boards_without_the_counter():
"""No FilterTime_ token, no counter to reset. Newer boards report filter
usage through their own resource and would need a different mechanism, so
offering a button that writes a legacy token there would be a guess."""
assert _desc(_load_device("airconditioner_tp1x_rac"), "filter_time_reset") is None
def test_filter_alarm_time_stays_off_boards_with_a_real_threshold_resource():
"""Newer boards carry air_filter_threshold off supportedFilterDesiredUsage;
two thresholds on one device would be a coin flip for the user.