Merge pull request #293 from mbillow/claude/ac-filter-reset-cleanup

feat(airconditioner): reset the legacy filter counter locally
This commit is contained in:
Marc Billow
2026-08-05 08:16:02 -04:00
committed by GitHub
9 changed files with 93 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": "Zastavit samočištění"
},
"filter_time_reset": {
"name": "Vynulovat počítadlo filtru"
},
"pause": {
"name": "Pozastavit"
},
@@ -120,6 +120,9 @@
"auto_clean_stop": {
"name": "Stop auto clean"
},
"filter_time_reset": {
"name": "Reset filter time"
},
"pause": {
"name": "Pause"
},
@@ -242,6 +242,9 @@
"auto_clean_stop": {
"name": "Detener la autolimpieza"
},
"filter_time_reset": {
"name": "Restablecer el contador del filtro"
},
"pause": {
"name": "Pausar"
},
@@ -120,6 +120,9 @@
"auto_clean_stop": {
"name": "Arresta autopulizia"
},
"filter_time_reset": {
"name": "Azzera il contatore del filtro"
},
"pause": {
"name": "Pausa"
},
@@ -120,6 +120,9 @@
"auto_clean_stop": {
"name": "자동 청소 정지"
},
"filter_time_reset": {
"name": "필터 사용 시간 초기화"
},
"pause": {
"name": "일시정지"
},
@@ -120,6 +120,9 @@
"auto_clean_stop": {
"name": "Zelfreiniging stoppen"
},
"filter_time_reset": {
"name": "Filterteller op nul zetten"
},
"pause": {
"name": "Pauzeren"
},
+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.