From 9ee0329467f917423cd9afa50491a6df69d9ef7c Mon Sep 17 00:00:00 2001 From: perseus177 Date: Tue, 4 Aug 2026 23:55:53 +0200 Subject: [PATCH 1/2] 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. --- .../registry/capabilities/airconditioner.py | 26 ++++++++++-- .../localthings/translations/en.json | 3 ++ docs/investigations/ac-filter-reset.md | 41 ++++++++++++++----- tests/test_airconditioner_artik051_krac.py | 22 ++++++++++ 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index 2bc5cce..d231c52 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -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 diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 545e8c0..d403e24 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -120,6 +120,9 @@ "auto_clean_stop": { "name": "Stop auto clean" }, + "filter_time_reset": { + "name": "Reset filter time" + }, "pause": { "name": "Pause" }, diff --git a/docs/investigations/ac-filter-reset.md b/docs/investigations/ac-filter-reset.md index 858b049..e9c35e1 100644 --- a/docs/investigations/ac-filter-reset.md +++ b/docs/investigations/ac-filter-reset.md @@ -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_` 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_` 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). diff --git a/tests/test_airconditioner_artik051_krac.py b/tests/test_airconditioner_artik051_krac.py index 1034c94..5738b9e 100644 --- a/tests/test_airconditioner_artik051_krac.py +++ b/tests/test_airconditioner_artik051_krac.py @@ -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. From e6909fb8476e452f14c05e2a8ce8e6d73eabc54d Mon Sep 17 00:00:00 2001 From: perseus177 Date: Wed, 5 Aug 2026 00:02:30 +0200 Subject: [PATCH 2/2] fix(translations): mirror the new button string into every catalog test_every_language_mirrors_the_english_catalog is right to fail on this: a key present only in English falls back silently at runtime, which reads as a half-finished translation rather than a missing one. Also fills in the same key for ko.json, which the original fix predates: Korean's translation catalog was added after this branch was cut and never picked up filter_time_reset either. --- custom_components/localthings/translations/cs.json | 3 +++ custom_components/localthings/translations/es.json | 3 +++ custom_components/localthings/translations/it.json | 3 +++ custom_components/localthings/translations/ko.json | 3 +++ custom_components/localthings/translations/nl.json | 3 +++ 5 files changed, 15 insertions(+) diff --git a/custom_components/localthings/translations/cs.json b/custom_components/localthings/translations/cs.json index aeece88..83e5b72 100644 --- a/custom_components/localthings/translations/cs.json +++ b/custom_components/localthings/translations/cs.json @@ -120,6 +120,9 @@ "auto_clean_stop": { "name": "Zastavit samočištění" }, + "filter_time_reset": { + "name": "Vynulovat počítadlo filtru" + }, "pause": { "name": "Pozastavit" }, diff --git a/custom_components/localthings/translations/es.json b/custom_components/localthings/translations/es.json index 729b853..ec8e6a0 100644 --- a/custom_components/localthings/translations/es.json +++ b/custom_components/localthings/translations/es.json @@ -242,6 +242,9 @@ "auto_clean_stop": { "name": "Detener la autolimpieza" }, + "filter_time_reset": { + "name": "Restablecer el contador del filtro" + }, "pause": { "name": "Pausar" }, diff --git a/custom_components/localthings/translations/it.json b/custom_components/localthings/translations/it.json index ecdb604..be69d02 100644 --- a/custom_components/localthings/translations/it.json +++ b/custom_components/localthings/translations/it.json @@ -120,6 +120,9 @@ "auto_clean_stop": { "name": "Arresta autopulizia" }, + "filter_time_reset": { + "name": "Azzera il contatore del filtro" + }, "pause": { "name": "Pausa" }, diff --git a/custom_components/localthings/translations/ko.json b/custom_components/localthings/translations/ko.json index 625a81a..7a86ba4 100644 --- a/custom_components/localthings/translations/ko.json +++ b/custom_components/localthings/translations/ko.json @@ -120,6 +120,9 @@ "auto_clean_stop": { "name": "자동 청소 정지" }, + "filter_time_reset": { + "name": "필터 사용 시간 초기화" + }, "pause": { "name": "일시정지" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index e6658e9..09e15db 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -120,6 +120,9 @@ "auto_clean_stop": { "name": "Zelfreiniging stoppen" }, + "filter_time_reset": { + "name": "Filterteller op nul zetten" + }, "pause": { "name": "Pauzeren" },