Add dryer Drum Clean+ tracking; fix multi-entry DrumCleanLog parsing (#258)
Dryers report the same DrumCleanProposal_/WashingTimes_/DrumCleanLog_ options[] tokens washer.py already models for issue #9, so drum_clean_cycles_remaining/drum_clean_last_cleaned move to laundry.py and get bound on dryer's /course/vs/0 too. DrumCleanLog_ on the reporter's dump is a '|'-joined history of every past clean rather than washer's single bare timestamp -- the shared helper now takes the last (most recent) entry, which turns out to also fix a latent bug on four existing washer fixtures whose own DrumCleanLog_ was already multi-entry and silently failing to parse into drum_clean_last_cleaned. No heat-exchanger-clean tracking was found in either dump #258 supplied; noted in dryer.py so a future report knows this was checked.
This commit is contained in:
1 parent
cf09247e39
commit
e6d7dcddc8
11 files changed
+674
-42
No files matched your search
@@ -11,7 +11,7 @@ the /course/vs/0 cycle select -- lives in laundry.py.
|
||||
|
||||
from ..capability import Capability
|
||||
from ..entities import SensorDesc, SwitchDesc
|
||||
from .laundry import cycle_select
|
||||
from .laundry import cycle_select, drum_clean_cycles_remaining, drum_clean_last_cleaned
|
||||
|
||||
|
||||
def _wrinkle_write(p, rep, href=None):
|
||||
@@ -49,6 +49,17 @@ DRYER_SETTINGS = Capability(
|
||||
# #80). The /st/dryercourse/vs/0 resource re-encodes the same selected
|
||||
# course and is ignored (ignored.py) -- the mirror of how /st/washercourse/vs/0
|
||||
# is ignored for washers.
|
||||
#
|
||||
# Drum Clean+ maintenance tracking (issue #258) reuses washer.py's
|
||||
# DrumCleanProposal_/WashingTimes_/DrumCleanLog_ tokens on this same
|
||||
# options[] array -- see laundry.drum_clean_cycles_remaining/
|
||||
# drum_clean_last_cleaned's docstrings for the field contract, including
|
||||
# the dryer-specific '|'-joined multi-entry DrumCleanLog_ shape. No
|
||||
# separate heat-exchanger-clean tracking was found on either dump #258
|
||||
# supplied (DV90BB7445GES7, DV91T6440LE/SA) -- no HeatExchanger*-prefixed
|
||||
# token, nor any other options[] entry that looks like a second maintenance
|
||||
# counter -- so if the Samsung app surfaces that reminder for these units,
|
||||
# it isn't computed from anything this integration can read locally.
|
||||
DRYER_COURSE = Capability(
|
||||
href="/course/vs/0",
|
||||
entities=(
|
||||
@@ -57,6 +68,22 @@ DRYER_COURSE = Capability(
|
||||
icon="mdi:tumble-dryer",
|
||||
table_href="/st/dryercourse/vs/0",
|
||||
),
|
||||
SensorDesc(
|
||||
key="drum_clean_cycles_remaining",
|
||||
unit="cycles",
|
||||
icon="mdi:tumble-dryer-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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ Door-LED keys use NO `x.com.samsung.da.` prefix -- `setBrightness` /
|
||||
`setNightLight` -- preserved exactly as they appear in the OCF resource rep.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from datetime import time as dt_time
|
||||
|
||||
from ...catalog import has_entity_translation
|
||||
@@ -234,6 +235,47 @@ def option_value(options, prefix):
|
||||
return None
|
||||
|
||||
|
||||
# Drum Clean+ maintenance tracking, from the same options[] array as the
|
||||
# selected course -- shared by washer.py (issue #9) and dryer.py (issue
|
||||
# #258); both families use identical DrumCleanProposal_/WashingTimes_/
|
||||
# DrumCleanLog_ tokens. DrumCleanProposal_<N> is the wash/dry-cycle interval
|
||||
# between recommended cleans; WashingTimes_<N> is the count since the last
|
||||
# one -- their difference is exactly the "N cycles until due" figure the
|
||||
# Samsung app shows (verified on a washer: DrumCleanProposal_40 -
|
||||
# WashingTimes_3 == 37, matching a live app screenshot's "Potreba cistenia
|
||||
# po 37 cykloch").
|
||||
def drum_clean_cycles_remaining(rep):
|
||||
opts = rep.get("x.com.samsung.da.options") or []
|
||||
proposal = option_value(opts, "DrumCleanProposal")
|
||||
washed = option_value(opts, "WashingTimes")
|
||||
if proposal is None or washed is None:
|
||||
return None
|
||||
try:
|
||||
return max(int(proposal) - int(washed), 0)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
# DrumCleanLog_ is the clean-history field: a washer reports one bare ISO
|
||||
# datetime (the last clean, verified against the same app screenshot's "10
|
||||
# days ago"); a dryer (issue #258's Dillton-reported dump) instead reports a
|
||||
# '|'-joined history of every past clean, ten deep on that dump, in
|
||||
# strictly increasing order. Splitting on '|' and taking the last element
|
||||
# handles both shapes identically -- a no-'|' value is unaffected. No
|
||||
# explicit timezone field accompanies either shape, so it's treated as UTC,
|
||||
# matching this integration's convention for other bare ISO datetime fields
|
||||
# (see fridge.py's night-light schedule comment).
|
||||
def drum_clean_last_cleaned(rep):
|
||||
raw = option_value(rep.get("x.com.samsung.da.options"), "DrumCleanLog")
|
||||
if not raw:
|
||||
return None
|
||||
last = raw.rsplit("|", 1)[-1]
|
||||
try:
|
||||
return datetime.fromisoformat(last).replace(tzinfo=UTC)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _course_codes_from_supported_options(course_rep):
|
||||
"""Fallback for an empty/missing editCourseList: derive the selectable
|
||||
course list from /course/vs/0's own x.com.samsung.da.supportedOptions
|
||||
|
||||
@@ -15,8 +15,6 @@ here; they read washer-only fields off the same shared /course/vs/0 options
|
||||
array.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
|
||||
from .laundry import (
|
||||
@@ -24,6 +22,8 @@ from .laundry import (
|
||||
bool_option_switch,
|
||||
cycle_options,
|
||||
cycle_select,
|
||||
drum_clean_cycles_remaining,
|
||||
drum_clean_last_cleaned,
|
||||
hex_pairs,
|
||||
option_value,
|
||||
option_write,
|
||||
@@ -149,36 +149,10 @@ WASHER_SETTINGS = Capability(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Drum Clean+ maintenance tracking, from the same options[] array as the
|
||||
# selected course. DrumCleanProposal_<N> is the wash-cycle interval between
|
||||
# recommended cleans; WashingTimes_<N> is the count since the last one --
|
||||
# their difference is exactly the "N cycles until due" figure the Samsung
|
||||
# app shows (verified: DrumCleanProposal_40 - WashingTimes_3 == 37, matching
|
||||
# a live app screenshot's "Potreba cistenia po 37 cykloch"). DrumCleanLog_
|
||||
# is the last-clean timestamp (verified against the same screenshot's "10
|
||||
# days ago"); no explicit timezone field accompanies it on this resource,
|
||||
# so it's treated as UTC, matching this integration's convention for other
|
||||
# bare ISO datetime fields (see fridge.py's night-light schedule comment).
|
||||
def _drum_clean_cycles_remaining(rep):
|
||||
opts = rep.get("x.com.samsung.da.options") or []
|
||||
proposal = option_value(opts, "DrumCleanProposal")
|
||||
washed = option_value(opts, "WashingTimes")
|
||||
if proposal is None or washed is None:
|
||||
return None
|
||||
try:
|
||||
return max(int(proposal) - int(washed), 0)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _drum_clean_last_cleaned(rep):
|
||||
raw = option_value(rep.get("x.com.samsung.da.options"), "DrumCleanLog")
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(raw).replace(tzinfo=UTC)
|
||||
except ValueError:
|
||||
return None
|
||||
# Drum Clean+ maintenance tracking (issue #9): drum_clean_cycles_remaining/
|
||||
# drum_clean_last_cleaned live in laundry.py, shared with dryer.py (issue
|
||||
# #258) since both families report identical DrumCleanProposal_/
|
||||
# WashingTimes_/DrumCleanLog_ tokens on the same options[] array.
|
||||
|
||||
|
||||
# Detergent/softener auto-dispense dosing, from the same options[] array
|
||||
@@ -340,16 +314,16 @@ WASHER_COURSE = Capability(
|
||||
unit="cycles",
|
||||
icon="mdi:washing-machine-alert",
|
||||
state_class="measurement",
|
||||
exists_fn=lambda rep, resources: _drum_clean_cycles_remaining(rep) is not None,
|
||||
rep_fn=_drum_clean_cycles_remaining,
|
||||
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,
|
||||
exists_fn=lambda rep, resources: drum_clean_last_cleaned(rep) is not None,
|
||||
rep_fn=drum_clean_last_cleaned,
|
||||
),
|
||||
SelectDesc(
|
||||
key="detergent_quantity",
|
||||
|
||||
Reference in new issue
Block a user