Merge pull request #272 from mbillow/claude/issue-triaging-fuq5sn

Issue triage batch: dehumidifier, AC, dryer, fridge, dishwasher, climate fixes
This commit is contained in:
Marc Billow
2026-08-03 15:45:12 -04:00
committed by GitHub
38 changed files with 3190 additions and 79 deletions
@@ -399,6 +399,24 @@ no `[%key:...%]` resolution (that's Core build tooling). Every other language
must mirror `en.json` key for key — also enforced by
`tests/test_translations.py`.
**Don't write a test that just re-asserts a translation string.** Adding
labels is a data change, not a logic change, and `tests/test_translations.py`
already holds the invariants that matter for data (every descriptor has a
catalog entry, every language mirrors English key-for-key, no unresolved
`[%key:...%]`). A test that loads the catalog and asserts
`catalog["select"]["foo"]["state"]["16"] == "Cotton"` right after you just
wrote that exact line into `en.json` doesn't exercise any code path — it
re-states the JSON file in Python, passes by construction, and only ever
fails when someone *correctly* edits the label later (a wording fix, a
translator's improvement). It's not a regression test, because there's no
`select.py`/`adapter.py` logic between "the JSON says X" and "the test reads
X" for it to catch drift in. If a code/label mapping is worth locking in,
test it through the code that actually consumes it instead — a write
contract (`desc.write_fn(...)` returns the right raw code), a read contract
(`flatten()` produces the right raw value from a fixture rep), or a routing
decision — never a bare literal-string comparison against the catalog you
just edited.
## 8. Coverage discipline: bound or ignored
Every href in the dump must resolve, or the repair fires. If a resource isn't
+33 -4
View File
@@ -128,6 +128,23 @@ _HVAC_TO_DEVICE = {v: k for k, v in _DEVICE_TO_HVAC.items()}
_AI_COMFORT_MODE = "AIComfort"
PRESET_AI_COMFORT = "ai_comfort"
# Codes that appear in /mode/vs/0's supportedModes but are option/capability
# flags rather than selectable thermodynamic operations -- dropped silently
# (no _warn_unmapped call) rather than every owner of an affected unit
# tripping the issue #93 warning on every start.
#
# HOMECARE_WIZARD_V2 (issue #235, TP2X_RAC_20K): also appears in
# /configuration/vs/0's x.com.samsung.da.airconOptionList alongside
# PRODUCT_GLOBAL/AI_3.0/SingleCommand_1 -- clearly a capability flag, not a
# mode, on that resource. The unit's own `modes` (current mode) never
# reported it as active across the reporter's logs, only ever a real
# thermodynamic mode -- consistent with it being echoed into supportedModes
# rather than genuinely selectable. Unlike _AI_COMFORT_MODE above, it isn't
# modeled as a preset: there's no confirmation it's user-selectable at all,
# so silently dropping it (rather than guessing a write contract) is the
# 'don't guess' rule applied to a mode code instead of a resource field.
_NON_HVAC_OPTION_CODES = frozenset({"HOMECARE_WIZARD_V2"})
# Fan (wind strength): device codes "0".."4" -> HA standard fan constants where
# a clean match exists so they auto-localize; "turbo" is custom (translated).
_DEVICE_TO_FAN: dict[str, str] = {
@@ -363,7 +380,15 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
def _warn_unmapped(self, href: str, code: str) -> None:
"""Log once per (href, code) when a device-reported mode has no
entry in the relevant device<->HA map, so a real device gap surfaces
in the log instead of silently vanishing (issue #93)."""
in the log instead of silently vanishing (issue #93).
Falls back to `unique_id` when `entity_id` is unset (issue #235):
this fires during setup's first discovery pass, before the entity is
added to hass, so `entity_id` is always None at that point --
indistinguishable across multiple same-type devices in the log.
`unique_id` is set eagerly in `__init__` (see entity.py), so it's
always available here even though `entity_id` isn't.
"""
key = (href, code)
if key in self._warned_unmapped:
return
@@ -371,7 +396,7 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
_LOGGER.warning(
"%s: device mode %r on %s has no HA mapping and was dropped; "
"please file an issue with your diagnostics dump",
self.entity_id,
self.entity_id or self.unique_id,
code,
href,
)
@@ -472,7 +497,11 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
device = _first(self._rep(MODE_HREF).get(_MODES_FIELD))
if device == _AI_COMFORT_MODE:
return HVACMode.AUTO
if device is not None and device not in _DEVICE_TO_HVAC:
if (
device is not None
and device not in _DEVICE_TO_HVAC
and device not in _NON_HVAC_OPTION_CODES
):
self._warn_unmapped(MODE_HREF, device)
return _DEVICE_TO_HVAC.get(device, HVACMode.AUTO)
@@ -480,7 +509,7 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
def hvac_modes(self) -> list[HVACMode]:
modes = [HVACMode.OFF]
for m in self._supported(MODE_HREF):
if m == _AI_COMFORT_MODE:
if m == _AI_COMFORT_MODE or m in _NON_HVAC_OPTION_CODES:
continue
mapped = _DEVICE_TO_HVAC.get(m)
if mapped is None:
@@ -259,6 +259,7 @@ _OIC_TYPE_TO_KEY: dict[str, str] = {
"oic.d.refrigerator": "refrigerator",
"oic.d.washer": "washer",
"x.com.st.d.airqualitysensor": "air_monitor",
"x.com.st.d.dehumidifier": "dehumidifier",
"x.com.st.d.hood": "range_hood", # AHD-WW-TP1-22-COMMON
"x.com.st.d.stickcleaner": "vacuum_station",
"x.com.st.d.steamcloset": "air_dresser",
@@ -33,8 +33,11 @@ REGISTRY = DeviceRegistry(
airconditioner.AIR_PURIFY,
airconditioner.AUTO_CLEAN,
airconditioner.AIR_FILTER,
airconditioner.AIR_FILTER_PM1,
airconditioner.AIR_QUALITY,
airconditioner.DISPLAY_LIGHT,
airconditioner.UV_LED,
airconditioner.VENTILATION_ALARM,
airconditioner.MUTE_ONCE,
airconditioner.CURRENT_LIMIT,
airconditioner.ANOMALY_LOAD,
@@ -2,10 +2,13 @@
Shares the DA_AC_ board family with airconditioner.py (power, air filter,
auto-clean, mute-once all use the identical resource shapes), so those three
Capability objects are reused directly rather than duplicated.
Capability objects are reused directly rather than duplicated. The
TP1X_DA_AC_DHM_01001_0000 revision (issues #271/#231) also reports
air_purifier.py's screen-on/off resource on the identical href/shape, so
that's reused too rather than re-defined.
"""
from ..capabilities import airconditioner, common, dehumidifier, ignored
from ..capabilities import air_purifier, airconditioner, common, dehumidifier, ignored
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
@@ -17,9 +20,11 @@ REGISTRY = DeviceRegistry(
*common.POWER,
dehumidifier.MODE,
dehumidifier.HUMIDITY,
dehumidifier.WATERTANK_LIGHTING,
airconditioner.AUTO_CLEAN,
airconditioner.AIR_FILTER,
airconditioner.MUTE_ONCE,
air_purifier.DISPLAY,
*dehumidifier.COVERAGE,
]
),
@@ -28,6 +28,7 @@ REGISTRY = DeviceRegistry(
fridge.DEFROST_DELAY_NATIVE_DUPLICATE,
fridge.DEFROST_BLOCK_STATUS,
fridge.DEFINITE_TEMPERATURE_COOLER,
fridge.DEFINITE_TEMPERATURE_FREEZER,
fridge.DOORS_FALLBACK,
fridge.TEMPERATURES_FALLBACK,
fridge.ICEMAKER_STATUS_FALLBACK,
@@ -909,6 +909,80 @@ AIR_FILTER = Capability(
),
)
def _pm1_threshold_write(payload, rep, href=None):
"""Same contract as _threshold_write, against this filter's own href --
not yet confirmed live (no dump seen advertises
supportedFilterDesiredUsage for this href), so the Select this backs
stays gated behind that field's presence, same as AIR_FILTER's."""
return ["filter", "airdustPM1filter", "vs", "0"], {
"x.com.samsung.da.filterDesiredUsage": payload,
}
def _has_filter_field(field):
return lambda rep, resources: rep.get(field) is not None
# Second, PM1-rated dust filter some TP1X_FAC-class boards report alongside
# AIR_FILTER's /filter/airdustfilter/vs/0. TP1X_FAC_TIME_23K (issue #270)
# reports only filterCapacity/filterCapacityUnit/filterResetType
# ('notresetable') on this href, with no live filterUsage/filterStatus at
# all -- but the TP1X_DA-AC-CAC-01001_0000 cassette AC (issue #191) reports
# the identical href with full live fields. So this stays a real capability
# rather than a blanket ignore, with every entity individually gated on its
# own field's presence per the 'don't guess' rule: no entity on a board that
# has nothing behind it, real readings on one that does.
AIR_FILTER_PM1 = Capability(
href="/filter/airdustPM1filter/vs/0",
poll_tier="cold",
entities=(
SensorDesc(
key="air_filter_pm1_usage",
rep_fn=filter_usage_percent,
unit="%",
state_class="measurement",
icon="mdi:air-filter",
entity_category="diagnostic",
exists_fn=_has_filter_field("x.com.samsung.da.filterUsage"),
),
SensorDesc(
key="air_filter_pm1_usage_hours",
field="x.com.samsung.da.filterUsage",
device_class="duration",
state_class="total_increasing",
unit_fn=_filter_unit,
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=_int,
exists_fn=_has_filter_field("x.com.samsung.da.filterUsage"),
),
SelectDesc(
key="air_filter_pm1_threshold",
field="x.com.samsung.da.filterDesiredUsage",
options_field="x.com.samsung.da.supportedFilterDesiredUsage",
exists_fn=lambda rep, res: bool(
rep.get("x.com.samsung.da.supportedFilterDesiredUsage")
),
icon="mdi:alarm",
entity_category="config",
write_fn=_pm1_threshold_write,
value_fn=lambda v: str(v) if v is not None else None,
),
SensorDesc(
key="air_filter_pm1_status",
field="x.com.samsung.da.filterStatus",
device_class="enum",
options=("normal", "wash", "replace"),
translation_key="filter_status",
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=lambda v: v.lower() if isinstance(v, str) else v,
exists_fn=_has_filter_field("x.com.samsung.da.filterStatus"),
),
),
)
DISPLAY_LIGHT = Capability(
href="/light/vs/0",
poll_tier="cold",
@@ -927,6 +1001,51 @@ DISPLAY_LIGHT = Capability(
),
)
# UV-C sterilization LED (issue #270, TP1X_FAC_TIME_23K). Same On/Off
# convention as everything else on this board, and (unlike VENTILATION_ALARM
# below) a real supportedModes list confirms the value set.
UV_LED = Capability(
href="/uvled/vs/0",
poll_tier="cold",
entities=(
SwitchDesc(
key="uv_led",
field="x.com.samsung.da.modes",
icon="mdi:lightbulb-fluorescent-tube",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=lambda p, rep, href=None: (
["uvled", "vs", "0"],
{"x.com.samsung.da.modes": "On" if p == "On" else "Off"},
),
),
),
)
# Ventilation-reminder alarm toggle (issue #270). Only a bare `alarm` field
# is present -- no supportedModes list to confirm the value set against,
# unlike UV_LED above. Modeled as a switch on the strength of the same
# On/Off convention used everywhere else in this API; a rejected write is
# the worst case for a wrong guess (see the adding-device-support skill),
# but this hasn't been round-trip confirmed on real hardware.
VENTILATION_ALARM = Capability(
href="/ventilation/setting/vs/0",
poll_tier="cold",
entities=(
SwitchDesc(
key="ventilation_alarm",
field="alarm",
icon="mdi:bell-alert",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=lambda p, rep, href=None: (
["ventilation", "setting", "vs", "0"],
{"alarm": "On" if p == "On" else "Off"},
),
),
),
)
# Confirmed against issue #38's dump (TP1X_DA-AC-RAC-01001_0000): a single
# boolean field, no vendor prefix, mirroring the On/Off convention used
# throughout the rest of this API.
@@ -9,7 +9,7 @@ entities rather than folded into one card.
"""
from ..capability import Capability
from ..entities import NumberDesc, SelectDesc, SensorDesc
from ..entities import NumberDesc, SelectDesc, SensorDesc, SwitchDesc
from .common import int_or_none
@@ -77,6 +77,60 @@ HUMIDITY = Capability(
),
)
# Water-tank ambient light (issues #271/#231, TP1X_DA_AC_DHM_01001_0000):
# on/off, color, and brightness are three independent controls on this one
# resource. `waterfullAlarmStatus` differs between the two dumps that
# reported this href (On vs. Off) so it's a real live flag, not a constant --
# but its exact meaning (tank actually full vs. the chime feature merely
# enabled) isn't confirmed by either dump alone, and /alarms/vs/0's
# alarm_code already surfaces a live WaterTankFull condition when one fires
# (see common._active_alarm_codes), so this is exposed read-only as a plain
# diagnostic value rather than guessed at as a binary_sensor.
WATERTANK_LIGHTING = Capability(
href="/watertank/lighting/vs/0",
poll_tier="cold",
entities=(
SwitchDesc(
key="watertank_light",
field="status",
icon="mdi:led-on",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=lambda p, rep, href=None: (
["watertank", "lighting", "vs", "0"],
{"status": "On" if p == "On" else "Off"},
),
),
SelectDesc(
key="watertank_light_color",
field="colorOption",
icon="mdi:palette",
entity_category="config",
options_field="colorSupportedList",
write_fn=lambda p, rep, href=None: (
["watertank", "lighting", "vs", "0"],
{"colorOption": p},
),
),
SelectDesc(
key="watertank_light_brightness",
field="mode",
icon="mdi:brightness-6",
entity_category="config",
options_field="modeSupportedList",
write_fn=lambda p, rep, href=None: (
["watertank", "lighting", "vs", "0"],
{"mode": p},
),
),
SensorDesc(
key="watertank_full_alarm_status",
field="waterfullAlarmStatus",
entity_category="diagnostic",
),
),
)
# ---------------------------------------------------------------------------
# Dehumidifier-scoped coverage: vendor plumbing with no user-actionable state
# or no documented write contract, following the same 'don't guess' rule as
@@ -48,6 +48,18 @@ DISHWASHER_SETTINGS = Capability(
# boolean-toggle contract washer's bubble-soak/pre-wash/intensive switches
# use). Course display names live in translations under
# entity.select.dishwasher_cycle (see laundry.cycle_select).
#
# '83'/'86' were transposed in that catalog until issue #226: both the
# original DW9000F-class fixture this table was built from and the issue
# #226 reporter's board report the identical DeviceType_0812 (a real
# per-board-generation id also seen on unrelated washer/dryer fixtures, so
# this is one shared course table, not a Table_02/Table_03-style generation
# split), and the original fixture's own live editCourseList
# ('EditCourseList_0E07908683848D808E8F') puts '86' and '83' back to back at
# positions 4-5 -- exactly the 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) settles
# which way: '86' is Express 60, '83' is Normal.
# ---------------------------------------------------------------------------
CYCLE_OPTIONS = Capability(
@@ -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):
@@ -41,14 +41,25 @@ DRYER_SETTINGS = Capability(
# laundry.cycle_select (options read live from /wm/editcourse/vs/0, written as
# an RMW on the options array). Course display names live in translations
# under entity.select.dryer_cycle (Table_03, DV5000-class, captured
# 2026-05-29). Codes 0x21 and 0x4C appear in the issue #14 DV90BB5245AES1
# editCourseList but aren't identified yet -- they render as the raw code
# until named. Codes '01' Normal and '06' Time dry were confirmed on a
# 2026-05-29). Codes '01' Normal and '06' Time dry were confirmed on a
# DVE50A8600V/A3 (also Table_03) by selecting each cycle on the physical
# appliance and reading back the raw code from the entity's state (issue
# #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.
# #80). Codes '51' Eco Cotton, '53' AI Dry+, and '4e' Self Dry were
# confirmed the same way on a DV90DG6845LHU5 (issue #244). 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,
),
),
)
@@ -163,6 +163,34 @@ DEFINITE_TEMPERATURE_COOLER = Capability(
),
)
def _definite_freezer_write(p, rep, href=None):
return (
["temperature", "definite", "freezer", "vs", "0"],
{"x.com.samsung.da.definite.desired": p},
)
# Freezer half of the same discrete-setpoint pattern (issue #229): a
# fridge/freezer combo reporting no /temperature/current|desired/freezer
# pair, only this bundled vendor resource -- identical shape to
# DEFINITE_TEMPERATURE_COOLER above (down to the field names), just negative
# supportedList values (e.g. ['-23','-21','-19','-17','-15']).
DEFINITE_TEMPERATURE_FREEZER = Capability(
href="/temperature/definite/freezer/vs/0",
poll_tier="warm",
entities=(
SelectDesc(
key="freezer_temperature_setpoint",
field="x.com.samsung.da.definite.desired",
icon="mdi:thermometer",
entity_category="config",
options_field="x.com.samsung.da.definite.supportedList",
write_fn=_definite_freezer_write,
),
),
)
# ---------------------------------------------------------------------------
# Icemaker nighttime quiet mode
# ---------------------------------------------------------------------------
@@ -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",
@@ -278,9 +278,9 @@
"name": "Cyklus",
"state": {
"80": "Jemné",
"83": "Express 60",
"83": "Normální",
"84": "Intenzivní",
"86": "Normální",
"86": "Express 60",
"90": "Samočištění",
"0e": "AI mytí",
"07": "Předoplach",
@@ -325,7 +325,10 @@
"1e": "Outdoor",
"1f": "Smíšená náplň",
"2b": "Sušení bubnu",
"4c": "Osvěžení vzduchem"
"4c": "Osvěžení vzduchem",
"51": "Eko bavlna",
"53": "AI sušení+",
"4e": "Automatické sušení"
}
},
"favorite_capacity": {
@@ -621,6 +624,28 @@
},
"zone_mode": {
"name": "Režim zóny"
},
"watertank_light_color": {
"name": "Barva osvětlení nádržky na vodu",
"state": {
"warmwhite": "Teplá bílá",
"naturalwhite": "Přirozená bílá",
"coolwhite": "Studená bílá",
"green": "Zelená",
"purple": "Fialová",
"blue": "Modrá",
"orange": "Oranžová",
"pink": "Růžová"
}
},
"watertank_light_brightness": {
"name": "Jas osvětlení nádržky na vodu"
},
"air_filter_pm1_threshold": {
"name": "Práh alarmu PM1 filtru"
},
"freezer_temperature_setpoint": {
"name": "Teplota mrazicí zóny"
}
},
"sensor": {
@@ -969,6 +994,15 @@
},
"zone_temperature": {
"name": "Teplota zóny"
},
"watertank_full_alarm_status": {
"name": "Stav alarmu plné nádržky na vodu"
},
"air_filter_pm1_usage": {
"name": "Využití PM1 filtru"
},
"air_filter_pm1_usage_hours": {
"name": "Hodiny využití PM1 filtru"
}
},
"switch": {
@@ -1136,6 +1170,15 @@
},
"away_mode": {
"name": "Režim nepřítomnosti"
},
"watertank_light": {
"name": "Osvětlení nádržky na vodu"
},
"uv_led": {
"name": "UV LED"
},
"ventilation_alarm": {
"name": "Alarm větrání"
}
},
"time": {
@@ -278,9 +278,9 @@
"name": "Cycle",
"state": {
"80": "Delicate",
"83": "Express 60",
"83": "Normal",
"84": "Heavy",
"86": "Normal",
"86": "Express 60",
"90": "Self clean",
"0e": "AI Wash",
"07": "Pre blast",
@@ -325,7 +325,10 @@
"1e": "Outdoor",
"1f": "Mixed load",
"2b": "Self Tub Dry",
"4c": "Air Refresh"
"4c": "Air Refresh",
"51": "Eco Cotton",
"53": "AI Dry+",
"4e": "Self Dry"
}
},
"favorite_capacity": {
@@ -621,6 +624,28 @@
},
"zone_mode": {
"name": "Zone mode"
},
"watertank_light_color": {
"name": "Water tank light color",
"state": {
"warmwhite": "Warm white",
"naturalwhite": "Natural white",
"coolwhite": "Cool white",
"green": "Green",
"purple": "Purple",
"blue": "Blue",
"orange": "Orange",
"pink": "Pink"
}
},
"watertank_light_brightness": {
"name": "Water tank light brightness"
},
"air_filter_pm1_threshold": {
"name": "PM1 filter alarm threshold"
},
"freezer_temperature_setpoint": {
"name": "Freezer temperature"
}
},
"sensor": {
@@ -969,6 +994,15 @@
},
"zone_temperature": {
"name": "Zone temperature"
},
"watertank_full_alarm_status": {
"name": "Water tank full alarm status"
},
"air_filter_pm1_usage": {
"name": "PM1 filter usage"
},
"air_filter_pm1_usage_hours": {
"name": "PM1 filter usage hours"
}
},
"switch": {
@@ -1136,6 +1170,15 @@
},
"away_mode": {
"name": "Away mode"
},
"watertank_light": {
"name": "Water tank light"
},
"uv_led": {
"name": "UV LED"
},
"ventilation_alarm": {
"name": "Ventilation alarm"
}
},
"time": {
@@ -392,9 +392,9 @@
"name": "Ciclo",
"state": {
"80": "Delicado",
"83": "Express 60",
"83": "Normal",
"84": "Intenso",
"86": "Normal",
"86": "Express 60",
"90": "Autolimpieza",
"0e": "Lavado IA",
"07": "Prelavado intenso",
@@ -439,7 +439,10 @@
"1e": "Exterior",
"1f": "Carga mixta",
"2b": "Secado de tambor",
"4c": "Renovación de aire"
"4c": "Renovación de aire",
"51": "Eco algodón",
"53": "Secado IA+",
"4e": "Autosecado"
}
},
"favorite_capacity": {
@@ -735,6 +738,28 @@
},
"zone_mode": {
"name": "Modo de zona"
},
"watertank_light_color": {
"name": "Color de la luz del depósito de agua",
"state": {
"warmwhite": "Blanco cálido",
"naturalwhite": "Blanco natural",
"coolwhite": "Blanco frío",
"green": "Verde",
"purple": "Púrpura",
"blue": "Azul",
"orange": "Naranja",
"pink": "Rosa"
}
},
"watertank_light_brightness": {
"name": "Brillo de la luz del depósito de agua"
},
"air_filter_pm1_threshold": {
"name": "Umbral de alarma del filtro PM1"
},
"freezer_temperature_setpoint": {
"name": "Temperatura del congelador"
}
},
"sensor": {
@@ -1083,6 +1108,15 @@
},
"zone_temperature": {
"name": "Temperatura de zona"
},
"watertank_full_alarm_status": {
"name": "Estado de alarma de depósito de agua lleno"
},
"air_filter_pm1_usage": {
"name": "Uso del filtro PM1"
},
"air_filter_pm1_usage_hours": {
"name": "Horas de uso del filtro PM1"
}
},
"switch": {
@@ -1250,6 +1284,15 @@
},
"zone_power": {
"name": "Alimentación de zona"
},
"watertank_light": {
"name": "Luz del depósito de agua"
},
"uv_led": {
"name": "LED UV"
},
"ventilation_alarm": {
"name": "Alarma de ventilación"
}
},
"time": {
@@ -278,9 +278,9 @@
"name": "Ciclo",
"state": {
"80": "Delicato",
"83": "Express 60",
"83": "Normale",
"84": "Intenso",
"86": "Normale",
"86": "Express 60",
"90": "Autopulizia",
"0e": "AI Wash",
"07": "Prelavaggio intenso",
@@ -325,7 +325,10 @@
"1e": "Capi outdoor",
"1f": "Carico misto",
"2b": "Asciugatura cestello",
"4c": "Rinfresca"
"4c": "Rinfresca",
"51": "Eco cotone",
"53": "Asciugatura AI+",
"4e": "Autoasciugatura"
}
},
"favorite_capacity": {
@@ -621,6 +624,28 @@
},
"zone_mode": {
"name": "Modalità zona"
},
"watertank_light_color": {
"name": "Colore della luce del serbatoio dell'acqua",
"state": {
"warmwhite": "Bianco caldo",
"naturalwhite": "Bianco naturale",
"coolwhite": "Bianco freddo",
"green": "Verde",
"purple": "Viola",
"blue": "Blu",
"orange": "Arancione",
"pink": "Rosa"
}
},
"watertank_light_brightness": {
"name": "Luminosità della luce del serbatoio dell'acqua"
},
"air_filter_pm1_threshold": {
"name": "Soglia di allarme del filtro PM1"
},
"freezer_temperature_setpoint": {
"name": "Temperatura freezer"
}
},
"sensor": {
@@ -969,6 +994,15 @@
},
"zone_temperature": {
"name": "Temperatura zona"
},
"watertank_full_alarm_status": {
"name": "Stato allarme serbatoio d'acqua pieno"
},
"air_filter_pm1_usage": {
"name": "Utilizzo del filtro PM1"
},
"air_filter_pm1_usage_hours": {
"name": "Ore di utilizzo del filtro PM1"
}
},
"switch": {
@@ -1136,6 +1170,15 @@
},
"away_mode": {
"name": "Modalità assenza"
},
"watertank_light": {
"name": "Luce del serbatoio dell'acqua"
},
"uv_led": {
"name": "LED UV"
},
"ventilation_alarm": {
"name": "Allarme ventilazione"
}
},
"time": {
@@ -278,9 +278,9 @@
"name": "Programma",
"state": {
"80": "Delicaat",
"83": "Express 60",
"83": "Normaal",
"84": "Intensief",
"86": "Normaal",
"86": "Express 60",
"90": "Zelfreiniging",
"0e": "AI Wash",
"07": "Voorspoelen",
@@ -325,7 +325,10 @@
"1e": "Outdoor",
"1f": "Gemengde was",
"2b": "Zelf trommel drogen",
"4c": "Opfrissen"
"4c": "Opfrissen",
"51": "Eco katoen",
"53": "AI drogen+",
"4e": "Zelf drogen"
}
},
"favorite_capacity": {
@@ -621,6 +624,28 @@
},
"zone_mode": {
"name": "Zonemodus"
},
"watertank_light_color": {
"name": "Kleur van het waterreservoirlicht",
"state": {
"warmwhite": "Warm wit",
"naturalwhite": "Natuurlijk wit",
"coolwhite": "Koel wit",
"green": "Groen",
"purple": "Paars",
"blue": "Blauw",
"orange": "Oranje",
"pink": "Roze"
}
},
"watertank_light_brightness": {
"name": "Helderheid van het waterreservoirlicht"
},
"air_filter_pm1_threshold": {
"name": "PM1-filteralarmdrempel"
},
"freezer_temperature_setpoint": {
"name": "Temperatuur vriesgedeelte"
}
},
"sensor": {
@@ -969,6 +994,15 @@
},
"zone_temperature": {
"name": "Zonetemperatuur"
},
"watertank_full_alarm_status": {
"name": "Status alarm waterreservoir vol"
},
"air_filter_pm1_usage": {
"name": "PM1-filtergebruik"
},
"air_filter_pm1_usage_hours": {
"name": "PM1-filtergebruik in uren"
}
},
"switch": {
@@ -1136,6 +1170,15 @@
},
"away_mode": {
"name": "Afwezigheidsmodus"
},
"watertank_light": {
"name": "Waterreservoirlicht"
},
"uv_led": {
"name": "Uv-led"
},
"ventilation_alarm": {
"name": "Ventilatiealarm"
}
},
"time": {
@@ -0,0 +1,641 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/personality/presence/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "",
"x.com.samsung.da.deviceId": "**REDACTED**",
"x.com.samsung.da.value": ""
}
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "true",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "100",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterDesiredUsage": "224",
"x.com.samsung.da.filterStatus": "wash",
"x.com.samsung.da.filterCapacity": "224",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"washable"
],
"x.com.samsung.da.supportedFilterDesiredUsage": [
"112",
"224",
"336",
"448"
]
}
},
{
"href": "/filter/airdustPM1filter/vs/0",
"rep": {
"x.com.samsung.da.filterCapacity": "3",
"x.com.samsung.da.filterCapacityUnit": "Month",
"x.com.samsung.da.filterResetType": [
"notresetable"
]
}
},
{
"href": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "0.5"
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Nano",
"NanoSleep"
]
}
},
{
"href": "/option/airpurify/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "On",
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.supportedStatus": [
"Start",
"SpeedClean",
"QuietClean",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"SpeedClean",
"QuietClean",
"Off"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "12",
"x.com.samsung.da.supportedModes": [
"0",
"11",
"12",
"13",
"14",
"15",
"16"
],
"x.com.samsung.da.modesName": [
"Auto",
"1",
"2",
"3",
"4",
"5",
"Max"
]
}
},
{
"href": "/wind/direction/vs/0",
"rep": {
"x.com.samsung.da.modes": "NotSupported",
"x.com.samsung.da.supportedModes": [
"NotSupported"
]
}
},
{
"href": "/subdevices/vs/0",
"rep": {
"x.com.samsung.da.subdeviceIdList": "**REDACTED**"
}
},
{
"href": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-08-03T14:14:25"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "AC_V_0002_OFF",
"x.com.samsung.da.triggeredTime": "2026-08-03T14:14:25"
}
]
}
},
{
"href": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Temperature",
"x.com.samsung.da.desired": "27.0",
"x.com.samsung.da.current": "28.0",
"x.com.samsung.da.maximum": "30",
"x.com.samsung.da.minimum": "18",
"x.com.samsung.da.increment": "0.5",
"x.com.samsung.da.unit": "Celsius"
}
]
}
},
{
"href": "/temperature/current/0",
"rep": {
"range": [
18.0,
30.0
],
"units": "C",
"temperature": 28.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
18.0,
30.0
],
"units": "C",
"temperature": 27.0
}
},
{
"href": "/diagnosis/vs/0",
"rep": {}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeConsumption": "400.000000",
"x.com.samsung.da.instantaneousPower": "14.000000",
"x.com.samsung.da.cumulativePower": "882861",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeDate": "1785798000",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "individual"
}
},
{
"href": "/energy/consumption/0",
"rep": {
"energy": 400.0,
"power": 14.0
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"AIComfort",
"Cool",
"Dry",
"Wind"
],
"x.com.samsung.da.modes": [
"Cool"
],
"x.com.samsung.da.options": [
"ArtificialWorking_Off",
"welcomecare_Off",
"Volume_Mute",
"Operation_Family",
"ComfortAICooling_Off",
"OnTimer_0",
"OffTimer_0",
"Sleep_0",
"Booster_Off",
"OutdoorSilence_Off",
"AiTempChanged_Off",
"AiTemp_250",
"StopAutoClean_Idle",
"Weather_Off",
"Service_Off",
"SmartCoolClean_Off",
"ProgressSmartClean_0",
"OutDoorVentil_Off",
"InDoorVentil_Off",
"FreezeAlarmSetting_Off",
"DesiredFreezeAlarm_240",
"ProtectOperation_Off",
"EnergySaveIcon_Off",
"ExtendOptionCode_16975",
"OptionCode_2589",
"RacInfo_First",
"ModelInfo_16K_BORA_VENT2",
"RacInfo_None_Second",
"OutdoorConnection_Connected",
"DurationOn_0",
"UpdateAllow_NotAllowed",
"welcomecareElapsedTime_0",
"welcomecareThresholdTemp_0",
"welcomecareStartDate_0000",
"welcomecareEndDate_0000",
"welcomecareSeason_None"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off",
"causeSource": "SMTS"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_FAC_TIME_23K|10250841|600007110017112BCE00CE2200937000",
"x.com.samsung.da.description": "TP1X_FAC_TIME_23K",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "AF2",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.serialNumOption": "**REDACTED**",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02517A260330",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "02508A23020200,02509A24030701",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Outdoor",
"x.com.samsung.da.number": "02502A23011300,02299A10000300"
},
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "LED",
"x.com.samsung.da.number": "02510A22121900,FFFFFFFFFFFFFF"
},
{
"x.com.samsung.da.id": "4",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "BLERemoteController",
"x.com.samsung.da.number": "22021760"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "1138000000",
"x.com.samsung.da.airconOptionList": [
"HOMECARE_WIZARD_V2",
"ENERGY_2.0",
"AI_2.0",
"DeviceTypeMaster",
"CLEAN_OPTION_SUPPORT",
"DR",
"SingleCommand_1"
]
}
},
{
"href": "/humidity/0",
"rep": {
"humidity": 0
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "0.000000",
"x.com.samsung.da.fivepercentHumidity": "71"
}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 0,
"start": "1970-01-01T00:00:00Z",
"durationminutes": 0,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/runn/vs/0",
"rep": {
"x.com.samsung.da.runningMode": 0
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "000000B4012C0000000D04000000",
"x.com.samsung.da.id": "FAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/uvled/vs/0",
"rep": {
"x.com.samsung.da.modes": "On",
"x.com.samsung.da.supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/remotedatacontrol/vs/0",
"rep": {
"x.com.samsung.da.status": "Off",
"x.com.samsung.da.connectionStatus": "Disconnected"
}
},
{
"href": "/remotetemperature/vs/0",
"rep": {
"x.com.samsung.da.temperature": "",
"x.com.samsung.da.unit": "",
"x.com.samsung.da.error": ""
}
},
{
"href": "/aisleep/vs/0",
"rep": {
"x.com.samsung.da.displayNightMode": "Off",
"x.com.samsung.da.elapsedTime": "0",
"x.com.samsung.da.requestFeedback": "Off",
"x.com.samsung.da.resultFeedback": "0",
"x.com.samsung.da.statusFeedback": "Idle",
"x.com.samsung.da.sleepTime": "14002200"
}
},
{
"href": "/remotedeviceinfo/vs/0",
"rep": {
"x.com.samsung.da.didList": ""
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "noHistory",
"otnList": [
{
"type": "WIFI",
"modelId": "AFA-WW-TP1-23-AFXX00",
"versions": [
"10260330"
],
"visVersion": "260330"
},
{
"type": "Micom",
"modelId": "845110250241FFFFFFFF",
"versions": [
"23011300",
"FFFFFFFF"
],
"visVersion": "230113"
},
{
"type": "Micom",
"modelId": "04511025084110250941",
"versions": [
"23020200",
"24030701"
],
"visVersion": "240307"
},
{
"type": "Micom",
"modelId": "04511025024110229941",
"versions": [
"23011300",
"10000300"
],
"visVersion": "230113"
},
{
"type": "Micom",
"modelId": "045110240441FFFFFFFF",
"versions": [
"23121900",
"FFFFFFFF"
],
"visVersion": "231219"
},
{
"type": "Micom",
"modelId": "045110251041FFFFFFFF",
"versions": [
"22121900",
"FFFFFFFF"
],
"visVersion": "221219"
}
]
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"href": "/selfcheck/vs/0",
"rep": {
"x.com.samsung.da.start": "Cancel",
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.progress": "1",
"x.com.samsung.da.result": "NA",
"x.com.samsung.da.supportedActions": [
"Start",
"Cancel"
],
"x.com.samsung.da.error": [
"DA_ERROR_NONE"
]
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "1DFFFFFFFF121E121EFFFFFFFFFFFF01000000013F0100013F000000001C0C",
"x.com.samsung.da.id": "FAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/ventilation/setting/vs/0",
"rep": {
"alarm": "Off"
}
},
{
"href": "/light/vs/0",
"rep": {
"mode": "Off",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+426
View File
@@ -0,0 +1,426 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/personality/presence/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "",
"x.com.samsung.da.deviceId": "**REDACTED**",
"x.com.samsung.da.value": ""
}
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "false",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "100",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterDesiredUsage": "224",
"x.com.samsung.da.filterStatus": "wash",
"x.com.samsung.da.filterCapacity": "224",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"washable"
],
"x.com.samsung.da.supportedFilterDesiredUsage": [
"224",
"300",
"500",
"700"
]
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "On",
"x.com.samsung.da.progress": "0",
"cleaningTime": "30",
"minCleaningTime": "10",
"maxCleaningTime": "60",
"cleaningTimeUnit": "10",
"x.com.samsung.da.supportedStatus": [
"Start",
"TimedClean",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"TimedClean",
"Off"
]
}
},
{
"href": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-08-03T14:54:15",
"x.com.samsung.da.state": "Deleted"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "WaterTankFull_OFF",
"x.com.samsung.da.triggeredTime": "2026-08-03T14:54:15",
"x.com.samsung.da.state": "Deleted"
}
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "145.000000",
"x.com.samsung.da.cumulativePower": "101100",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "total"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Smart",
"Max",
"High",
"Medium",
"Quiet",
"ClothesDrying"
],
"x.com.samsung.da.modes": [
"Quiet"
],
"x.com.samsung.da.options": [
"Volume_Mute",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"WelcomeCoolingState_Off"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On",
"operationNumber": "1",
"displayCondition": "Enable"
}
},
{
"href": "/sensors/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA_AC_DHM_01001_0000|10283341|77000000001700000A00000000000000",
"x.com.samsung.da.description": "TP1X_DA_AC_DHM_01001_0000",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "AY0",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02544A260401",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "02833A26010600,FFFFFFFFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Outdoor",
"x.com.samsung.da.number": "02590A10000100,FFFFFFFFFFFFFF"
},
{
"x.com.samsung.da.id": "4",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Touch IC",
"x.com.samsung.da.number": "02552A23020200,FFFFFFFFFFFFFF"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.airconOptionList": [
"SingleCommand_1",
"DR",
"HOMECARE_WIZARD_V2",
"PRODUCT_GLOBAL",
"AI_RAC_GLOBAL_2.0",
"AI_Heat"
]
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "63",
"x.com.samsung.da.desiredHumidity": "60",
"increment": "5",
"x.com.samsung.da.fivepercentHumidity": "64"
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "04001E46070A5A00000000000000",
"x.com.samsung.da.id": "DHM",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/da/softreset/vs/0",
"rep": {
"x.com.samsung.da.softwarereset": "false"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "2026-06-24",
"otnList": [
{
"type": "WIFI",
"modelId": "ADH-WW-TP1-23-AY7100",
"versions": [
"18260401"
],
"visVersion": "260401"
},
{
"type": "Micom",
"modelId": "042110283341FFFFFFFF",
"versions": [
"26010600",
"FFFFFFFF"
],
"visVersion": "260106"
},
{
"type": "Micom",
"modelId": "042110259041FFFFFFFF",
"versions": [
"10000100",
"FFFFFFFF"
],
"visVersion": "100001"
},
{
"type": "Micom",
"modelId": "042110255241FFFFFFFF",
"versions": [
"23020200",
"FFFFFFFF"
],
"visVersion": "230202"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "3F1E461E461E46FFFFFFFF1E4600",
"x.com.samsung.da.id": "DHM",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/display/vs/0",
"rep": {
"mode": "On",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/welcome/humidity/vs/0",
"rep": {
"requestId": "0000",
"operatingStatus": "None"
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/watertank/lighting/vs/0",
"rep": {
"status": "On",
"colorOption": "NaturalWhite",
"mode": "High",
"waterfullAlarmStatus": "On",
"colorSupportedList": [
"WarmWhite",
"NaturalWhite",
"CoolWhite",
"Green",
"Purple",
"Blue",
"Orange",
"Pink"
],
"modeSupportedList": [
"High",
"Low"
]
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+438
View File
@@ -0,0 +1,438 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/alarms/vs/0",
"rep": {}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePower": "211700",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.cumulativeDate": "1785693600",
"x.com.samsung.da.cumulativeDateUTC": "1785686400",
"x.com.samsung.da.cumulativeSavedPower": "0"
}
},
{
"href": "/energy/consumption/0",
"rep": {}
},
{
"href": "/course/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.options": [
"DeviceType_0165",
"UpdateAllow_NotAllowed",
"Course_16",
"AiOption_On",
"MixedLoadBell_Disable",
"MixedLoadBellNoti_Nothing",
"LaundryOutTime_0",
"SeamlessControl_Enable",
"KidsLockBypass_On",
"WashingTimes_25",
"DrumCleanProposal_25",
"DetergentOnce_1",
"DetergentLeft_0",
"DetergentBase_5",
"DetergentAlarm_Off",
"DetergentType_3",
"DetergentTotal_0",
"SpecialFunction_20",
"AvailableDelayTime_215",
"LaundryPlannerUserSetTime_0",
"ProgressTimeSet_B214DCB4003C",
"WrinklePreventRunning_Off",
"QuickDrive_Off",
"SendToDevice_Off",
"GMT_04",
"DrumLight_Off",
"DryTimeSet_FF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060007FE020007FE020007FE0000000000000000",
"EnergyLevelSet_05030403050302040503050203030402010205",
"MostUsed_16D31E",
"WrinklePreventSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0FF0",
"MixedLoadBellSet_0202FF0202FFFFFF02FFFFFFFFFFFFFFFFFF",
"EmptyingTheBucket_Nothing",
"SavingModeCondition_01010202171D0300",
"SavingMode_Off",
"QuickDriveSet_F0000000F000000000000000000000000000",
"GeoFenceAlarm",
"UsagesDB_ok",
"EnergyKW_396",
"DrumCleanLog_2023-12-03T22:07:18|2024-02-09T15:18:59|2024-07-07T23:06:16|2024-10-19T19:29:45|2025-01-26T16:10:23|2025-05-10T19:59:59|2025-07-13T14:58:26|2026-01-09T21:27:43|2026-03-21T18:52:46|2026-05-24T21:38:08",
"TimeSync_NotSupported"
],
"x.com.samsung.da.supportedOptions": [
"116D31E29D31E23D00017D31E18D21E19D20C1DD3181BD3181CD31E21D4101AD1021ED31820D10227D00025D00024D0004CD21C2BD000"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/cycleinterface/vs/0",
"rep": {
"x.com.samsung.da.cycleInterfaceEnabled": "On"
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/kidslock/0",
"rep": {
"value": false
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.remainingTime": "03:35:00",
"x.com.samsung.da.progressPercentage": "1",
"x.com.samsung.da.progress": "None",
"x.com.samsung.da.delayEndTime": "00:00:00",
"x.com.samsung.da.supportedProgress": [
"None",
"Drying",
"Cooling",
"Finish"
]
}
},
{
"href": "/operational/state/0",
"rep": {
"currentMachineState": "**REDACTED**",
"machineStates": "**REDACTED**",
"jobStates": [
"None",
"Drying",
"Cooling",
"Finish"
],
"currentJobState": "None",
"remainingTime": "03:35:00",
"progressPercentage": "1"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "DA_WM_TP1_21_COMMON|20317441|30010102001611000AA3031F00000000",
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON_DV9400B/DC92-03267A_2C56",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "DR1",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON|20317441|30010102001611000AA3031F00000000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02986A260118(A182)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Firmware_1_DB_20317441221114080FFFFF203267412210245204FFFF(01652031744120326741_30221118)(FileDown:0)(Type:0)",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "03174A22111408,03267A22102452",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Firmware_2_DB_2032244122050903042FFFFFFFFFFFFFFFFFFFFFFFFE(016520322441FFFFFFFF_30000000)(FileDown:0)(Type:0)",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "03224A22050903,FFFFFFFFFFFFFF"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+02:00"
}
},
{
"href": "/washer/vs/0",
"rep": {
"x.com.samsung.da.wrinklePrevent": "Off",
"x.com.samsung.da.dryLevel": "Normal",
"x.com.samsung.da.supportedDryLevel": [
"None",
"Damp",
"Less",
"Normal",
"More"
],
"x.com.samsung.da.dryTime": "00:00:00",
"x.com.samsung.da.supportedDryTime": [
"00:00:00",
"00:20:00",
"00:30:00",
"00:40:00",
"00:50:00",
"01:00:00",
"01:30:00",
"02:00:00",
"02:30:00",
"03:00:00",
"04:00:00"
],
"x.com.samsung.da.dryerType": "Electricity"
}
},
{
"href": "/st/dryercourse/vs/0",
"rep": {
"x.com.samsung.da.st.dryerMode": "Table_03_Course_16",
"x.com.samsung.da.st.courseTable": "Table_03"
}
},
{
"href": "/setting/vs/0",
"rep": {}
},
{
"href": "/wm/editcourse/vs/0",
"rep": {
"x.com.samsung.da.editCourseList": "EditCourseList_161B1D29231718191C211A1E202725244C2B",
"x.com.samsung.da.fixedCourseList": "FixedCourseList_1618202B"
}
},
{
"href": "/wm/setinfo/vs/0",
"rep": {
"x.com.samsung.da.isModelSettingWithoutSC": "true",
"x.com.samsung.da.aiCourse": "false",
"x.com.samsung.da.isModelSettingPowerOnOff": "false",
"x.com.samsung.da.modelCode": "M(None),W(DV90BB7445GES7)"
}
},
{
"href": "/wm/jobbeginingstatus/vs/0",
"rep": {
"x.com.samsung.da.currentStatus": "None"
}
},
{
"href": "/wm/welcomemsg/vs/0",
"rep": {}
},
{
"href": "/wm/personalcourse/vs/0",
"rep": {
"x.com.samsung.da.courses": [
"F1_00",
"F2_00",
"F3_00",
"F4_00",
"F5_00",
"F6_00",
"F7_00",
"F8_00",
"F9_00",
"FA_00"
],
"x.com.samsung.da.maxCourseNum": "10"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "2026-03-04",
"otnList": [
{
"type": "WIFI",
"modelId": "DA_WM_TP1_21_COMMON",
"versions": [
"30260118"
],
"visVersion": "260118"
},
{
"type": "Micom",
"modelId": "01652031744120326741",
"versions": [
"22111408",
"22102452"
],
"visVersion": "221114"
},
{
"type": "Micom",
"modelId": "016520322441FFFFFFFF",
"versions": [
"22050903",
"FFFFFFFF"
],
"visVersion": "220509"
}
]
}
},
{
"href": "/buzzersound/vs/0",
"rep": {
"supportedBuzzerSound": [
"Off",
"On"
],
"setBuzzerSound": "Off"
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "false"
}
},
{
"href": "/remotectrl/0",
"rep": {
"value": false
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.countryCode": "CZ"
}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 0,
"start": "0000-00-00T00:00:00Z",
"duration": 0,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.start": "0000-00-00T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Europe/Prague",
"offset": "+02:00",
"DST": "ON"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+5 -1
View File
@@ -2,6 +2,9 @@
"state_keys": [
"absence_power_saving_active",
"absence_power_saving_mode",
"air_filter_pm1_status",
"air_filter_pm1_usage",
"air_filter_pm1_usage_hours",
"air_filter_status",
"air_filter_usage",
"air_filter_usage_hours",
@@ -29,6 +32,7 @@
"selfcheck_result",
"selfcheck_status",
"super_fine_dust",
"tropical_night_mode"
"tropical_night_mode",
"uv_led"
]
}
@@ -0,0 +1,33 @@
{
"state_keys": [
"air_filter_status",
"air_filter_threshold",
"air_filter_usage",
"air_filter_usage_hours",
"air_purify",
"alarm_code",
"auto_clean",
"auto_clean_progress",
"auto_clean_running",
"beep",
"climate",
"current_temperature_c",
"diagnosis_status",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"odor_controller_active",
"odor_controller_progress",
"power_energy_kwh",
"power_watts",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status",
"tropical_night_mode",
"uv_led",
"ventilation_alarm"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"state_keys": [
"air_filter_status",
"air_filter_threshold",
"air_filter_usage",
"air_filter_usage_hours",
"alarm_code",
"auto_clean",
"auto_clean_progress",
"auto_clean_running",
"child_lock",
"display",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"operating_mode",
"power_switch",
"power_watts",
"target_humidity",
"watertank_full_alarm_status",
"watertank_light",
"watertank_light_brightness",
"watertank_light_color"
]
}
+28
View File
@@ -0,0 +1,28 @@
{
"state_keys": [
"alarm_code",
"buzzer_sound",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
"diagnosis",
"drum_clean_cycles_remaining",
"drum_clean_last_cleaned",
"dry_level",
"dry_time",
"dryer_type",
"energy_kwh",
"energy_saved_kwh",
"finish_time",
"firmware_update",
"job_beginning_status",
"machine_state",
"power_switch",
"progress",
"progress_percentage",
"remote_control",
"wrinkle_prevent"
]
}
@@ -0,0 +1,30 @@
{
"state_keys": [
"ai_energy_level",
"alarm_code",
"auto_door_opener",
"brightness_level",
"cabinet_light_dim",
"cabinet_light_switch",
"cooler_temperature_setpoint",
"day_brightness",
"door_alert",
"door_cooler_open",
"door_freezer_open",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"flex_zone_mode",
"freezer_temperature_setpoint",
"fridge_sound",
"night_end",
"night_start",
"power_energy_kwh",
"power_watts",
"rapid_freezing",
"rapid_fridge",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status"
]
}
+1
View File
@@ -10,6 +10,7 @@
"detergent_low",
"diagnosis_status",
"drum_clean_cycles_remaining",
"drum_clean_last_cleaned",
"dry_level",
"energy_kwh",
"finish_time",
+1
View File
@@ -9,6 +9,7 @@
"detergent_low",
"diagnosis_status",
"drum_clean_cycles_remaining",
"drum_clean_last_cleaned",
"energy_kwh",
"finish_time",
"job_beginning_status",
+2 -1
View File
@@ -11,6 +11,7 @@
"detergent_water_hardness",
"diagnosis_status",
"drum_clean_cycles_remaining",
"drum_clean_last_cleaned",
"energy_kwh",
"finish_time",
"firmware_update",
@@ -27,4 +28,4 @@
"spin_speed",
"wash_temperature"
]
}
}
+1
View File
@@ -9,6 +9,7 @@
"detergent_low",
"diagnosis_status",
"drum_clean_cycles_remaining",
"drum_clean_last_cleaned",
"energy_kwh",
"finish_time",
"firmware_update",
@@ -0,0 +1,467 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/alarms/vs/0",
"rep": {
"href": "/alarms/vs/0",
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/cabinet/light/total/vs/0",
"rep": {
"x.com.samsung.da.lightLevel": "100",
"x.com.samsung.da.lightResolution": "3",
"x.com.samsung.da.lightControl.off.include": "Off",
"x.com.samsung.da.lightControl": "Off",
"x.com.samsung.da.lightControl.hide": "true",
"light.dimming.status": "Off",
"href": "/cabinet/light/total/vs/0",
"rt": [
"x.com.samsung.da.cabinetlight"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/settings/sound/alert/door/vs/0",
"rep": {
"alert.door": "1",
"supportedAlert.door": [
"1",
"2",
"3",
"4"
],
"href": "/settings/sound/alert/door/vs/0",
"rt": [
"x.com.samsung.alert.door"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/cabinet/light/enhanced/vs/0",
"rep": {
"light.control.status": "On",
"level.brightness.daytime": "100",
"level.brightness.nighttime": "66",
"night.starttime": "2026-07-31T15:30:00",
"night.duration.minute": "540",
"timezone.offset": "+05:30",
"href": "/cabinet/light/enhanced/vs/0",
"rt": [
"x.com.samsung.da.light.enhanced"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "",
"x.com.samsung.da.countryCode": "",
"href": "/configuration/vs/0"
}
},
{
"href": "/temperature/definite/freezer/vs/0",
"rep": {
"x.com.samsung.da.definite.desired": "-19",
"x.com.samsung.da.definite.supportedList": [
"-23",
"-21",
"-19",
"-17",
"-15"
],
"x.com.samsung.da.unit": "Celsius",
"href": "/temperature/definite/freezer/vs/0"
}
},
{
"href": "/temperature/definite/cooler/vs/0",
"rep": {
"x.com.samsung.da.definite.desired": "4",
"x.com.samsung.da.definite.supportedList": [
"1",
"2",
"3",
"4",
"7"
],
"x.com.samsung.da.unit": "Celsius",
"href": "/temperature/definite/cooler/vs/0"
}
},
{
"href": "/door/freezer/0",
"rep": {
"openState": "Close",
"href": "/door/freezer/0",
"rt": [
"oic.r.door"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/door/cooler/0",
"rep": {
"openState": "Close",
"href": "/door/cooler/0",
"rt": [
"oic.r.door"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.openState": "Close",
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Door"
},
{
"x.com.samsung.da.openState": "Close",
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Door"
}
],
"href": "/doors/vs/0"
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.override": "Not_Supported",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.realSaving": "Off",
"href": "/drlc/vs/0"
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeConsumption": "1274",
"x.com.samsung.da.instantaneousPower": "46",
"x.com.samsung.da.cumulativePower": "34106",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"href": "/energy/consumption/vs/0",
"x.com.samsung.da.cumulativeDateUTC": "1785402720"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+05:30",
"x.com.samsung.supprtedtype": 1,
"href": "/file/information/vs/0"
}
},
{
"href": "/refrigeration/vs/0",
"rep": {
"x.com.samsung.da.rapidFridge": "Off",
"x.com.samsung.da.rapidFreezing": "Off",
"href": "/refrigeration/vs/0",
"rt": [
"x.com.samsung.da.fridge"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/energy/ailevel/vs/0",
"rep": {
"aiLevel": "1",
"supportedAiLevel": [
"1",
"2"
],
"href": "/energy/ailevel/vs/0"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_REF_21K|70657341|00060123001811344100000020090400",
"x.com.samsung.da.description": "TP1X_REF_21K",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "RT1",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "WiFi Module",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "260617",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Micom",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "2602020C, FFFFFFFF, 24062000, FFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
}
],
"href": "/information/vs/0"
}
},
{
"href": "/status/lock/vs/0",
"rep": {
"x.com.samsung.da.device.sound": "On",
"x.com.samsung.da.doorAlarmSound": "On",
"href": "/status/lock/vs/0",
"rt": [
"x.com.samsung.da.lockstatus"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.modes": [
"RVACATION_OFF",
"FCONVERT_FREEZER"
],
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2",
"ENERGY_REPORT_MODEL",
"18K_REF_OUTDOOR_CONTROL_V2",
"VACATION_MODE_CONTROL"
],
"x.com.samsung.da.supportedOptions": [
"FCONVERT_FREEZER",
"FCONVERT_FRIDGE",
"FCONVERT_OFF"
],
"href": "/mode/vs/0",
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true",
"href": "/realtimenotiforclient/vs/0"
}
},
{
"href": "/runningmode/vs/0",
"rep": {
"x.com.samsung.da.runningMode": 0,
"href": "/runningmode/vs/0"
}
},
{
"href": "/selfcheck/vs/0",
"rep": {
"x.com.samsung.da.supportedActions": [
"Start"
],
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.result": "Success",
"x.com.samsung.da.error": [
"ErrorCode_None"
],
"href": "/selfcheck/vs/0"
}
},
{
"href": "/rm/control/vs/0",
"rep": {
"minPeriod": "9000",
"href": "/rm/control/vs/0"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"otnStatus": "None",
"flashingProgress": "0",
"otnCompleteDate": "noHistory",
"scheduledTime": "None",
"swVersionInfo": {
"platform": "Tizen Lite",
"oneUiVersion": "7.0 Refrigerator",
"osVersion": "4.0"
},
"otnList": [
{
"type": "WIFI",
"modelId": "A-RFWW-TP1-24-T4-COM",
"versions": [
"20260617"
],
"visVersion": "260617"
},
{
"type": "Micom",
"modelId": "835070657341FFFFFFFF",
"versions": [
"2602020C",
"FFFFFFFF"
],
"visVersion": "260202"
},
{
"type": "Micom",
"modelId": "035070657341FFFFFFFF",
"versions": [
"2602020C",
"FFFFFFFF"
],
"visVersion": "260202"
},
{
"type": "Micom",
"modelId": "035070658641FFFFFFFF",
"versions": [
"24062000",
"FFFFFFFF"
],
"visVersion": "240620"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Kolkata",
"offset": "+05:30",
"DST": "OFF"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "IoT0"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
},
{
"href": "/dginformation/vs/0",
"rep": {
"enrolmentstatus": "UnEnrolled",
"devicestate": "Deactivated",
"lockstatus": "Unknown",
"nextduedate": "",
"workingminutes": 0,
"paymentinfo": {
"emiplan": "Unknown",
"currency": "Unknown",
"totalemi": 0,
"totalemipaid": 0
}
}
}
]
}
+12 -8
View File
@@ -6,12 +6,18 @@ token -- this board was the one exception (its oneUiVersion self-reports
"7.0 Air conditioner", but 'CAC' had never been added to the board-token
table), so it silently fell back to common caps and lost its climate entity.
This dump is NOT fully covered yet -- ten hrefs remain unbound (edge
lighting, PM1 filter, a second stateful light resource, absence-clean, four
`/settings/sound/*` resources, smart-sensing-cooling, UV LED), all genuinely
new to this board generation. That's a real device-support gap, left
documented here rather than guessed at, per the 'don't guess' rule -- fixing
the routing regression was the scope of #191.
This dump is NOT fully covered yet -- eight hrefs remain unbound (edge
lighting, a second stateful light resource, absence-clean, four
`/settings/sound/*` resources, smart-sensing-cooling), all genuinely new to
this board generation. That's a real device-support gap, left documented
here rather than guessed at, per the 'don't guess' rule -- fixing the
routing regression was the scope of #191.
/uvled/vs/0 and /filter/airdustPM1filter/vs/0 used to be on this list too,
until issue #270 (TP1X_FAC_TIME_23K) added real capabilities for both --
this board's own live filterUsage/filterStatus data on the PM1 filter binds
through the same exists_fn-gated entities #270's dump (which has neither
field) leaves empty.
"""
from custom_components.localthings.registry.adapter import flatten
@@ -22,7 +28,6 @@ from tests.conftest import _load_device
_STILL_UNBOUND = frozenset(
{
"/edgelighting/vs/0",
"/filter/airdustPM1filter/vs/0",
"/light/stateful/vs/0",
"/mds/absenceclean/vs/0",
"/settings/sound/mode/vs/0",
@@ -30,7 +35,6 @@ _STILL_UNBOUND = frozenset(
"/settings/sound/output/vs/0",
"/settings/sound/volume/vs/0",
"/smartsensingcooling/vs/0",
"/uvled/vs/0",
}
)
@@ -0,0 +1,83 @@
"""Tests for the TP1X_FAC_TIME_23K air conditioner (issue #270).
A 2-in-1 system (x.com.samsung.da.numofsubdevice='2') whose UUID-prefixed
sibling materializes via the flat fallback (issue #205) -- unrelated to the
three capability gaps this fixture actually locks in (UV LED, ventilation
alarm, and the unmodeled PM1 filter), which live entirely on canonical
hrefs already exercised by the master's own resources.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import SwitchDesc
from tests.conftest import _load_device
def _airconditioner():
resources = _load_device("airconditioner_tp1x_fac_time_23k")
info = resources["/information/vs/0"]
reg = for_device_by_model(
info["x.com.samsung.da.modelNum"],
info["x.com.samsung.da.description"],
)
return reg, resources
def _bound():
reg, resources = _airconditioner()
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
def _state():
bound, resources = _bound()
return flatten(bound, resources)
def _desc(key):
bound, _ = _bound()
return next(b.desc for b in bound if b.desc.key == key)
def test_model_resolves_to_airconditioner_registry():
reg, _ = _airconditioner()
assert reg is not None and reg.name == "airconditioner"
def test_no_unbound_hrefs():
reg, resources = _airconditioner()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_uv_led_switch_reads_on_and_has_write_contract():
state = _state()
assert state["uv_led"] is True
desc = _desc("uv_led")
assert isinstance(desc, SwitchDesc)
path, body = desc.write_fn("Off", {})
assert path == ["uvled", "vs", "0"]
assert body == {"x.com.samsung.da.modes": "Off"}
def test_ventilation_alarm_switch_reads_off_and_has_write_contract():
state = _state()
assert state["ventilation_alarm"] is False
desc = _desc("ventilation_alarm")
assert isinstance(desc, SwitchDesc)
path, body = desc.write_fn("On", {})
assert path == ["ventilation", "setting", "vs", "0"]
assert body == {"alarm": "On"}
def test_pm1_filter_has_no_live_fields_so_no_state_is_reported():
"""This dump's /filter/airdustPM1filter/vs/0 has only
filterCapacity/filterCapacityUnit/filterResetType -- no filterUsage or
filterStatus -- so every AIR_FILTER_PM1 entity's exists_fn gates it out.
The href still covers (see test_no_unbound_hrefs above) without
fabricating a reading from nothing (the 'don't guess' rule); contrast
with test_airconditioner_cac.py, whose dump has live data on the same
href and does get real air_filter_pm1_* state."""
state = _state()
assert not any(k.startswith("air_filter_pm1_") for k in state)
+80
View File
@@ -0,0 +1,80 @@
"""Tests for issue #235: HOMECARE_WIZARD_V2 false-positive unmapped-mode
warning, and the entity_id-is-None logging gap it surfaced.
Uses the pre-existing airconditioner_tp2x_rac_20k fixture (issue #37) --
its /mode/vs/0 already carries the exact reporter-confirmed shape:
supportedModes includes 'HOMECARE_WIZARD_V2' alongside the real modes, the
current mode ('Cool') never is it, and /configuration/vs/0's
airconOptionList lists it alongside other plain capability flags.
"""
from __future__ import annotations
import logging
import pytest
from homeassistant.components.climate import HVACMode
from homeassistant.core import HomeAssistant
from custom_components.localthings.climate import _NON_HVAC_OPTION_CODES, LocalThingsClimate
from custom_components.localthings.registry.entities import ClimateDesc
from tests.test_subdevice_discovery import _coordinator, _discover
@pytest.fixture
async def climate(hass: HomeAssistant):
coordinator = _coordinator(hass)
await _discover(coordinator, "airconditioner_tp2x_rac_20k")
bound = next(b for b in coordinator.bound if isinstance(b.desc, ClimateDesc))
return LocalThingsClimate(coordinator, bound)
def test_homecare_wizard_v2_excluded_from_hvac_modes(climate):
"""supportedModes is ['Auto', 'Cool', 'Dry', 'Wind', 'Heat',
'HOMECARE_WIZARD_V2'] -- five real modes plus the option flag. Every
real mode maps through; the flag contributes no extra (or bogus)
HVACMode entry."""
modes = climate.hvac_modes
assert set(modes) == {
HVACMode.OFF,
HVACMode.AUTO,
HVACMode.COOL,
HVACMode.DRY,
HVACMode.HEAT,
HVACMode.FAN_ONLY,
}
def test_homecare_wizard_v2_reports_no_warning(climate, caplog):
"""Reading hvac_modes must not emit the issue #93 unmapped-mode warning
for HOMECARE_WIZARD_V2 -- that's the actual bug report: every owner of
an affected unit saw this fire on every start."""
with caplog.at_level(logging.WARNING):
_ = climate.hvac_modes
assert "HOMECARE_WIZARD_V2" not in caplog.text
def test_hvac_mode_is_off_when_powered_off(climate):
"""The fixture's power is off -- hvac_mode short-circuits to OFF before
ever reading /mode/vs/0's 'Cool', matching real behavior (mode is only
meaningful while the unit is on)."""
assert climate.hvac_mode == HVACMode.OFF
def test_homecare_wizard_v2_is_the_known_non_hvac_set():
assert "HOMECARE_WIZARD_V2" in _NON_HVAC_OPTION_CODES
def test_warn_unmapped_falls_back_to_unique_id_when_entity_id_unset(climate, caplog):
"""Issue #235's secondary bug: _warn_unmapped logged 'None: device mode
...' because entity_id is unset until the entity is added to hass --
exactly the state a fixture-backed entity is in here, without ever
calling async_add_entities. unique_id is set eagerly in __init__
(entity.py), so it must appear in the log line instead of a bare
'None'."""
assert climate.entity_id is None
assert climate.unique_id is not None
with caplog.at_level(logging.WARNING):
climate._warn_unmapped("/mode/vs/0", "TotallyUnknownCode")
assert "None:" not in caplog.text
assert climate.unique_id in caplog.text
@@ -0,0 +1,94 @@
"""Tests for the TP1X_DA_AC_DHM_01001_0000 dehumidifier revision
(issues #271/#231, model AY70H18100GTD).
Both issues submitted the identical dump: same board, same two previously
unbound hrefs (/display/vs/0, /watertank/lighting/vs/0). Confirms both the
new capabilities and the /oic/d device-type routing added alongside them.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import (
for_device_by_model,
for_device_by_oic_type,
)
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import SelectDesc, SwitchDesc
from tests.conftest import _load_device
def _dehumidifier():
resources = _load_device("dehumidifier_tp1x_dhm01001")
info = resources["/information/vs/0"]
reg = for_device_by_model(
info["x.com.samsung.da.modelNum"],
info["x.com.samsung.da.description"],
)
return reg, resources
def _bound():
reg, resources = _dehumidifier()
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
def _state():
bound, resources = _bound()
return flatten(bound, resources)
def _desc(key):
bound, _ = _bound()
return next(b.desc for b in bound if b.desc.key == key)
def test_model_resolves_to_dehumidifier_registry():
reg, _ = _dehumidifier()
assert reg is not None and reg.name == "dehumidifier"
def test_oic_device_type_resolves_to_dehumidifier_registry():
reg = for_device_by_oic_type(("oic.wk.d", "x.com.st.d.dehumidifier"))
assert reg is not None and reg.name == "dehumidifier"
def test_no_unbound_hrefs():
reg, resources = _dehumidifier()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_display_switch_present_and_reads_on():
state = _state()
assert state["display"] is True
def test_watertank_light_switch_write_contract():
desc = _desc("watertank_light")
assert isinstance(desc, SwitchDesc)
path, body = desc.write_fn("On", {})
assert path == ["watertank", "lighting", "vs", "0"]
assert body == {"status": "On"}
def test_watertank_light_color_options_come_from_live_supported_list():
desc = _desc("watertank_light_color")
assert isinstance(desc, SelectDesc)
assert desc.options_field == "colorSupportedList"
path, body = desc.write_fn("Green", {})
assert path == ["watertank", "lighting", "vs", "0"]
assert body == {"colorOption": "Green"}
def test_watertank_light_brightness_options_come_from_live_supported_list():
desc = _desc("watertank_light_brightness")
assert isinstance(desc, SelectDesc)
assert desc.options_field == "modeSupportedList"
path, body = desc.write_fn("Low", {})
assert path == ["watertank", "lighting", "vs", "0"]
assert body == {"mode": "Low"}
def test_watertank_full_alarm_status_exposed_read_only():
state = _state()
assert state["watertank_full_alarm_status"] == "On"
+87
View File
@@ -0,0 +1,87 @@
"""Tests for dryer Drum Clean+ maintenance tracking (issue #258).
Same DrumCleanProposal_/WashingTimes_/DrumCleanLog_ options[] tokens as
washer.py's issue #9 feature, shared via laundry.drum_clean_cycles_remaining/
drum_clean_last_cleaned -- except DrumCleanLog_ on a dryer is a '|'-joined
history of every past clean rather than one bare timestamp.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.capabilities.laundry import (
drum_clean_cycles_remaining,
drum_clean_last_cleaned,
)
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
def _dryer(fixture_name):
resources = _load_device(fixture_name)
info = resources["/information/vs/0"]
reg = for_device_by_model(
info["x.com.samsung.da.modelNum"],
info["x.com.samsung.da.description"],
)
return reg, resources
def _state(fixture_name):
reg, resources = _dryer(fixture_name)
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
return flatten(bound, resources)
def test_no_unbound_hrefs():
reg, resources = _dryer("dryer_tp1_21_drum_clean")
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_confirmed_dump_reports_cycles_remaining_and_last_cleaned():
"""DA_WM_TP1_21_COMMON/DV9400B reports DrumCleanProposal_25/
WashingTimes_25 (0 cycles remaining -- due now) and a ten-entry
'|'-joined DrumCleanLog_, most recent 2026-05-24T21:38:08."""
state = _state("dryer_tp1_21_drum_clean")
assert state["drum_clean_cycles_remaining"] == 0
assert str(state["drum_clean_last_cleaned"]) == "2026-05-24 21:38:08+00:00"
def test_dump_with_no_drum_clean_data_reports_neither_entity():
"""The default dryer fixture only has 'DrumCleanLog_Empty', no
DrumCleanProposal_/WashingTimes_ tokens at all -- both entities stay
gated off rather than reporting a fabricated 0 or an unparseable date."""
state = _state("dryer")
assert "drum_clean_cycles_remaining" not in state
assert "drum_clean_last_cleaned" not in state
def test_multi_entry_drum_clean_log_takes_the_last_timestamp():
rep = {
"x.com.samsung.da.options": [
"DrumCleanLog_2023-12-03T22:07:18|2024-02-09T15:18:59|2026-05-24T21:38:08",
]
}
assert str(drum_clean_last_cleaned(rep)) == "2026-05-24 21:38:08+00:00"
def test_single_entry_drum_clean_log_matches_washer_shape():
"""A bare, no-'|' value (the washer's own shape) parses identically."""
rep = {"x.com.samsung.da.options": ["DrumCleanLog_2024-02-09T15:18:59"]}
assert str(drum_clean_last_cleaned(rep)) == "2024-02-09 15:18:59+00:00"
def test_empty_drum_clean_log_reports_none():
rep = {"x.com.samsung.da.options": ["DrumCleanLog_Empty"]}
assert drum_clean_last_cleaned(rep) is None
def test_cycles_remaining_never_negative():
rep = {
"x.com.samsung.da.options": [
"DrumCleanProposal_25",
"WashingTimes_40",
]
}
assert drum_clean_cycles_remaining(rep) == 0
+50
View File
@@ -329,6 +329,56 @@ class TestDefiniteTemperatureCooler:
assert rep["x.com.samsung.da.definite.supportedList"] == ["1", "2", "3", "4", "7"]
class TestDefiniteTemperatureFreezer:
"""Discrete freezer setpoint (TP1X_REF_21K, issue #229) -- the same
definite-setpoint pattern as DEFINITE_TEMPERATURE_COOLER above, on a
fridge/freezer combo that reports it for *both* compartments. Identical
field shape, just negative supportedList values."""
def test_href(self):
assert fridge.DEFINITE_TEMPERATURE_FREEZER.href == "/temperature/definite/freezer/vs/0"
def test_write(self):
desc = next(
e for e in fridge.DEFINITE_TEMPERATURE_FREEZER.entities if isinstance(e, SelectDesc)
)
assert desc.write_fn is not None
result = desc.write_fn("-19", {})
assert result is not None
path, body = result
assert path == ["temperature", "definite", "freezer", "vs", "0"]
assert body == {"x.com.samsung.da.definite.desired": "-19"}
def test_no_unbound_hrefs_and_discrete_options(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp1x_ref_21k_definite")
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["cooler_temperature_setpoint"] == "4"
assert state["freezer_temperature_setpoint"] == "-19"
rep = resources["/temperature/definite/freezer/vs/0"]
assert rep["x.com.samsung.da.definite.supportedList"] == [
"-23",
"-21",
"-19",
"-17",
"-15",
]
class TestKimchiZone:
"""Kimchi-refrigerator compartments (TP2X_REF_20K-class, issue #26) --
top/middle/bottom each report an identically-shaped currentMode/
+91 -4
View File
@@ -72,7 +72,12 @@ def test_registry_reproduces_golden_state_keys_for_ehs():
def test_registry_reproduces_golden_state_keys_for_washer_wa8000t():
"""Top-load washer (WA8000T, issue #106) reports no oneUiVersion and
used the 'WA' consumer-model prefix, previously unmapped in
_CONSUMER_PREFIX_TO_KEY -- fell back to 'unknown'."""
_CONSUMER_PREFIX_TO_KEY -- fell back to 'unknown'.
Also gains drum_clean_last_cleaned (issue #258): this fixture's
DrumCleanLog_ is a '|'-joined multi-entry history, which always failed
datetime.fromisoformat and silently returned None before
laundry.drum_clean_last_cleaned learned to take the last entry."""
from tests.conftest import _load_device
resources = _load_device("washer_wa8000t")
@@ -270,7 +275,10 @@ def test_registry_reproduces_golden_state_keys_for_range_hood():
def test_registry_reproduces_golden_state_keys_for_washer_flexwash():
"""FlexWash twin washers (WV-prefix consumer model, e.g. WV55M9600AW)
report no oneUiVersion and previously fell through for_device_by_model's
consumer-prefix map entirely -- issue #19."""
consumer-prefix map entirely -- issue #19.
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
above for why."""
from tests.conftest import _load_device
resources = _load_device("washer_flexwash")
@@ -285,7 +293,10 @@ def test_registry_reproduces_golden_state_keys_for_washer_flexwash():
def test_registry_reproduces_golden_state_keys_for_washer_dryer_combo():
"""Washer/dryer combo units carry a writable dryLevel field on
/washer/vs/0 itself, with no separate dryer resource -- issue #22."""
/washer/vs/0 itself, with no separate dryer resource -- issue #22.
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
above for why."""
from tests.conftest import _load_device
resources = _load_device("washer_dryer_combo")
@@ -622,7 +633,10 @@ def test_registry_reproduces_golden_state_keys_for_washer_wa55a7700av():
-- a different board generation than the WA8000T's TP2_20_COMMON,
reached through the same 'WA' consumer-model-prefix fallback (issue
#106). Binds cleanly against the existing washer registry with zero
unbound hrefs."""
unbound hrefs.
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
above for why."""
from tests.conftest import _load_device
resources = _load_device("washer_wa55a7700av")
@@ -1226,6 +1240,79 @@ def test_registry_reproduces_golden_state_keys_for_range_ne6516a():
)
def test_registry_reproduces_golden_state_keys_for_dehumidifier_tp1x_dhm01001():
"""TP1X_DA_AC_DHM_01001_0000 revision (issues #271/#231) additionally
reports /display/vs/0 (air_purifier.DISPLAY's exact shape, reused as-is)
and /watertank/lighting/vs/0 (new WATERTANK_LIGHTING capability), both
unbound on the original issue #88 dump."""
from tests.conftest import _load_device
resources = _load_device("dehumidifier_tp1x_dhm01001")
golden = json.loads((GOLDEN / "dehumidifier_tp1x_dhm01001.json").read_text())
state_keys = _new_state_keys("dehumidifier_tp1x_dhm01001", resources)
assert set(state_keys) == set(golden["state_keys"]), (
f"state_keys mismatch:\n"
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
)
def test_registry_reproduces_golden_state_keys_for_airconditioner_tp1x_fac_time_23k():
"""TP1X_FAC_TIME_23K (issue #270) reports a UV-C LED, a ventilation-alarm
toggle, and a second (PM1-rated, no live usage field) dust filter, none
previously modeled. Also a 2-in-1 system on this dump (numofsubdevice=2,
a UUID-prefixed sibling materializes via the flat fallback) -- the three
new/ignored hrefs are fixed on their canonical form so the sibling picks
them up too, per the adding-device-support skill's 'never index or
prefix a registry href' rule."""
from tests.conftest import _load_device
resources = _load_device("airconditioner_tp1x_fac_time_23k")
golden = json.loads((GOLDEN / "airconditioner_tp1x_fac_time_23k.json").read_text())
state_keys = _new_state_keys("airconditioner_tp1x_fac_time_23k", resources)
assert set(state_keys) == set(golden["state_keys"]), (
f"state_keys mismatch:\n"
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
)
def test_registry_reproduces_golden_state_keys_for_dryer_tp1_21_drum_clean():
"""DA_WM_TP1_21_COMMON/DV9400B (issue #258) is the first dryer dump with
live DrumCleanProposal_/WashingTimes_ tokens (dryer's default fixture
only ever had 'DrumCleanLog_Empty', same as the parallel washer capability
started from) -- locks in the new drum_clean_cycles_remaining/
drum_clean_last_cleaned entities, including the dryer-specific
'|'-joined multi-timestamp DrumCleanLog_ shape."""
from tests.conftest import _load_device
resources = _load_device("dryer_tp1_21_drum_clean")
golden = json.loads((GOLDEN / "dryer_tp1_21_drum_clean.json").read_text())
state_keys = _new_state_keys("dryer_tp1_21_drum_clean", resources)
assert set(state_keys) == set(golden["state_keys"]), (
f"state_keys mismatch:\n"
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
)
def test_registry_reproduces_golden_state_keys_for_refrigerator_tp1x_ref_21k_definite():
"""TP1X_REF_21K (issue #229) reports the discrete definite-setpoint
pattern (issue #186) on *both* compartments -- previously only the
cooler half was modeled; DEFINITE_TEMPERATURE_FREEZER covers the
freezer's identical-shape /temperature/definite/freezer/vs/0."""
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp1x_ref_21k_definite")
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_definite.json").read_text())
state_keys = _new_state_keys("refrigerator_tp1x_ref_21k_definite", resources)
assert set(state_keys) == set(golden["state_keys"]), (
f"state_keys mismatch:\n"
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
)
def test_resources_from_batch_preferred_over_flat():
from tests.conftest import _resources_from_dump