Merge pull request #399 from mbillow/claude/issues-398-397-vkne4j

Dishwasher: translate Sanitizing progress stage, drop churning drum-clean sensor
This commit is contained in:
Marc Billow
2026-08-18 22:06:28 -05:00
committed by GitHub
10 changed files with 32 additions and 45 deletions
@@ -14,7 +14,6 @@ from .laundry import (
bool_option_switch, bool_option_switch,
cycle_select, cycle_select,
drum_clean_cycles_remaining, drum_clean_cycles_remaining,
drum_clean_last_cleaned,
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -62,9 +61,14 @@ DISHWASHER_SETTINGS = Capability(
# #258) DrumCleanProposal_/WashingTimes_/DrumCleanLog_ tokens riding on this # #258) DrumCleanProposal_/WashingTimes_/DrumCleanLog_ tokens riding on this
# same options[] array -- a live dump confirmed the dishwasher reports the # same options[] array -- a live dump confirmed the dishwasher reports the
# identical trio (WashingTimes_18/DrumCleanProposal_20, plus a '|'-joined # identical trio (WashingTimes_18/DrumCleanProposal_20, plus a '|'-joined
# DrumCleanLog_ history matching the dryer's multi-entry shape), so the # DrumCleanLog_ history matching the dryer's multi-entry shape), so
# shared laundry.drum_clean_cycles_remaining/drum_clean_last_cleaned readers # laundry.drum_clean_cycles_remaining applies unchanged.
# apply unchanged; see laundry.py for the field semantics. #
# laundry.drum_clean_last_cleaned (DrumCleanLog_'s own newest entry) is
# deliberately NOT wired up here (issue #398): a live dishwasher dump
# showed it moving every 30-90s on its own, including well after a cycle
# had already finished -- unlike the washer/dryer reports this reader was
# built from (issues #9, #258), it never settles on a value worth showing.
CYCLE_OPTIONS = Capability( CYCLE_OPTIONS = Capability(
href="/course/vs/0", href="/course/vs/0",
@@ -82,14 +86,6 @@ CYCLE_OPTIONS = Capability(
exists_fn=lambda rep, resources: drum_clean_cycles_remaining(rep) is not None, exists_fn=lambda rep, resources: drum_clean_cycles_remaining(rep) is not None,
rep_fn=drum_clean_cycles_remaining, rep_fn=drum_clean_cycles_remaining,
), ),
SensorDesc(
key="drum_clean_last_cleaned",
device_class="timestamp",
icon="mdi:calendar-clock",
entity_category="diagnostic",
exists_fn=lambda rep, resources: drum_clean_last_cleaned(rep) is not None,
rep_fn=drum_clean_last_cleaned,
),
), ),
) )
@@ -1142,7 +1142,8 @@
"dryingwithdooropen": "Větrání", "dryingwithdooropen": "Větrání",
"cooling": "Chlazení", "cooling": "Chlazení",
"predrain": "Vypouštění", "predrain": "Vypouštění",
"prewash": "Předpírka" "prewash": "Předpírka",
"sanitizing": "Dezinfekce"
} }
}, },
"progress_percentage": { "progress_percentage": {
@@ -1136,7 +1136,8 @@
"dryingwithdooropen": "Lüften", "dryingwithdooropen": "Lüften",
"cooling": "Abkühlen", "cooling": "Abkühlen",
"predrain": "Abpumpen", "predrain": "Abpumpen",
"prewash": "Vorwäsche" "prewash": "Vorwäsche",
"sanitizing": "Hygienespülung"
} }
}, },
"progress_percentage": { "progress_percentage": {
@@ -1142,7 +1142,8 @@
"dryingwithdooropen": "Venting", "dryingwithdooropen": "Venting",
"cooling": "Cooling", "cooling": "Cooling",
"predrain": "Pre-drain", "predrain": "Pre-drain",
"prewash": "Pre-wash" "prewash": "Pre-wash",
"sanitizing": "Sanitizing"
} }
}, },
"progress_percentage": { "progress_percentage": {
@@ -1336,7 +1336,8 @@
"dryingwithdooropen": "Ventilación", "dryingwithdooropen": "Ventilación",
"cooling": "Enfriamiento", "cooling": "Enfriamiento",
"predrain": "Drenaje previo", "predrain": "Drenaje previo",
"prewash": "Prelavado" "prewash": "Prelavado",
"sanitizing": "Desinfección"
} }
}, },
"progress_percentage": { "progress_percentage": {
@@ -1142,7 +1142,8 @@
"dryingwithdooropen": "Ventilazione", "dryingwithdooropen": "Ventilazione",
"cooling": "Raffreddamento", "cooling": "Raffreddamento",
"predrain": "Scarico preliminare", "predrain": "Scarico preliminare",
"prewash": "Prelavaggio" "prewash": "Prelavaggio",
"sanitizing": "Igienizzazione"
} }
}, },
"progress_percentage": { "progress_percentage": {
@@ -1142,7 +1142,8 @@
"dryingwithdooropen": "환기", "dryingwithdooropen": "환기",
"cooling": "냉각", "cooling": "냉각",
"predrain": "사전 배수", "predrain": "사전 배수",
"prewash": "애벌빨래" "prewash": "애벌빨래",
"sanitizing": "살균"
} }
}, },
"progress_percentage": { "progress_percentage": {
@@ -1142,7 +1142,8 @@
"dryingwithdooropen": "Ventileren", "dryingwithdooropen": "Ventileren",
"cooling": "Koelen", "cooling": "Koelen",
"predrain": "Vooraf afpompen", "predrain": "Vooraf afpompen",
"prewash": "Voorwas" "prewash": "Voorwas",
"sanitizing": "Ontsmetten"
} }
}, },
"progress_percentage": { "progress_percentage": {
-2
View File
@@ -9,7 +9,6 @@
"delay_start_hours", "delay_start_hours",
"diagnosis_status", "diagnosis_status",
"drum_clean_cycles_remaining", "drum_clean_cycles_remaining",
"drum_clean_last_cleaned",
"energy_kwh", "energy_kwh",
"energy_saved_kwh", "energy_saved_kwh",
"finish_time", "finish_time",
@@ -43,7 +42,6 @@
"samsung_dishwasher_diagnosis_start", "samsung_dishwasher_diagnosis_start",
"samsung_dishwasher_diagnosis_status", "samsung_dishwasher_diagnosis_status",
"samsung_dishwasher_drum_clean_cycles_remaining", "samsung_dishwasher_drum_clean_cycles_remaining",
"samsung_dishwasher_drum_clean_last_cleaned",
"samsung_dishwasher_energy_kwh", "samsung_dishwasher_energy_kwh",
"samsung_dishwasher_energy_saved_kwh", "samsung_dishwasher_energy_saved_kwh",
"samsung_dishwasher_finish_time", "samsung_dishwasher_finish_time",
+10 -24
View File
@@ -5,8 +5,6 @@ cycle_options, cycle_write) is tested in test_laundry_capabilities.py; here we
check the dishwasher wiring and its device-specific options. check the dishwasher wiring and its device-specific options.
""" """
from datetime import UTC, datetime
from custom_components.localthings.registry.capabilities import dishwasher from custom_components.localthings.registry.capabilities import dishwasher
from custom_components.localthings.registry.entities import SensorDesc, SwitchDesc from custom_components.localthings.registry.entities import SensorDesc, SwitchDesc
@@ -68,7 +66,13 @@ class TestDrumClean:
"""Drum Clean+ maintenance tracking shares washer.py's (issue #9) / """Drum Clean+ maintenance tracking shares washer.py's (issue #9) /
dryer.py's (issue #258) options[]-array readers -- a live dishwasher dryer.py's (issue #258) options[]-array readers -- a live dishwasher
dump confirmed the same WashingTimes_/DrumCleanProposal_/DrumCleanLog_ dump confirmed the same WashingTimes_/DrumCleanProposal_/DrumCleanLog_
trio, so these entities are wired the same way here.""" trio, so drum_clean_cycles_remaining is wired the same way here.
drum_clean_last_cleaned is deliberately not (issue #398): a live dump
showed DrumCleanLog_'s newest entry moving every 30-90s on its own,
including well after a cycle had finished -- unlike the washer/dryer
reports this reader was built from (issues #9, #258), it never settles
on a value worth showing."""
def test_cycles_remaining(self): def test_cycles_remaining(self):
desc = next( desc = next(
@@ -87,28 +91,10 @@ class TestDrumClean:
rep = {"x.com.samsung.da.options": ["WashingTimes_18", "DrumCleanProposal_20"]} rep = {"x.com.samsung.da.options": ["WashingTimes_18", "DrumCleanProposal_20"]}
assert desc.exists_fn(rep, {}) is True assert desc.exists_fn(rep, {}) is True
def test_last_cleaned(self): def test_last_cleaned_is_not_wired(self):
"""A live dump's DrumCleanLog_ is a '|'-joined history (the dryer assert not any(
shape, not the washer's single-entry one) -- the last entry wins.""" e.key == "drum_clean_last_cleaned" for e in dishwasher.CYCLE_OPTIONS.entities
desc = next(
e for e in dishwasher.CYCLE_OPTIONS.entities if e.key == "drum_clean_last_cleaned"
) )
assert desc.rep_fn is not None
rep = {
"x.com.samsung.da.options": [
"DrumCleanLog_2026-06-26T04:18:58|2026-06-28T00:36:34",
]
}
assert desc.rep_fn(rep) == datetime(2026, 6, 28, 0, 36, 34, tzinfo=UTC)
def test_last_cleaned_missing(self):
desc = next(
e for e in dishwasher.CYCLE_OPTIONS.entities if e.key == "drum_clean_last_cleaned"
)
assert desc.rep_fn is not None
assert desc.rep_fn({"x.com.samsung.da.options": []}) is None
assert desc.exists_fn is not None
assert desc.exists_fn({"x.com.samsung.da.options": []}, {}) is False
def test_diagnosis_status_is_a_translatable_enum(): def test_diagnosis_status_is_a_translatable_enum():