From 290a348017f379944db169c9534784376ee8ce02 Mon Sep 17 00:00:00 2001 From: perseus177 Date: Wed, 5 Aug 2026 00:31:11 +0200 Subject: [PATCH] feat(airconditioner): stop a running auto clean, and read its progress Three tokens describe the drying cycle these boards run after cooling, and the switch only covered the first. AutocleanProgress_ is how far a running cycle has got, and StopAutoClean_ is a channel for ending one early -- its presence is what says the appliance accepts that at all, which is how the appliance's own app gates its stop button. Both tokens are reported by the ARTIK051_KRAC_18K that issue #136 was about, and by every KRAC fixture here. The percentage scale is the app's own: it renders the token into a `` with a "{{value}}%" label beside it. An idle unit reports 1 rather than 0 -- the same floor the laundry firmware's progressPercentage sits at when Ready -- so 0-vs-1 is not a reliable "is it running" test, and the button is deliberately not gated on it. The sensor shares AUTO_CLEAN's catalog entry the way auto_clean_legacy already shares the switch's: same figure, different board generation, distinct key so nothing collides if a board ever reported both. Stacked on #289 (this branch is cut from it) -- rebase or merge that first. --- .../registry/capabilities/airconditioner.py | 40 +++++++++++++++++++ .../localthings/translations/cs.json | 3 ++ .../localthings/translations/en.json | 3 ++ .../localthings/translations/es.json | 3 ++ .../localthings/translations/it.json | 3 ++ .../localthings/translations/nl.json | 3 ++ ...irconditioner_artik051_dongle_fac_18k.json | 2 + .../airconditioner_artik051_krac_18k.json | 1 + ...airconditioner_artik051_krac_18k_slot.json | 1 + .../airconditioner_artik051_krac_energy.json | 1 + tests/test_airconditioner_artik051_krac.py | 26 ++++++++++++ 11 files changed, 86 insertions(+) diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index d0de28d..533a7cb 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -20,6 +20,7 @@ from dataclasses import replace from ..capability import Capability from ..entities import ( BinarySensorDesc, + ButtonDesc, ClimateDesc, NumberDesc, SelectDesc, @@ -623,6 +624,45 @@ CLIMATE = Capability( icon="mdi:fan-auto", entity_category="config", ), + # A drying cycle the unit runs after cooling, to keep the coil from going + # mouldy. Three tokens describe it and the switch above only covered the + # first: Autoclean_ is the setting, AutocleanProgress_ is how far a + # running cycle has got, and StopAutoClean_ is a channel for ending one + # early -- its presence is what says the appliance takes that at all + # (the app gates its own stop button on exactly that), and the value it + # reports while nothing is running is Idle. + # + # The percentage scale is the app's own: `` with the + # token rendered as "{{value}}%" beside it. An idle unit here reports 1 + # rather than 0, the same floor the laundry firmware's progressPercentage + # sits at when Ready, so 0-vs-1 is not a reliable "is it running" test -- + # which is why the button below is not gated on it. + # + # The sensor shares AUTO_CLEAN's catalog entry, like auto_clean_legacy + # above: same figure, different board generation. Distinct key, so + # nothing collides if a board ever reported both. + SensorDesc( + key="auto_clean_progress_legacy", + translation_key="auto_clean_progress", + rep_fn=_option_token_num("AutocleanProgress"), + exists_fn=_has_option_token("AutocleanProgress"), + unit="%", + state_class="measurement", + icon="mdi:progress-check", + entity_category="diagnostic", + ), + ButtonDesc( + key="auto_clean_stop", + field="", + payload="StopAutoClean_Set", + icon="mdi:fan-off", + entity_category="config", + exists_fn=_has_option_token("StopAutoClean"), + write_fn=lambda p, rep, href=None: ( + ["mode", "vs", "0"], + {"x.com.samsung.da.options": [p]}, + ), + ), SwitchDesc( key="air_monitoring", rep_fn=_option_token_on("AirMonitoring"), diff --git a/custom_components/localthings/translations/cs.json b/custom_components/localthings/translations/cs.json index 0d7c11b..0ec77d6 100644 --- a/custom_components/localthings/translations/cs.json +++ b/custom_components/localthings/translations/cs.json @@ -117,6 +117,9 @@ "diagnosis_start": { "name": "Spustit diagnostiku" }, + "auto_clean_stop": { + "name": "Zastavit samočištění" + }, "pause": { "name": "Pozastavit" }, diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index efea0a9..6781a7a 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -117,6 +117,9 @@ "diagnosis_start": { "name": "Start diagnosis" }, + "auto_clean_stop": { + "name": "Stop auto clean" + }, "pause": { "name": "Pause" }, diff --git a/custom_components/localthings/translations/es.json b/custom_components/localthings/translations/es.json index c45af0b..9a78917 100644 --- a/custom_components/localthings/translations/es.json +++ b/custom_components/localthings/translations/es.json @@ -239,6 +239,9 @@ "diagnosis_start": { "name": "Iniciar diagnóstico" }, + "auto_clean_stop": { + "name": "Detener la autolimpieza" + }, "pause": { "name": "Pausar" }, diff --git a/custom_components/localthings/translations/it.json b/custom_components/localthings/translations/it.json index d8939cb..5335b95 100644 --- a/custom_components/localthings/translations/it.json +++ b/custom_components/localthings/translations/it.json @@ -117,6 +117,9 @@ "diagnosis_start": { "name": "Avvia diagnosi" }, + "auto_clean_stop": { + "name": "Arresta autopulizia" + }, "pause": { "name": "Pausa" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 63614a1..7b7d39a 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -117,6 +117,9 @@ "diagnosis_start": { "name": "Diagnose starten" }, + "auto_clean_stop": { + "name": "Zelfreiniging stoppen" + }, "pause": { "name": "Pauzeren" }, diff --git a/tests/fixtures/golden/airconditioner_artik051_dongle_fac_18k.json b/tests/fixtures/golden/airconditioner_artik051_dongle_fac_18k.json index 379fcbb..22b6c37 100644 --- a/tests/fixtures/golden/airconditioner_artik051_dongle_fac_18k.json +++ b/tests/fixtures/golden/airconditioner_artik051_dongle_fac_18k.json @@ -2,6 +2,7 @@ "state_keys": [ "alarm_code", "auto_clean_legacy", + "auto_clean_progress_legacy", "beep", "clean_level", "climate", @@ -18,6 +19,7 @@ "super_fine_dust", "subdevice1_alarm_code", "subdevice1_auto_clean_legacy", + "subdevice1_auto_clean_progress_legacy", "subdevice1_beep", "subdevice1_climate", "subdevice1_current_temperature_c", diff --git a/tests/fixtures/golden/airconditioner_artik051_krac_18k.json b/tests/fixtures/golden/airconditioner_artik051_krac_18k.json index 4008747..9bb4c7c 100644 --- a/tests/fixtures/golden/airconditioner_artik051_krac_18k.json +++ b/tests/fixtures/golden/airconditioner_artik051_krac_18k.json @@ -3,6 +3,7 @@ "air_monitoring", "alarm_code", "auto_clean_legacy", + "auto_clean_progress_legacy", "beep", "climate", "current_temperature_c", diff --git a/tests/fixtures/golden/airconditioner_artik051_krac_18k_slot.json b/tests/fixtures/golden/airconditioner_artik051_krac_18k_slot.json index 3008f12..cf236cd 100644 --- a/tests/fixtures/golden/airconditioner_artik051_krac_18k_slot.json +++ b/tests/fixtures/golden/airconditioner_artik051_krac_18k_slot.json @@ -3,6 +3,7 @@ "air_monitoring", "alarm_code", "auto_clean_legacy", + "auto_clean_progress_legacy", "beep", "climate", "current_temperature_c", diff --git a/tests/fixtures/golden/airconditioner_artik051_krac_energy.json b/tests/fixtures/golden/airconditioner_artik051_krac_energy.json index 4008747..9bb4c7c 100644 --- a/tests/fixtures/golden/airconditioner_artik051_krac_energy.json +++ b/tests/fixtures/golden/airconditioner_artik051_krac_energy.json @@ -3,6 +3,7 @@ "air_monitoring", "alarm_code", "auto_clean_legacy", + "auto_clean_progress_legacy", "beep", "climate", "current_temperature_c", diff --git a/tests/test_airconditioner_artik051_krac.py b/tests/test_airconditioner_artik051_krac.py index 6081e78..1034c94 100644 --- a/tests/test_airconditioner_artik051_krac.py +++ b/tests/test_airconditioner_artik051_krac.py @@ -235,6 +235,32 @@ def test_filter_alarm_time_reads_the_threshold_and_writes_one_token(): ) +def test_auto_clean_progress_and_stop_come_off_their_own_tokens(): + """Three tokens describe the drying cycle and the switch only covered the + first. AutocleanProgress_ is a percentage -- the app renders it into a + `` beside a "{{value}}%" label -- and StopAutoClean_ is + the channel for ending a cycle early, whose presence is what says the + appliance accepts that at all (the fixture reports Idle, this unit's + resting value).""" + assert _state()["auto_clean_progress_legacy"] == 1.0 # the fixture's own value + + desc = _desc(_load_device(FIXTURE), "auto_clean_stop") + assert desc is not None + assert desc.write_fn(desc.payload, {}) == ( + ["mode", "vs", "0"], + {"x.com.samsung.da.options": ["StopAutoClean_Set"]}, + ) + + +def test_auto_clean_stop_stays_off_boards_without_the_token(): + """Newer boards run the same cycle off /option/autoclean/vs/0 and say + nothing about stopping it, so writing a legacy token there would be a + guess.""" + newer = _load_device("airconditioner_tp1x_rac") + assert _desc(newer, "auto_clean_stop") is None + assert "auto_clean_progress_legacy" not in _state("airconditioner_tp1x_rac") + + 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.