Add Drum Clean+ sensors for dishwasher
The dishwasher's /course/vs/0 options[] array carries the same WashingTimes_/DrumCleanProposal_/DrumCleanLog_ trio already read by washer.py (issue #9) and dryer.py (issue #258), confirmed against a live dump, but dishwasher.py never wired the shared laundry.drum_clean_cycles_remaining/drum_clean_last_cleaned readers in. Add the two sensors to CYCLE_OPTIONS the same way, plus tests and an updated golden fixture.
This commit is contained in:
@@ -10,7 +10,12 @@ wash, auto release dry) are read locally here.
|
||||
from ..capability import Capability
|
||||
from ..entities import ButtonDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||
from .common import diagnosis_status
|
||||
from .laundry import bool_option_switch, cycle_select
|
||||
from .laundry import (
|
||||
bool_option_switch,
|
||||
cycle_select,
|
||||
drum_clean_cycles_remaining,
|
||||
drum_clean_last_cleaned,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /dishwasher/vs/0 — cycle wash/dry settings
|
||||
@@ -52,6 +57,14 @@ DISHWASHER_SETTINGS = Capability(
|
||||
# kind of adjacent pair a manual screenshot transcription slips on. The
|
||||
# reporter's live confirmation (selecting 'Normal' ran the physical Express
|
||||
# 60 program and vice versa) settled it: '86' is Express 60, '83' is Normal.
|
||||
#
|
||||
# Drum Clean+ maintenance tracking reuses washer.py/dryer.py's (issues #9,
|
||||
# #258) DrumCleanProposal_/WashingTimes_/DrumCleanLog_ tokens riding on this
|
||||
# same options[] array -- a live dump confirmed the dishwasher reports the
|
||||
# identical trio (WashingTimes_18/DrumCleanProposal_20, plus a '|'-joined
|
||||
# DrumCleanLog_ history matching the dryer's multi-entry shape), so the
|
||||
# shared laundry.drum_clean_cycles_remaining/drum_clean_last_cleaned readers
|
||||
# apply unchanged; see laundry.py for the field semantics.
|
||||
|
||||
CYCLE_OPTIONS = Capability(
|
||||
href="/course/vs/0",
|
||||
@@ -61,6 +74,22 @@ CYCLE_OPTIONS = Capability(
|
||||
bool_option_switch(
|
||||
"auto_release_dry", "mdi:door-open", "AutoDoorRelease", gate_on_presence=True
|
||||
),
|
||||
SensorDesc(
|
||||
key="drum_clean_cycles_remaining",
|
||||
unit="cycles",
|
||||
icon="mdi:dishwasher-alert",
|
||||
state_class="measurement",
|
||||
exists_fn=lambda rep, resources: drum_clean_cycles_remaining(rep) is not None,
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
+4
@@ -8,6 +8,8 @@
|
||||
"cycle_active",
|
||||
"delay_start_hours",
|
||||
"diagnosis_status",
|
||||
"drum_clean_cycles_remaining",
|
||||
"drum_clean_last_cleaned",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"finish_time",
|
||||
@@ -40,6 +42,8 @@
|
||||
"samsung_dishwasher_delay_start_hours",
|
||||
"samsung_dishwasher_diagnosis_start",
|
||||
"samsung_dishwasher_diagnosis_status",
|
||||
"samsung_dishwasher_drum_clean_cycles_remaining",
|
||||
"samsung_dishwasher_drum_clean_last_cleaned",
|
||||
"samsung_dishwasher_energy_kwh",
|
||||
"samsung_dishwasher_energy_saved_kwh",
|
||||
"samsung_dishwasher_finish_time",
|
||||
|
||||
@@ -5,6 +5,8 @@ cycle_options, cycle_write) is tested in test_laundry_capabilities.py; here we
|
||||
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.entities import SensorDesc, SwitchDesc
|
||||
|
||||
@@ -62,6 +64,53 @@ class TestDishwasherOptions:
|
||||
assert desc.exists_fn({"x.com.samsung.da.options": ["AutoDoorRelease_On"]}, {}) is True
|
||||
|
||||
|
||||
class TestDrumClean:
|
||||
"""Drum Clean+ maintenance tracking shares washer.py's (issue #9) /
|
||||
dryer.py's (issue #258) options[]-array readers -- a live dishwasher
|
||||
dump confirmed the same WashingTimes_/DrumCleanProposal_/DrumCleanLog_
|
||||
trio, so these entities are wired the same way here."""
|
||||
|
||||
def test_cycles_remaining(self):
|
||||
desc = next(
|
||||
e for e in dishwasher.CYCLE_OPTIONS.entities if e.key == "drum_clean_cycles_remaining"
|
||||
)
|
||||
assert desc.rep_fn is not None
|
||||
rep = {"x.com.samsung.da.options": ["WashingTimes_18", "DrumCleanProposal_20"]}
|
||||
assert desc.rep_fn(rep) == 2
|
||||
|
||||
def test_cycles_remaining_exists_only_when_computable(self):
|
||||
desc = next(
|
||||
e for e in dishwasher.CYCLE_OPTIONS.entities if e.key == "drum_clean_cycles_remaining"
|
||||
)
|
||||
assert desc.exists_fn is not None
|
||||
assert desc.exists_fn({"x.com.samsung.da.options": []}, {}) is False
|
||||
rep = {"x.com.samsung.da.options": ["WashingTimes_18", "DrumCleanProposal_20"]}
|
||||
assert desc.exists_fn(rep, {}) is True
|
||||
|
||||
def test_last_cleaned(self):
|
||||
"""A live dump's DrumCleanLog_ is a '|'-joined history (the dryer
|
||||
shape, not the washer's single-entry one) -- the last entry wins."""
|
||||
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():
|
||||
desc = next(
|
||||
e
|
||||
|
||||
Reference in New Issue
Block a user