Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac93549f2a | ||
|
|
b32074c8dd |
No files matched your search
@@ -399,24 +399,6 @@ 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
|
||||
|
||||
@@ -128,23 +128,6 @@ _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] = {
|
||||
@@ -380,15 +363,7 @@ 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).
|
||||
|
||||
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.
|
||||
"""
|
||||
in the log instead of silently vanishing (issue #93)."""
|
||||
key = (href, code)
|
||||
if key in self._warned_unmapped:
|
||||
return
|
||||
@@ -396,7 +371,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 or self.unique_id,
|
||||
self.entity_id,
|
||||
code,
|
||||
href,
|
||||
)
|
||||
@@ -497,11 +472,7 @@ 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
|
||||
and device not in _NON_HVAC_OPTION_CODES
|
||||
):
|
||||
if device is not None and device not in _DEVICE_TO_HVAC:
|
||||
self._warn_unmapped(MODE_HREF, device)
|
||||
return _DEVICE_TO_HVAC.get(device, HVACMode.AUTO)
|
||||
|
||||
@@ -509,7 +480,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 or m in _NON_HVAC_OPTION_CODES:
|
||||
if m == _AI_COMFORT_MODE:
|
||||
continue
|
||||
mapped = _DEVICE_TO_HVAC.get(m)
|
||||
if mapped is None:
|
||||
|
||||
@@ -462,10 +462,14 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Re-poll a flat-mode prefixed subdevice's hrefs individually
|
||||
(issue #205) -- it has no Collection endpoint to batch-refresh
|
||||
through (see enumerate_subdevices' fallback), so each canonical
|
||||
href confirmed at enumeration time gets its own GET under the
|
||||
subdevice's prefix. A href failing to answer this cycle just drops
|
||||
out of the result, same "never let a sibling's flakiness fail the
|
||||
master's poll" posture as the Collection path above.
|
||||
href assumed at enumeration time (cloned from the master's own
|
||||
state, issue #265 -- not yet confirmed live under this subdevice's
|
||||
own prefix) gets its own GET under the subdevice's prefix, which is
|
||||
what turns the clone into this sibling's real value once it
|
||||
answers. A href failing to answer this cycle just drops out of the
|
||||
result and leaves whatever value is already cached (the clone, until
|
||||
corrected) in place -- same "never let a sibling's flakiness fail
|
||||
the master's poll" posture as the Collection path above.
|
||||
|
||||
Takes `sess` from the caller (already None-checked there) rather
|
||||
than re-reading self._session -- async_close() can null that
|
||||
@@ -477,7 +481,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
this method builds) -- those are already refreshed every 3s/6s by
|
||||
_run_subpolls, strictly more current than this once-per-summary-poll
|
||||
pass could offer, so re-fetching them here would only add GETs
|
||||
without adding freshness. A subdevice with many confirmed hrefs
|
||||
without adding freshness. A subdevice with many assumed hrefs
|
||||
(unlike a Collection batch, which is always one GET regardless of
|
||||
count) is otherwise a summary-poll cost that scales with its href
|
||||
count."""
|
||||
|
||||
@@ -259,7 +259,6 @@ _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,11 +33,8 @@ 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,13 +2,10 @@
|
||||
|
||||
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. 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.
|
||||
Capability objects are reused directly rather than duplicated.
|
||||
"""
|
||||
|
||||
from ..capabilities import air_purifier, airconditioner, common, dehumidifier, ignored
|
||||
from ..capabilities import airconditioner, common, dehumidifier, ignored
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
@@ -20,11 +17,9 @@ 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,7 +28,6 @@ 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,80 +909,6 @@ 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",
|
||||
@@ -1001,51 +927,6 @@ 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, SwitchDesc
|
||||
from ..entities import NumberDesc, SelectDesc, SensorDesc
|
||||
from .common import int_or_none
|
||||
|
||||
|
||||
@@ -77,60 +77,6 @@ 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,18 +48,6 @@ 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, drum_clean_cycles_remaining, drum_clean_last_cleaned
|
||||
from .laundry import cycle_select
|
||||
|
||||
|
||||
def _wrinkle_write(p, rep, href=None):
|
||||
@@ -41,25 +41,14 @@ 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 '01' Normal and '06' Time dry were confirmed on a
|
||||
# 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
|
||||
# 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). 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.
|
||||
# #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.
|
||||
DRYER_COURSE = Capability(
|
||||
href="/course/vs/0",
|
||||
entities=(
|
||||
@@ -68,22 +57,6 @@ 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,34 +163,6 @@ 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,7 +20,6 @@ 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
|
||||
@@ -235,47 +234,6 @@ 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,6 +15,8 @@ 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 (
|
||||
@@ -22,8 +24,6 @@ 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,10 +149,36 @@ WASHER_SETTINGS = Capability(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# 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.
|
||||
# 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
|
||||
|
||||
|
||||
# Detergent/softener auto-dispense dosing, from the same options[] array
|
||||
@@ -314,16 +340,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",
|
||||
|
||||
@@ -30,11 +30,37 @@ second subdevice exists at that prefix, but not evidence that `GET
|
||||
around) itself returns anything. Issue #205, the same unit on a later
|
||||
version, is that assumption failing: `/<uuid>/device/0` comes back empty.
|
||||
So `enumerate_subdevices` tries it first (a future board might genuinely
|
||||
expose it) and falls back, when it's empty, to probing every href the
|
||||
master itself answered this cycle individually under the UUID prefix --
|
||||
the only thing ever actually confirmed to work for this pattern -- on the
|
||||
assumption that a composite device's siblings share the master's resource
|
||||
surface. See `Subdevice.flat_hrefs`.
|
||||
expose it) and, when it's empty, assumes the sibling shares the master's
|
||||
resource surface outright rather than trying to confirm it href by href.
|
||||
|
||||
That per-href confirmation is what this module did originally, and issue
|
||||
#265 is why it doesn't anymore: `ARTIK051_FAC_TIME_21K`'s firmware doesn't
|
||||
4.04 an href it doesn't recognize under a foreign prefix, it drops the
|
||||
packet -- so probing the ~30 hrefs a real AC dump carries, each waiting out
|
||||
the full 10s RETRIEVE timeout before moving on, took close enough to five
|
||||
minutes to trip Home Assistant's own bootstrap-stage timeout and fail the
|
||||
whole config entry, main unit included, before a single entity existed.
|
||||
A device naming this UUID in its own `subdeviceIdList` (or `/oic/res` link
|
||||
prefix, Pattern C) is already the device's own claim that a real subdevice
|
||||
lives there; spending an unbounded, firmware-dependent amount of RETRIEVEs
|
||||
trying to re-confirm that claim one href at a time bought unreliable
|
||||
confirmation at a cost that could take the integration down with it. So the
|
||||
fallback now trusts the claim and clones the master's current hrefs *and*
|
||||
values verbatim under the prefix as this cycle's assumed state for the
|
||||
sibling, with one bounded exception: it still retries a single individual
|
||||
`GET /<uuid>/information/vs/0` -- the one href this pattern has ever
|
||||
actually confirmed to answer on its own, per the paragraph above -- so the
|
||||
sibling's HA device shows its own model/serial rather than the master's
|
||||
cloned ones (identical model/serial across two otherwise-distinct devices
|
||||
reads as a duplicate, even though their device-registry identifiers are
|
||||
genuinely different). That's one extra RETRIEVE per candidate, not the ~30
|
||||
that caused issue #265. `coordinator._poll_subdevice_flat_hrefs` still
|
||||
re-GETs every cloned href individually, under the prefix, on every later
|
||||
summary poll (unchanged by this), correcting the assumption toward the
|
||||
sibling's real values as responses arrive; an href the firmware never
|
||||
answers under that prefix just keeps mirroring the master indefinitely --
|
||||
which, for a board proven not to expose that href at all, is what "assume
|
||||
it exists" has to mean in practice. See `Subdevice.flat_hrefs`.
|
||||
|
||||
Pattern C -- UUID prefix advertised only via `/oic/res`
|
||||
(`AWM-WW-AID-26-ONEBODY` washer+dryer combo, issue #241). The board answers
|
||||
@@ -134,10 +160,13 @@ class Subdevice:
|
||||
`flat_hrefs` is non-empty only for a 'prefixed' subdevice that doesn't
|
||||
expose its own Collection at `seed_path` (issue #205 -- not even
|
||||
TP2X_FAC_BORA_21K, the board this pattern was built against, always
|
||||
does). When set, `seed_path` is meaningless (left as `()`) and this
|
||||
subdevice's state comes from GETting each of these canonical hrefs
|
||||
individually under its prefix instead of one Collection batch -- see
|
||||
enumerate_subdevices' fallback and coordinator._poll_subdevice_seed.
|
||||
does). When set, `seed_path` is meaningless (left as `()`); enumeration
|
||||
seeds this subdevice's initial state by cloning the master's own current
|
||||
hrefs/values under its prefix rather than confirming them live (issue
|
||||
#265 -- see enumerate_subdevices' fallback), and from then on
|
||||
coordinator._poll_subdevice_seed refreshes it by GETting each of these
|
||||
canonical hrefs individually under its prefix instead of one Collection
|
||||
batch, correcting the clone toward real values as responses arrive.
|
||||
"""
|
||||
|
||||
kind: str # 'main' | 'indexed' | 'prefixed'
|
||||
@@ -379,49 +408,63 @@ def enumerate_subdevices(
|
||||
fetched.update(normalize_seed_batch(subdevice, batch))
|
||||
subdevices.append(subdevice)
|
||||
return
|
||||
# Fallback (issue #205): TP2X_FAC_BORA_21K itself -- the board this
|
||||
# Fallback (issue #265, superseding the per-href probe issue #205
|
||||
# originally added here): TP2X_FAC_BORA_21K itself -- the board this
|
||||
# pattern was built against -- turns out not to always expose its own
|
||||
# `/<uuid>/device/0` Collection either, so "every prefixed subdevice
|
||||
# has one" doesn't hold even on the reference hardware. With no
|
||||
# Collection to seed from and no per-UUID entry in `/oic/res` to
|
||||
# enumerate hrefs from, the only signal left is that a composite
|
||||
# device's siblings are the same physical board family as the
|
||||
# subdevice this config entry already talks to -- so probe every
|
||||
# href the master itself answered this cycle, individually, under
|
||||
# this UUID's prefix, and keep whichever ones answer. Each is a
|
||||
# plain tolerated-404 RETRIEVE, same posture as every other probe in
|
||||
# this function.
|
||||
#
|
||||
# Known gap, not yet guarded against: a firmware that answers *any*
|
||||
# request under an unrecognized prefix (echoing the master's own
|
||||
# state back rather than 4.04ing) would pass every one of these
|
||||
# probes and, if the echoed state also clears discover_partitioned's
|
||||
# liveness gate, materialize a phantom duplicate of the master
|
||||
# rather than a real sibling. Every board seen so far genuinely
|
||||
# 4.04s on paths it doesn't own (issue #205's own unit answered only
|
||||
# 1 of 31 probes), so this hasn't been built -- the one place it
|
||||
# could hook in later is comparing a candidate's confirmed reps
|
||||
# against the master's own values for those same canonical hrefs.
|
||||
flat_hrefs = []
|
||||
first = True
|
||||
for href in sorted(resources):
|
||||
if not first:
|
||||
sess.pace()
|
||||
first = False
|
||||
actual = f"/{sub_id}{href}"
|
||||
rep = _get_property(sess, tuple(actual.strip("/").split("/")))
|
||||
_probed(actual, rep)
|
||||
if rep:
|
||||
flat_hrefs.append(href)
|
||||
fetched[actual] = rep
|
||||
# has one" doesn't hold even on the reference hardware. This used to
|
||||
# respond by probing every href the master itself answered this
|
||||
# cycle, individually, under this UUID's prefix, keeping whichever
|
||||
# ones answered -- but issue #265's ARTIK051_FAC_TIME_21K drops
|
||||
# packets for hrefs it doesn't recognize under a foreign prefix
|
||||
# instead of 4.04ing them, so that loop's ~30 RETRIEVEs (a real AC
|
||||
# dump's href count) each ran out the full 10s timeout, took close
|
||||
# to five minutes, and tripped Home Assistant's own bootstrap-stage
|
||||
# timeout -- failing the whole config entry, main unit included,
|
||||
# before a single entity existed. The device already named this
|
||||
# UUID in its own subdeviceIdList (or an /oic/res link prefix,
|
||||
# Pattern C) -- that's the device's own claim a real subdevice lives
|
||||
# there, so trust it outright rather than spend an unbounded,
|
||||
# firmware-dependent number of RETRIEVEs re-confirming it href by
|
||||
# href: clone the master's current hrefs and values verbatim under
|
||||
# this prefix as the sibling's assumed state, no per-href probing
|
||||
# loop at all. coordinator._poll_subdevice_flat_hrefs re-GETs each of
|
||||
# these hrefs individually, under the prefix, on every later summary
|
||||
# poll (unchanged by this), correcting the clone toward the
|
||||
# sibling's real values as responses arrive; an href the firmware
|
||||
# never answers under this prefix just keeps mirroring the master
|
||||
# indefinitely -- the necessary meaning of "assume it exists" for a
|
||||
# board already proven not to expose that href at all.
|
||||
flat_hrefs = tuple(sorted(resources))
|
||||
if not flat_hrefs:
|
||||
return
|
||||
for href in flat_hrefs:
|
||||
fetched[f"/{sub_id}{href}"] = resources[href]
|
||||
# One exception, worth its own bounded RETRIEVE rather than trusting
|
||||
# the clone: /information/vs/0 is what device_info_for() reads for
|
||||
# this subdevice's own model/serial. Left cloned from the master,
|
||||
# the sibling's HA device page would show the *master's* model and
|
||||
# serial verbatim -- distinct device-registry identifiers (derived
|
||||
# from this UUID, see device_info_for), but reading as a duplicate
|
||||
# of the master to anyone looking at the two devices' info. This is
|
||||
# also the one href ever actually confirmed to answer under this
|
||||
# pattern's prefix on real hardware (issue #177 comment 5113518087),
|
||||
# so it's worth retrying here even though the rest of the fallback
|
||||
# no longer probes anything -- one extra RETRIEVE per candidate,
|
||||
# not the ~30 that caused issue #265.
|
||||
info_seed = (sub_id, "information", "vs", "0")
|
||||
info = _get_property(sess, info_seed)
|
||||
_probed(_seed_href(info_seed), info)
|
||||
if info:
|
||||
fetched[f"/{sub_id}/information/vs/0"] = info
|
||||
if "/information/vs/0" not in flat_hrefs:
|
||||
flat_hrefs = tuple(sorted({*flat_hrefs, "/information/vs/0"}))
|
||||
subdevices.append(
|
||||
Subdevice(
|
||||
kind="prefixed",
|
||||
key=sub_id,
|
||||
seed_path=(),
|
||||
flat_hrefs=tuple(flat_hrefs),
|
||||
flat_hrefs=flat_hrefs,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -278,9 +278,9 @@
|
||||
"name": "Cyklus",
|
||||
"state": {
|
||||
"80": "Jemné",
|
||||
"83": "Normální",
|
||||
"83": "Express 60",
|
||||
"84": "Intenzivní",
|
||||
"86": "Express 60",
|
||||
"86": "Normální",
|
||||
"90": "Samočištění",
|
||||
"0e": "AI mytí",
|
||||
"07": "Předoplach",
|
||||
@@ -325,10 +325,7 @@
|
||||
"1e": "Outdoor",
|
||||
"1f": "Smíšená náplň",
|
||||
"2b": "Sušení bubnu",
|
||||
"4c": "Osvěžení vzduchem",
|
||||
"51": "Eko bavlna",
|
||||
"53": "AI sušení+",
|
||||
"4e": "Automatické sušení"
|
||||
"4c": "Osvěžení vzduchem"
|
||||
}
|
||||
},
|
||||
"favorite_capacity": {
|
||||
@@ -624,28 +621,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -994,15 +969,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -1170,15 +1136,6 @@
|
||||
},
|
||||
"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": "Normal",
|
||||
"83": "Express 60",
|
||||
"84": "Heavy",
|
||||
"86": "Express 60",
|
||||
"86": "Normal",
|
||||
"90": "Self clean",
|
||||
"0e": "AI Wash",
|
||||
"07": "Pre blast",
|
||||
@@ -325,10 +325,7 @@
|
||||
"1e": "Outdoor",
|
||||
"1f": "Mixed load",
|
||||
"2b": "Self Tub Dry",
|
||||
"4c": "Air Refresh",
|
||||
"51": "Eco Cotton",
|
||||
"53": "AI Dry+",
|
||||
"4e": "Self Dry"
|
||||
"4c": "Air Refresh"
|
||||
}
|
||||
},
|
||||
"favorite_capacity": {
|
||||
@@ -624,28 +621,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -994,15 +969,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -1170,15 +1136,6 @@
|
||||
},
|
||||
"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": "Normal",
|
||||
"83": "Express 60",
|
||||
"84": "Intenso",
|
||||
"86": "Express 60",
|
||||
"86": "Normal",
|
||||
"90": "Autolimpieza",
|
||||
"0e": "Lavado IA",
|
||||
"07": "Prelavado intenso",
|
||||
@@ -439,10 +439,7 @@
|
||||
"1e": "Exterior",
|
||||
"1f": "Carga mixta",
|
||||
"2b": "Secado de tambor",
|
||||
"4c": "Renovación de aire",
|
||||
"51": "Eco algodón",
|
||||
"53": "Secado IA+",
|
||||
"4e": "Autosecado"
|
||||
"4c": "Renovación de aire"
|
||||
}
|
||||
},
|
||||
"favorite_capacity": {
|
||||
@@ -738,28 +735,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -1108,15 +1083,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -1284,15 +1250,6 @@
|
||||
},
|
||||
"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": "Normale",
|
||||
"83": "Express 60",
|
||||
"84": "Intenso",
|
||||
"86": "Express 60",
|
||||
"86": "Normale",
|
||||
"90": "Autopulizia",
|
||||
"0e": "AI Wash",
|
||||
"07": "Prelavaggio intenso",
|
||||
@@ -325,10 +325,7 @@
|
||||
"1e": "Capi outdoor",
|
||||
"1f": "Carico misto",
|
||||
"2b": "Asciugatura cestello",
|
||||
"4c": "Rinfresca",
|
||||
"51": "Eco cotone",
|
||||
"53": "Asciugatura AI+",
|
||||
"4e": "Autoasciugatura"
|
||||
"4c": "Rinfresca"
|
||||
}
|
||||
},
|
||||
"favorite_capacity": {
|
||||
@@ -624,28 +621,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -994,15 +969,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -1170,15 +1136,6 @@
|
||||
},
|
||||
"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": "Normaal",
|
||||
"83": "Express 60",
|
||||
"84": "Intensief",
|
||||
"86": "Express 60",
|
||||
"86": "Normaal",
|
||||
"90": "Zelfreiniging",
|
||||
"0e": "AI Wash",
|
||||
"07": "Voorspoelen",
|
||||
@@ -325,10 +325,7 @@
|
||||
"1e": "Outdoor",
|
||||
"1f": "Gemengde was",
|
||||
"2b": "Zelf trommel drogen",
|
||||
"4c": "Opfrissen",
|
||||
"51": "Eco katoen",
|
||||
"53": "AI drogen+",
|
||||
"4e": "Zelf drogen"
|
||||
"4c": "Opfrissen"
|
||||
}
|
||||
},
|
||||
"favorite_capacity": {
|
||||
@@ -624,28 +621,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -994,15 +969,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -1170,15 +1136,6 @@
|
||||
},
|
||||
"away_mode": {
|
||||
"name": "Afwezigheidsmodus"
|
||||
},
|
||||
"watertank_light": {
|
||||
"name": "Waterreservoirlicht"
|
||||
},
|
||||
"uv_led": {
|
||||
"name": "Uv-led"
|
||||
},
|
||||
"ventilation_alarm": {
|
||||
"name": "Ventilatiealarm"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -716,5 +716,5 @@
|
||||
"x.com.samsung.da.serialNum": "TEST-SUBDEVICE-SERIAL-0000"
|
||||
}
|
||||
},
|
||||
"seeds_note": "device0 and oic_res are real, captured from the reporter's issue #205 report. subdeviceIdList in device0's /subdevices/vs/0 is restored to the real ['6c2dff6d-ee5c-dad1-6a5e-000000000001'] (HA's diagnostics download redacts it, matching redact.py's 'deviceid' substring rule, not because it's account data -- same restoration airconditioner_fac_bora_2in1_device.json documents for the same field), and every other value is otherwise exactly what HA's download redacts (serials/otnDUID etc.) against the same physical TP2X_FAC_BORA_21K unit as airconditioner_fac_bora_2in1_device.json -- same subdeviceIdList UUID, same wall subdevice. Filed specifically because, contrary to the assumption that fixture's seed batch was built on, this unit's /6c2dff6d-ee5c-dad1-6a5e-000000000001/device/0 does NOT answer (subdevice_probes in the report shows it False), so the Collection-batch fallback this fixture exercises (registry.subdevices.enumerate_subdevices' per-href probe, issue #205) is what has to find the subdevice instead. The one probes entry, /6c2dff6d-ee5c-dad1-6a5e-000000000001/information/vs/0, is the same real capture already used in airconditioner_fac_bora_2in1_device.json's seed batch (issue #177 comment 5113518087) -- the only href ever actually confirmed to answer under this UUID prefix. No other /6c2dff6d-ee5c-dad1-6a5e-000000000001/* href has been confirmed live yet, so none are seeded here; this fixture's expected outcome is the candidate being found by the flat-href probe but then correctly held back by discover_partitioned's liveness gate (information alone binds no entity), matching where the real issue stands -- not a materialized climate entity, which would require guessing at unconfirmed hrefs. The probe's serialNum (\"TEST-SUBDEVICE-SERIAL-0000\") is a hand-placed placeholder for the real value, not HA's own redaction output -- same placeholder airconditioner_fac_bora_2in1_device.json uses for the identical field."
|
||||
"seeds_note": "device0 and oic_res are real, captured from the reporter's issue #205 report. subdeviceIdList in device0's /subdevices/vs/0 is restored to the real ['6c2dff6d-ee5c-dad1-6a5e-000000000001'] (HA's diagnostics download redacts it, matching redact.py's 'deviceid' substring rule, not because it's account data -- same restoration airconditioner_fac_bora_2in1_device.json documents for the same field), and every other value is otherwise exactly what HA's download redacts (serials/otnDUID etc.) against the same physical TP2X_FAC_BORA_21K unit as airconditioner_fac_bora_2in1_device.json -- same subdeviceIdList UUID, same wall subdevice. Filed specifically because this unit's /6c2dff6d-ee5c-dad1-6a5e-000000000001/device/0 does NOT answer (subdevice_probes in the report shows it False), which is what this fixture exercises: registry.subdevices.enumerate_subdevices' clone-the-master fallback (issue #265, which replaced the per-href probe loop this fixture originally exercised for issue #205). The one probes entry, /6c2dff6d-ee5c-dad1-6a5e-000000000001/information/vs/0, is the same real capture already used in airconditioner_fac_bora_2in1_device.json's seed batch (issue #177 comment 5113518087) -- the only href ever actually confirmed to answer under this UUID prefix. It's kept here as that real confirmation even though the current fallback no longer probes any href beyond /device/0 to decide whether the subdevice exists at all: since issue #265, this fixture's expected outcome is the candidate materializing immediately with the master's own device0 hrefs/values cloned under the prefix as its assumed state -- a full climate entity included -- rather than waiting on per-href confirmation this firmware may never give. The probe's serialNum (\"TEST-SUBDEVICE-SERIAL-0000\") is a hand-placed placeholder for the real value, not HA's own redaction output -- same placeholder airconditioner_fac_bora_2in1_device.json uses for the identical field."
|
||||
}
|
||||
@@ -1,641 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,426 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,438 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-5
@@ -2,9 +2,6 @@
|
||||
"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",
|
||||
@@ -32,7 +29,6 @@
|
||||
"selfcheck_result",
|
||||
"selfcheck_status",
|
||||
"super_fine_dust",
|
||||
"tropical_night_mode",
|
||||
"uv_led"
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
@@ -19,6 +19,26 @@
|
||||
"odor_controller_progress",
|
||||
"power_energy_kwh",
|
||||
"power_watts",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_air_filter_status",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_air_filter_threshold",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_air_filter_usage",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_air_filter_usage_hours",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_alarm_code",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_auto_clean",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_auto_clean_progress",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_auto_clean_running",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_beep",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_climate",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_current_temperature_c",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_diagnosis_status",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_energy_kwh",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_firmware_update",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_humidity",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_odor_controller_active",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_odor_controller_progress",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_power_energy_kwh",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_power_watts",
|
||||
"subdevice_6c2dff6dee5cdad16a5e000000000001_tropical_night_mode",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
"detergent_low",
|
||||
"diagnosis_status",
|
||||
"drum_clean_cycles_remaining",
|
||||
"drum_clean_last_cleaned",
|
||||
"dry_level",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
|
||||
-1
@@ -9,7 +9,6 @@
|
||||
"detergent_low",
|
||||
"diagnosis_status",
|
||||
"drum_clean_cycles_remaining",
|
||||
"drum_clean_last_cleaned",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"job_beginning_status",
|
||||
|
||||
+1
-2
@@ -11,7 +11,6 @@
|
||||
"detergent_water_hardness",
|
||||
"diagnosis_status",
|
||||
"drum_clean_cycles_remaining",
|
||||
"drum_clean_last_cleaned",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
@@ -28,4 +27,4 @@
|
||||
"spin_speed",
|
||||
"wash_temperature"
|
||||
]
|
||||
}
|
||||
}
|
||||
-1
@@ -9,7 +9,6 @@
|
||||
"detergent_low",
|
||||
"diagnosis_status",
|
||||
"drum_clean_cycles_remaining",
|
||||
"drum_clean_last_cleaned",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
|
||||
@@ -1,467 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,18 +6,12 @@ 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 -- 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.
|
||||
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.
|
||||
"""
|
||||
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
@@ -28,6 +22,7 @@ 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",
|
||||
@@ -35,6 +30,7 @@ _STILL_UNBOUND = frozenset(
|
||||
"/settings/sound/output/vs/0",
|
||||
"/settings/sound/volume/vs/0",
|
||||
"/smartsensingcooling/vs/0",
|
||||
"/uvled/vs/0",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,80 +0,0 @@
|
||||
"""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
|
||||
@@ -1,94 +0,0 @@
|
||||
"""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"
|
||||
@@ -111,24 +111,26 @@ async def test_diagnostics_reports_prefixed_subdevice(
|
||||
assert diag["subdevices_skipped"] == []
|
||||
|
||||
|
||||
async def test_diagnostics_reports_flat_hrefs_for_skipped_prefixed_candidate(
|
||||
async def test_diagnostics_reports_flat_hrefs_for_materialized_prefixed_candidate(
|
||||
hass: HomeAssistant,
|
||||
enable_custom_integrations,
|
||||
) -> None:
|
||||
"""issue #205: a prefixed candidate found through the per-href flat
|
||||
"""issue #205: a prefixed candidate found through the clone-the-master
|
||||
fallback (no working /<uuid>/device/0 Collection) has no meaningful
|
||||
seed_path -- diagnostics reports None there instead of the misleading
|
||||
bare "/" an empty tuple would otherwise join to, and lists the actual
|
||||
hrefs the fallback confirmed instead."""
|
||||
bare "/" an empty tuple would otherwise join to, and lists the full set
|
||||
of hrefs the fallback assumed (issue #265 -- every href the master
|
||||
itself has, not just a confirmed few) instead."""
|
||||
coordinator = _coordinator(hass)
|
||||
await _discover(coordinator, "airconditioner_fac_bora_205_flat")
|
||||
hass.data.setdefault(DOMAIN, {})[coordinator._entry.entry_id] = coordinator
|
||||
|
||||
diag = await async_get_config_entry_diagnostics(hass, coordinator._entry)
|
||||
|
||||
assert diag["subdevices"] == []
|
||||
assert len(diag["subdevices_skipped"]) == 1
|
||||
skipped = diag["subdevices_skipped"][0]
|
||||
assert skipped["kind"] == "prefixed"
|
||||
assert skipped["seed_path"] is None
|
||||
assert skipped["flat_hrefs"] == ["/information/vs/0"]
|
||||
assert diag["subdevices_skipped"] == []
|
||||
assert len(diag["subdevices"]) == 1
|
||||
subdevice = diag["subdevices"][0]
|
||||
assert subdevice["kind"] == "prefixed"
|
||||
assert subdevice["seed_path"] is None
|
||||
assert subdevice["flat_hrefs"] == sorted(diag["resources"])
|
||||
assert subdevice["bound_entity_count"] > 0
|
||||
@@ -1,87 +0,0 @@
|
||||
"""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
|
||||
@@ -329,56 +329,6 @@ 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/
|
||||
|
||||
+14
-103
@@ -72,12 +72,7 @@ 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'.
|
||||
|
||||
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."""
|
||||
_CONSUMER_PREFIX_TO_KEY -- fell back to 'unknown'."""
|
||||
from tests.conftest import _load_device
|
||||
|
||||
resources = _load_device("washer_wa8000t")
|
||||
@@ -275,10 +270,7 @@ 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.
|
||||
|
||||
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
|
||||
above for why."""
|
||||
consumer-prefix map entirely -- issue #19."""
|
||||
from tests.conftest import _load_device
|
||||
|
||||
resources = _load_device("washer_flexwash")
|
||||
@@ -293,10 +285,7 @@ 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.
|
||||
|
||||
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
|
||||
above for why."""
|
||||
/washer/vs/0 itself, with no separate dryer resource -- issue #22."""
|
||||
from tests.conftest import _load_device
|
||||
|
||||
resources = _load_device("washer_dryer_combo")
|
||||
@@ -633,10 +622,7 @@ 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.
|
||||
|
||||
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
|
||||
above for why."""
|
||||
unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
|
||||
resources = _load_device("washer_wa55a7700av")
|
||||
@@ -1118,18 +1104,16 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_fac_bora_2in1(
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_airconditioner_fac_bora_205_flat():
|
||||
"""The same reporter's same physical TP2X_FAC_BORA_21K unit as the _2in1
|
||||
fixture above, but a later capture (issue #205) where /<uuid>/device/0 doesn't
|
||||
answer -- contrary to what that fixture's own seed batch assumed the
|
||||
Collection endpoint would do. device0/oic_res are real; the only
|
||||
UUID-prefixed data is the one href ever actually confirmed live
|
||||
(/information/vs/0, same real capture the _2in1 fixture uses), fed
|
||||
through registry.subdevices.enumerate_subdevices' per-href flat
|
||||
fallback instead of a Collection batch. /information/vs/0 alone binds
|
||||
no entity, so the candidate is found but never materializes -- this
|
||||
golden has no `subdevice_...`-prefixed keys at all, same shape as
|
||||
tests/fixtures/golden/airconditioner_fac_bora.json, which is the point:
|
||||
a device whose sibling can't yet be confirmed live must regress to
|
||||
exactly the master-only state, never a phantom or partial subdevice."""
|
||||
fixture above, but a later capture (issue #205) where /<uuid>/device/0
|
||||
doesn't answer -- contrary to what that fixture's own seed batch assumed
|
||||
the Collection endpoint would do. device0/oic_res are real; there is no
|
||||
UUID-prefixed seed data at all, which is the point of this fixture:
|
||||
issue #265 replaced the old per-href confirmation fallback with an
|
||||
unconditional clone of the master's own hrefs/values under the prefix,
|
||||
so this golden's `subdevice_...`-prefixed keys are now the *same set* as
|
||||
the master's own unprefixed keys -- every entity the master has, the
|
||||
assumed sibling gets too, not just the handful the _2in1 fixture's
|
||||
synthetic partial seed happened to confirm."""
|
||||
name = "airconditioner_fac_bora_205_flat"
|
||||
golden = json.loads((GOLDEN / f"{name}.json").read_text())
|
||||
state_keys = _new_subdevice_aware_state_keys(name)
|
||||
@@ -1240,79 +1224,6 @@ 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
|
||||
|
||||
|
||||
@@ -273,35 +273,54 @@ async def test_fac_bora_2in1_unique_ids_include_subdevice_prefix(hass: HomeAssis
|
||||
# ---------------------------------------------------------------------------
|
||||
# Same reporter and physical unit/UUID again -- issue #205, but this
|
||||
# time /<uuid>/device/0 doesn't answer. Exercises enumerate_subdevices'
|
||||
# per-href flat-probe fallback (registry/subdevices.py) against a real
|
||||
# capture instead of the synthetic sessions test_subdevices.py uses.
|
||||
# clone-the-master fallback (registry/subdevices.py, issue #265) against a
|
||||
# real capture instead of the synthetic sessions test_subdevices.py uses.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_fac_bora_205_flat_fallback_finds_candidate_but_gate_holds_it_back(
|
||||
async def test_fac_bora_205_flat_fallback_clones_master_and_materializes(
|
||||
hass: HomeAssistant,
|
||||
):
|
||||
"""The fixture's only seeded UUID-prefixed href is /information/vs/0 --
|
||||
the one href ever actually confirmed live under this prefix (issue #177
|
||||
comment 5113518087) -- since nothing else has been confirmed yet for
|
||||
this unit. That's enough for the flat-probe fallback to find a
|
||||
candidate, but /information/vs/0 binds no entity on its own (it's only
|
||||
ever read for device-type resolution, never bound as a capability), so
|
||||
discover_partitioned's liveness gate correctly holds it back rather than
|
||||
materializing a phantom climate card from unconfirmed hrefs. This is the
|
||||
honest current state of issue #205, not a guess at its resolution."""
|
||||
"""/<uuid>/device/0 still doesn't answer for this reporter's unit
|
||||
(issue #205), but issue #265 replaced the old per-href confirmation loop
|
||||
-- which used to hold this candidate back with only /information/vs/0
|
||||
ever confirmed live under the prefix -- with an unconditional clone of
|
||||
the master's own hrefs and values under the prefix, plus one bounded
|
||||
retry of /information/vs/0 itself. So the sibling now materializes
|
||||
immediately with the master's own climate state as its assumed starting
|
||||
point, but its own real, distinct model/serial (this test used to
|
||||
assert the opposite: that the liveness gate correctly held the
|
||||
candidate back)."""
|
||||
coordinator = _coordinator(hass)
|
||||
resources, _oic_res, _seeds = _load_device_full("airconditioner_fac_bora_205_flat")
|
||||
await _discover(coordinator, "airconditioner_fac_bora_205_flat")
|
||||
|
||||
assert coordinator.subdevices == []
|
||||
assert [s.subdevice.key for s in coordinator._skipped_subdevices] == [_SUB_UUID]
|
||||
skipped = coordinator._skipped_subdevices[0].subdevice
|
||||
assert skipped.kind == "prefixed"
|
||||
assert skipped.seed_path == ()
|
||||
assert skipped.flat_hrefs == ("/information/vs/0",)
|
||||
assert [su.key for su in coordinator.subdevices] == [_SUB_UUID]
|
||||
assert coordinator._skipped_subdevices == []
|
||||
subdevice = coordinator.subdevices[0]
|
||||
assert subdevice.kind == "prefixed"
|
||||
assert subdevice.seed_path == ()
|
||||
assert subdevice.flat_hrefs == tuple(sorted(resources))
|
||||
|
||||
# The seed Collection and the one bounded /information/vs/0 retry were
|
||||
# probed over the network -- nothing else, no per-href confirmation loop.
|
||||
assert coordinator._subdevice_probes[f"/{_SUB_UUID}/device/0"] is False
|
||||
assert coordinator._subdevice_probes[f"/{_SUB_UUID}/information/vs/0"] is True
|
||||
allowed = {f"/{_SUB_UUID}/device/0", f"/{_SUB_UUID}/information/vs/0"}
|
||||
assert not any(
|
||||
href.startswith(f"/{_SUB_UUID}/") and href not in allowed
|
||||
for href in coordinator._subdevice_probes
|
||||
)
|
||||
|
||||
assert _climate_bound(coordinator, _SUB_UUID) is not None
|
||||
|
||||
# The confirmed /information/vs/0 reply -- not the master's cloned one --
|
||||
# is what device_info_for() reads, so the sibling's HA device shows its
|
||||
# own real model/serial rather than looking like a duplicate of the
|
||||
# master (issue #177 comment 5113518087's real hand-read capture).
|
||||
info = coordinator.device_info_for(subdevice)
|
||||
assert info["model"] == "TP2X_FAC_BORA_RAC_21K"
|
||||
assert info["model"] != coordinator.device_info.get("model")
|
||||
|
||||
# Confirms the master itself is completely unaffected by its sibling's
|
||||
# Collection endpoint not answering -- same guarantee every other
|
||||
@@ -310,14 +329,12 @@ async def test_fac_bora_205_flat_fallback_finds_candidate_but_gate_holds_it_back
|
||||
|
||||
|
||||
async def test_flat_subdevice_materializes_and_repolls_end_to_end(hass: HomeAssistant):
|
||||
"""Synthetic (not a real capture, unlike the fixture-driven test above) --
|
||||
exercises the one path nothing else covers: a flat-mode prefixed
|
||||
subdevice with *enough* confirmed hrefs to actually pass
|
||||
discover_partitioned's liveness gate and materialize a real climate
|
||||
entity, then a subsequent _poll_subdevice_seed re-poll refreshing its
|
||||
state all the way through to canonical_resources -- the path a real
|
||||
resolution of issue #205 (once more hrefs are confirmed live for some
|
||||
unit) would actually need.
|
||||
"""Synthetic seeds (not a real capture, unlike the fixture-driven test
|
||||
above) -- exercises the one path nothing else covers: a flat-mode
|
||||
prefixed subdevice materializing off a clone of the master's own state
|
||||
(issue #265) and then a subsequent _poll_subdevice_seed re-poll
|
||||
refreshing its state all the way through to canonical_resources as real
|
||||
per-href confirmation arrives.
|
||||
|
||||
Also pins _poll_subdevice_flat_hrefs' hot/warm skip: climate-critical
|
||||
hrefs (power/mode/temperature) land on the warm tier by discovery's own
|
||||
@@ -327,62 +344,45 @@ async def test_flat_subdevice_materializes_and_repolls_end_to_end(hass: HomeAssi
|
||||
/option/autoclean/vs/0 is cold-tier and is what actually needs this
|
||||
path."""
|
||||
resources, oic_res, _real_seeds = _load_device_full("airconditioner_fac_bora_2in1")
|
||||
seeds = {
|
||||
# No (_SUB_UUID, 'device', '0') entry -- forces the flat fallback,
|
||||
# same as the real issue #205 capture above, but this time with
|
||||
# enough hrefs answering to actually materialize. power/mode/
|
||||
# temperature values copied verbatim from that fixture's own (real)
|
||||
# Collection-batch seed, just served individually instead of
|
||||
# batched, to isolate "does flat mode produce the same result as
|
||||
# Collection mode" as the only variable.
|
||||
f"/{_SUB_UUID}/power/vs/0": {"x.com.samsung.da.power": "On"},
|
||||
f"/{_SUB_UUID}/mode/vs/0": {
|
||||
"x.com.samsung.da.supportedModes": ["Cool", "Dry", "Wind", "Auto"],
|
||||
"x.com.samsung.da.modes": ["Cool"],
|
||||
"x.com.samsung.da.options": [],
|
||||
},
|
||||
f"/{_SUB_UUID}/temperature/current/0": {
|
||||
"range": [18.0, 30.0],
|
||||
"units": "C",
|
||||
"temperature": 26.0,
|
||||
},
|
||||
f"/{_SUB_UUID}/temperature/desired/0": {
|
||||
"range": [18.0, 30.0],
|
||||
"units": "C",
|
||||
"temperature": 24.0,
|
||||
},
|
||||
# Cold-tier -- not covered by _run_subpolls, so this is the href
|
||||
# that actually depends on _poll_subdevice_flat_hrefs to ever
|
||||
# refresh at all.
|
||||
f"/{_SUB_UUID}/option/autoclean/vs/0": {
|
||||
"x.com.samsung.da.settingStatus": "Off",
|
||||
},
|
||||
}
|
||||
# No (_SUB_UUID, 'device', '0') entry -- forces the flat fallback, same
|
||||
# as the real issue #205 capture above. No per-href seeds are needed for
|
||||
# materialization itself anymore: the fallback clones the master's own
|
||||
# `resources` (real climate state included) under the prefix, with no
|
||||
# RETRIEVEs at all.
|
||||
coordinator = _coordinator(hass)
|
||||
await _discover_with(coordinator, resources, oic_res, seeds)
|
||||
await _discover_with(coordinator, resources, oic_res, seeds={})
|
||||
|
||||
assert [su.key for su in coordinator.subdevices] == [_SUB_UUID]
|
||||
subdevice = coordinator.subdevices[0]
|
||||
assert subdevice.seed_path == ()
|
||||
assert subdevice.flat_hrefs != ()
|
||||
assert subdevice.flat_hrefs == tuple(sorted(resources))
|
||||
|
||||
sub_climate = _climate_bound(coordinator, _SUB_UUID)
|
||||
assert sub_climate is not None
|
||||
|
||||
# Cloned initial state mirrors the master's own values verbatim.
|
||||
initial_temp = resources["/temperature/current/0"]["temperature"]
|
||||
initial_autoclean = resources["/option/autoclean/vs/0"]["x.com.samsung.da.settingStatus"]
|
||||
res = coordinator.canonical_resources(subdevice)
|
||||
assert res["/temperature/current/0"]["temperature"] == initial_temp
|
||||
assert res["/option/autoclean/vs/0"]["x.com.samsung.da.settingStatus"] == initial_autoclean
|
||||
|
||||
# Re-poll: a fresh reading under the prefix should reach
|
||||
# canonical_resources through _poll_subdevice_seed's flat-mode branch,
|
||||
# not just sit frozen at the one-time enumeration snapshot.
|
||||
# correcting the clone toward the sibling's real, confirmed value --
|
||||
# not just sitting frozen at the one-time enumeration snapshot.
|
||||
# FakeCoapSession's `seeds` is typed `dict[str, list]` for the common
|
||||
# batch-list shape, but (per its own docstring) also legitimately holds
|
||||
# plain Property maps for probe-style hrefs like these two.
|
||||
fresh_autoclean = "Off" if initial_autoclean != "Off" else "On"
|
||||
seeds_map = cast("dict[str, Any]", cast(FakeCoapSession, coordinator._session).seeds)
|
||||
seeds_map[f"/{_SUB_UUID}/temperature/current/0"] = {
|
||||
"range": [18.0, 30.0],
|
||||
"units": "C",
|
||||
"temperature": 27.5,
|
||||
"temperature": initial_temp + 1.5,
|
||||
}
|
||||
seeds_map[f"/{_SUB_UUID}/option/autoclean/vs/0"] = {
|
||||
"x.com.samsung.da.settingStatus": "On",
|
||||
"x.com.samsung.da.settingStatus": fresh_autoclean,
|
||||
}
|
||||
refreshed = coordinator._poll_subdevice_seed(subdevice)
|
||||
|
||||
@@ -391,16 +391,16 @@ async def test_flat_subdevice_materializes_and_repolls_end_to_end(hass: HomeAssi
|
||||
# through this path.
|
||||
assert f"/{_SUB_UUID}/temperature/current/0" not in refreshed
|
||||
assert refreshed == {
|
||||
f"/{_SUB_UUID}/option/autoclean/vs/0": {"x.com.samsung.da.settingStatus": "On"},
|
||||
f"/{_SUB_UUID}/option/autoclean/vs/0": {"x.com.samsung.da.settingStatus": fresh_autoclean},
|
||||
}
|
||||
|
||||
for href, rep in refreshed.items():
|
||||
coordinator._observe.apply(href, rep, source="poll")
|
||||
res = coordinator.canonical_resources(subdevice)
|
||||
assert res["/option/autoclean/vs/0"]["x.com.samsung.da.settingStatus"] == "On"
|
||||
assert res["/option/autoclean/vs/0"]["x.com.samsung.da.settingStatus"] == fresh_autoclean
|
||||
# Confirms the skip is about redundant re-fetching, not stale data --
|
||||
# the warm-tier value from initial discovery is still there, untouched.
|
||||
assert res["/temperature/current/0"]["temperature"] == 26.0
|
||||
assert res["/temperature/current/0"]["temperature"] == initial_temp
|
||||
|
||||
|
||||
class _FakeCollectionSession:
|
||||
|
||||
+73
-46
@@ -295,75 +295,103 @@ def test_enumerate_prefixed_from_subdevice_id_list():
|
||||
assert extra == {f"/{_UUID}/mode/vs/0": {"m": "cool"}}
|
||||
|
||||
|
||||
def test_enumerate_prefixed_falls_back_to_flat_hrefs_when_device0_collection_is_empty():
|
||||
def test_enumerate_prefixed_falls_back_to_cloning_master_state_when_device0_collection_is_empty():
|
||||
"""issue #205: not every prefixed subdevice exposes its own
|
||||
/<uuid>/device/0 Collection -- not even TP2X_FAC_BORA_21K, the board
|
||||
this pattern was built against, always does. When it doesn't,
|
||||
enumeration probes every href the master itself answered this cycle,
|
||||
individually, under the UUID prefix, and keeps whichever ones answer."""
|
||||
this pattern was built against, always does. issue #265: probing every
|
||||
master href individually under the prefix to confirm one used to be the
|
||||
fallback, but a firmware that drops packets instead of 4.04ing turned
|
||||
that into a ~300s hang that took the whole config entry's setup down
|
||||
with it. So the fallback no longer loops over every master href -- it
|
||||
trusts the device's own subdeviceIdList claim and clones the master's
|
||||
current hrefs and values verbatim under the prefix, plus one bounded
|
||||
retry of /information/vs/0 specifically (see the dedicated test below);
|
||||
that retry also fails to answer here, so the clone is all this
|
||||
candidate ends up with."""
|
||||
resources = {
|
||||
"/subdevices/vs/0": {"x.com.samsung.da.subdeviceIdList": [_UUID]},
|
||||
"/mode/vs/0": {"m": "cool"},
|
||||
"/power/vs/0": {"p": "On"},
|
||||
}
|
||||
sess = _FakeSession(
|
||||
{
|
||||
# (_UUID, 'device', '0') deliberately absent -> Collection probe fails.
|
||||
(_UUID, "mode", "vs", "0"): {"mode": "cool"},
|
||||
# (_UUID, 'power', 'vs', '0') deliberately absent -> drops out.
|
||||
}
|
||||
)
|
||||
sess = _FakeSession({}) # (_UUID, 'device', '0') absent -> Collection probe fails.
|
||||
subdevices, extra = enumerate_subdevices(sess, resources, oic_res_links=[])
|
||||
assert len(subdevices) == 1
|
||||
subdevice = subdevices[0]
|
||||
assert (subdevice.kind, subdevice.key) == ("prefixed", _UUID)
|
||||
assert subdevice.seed_path == ()
|
||||
assert subdevice.flat_hrefs == ("/mode/vs/0",)
|
||||
assert extra == {f"/{_UUID}/mode/vs/0": {"mode": "cool"}}
|
||||
|
||||
|
||||
def test_enumerate_prefixed_flat_fallback_materializes_nothing_when_no_href_answers():
|
||||
"""Same posture as every other candidate check in this module: nothing
|
||||
answering means no candidate, not a crash."""
|
||||
resources = {
|
||||
"/subdevices/vs/0": {"x.com.samsung.da.subdeviceIdList": [_UUID]},
|
||||
"/mode/vs/0": {"m": "cool"},
|
||||
assert subdevice.flat_hrefs == ("/mode/vs/0", "/power/vs/0", "/subdevices/vs/0")
|
||||
assert extra == {
|
||||
f"/{_UUID}/mode/vs/0": {"m": "cool"},
|
||||
f"/{_UUID}/power/vs/0": {"p": "On"},
|
||||
f"/{_UUID}/subdevices/vs/0": {"x.com.samsung.da.subdeviceIdList": [_UUID]},
|
||||
}
|
||||
subdevices, extra = enumerate_subdevices(_FakeSession({}), resources, oic_res_links=[])
|
||||
assert subdevices == []
|
||||
assert extra == {}
|
||||
|
||||
|
||||
def test_enumerate_prefixed_flat_fallback_probe_log_reports_every_href_tried():
|
||||
def test_enumerate_prefixed_flat_fallback_confirms_its_own_information():
|
||||
"""When the one bounded retry of /<uuid>/information/vs/0 *does* answer,
|
||||
its real reply overrides the master's cloned /information/vs/0 -- so the
|
||||
sibling's device_info shows its own model/serial instead of the
|
||||
master's, which would otherwise read as a duplicate device in the UI
|
||||
even though the two devices' registry identifiers are genuinely
|
||||
distinct (see device_info_for)."""
|
||||
resources = {
|
||||
"/subdevices/vs/0": {"x.com.samsung.da.subdeviceIdList": [_UUID]},
|
||||
"/information/vs/0": {"x.com.samsung.da.modelNum": "MASTER_MODEL"},
|
||||
"/mode/vs/0": {"m": "cool"},
|
||||
}
|
||||
sess = _FakeSession(
|
||||
{
|
||||
(_UUID, "mode", "vs", "0"): {"mode": "cool"},
|
||||
# (_UUID, 'device', '0') absent -> Collection probe fails.
|
||||
(_UUID, "information", "vs", "0"): {
|
||||
"x.com.samsung.da.modelNum": "SIBLING_MODEL",
|
||||
"x.com.samsung.da.serialNum": "SIBLING-SERIAL",
|
||||
},
|
||||
}
|
||||
)
|
||||
probes: dict[str, bool] = {}
|
||||
enumerate_subdevices(sess, resources, oic_res_links=[], probe_log=probes.__setitem__)
|
||||
assert probes[f"/{_UUID}/device/0"] is False
|
||||
assert probes[f"/{_UUID}/mode/vs/0"] is True
|
||||
|
||||
|
||||
def test_enumerate_prefixed_flat_fallback_with_no_master_hrefs_to_probe_is_a_no_op():
|
||||
"""The master itself having nothing but /subdevices/vs/0 in its own
|
||||
resources this cycle (e.g. a very first, mostly-empty poll) must not
|
||||
crash the fallback loop -- the only href in `resources` is
|
||||
/subdevices/vs/0 itself, which the fake session doesn't answer under
|
||||
the prefix either, so nothing materializes."""
|
||||
resources = {
|
||||
"/subdevices/vs/0": {"x.com.samsung.da.subdeviceIdList": [_UUID]},
|
||||
subdevices, extra = enumerate_subdevices(sess, resources, oic_res_links=[])
|
||||
assert len(subdevices) == 1
|
||||
subdevice = subdevices[0]
|
||||
assert subdevice.flat_hrefs == ("/information/vs/0", "/mode/vs/0", "/subdevices/vs/0")
|
||||
assert extra[f"/{_UUID}/information/vs/0"] == {
|
||||
"x.com.samsung.da.modelNum": "SIBLING_MODEL",
|
||||
"x.com.samsung.da.serialNum": "SIBLING-SERIAL",
|
||||
}
|
||||
subdevices, extra = enumerate_subdevices(_FakeSession({}), resources, oic_res_links=[])
|
||||
assert extra[f"/{_UUID}/mode/vs/0"] == {"m": "cool"}
|
||||
|
||||
|
||||
def test_enumerate_prefixed_flat_fallback_is_a_no_op_with_no_master_hrefs_to_clone():
|
||||
"""A degenerate `resources` (nothing at all, not even /subdevices/vs/0 --
|
||||
can't happen via the subdeviceIdList path but shared by _probe_prefixed
|
||||
with Pattern C, whose ids come from /oic/res instead) must not crash the
|
||||
fallback -- there's nothing to clone, so nothing materializes."""
|
||||
subdevices, extra = enumerate_subdevices(
|
||||
_FakeSession({}),
|
||||
{},
|
||||
oic_res_links=[{"href": f"/{_UUID}/multidevice/vs/0"}],
|
||||
)
|
||||
assert subdevices == []
|
||||
assert extra == {}
|
||||
|
||||
|
||||
def test_enumerate_prefixed_flat_fallback_probe_log_only_reports_seed_and_information():
|
||||
"""Only /<uuid>/device/0 (the seed) and /<uuid>/information/vs/0 (the one
|
||||
bounded exception -- so the sibling gets its own model/serial rather
|
||||
than the master's cloned ones) are ever probed over the network now --
|
||||
no more per-href probing loop to log."""
|
||||
resources = {
|
||||
"/subdevices/vs/0": {"x.com.samsung.da.subdeviceIdList": [_UUID]},
|
||||
"/mode/vs/0": {"m": "cool"},
|
||||
}
|
||||
probes: dict[str, bool] = {}
|
||||
enumerate_subdevices(
|
||||
_FakeSession({}), resources, oic_res_links=[], probe_log=probes.__setitem__
|
||||
)
|
||||
assert probes[f"/{_UUID}/device/0"] is False
|
||||
assert probes[f"/{_UUID}/information/vs/0"] is False
|
||||
allowed = {f"/{_UUID}/device/0", f"/{_UUID}/information/vs/0"}
|
||||
assert not any(href.startswith(f"/{_UUID}/") and href not in allowed for href in probes)
|
||||
|
||||
|
||||
def test_enumerate_prefixed_flat_fallback_does_not_cross_contaminate_a_second_uuid():
|
||||
"""Two prefixed candidates in the same subdeviceIdList, one whose
|
||||
Collection endpoint works and one that needs the flat fallback -- each
|
||||
@@ -379,9 +407,7 @@ def test_enumerate_prefixed_flat_fallback_does_not_cross_contaminate_a_second_uu
|
||||
_DEVCOL_REP,
|
||||
{"href": "/mode/vs/0", "rep": {"m": "a-collection"}},
|
||||
],
|
||||
# uuid_b's Collection deliberately absent -> falls back to the flat
|
||||
# per-href probe.
|
||||
(uuid_b, "mode", "vs", "0"): {"m": "b-flat"},
|
||||
# uuid_b's Collection deliberately absent -> falls back to cloning.
|
||||
}
|
||||
)
|
||||
subdevices, extra = enumerate_subdevices(sess, resources, oic_res_links=[])
|
||||
@@ -391,10 +417,11 @@ def test_enumerate_prefixed_flat_fallback_does_not_cross_contaminate_a_second_uu
|
||||
assert by_key[uuid_a].seed_path == (uuid_a, "device", "0")
|
||||
assert by_key[uuid_a].flat_hrefs == ()
|
||||
assert by_key[uuid_b].seed_path == ()
|
||||
assert by_key[uuid_b].flat_hrefs == ("/mode/vs/0",)
|
||||
assert by_key[uuid_b].flat_hrefs == ("/mode/vs/0", "/subdevices/vs/0")
|
||||
assert extra == {
|
||||
f"/{uuid_a}/mode/vs/0": {"m": "a-collection"},
|
||||
f"/{uuid_b}/mode/vs/0": {"m": "b-flat"},
|
||||
f"/{uuid_b}/mode/vs/0": {"m": "cool"},
|
||||
f"/{uuid_b}/subdevices/vs/0": {"x.com.samsung.da.subdeviceIdList": [uuid_a, uuid_b]},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user