Merge pull request #167 from mbillow/claude/issue-triage-138-latest-y90g78

Issue triage batch: oven/AC/microwave/air-dresser fixes, 4 new device types
This commit is contained in:
Marc Billow
2026-07-28 10:21:28 -05:00
committed by GitHub
47 changed files with 4225 additions and 82 deletions
+35 -4
View File
@@ -6,6 +6,7 @@ description: >-
raises the "incomplete capability coverage" repair, a diagnostics JSON needs
triaging, or you're mapping OCF resources to HA entities. Covers reading dumps,
OCF-standard vs vendor hrefs, the diagnostic/config/normal entity taxonomy,
preferring dynamic (device-reported) select options over hardcoded lists,
ensuring every href is bound or ignored, and locking it in with a fixture +
golden + test.
---
@@ -103,7 +104,37 @@ sub-polled between summary polls. Pick descriptor types from `entities.py`
as a gap for a human, or ignore it with a documented reason — never invent an
entity on a hunch (`ignored.py`'s rule).
## 5. Names and enum labels live in translations, never in Python
## 5. Select options: read them from the device, don't hardcode
A `SelectDesc`'s `options` should come from the device's own advertised list
whenever the resource carries one, not from a Python tuple typed in from a
single dump. Two dynamic forms already exist in the repo and should be
reached for first:
- `options_field='x.com.samsung.da.supportedModes'` (or whatever the
resource's own supported-values field is called) — reads the live rep on
the capability's own href. See `laundry.py`'s `buzzer_sound`/
`finish_sound` (`options_field='supportedBuzzerSound'`/
`'supportedFinishSound'`).
- `options=<callable>` — for option lists that live on a **different**
resource than the select's own href (e.g. a course table keyed off a
sibling href). See `laundry.cycle_select`'s `options=cycle_options`.
A static `options=(...)` tuple is a coverage gap waiting to happen: the next
dump from a different board generation will report modes/values the tuple
doesn't have, and both the HA options list *and* `write_fn`'s validation (if
it checks the same tuple) will silently reject values the device itself
advertises as supported. That's exactly what happened with `oven._OVEN_MODES`
in issue #138 — a hardcoded list rejected `AirFryer`/`Dehydrate`/
`SelfClean`/etc. even though the device's own `supportedModes` field listed
them. Reach for a static tuple only when the dump genuinely has no
supported-values field to read (e.g. the NV7000BS-class oven dump
`_OVEN_MODES` was inferred before any live oven dump existed — see that
module's docstring), and treat it as an interim best-guess rather than a
permanent design choice: migrate it to `options_field`/a callable the moment
a dump with a real supported-values list surfaces, instead of just adding
the new values to the static tuple.
## 6. Names and enum labels live in translations, never in Python
Descriptors have **no `name` field**. Every entity is named from the shipped
catalog, keyed by `translation_key` — which defaults to the descriptor's own
@@ -142,7 +173,7 @@ 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`.
## 6. Coverage discipline: bound or ignored
## 7. Coverage discipline: bound or ignored
Every href in the dump must resolve, or the repair fires. If a resource isn't
worth an entity, add it to `capabilities/ignored.py` (a no-entity `Capability`)
@@ -156,7 +187,7 @@ friendlier href**.
ignored because washers bind it. When only one family should ignore an href
that another binds, scope the ignore to that family's registry.
## 7. Reuse before writing new code
## 8. Reuse before writing new code
Check `common.py` (generic OCF: power, energy, alarms, water) and `laundry.py`
(shared washer/dryer/dishwasher: buzzer, job status, `cycle_select` + course
@@ -165,7 +196,7 @@ registry uses `fridge.FIRMWARE_UPDATE`; all three laundry families share
`laundry.cycle_select`. If two families hand-roll the same helper, hoist it to a
shared module rather than copying.
## 8. Lock it in
## 9. Lock it in
1. Add a **scrubbed** fixture `tests/fixtures/<type>_device.json`
(`{"device0": [ {devcol rep}, {href, rep}, ... ]}`) — replace serials, MACs,
+1
View File
@@ -48,6 +48,7 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi
| Washer | `by_type/washer.py` |
| Water purifier | `by_type/water_purifier.py` |
| Vacuum clean/auto-empty station | `by_type/vacuum_station.py` |
| Air dresser | `by_type/air_dresser.py` |
Each registry composes shared and family-specific `Capability` objects from `registry/capabilities/`; those modules document the individual resources/entities in more depth than a README table can stay current with.
+60 -5
View File
@@ -51,6 +51,7 @@ from .registry.capabilities.airconditioner import (
HREF_WIND_OSCILLATION as WIND_OSCILLATION_HREF,
HREF_CONVENIENT as CONVENIENT_HREF,
HREF_AIRFLOW as AIRFLOW_HREF,
is_legacy_board,
)
from .registry.capabilities.common import normalize_temp_unit
@@ -142,6 +143,22 @@ def _oscillation_swing(rep: dict) -> str | None:
return 'horizontal'
return 'off'
def _wind_strength_label(code, rep: dict) -> str:
"""Human label for a /wind/strength/vs/0 code from the device's own
modesName array (parallel-indexed with supportedModes), lowercased for
HA -- used only for codes _DEVICE_TO_FAN doesn't already cover (issue
#155, TP1X_DA-AC-RAC-01001_0000: codes "0"/"31"-"35" instead of the
"0"-"4" scale _DEVICE_TO_FAN was built from, with modesName giving
"Auto"/"1"/"2"/"3"/"4"/"MAX"). No per-model numeric map -- mirrors
preset_mode's dynamic code->str resolution. Falls back to the raw code
lowercased when modesName is absent or misaligned."""
supported = rep.get('x.com.samsung.da.supportedModes') or []
names = rep.get('x.com.samsung.da.modesName') or []
if code in supported and len(names) == len(supported):
return str(names[supported.index(code)]).lower()
return str(code).lower()
# Preset (convenient mode): resolved dynamically from the device's own
# /mode/convenient/vs/0 supportedModes -- no per-model table. The device 'Off'
# code maps to HA's PRESET_NONE ("no preset active"); every other code is
@@ -247,8 +264,20 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
def _legacy_airflow(self) -> dict:
"""The /airflow/vs/0 rep, but only when it is the fan/swing channel to
use -- i.e. this board has no /wind/strength/vs/0."""
if self.coordinator.resource(WIND_STRENGTH_HREF):
use -- i.e. this board has no /wind/strength/vs/0.
Delegates the board-generation test to is_legacy_board (the same
test capabilities/airconditioner.py's token entities are gated on)
instead of re-implementing it. Uses last_resources rather than a
two-key presence dict built from coordinator.resource()'s truthiness
-- resource() collapses "href absent" and "href present with an
empty {} rep" to the same falsy value, while is_legacy_board (and
discover()'s own binding) test key membership, not truthiness. A
presence dict built from truthiness alone would disagree with the
token entities on a board reporting a genuinely empty /airflow/vs/0,
silently reintroducing the drift this delegation exists to prevent.
"""
if not is_legacy_board(self.coordinator.last_resources):
return {}
return self.coordinator.resource(AIRFLOW_HREF) or {}
@@ -414,14 +443,24 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
airflow = self._legacy_airflow()
if airflow:
return _DEVICE_TO_FAN.get(str(airflow.get('x.com.samsung.da.speedLevel')))
return self._read_mode(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
rep = self._rep(WIND_STRENGTH_HREF)
code = _first(rep.get(_MODES_FIELD))
if code is None:
return None
return _DEVICE_TO_FAN.get(code) or _wind_strength_label(code, rep)
@property
def fan_modes(self) -> list[str]:
if self._legacy_airflow():
# This resource carries no supportedModes, so the full scale is offered.
return list(_DEVICE_TO_FAN.values())
return self._read_modes(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
rep = self._rep(WIND_STRENGTH_HREF)
modes = []
for code in self._supported(WIND_STRENGTH_HREF):
mode = _DEVICE_TO_FAN.get(code) or _wind_strength_label(code, rep)
if mode not in modes:
modes.append(mode)
return modes
def _swing_via_direction(self) -> bool:
"""True when WIND_DIRECTION_HREF is the swing channel to use --
@@ -523,7 +562,23 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
await self.coordinator.async_send_command(
self._bound, ('fan_legacy', level))
return
await self._set_mapped('fan', _FAN_TO_DEVICE, fan_mode)
supported = self._supported(WIND_STRENGTH_HREF)
device = _FAN_TO_DEVICE.get(fan_mode)
# A static hit is only trustworthy if this unit's own supportedModes
# actually includes that code -- a board can use non-standard codes
# (issue #155's "31"-"35") while still spelling a standard label
# ("Low"/"High") in modesName, in which case _FAN_TO_DEVICE.get would
# return a plausible-looking code ('1'/'3') the device never
# advertised at all. Fall through to the live scan whenever the
# static guess isn't actually one of this unit's own codes.
if device is None or (supported and device not in supported):
rep = self._rep(WIND_STRENGTH_HREF)
for code in supported:
if _wind_strength_label(code, rep) == fan_mode:
device = code
break
if device is not None:
await self.coordinator.async_send_command(self._bound, ('fan', device))
async def async_set_swing_mode(self, swing_mode: str) -> None:
if self._legacy_airflow():
+10 -9
View File
@@ -366,14 +366,22 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
if reg is None:
reg = for_device_by_resources(resources)
unbound: list[str] = []
hot, warm = set(), set()
def _tier_log(href: str, tier: str) -> None:
if tier == 'hot':
hot.add(href)
elif tier == 'warm':
warm.add(href)
if reg is not None:
self._log.debug("device type: %s (oneUiVersion=%r)", reg.name, one_ui)
bound = discover(resources, reg.capabilities, reg.pattern_capabilities,
log=unbound.append)
log=unbound.append, tier_log=_tier_log)
self.device_type_name = reg.name
else:
self._log.warning("unknown device type oneUiVersion=%r; using common caps", one_ui)
bound = discover(resources, CAPABILITIES, log=unbound.append)
bound = discover(resources, CAPABILITIES, log=unbound.append, tier_log=_tier_log)
self.device_type_name = None
self.bound = bound
self._unbound_hrefs = unbound
@@ -398,13 +406,6 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
)
self._update_coverage_gap_issue(reg is None, unbound, name)
hot, warm = set(), set()
for be in bound:
tier = be.capability.poll_tier
if tier == 'hot':
hot.add(be.href)
elif tier == 'warm':
warm.add(be.href)
self._hot_hrefs = sorted(hot)
self._warm_hrefs = sorted(warm)
+35 -11
View File
@@ -1,15 +1,18 @@
"""Fan platform for Samsung range hoods and air purifiers.
Three FanDesc-bound hrefs exist, dispatched by href in async_setup_entry
Four FanDesc-bound hrefs exist, dispatched by href in async_setup_entry
below since each needs different HA fan semantics: the range hood's fan
speed and the older ARTIK051_TVTL air-purifier family's Auto/Sleep/Low/
Medium/High (issue #56) are both an ordered set of numeric levels
(SET_SPEED) -- the latter confirmed monotonic in capabilities/
air_purifier.py's module docstring, with no named-mode list to preserve
since this board never self-reports one. The newer TP1X air-purifier
family's modes (Smart/Max/Mid/WindFree/Sleep, issue #130) are named
behaviors with no linear order (PRESET_MODE), reported directly by that
board's own supportedModes."""
since this board never self-reports one. The TP1X air-purifier family's
modes (Smart/Max/Mid/WindFree/Sleep, issue #130) and the A-VTWW-TP2-21
family's /wind/strength/vs/0 modes (issue #151) are both named behaviors
with no linear order (PRESET_MODE) -- LocalThingsAirPurifierFan handles
both hrefs, the only difference being whether the label comes straight
from supportedModes or from a parallel modesName array (see
_label_for_code)."""
from __future__ import annotations
@@ -29,6 +32,7 @@ from .coordinator import LocalThingsCoordinator
from .entity import LocalThingsEntity, _is_included
from .registry.capabilities.air_purifier import HREF_AIRFLOW
from .registry.capabilities.air_purifier import HREF_MODE as AIR_PURIFIER_FAN_HREF
from .registry.capabilities.air_purifier import HREF_WIND_STRENGTH as AIR_PURIFIER_WIND_STRENGTH_HREF
from .registry.entities import FanDesc
_LOGGER = logging.getLogger(__name__)
@@ -42,6 +46,7 @@ _OFF_SPEED_CODE = '0'
_MODES_FIELD = 'x.com.samsung.da.modes'
_SUPPORTED_MODES_FIELD = 'x.com.samsung.da.supportedModes'
_MODES_NAME_FIELD = 'x.com.samsung.da.modesName'
async def async_setup_entry(
@@ -54,7 +59,7 @@ async def async_setup_entry(
for bound in coordinator.bound:
if not (isinstance(bound.desc, FanDesc) and _is_included(bound, coordinator)):
continue
if bound.href == AIR_PURIFIER_FAN_HREF:
if bound.href in (AIR_PURIFIER_FAN_HREF, AIR_PURIFIER_WIND_STRENGTH_HREF):
entities.append(LocalThingsAirPurifierFan(coordinator, bound))
elif bound.href == HREF_AIRFLOW:
entities.append(LocalThingsAirflowFan(coordinator, bound))
@@ -243,10 +248,29 @@ class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity):
return str(power).lower() == 'on'
return bool(self._rep(POWER_HREF).get('value'))
def _label_for_code(self, code) -> str:
"""Lowercased HA preset label for a device mode code.
The TP1X_DA-AC-AIR board (issue #130) reports its named modes
directly as supportedModes ('Smart'/'Max'/...), so the code IS the
label. The A-VTWW-TP2-21 board (issue #151) instead reports numeric
wind-strength codes ('87'/'89'/...) with a separate modesName array
(parallel-indexed with supportedModes) giving the actual names --
same shape as climate.py's _wind_strength_label, and coincidentally
the same word set (Smart/Max/WindFree/Sleep), so both board
generations land on identical HA preset values without needing
their own translation catalog entry."""
rep = self._mode_rep()
supported = list(rep.get(_SUPPORTED_MODES_FIELD, ()))
names = rep.get(_MODES_NAME_FIELD)
if names and code in supported and len(names) == len(supported):
return str(names[supported.index(code)]).lower()
return str(code).lower()
@property
def preset_modes(self) -> list[str]:
return [
str(code).lower()
self._label_for_code(code)
for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ())
]
@@ -254,7 +278,7 @@ class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity):
def preset_mode(self) -> str | None:
modes = self._mode_rep().get(_MODES_FIELD)
code = modes[0] if isinstance(modes, (list, tuple)) and modes else modes
return str(code).lower() if code is not None else None
return self._label_for_code(code) if code is not None else None
async def async_turn_on(
self, percentage: int | None = None, preset_mode: str | None = None,
@@ -269,10 +293,10 @@ class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
# Reverse-resolve against the unit's own supportedModes -- the
# write needs the raw device code (e.g. 'WindFree'), not the
# lowercased HA value.
# write needs the raw device code (e.g. 'WindFree', or '90' on the
# modesName-labelled board), not the lowercased HA value.
for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ()):
if str(code).lower() == preset_mode:
if self._label_for_code(code) == preset_mode:
await self.coordinator.async_send_command(self._bound, ('mode', code))
return
_LOGGER.warning(
@@ -3,9 +3,9 @@ from typing import Optional
from ._base import DeviceRegistry
from . import (
air_purifier, airconditioner, cooktop, dehumidifier, dishwasher, dryer,
induction_cooktop, microwave, oven, range as _range, range_hood,
refrigerator, vacuum_station, washer, water_purifier,
air_dresser, air_purifier, airconditioner, cooktop, dehumidifier,
dishwasher, dryer, induction_cooktop, microwave, oven, range as _range,
range_hood, refrigerator, vacuum_station, washer, water_purifier,
)
__all__ = [
@@ -15,6 +15,7 @@ __all__ = [
_REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
'air_dresser': air_dresser.REGISTRY,
'air_purifier': air_purifier.REGISTRY,
'airpurifier': air_purifier.REGISTRY,
'airconditioner': airconditioner.REGISTRY,
@@ -200,6 +201,13 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
# here) -- no WAC-specific resources needed.
if key is None and '_WAC_' in (model_num or ''):
key = 'airconditioner'
# Wind-Free 2-in-1 systems (floor-standing + wall-mounted indoor units
# sharing one outdoor unit and one local IP, e.g. TP2X_FAC_BORA_21K,
# issues #150/#153) report no oneUiVersion and carry the '_FAC_' token
# instead of '_RAC_'/'_PRAC_'. Same TP1X/TP2X-class resource surface as
# the room-AC models above.
if key is None and '_FAC_' in (model_num or ''):
key = 'airconditioner'
# ARA-WW-class wall-mount RACs (e.g. ARA-WW-TP1-22-COMMON, issues #115/
# #116/#117/#120) report no oneUiVersion and no '_RAC_'/'-RAC-' token at
# all -- the board family is spelled 'ARA-WW-' instead. Same resource
@@ -222,6 +230,13 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
key = 'airconditioner'
if key is None and '_TVTL_' in (model_num or ''):
key = 'air_purifier'
# BESPOKE Cube Air (e.g. A-VTWW-TP2-21-COMMON, issue #151) reports no
# oneUiVersion and carries the hyphenated '-VTWW-' board-family token
# (distinct from the underscore-delimited '_TVTL_' ARTIK051 family
# above). Its fan lives on /wind/strength/vs/0 rather than /mode/vs/0 --
# see capabilities/air_purifier.py's WIND_STRENGTH_FAN.
if key is None and '-VTWW-' in (model_num or '').upper():
key = 'air_purifier'
model_identity = f'{model_num} {description}'.upper()
if key is None and ('_COOKTOP' in model_identity or '_GB_CT_' in model_identity):
key = 'cooktop'
@@ -270,6 +285,14 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
# station state) shares no hrefs with anything else already modeled.
if key is None and '-VSKR-' in (model_num or '').upper():
key = 'vacuum_station'
# AirDresser (e.g. DA_DF_A51_20_COMMON, issue #162) -- reports no
# oneUiVersion and carries the '_DF_' (Dresser Function) board-family
# token. Every resource it exposes (course select, wrinkle-prevent
# setting, diagnosis) is already handled by the shared laundry
# machinery; it needs its own device type only because none of the
# washer/dryer/dishwasher consumer-model prefixes below match it.
if key is None and '_DF_' in (model_num or ''):
key = 'air_dresser'
# Consumer-model prefix from `description` (washer/dryer/dishwasher) --
# last, since it's the fuzziest match (a bare 2-letter prefix) and would
# otherwise shadow the more specific board-family tokens above.
@@ -0,0 +1,24 @@
"""AirDresser device registry (issue #162).
Reuses washer/dryer/dishwasher's shared laundry surface -- job-beginning
status, operational state, and diagnosis. air_dresser.py holds the two
pieces specific to this device type: a minimal wrinkle-prevent-only
/washer/vs/0 capability, and the course select's own translation key.
"""
from ..capabilities import air_dresser, common, dishwasher, ignored, laundry, operational
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='air_dresser',
capabilities=_build([
*ignored.IGNORED,
*common.UNIVERSAL,
*common.POWER,
air_dresser.AIR_DRESSER_SETTINGS,
air_dresser.AIR_DRESSER_COURSE,
air_dresser.AIR_DRESSER_SANITIZE,
laundry.JOB_BEGINNING_STATUS,
operational.OPERATIONAL_STATE,
dishwasher.DIAGNOSIS,
]),
)
@@ -1,6 +1,6 @@
"""Air-purifier device registry.
Spans two board generations sharing this one registry (see
Spans three board generations sharing this one registry (see
capabilities/air_purifier.py's module docstring for the per-href
match_fn discriminators that keep them from colliding):
@@ -12,6 +12,10 @@ match_fn discriminators that keep them from colliding):
reported; reuses airconditioner.DISPLAY_LIGHT and airconditioner.MUTE_ONCE
for /light/vs/0 and /option/muteonce/vs/0, which are identical shapes on
the shared DA-AC- board family.
- A-VTWW-TP2-21-COMMON-class (issue #151). Reports no oneUiVersion and no
existing modelNum token; falls back to unknown until routed here. Its fan
is WIND_STRENGTH_FAN on /wind/strength/vs/0 rather than FAN on
/mode/vs/0 -- see that capability's comment.
Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0 (identical field/write
contract).
@@ -33,6 +37,7 @@ REGISTRY = DeviceRegistry(
air_purifier.AIRFLOW_VS_FALLBACK,
air_purifier.MODE,
air_purifier.FAN,
air_purifier.WIND_STRENGTH_FAN,
air_purifier.DISPLAY,
air_purifier.HEPA_FILTER,
air_purifier.PANEL_STATUS,
@@ -0,0 +1,80 @@
"""Capabilities specific to the AirDresser family (Samsung DA_DF-class,
issues #162/#157).
This board family reports no oneUiVersion and no /information/vs/0 token
any existing family routes on, so it gets its own device type -- but most
of the resources it exposes are already handled by the shared laundry
machinery:
/washer/vs/0 -> AIR_DRESSER_SETTINGS (wrinkle_prevent only -- a
dedicated capability rather than reusing
dryer.DRYER_SETTINGS wholesale: this board never
populates dryLevel/dryTime/dryerType at all, and
those are permanently-empty-not-just-absent
dryer-only fields on an AirDresser, not merely unset
ones, so binding them here would ship three sensors
that can never read anything on this device type)
/course/vs/0 -> AIR_DRESSER_COURSE (cycle select), table id from
/st/airdressercourse/vs/0 (issue #157's
DA_DF_TP2_20_COMMON reports "Table_00"; issue #162's
DA_DF_A51_20_COMMON has no table resource at all and
falls back to the name-only 'cycle' key, same as an
unrecognized table on washer/dryer). Options come
from laundry.cycle_options -- #157's board populates
/wm/editcourse/vs/0's editCourseList directly; #162's
has no editcourse resource at all and falls through
to cycle_options' supportedOptions decode instead.
Course names aren't identified yet for either table
(no code->name mapping was reported), so they render
as their raw codes until named in translations, same
as dryer.py's unidentified codes.
/diagnosis/vs/0 -> reuses dishwasher.DIAGNOSIS
/airdresseroption/sanitize/vs/0 -> AIR_DRESSER_SANITIZE (issue #157 only;
#162's board doesn't report this resource at all)
"""
from ..capability import Capability
from ..entities import SwitchDesc
from .laundry import cycle_select
def _wrinkle_write(p, rep, href=None):
if p not in ('On', 'Off'):
return None
return ['washer', 'vs', '0'], {'x.com.samsung.da.wrinklePrevent': p}
def _sanitize_write(p, rep, href=None):
if p not in ('On', 'Off'):
return None
return ['airdresseroption', 'sanitize', 'vs', '0'], {'x.com.samsung.da.sanitize': p}
AIR_DRESSER_SETTINGS = Capability(
href='/washer/vs/0',
poll_tier='warm',
entities=(
SwitchDesc(key='wrinkle_prevent', field='x.com.samsung.da.wrinklePrevent',
icon='mdi:iron',
value_fn=lambda v: v == 'On',
write_fn=_wrinkle_write),
),
)
AIR_DRESSER_COURSE = Capability(
href='/course/vs/0',
entities=(
cycle_select(translation_key='air_dresser_cycle', icon='mdi:tshirt-crew',
table_href='/st/airdressercourse/vs/0'),
),
)
AIR_DRESSER_SANITIZE = Capability(
href='/airdresseroption/sanitize/vs/0',
poll_tier='warm',
entities=(
SwitchDesc(key='sanitize', field='x.com.samsung.da.sanitize',
icon='mdi:weather-sunny',
value_fn=lambda v: v == 'On',
write_fn=_sanitize_write),
),
)
@@ -74,6 +74,7 @@ from .laundry import bool_option_exists, bool_option_value, option_value, option
# are mutually exclusive via this presence check rather than colliding.
HREF_MODE = '/mode/vs/0'
HREF_AIRFLOW = '/airflow/0'
HREF_WIND_STRENGTH = '/wind/strength/vs/0'
def _has_top_level_modes(rep, resources):
@@ -136,14 +137,22 @@ DEVICE_ACTIVE = Capability(
),
)
def _power_write(power_href, value):
"""Shared 'power' payload handling for this family's three FanDesc write
functions -- targets whichever power href fan.py's _power_payload picked
(the board may only report /power/0); a hardcoded vendor href here would
silently no-op on such a board even though the entity's own is_on
already falls back to reading it correctly."""
if power_href == '/power/0':
return ['power', '0'], {'value': bool(value)}
return (['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if value else 'Off'})
def _airflow_fan_write(payload, rep, href=None):
kind, value, *args = payload
if kind == 'power':
power_href = args[0] if args else '/power/vs/0'
if power_href == '/power/0':
return ['power', '0'], {'value': bool(value)}
return (['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if value else 'Off'})
return _power_write(args[0] if args else '/power/vs/0', value)
if kind == 'speed':
return ['airflow', '0'], {'speed': int(value)}
return None
@@ -233,15 +242,7 @@ MODE = Capability(
def _fan_write(payload, rep, href=None):
kind, value, *args = payload
if kind == 'power':
# Targets whichever power href fan.py's _power_payload picked (the
# board may only report /power/0) -- a hardcoded vendor href here
# would silently no-op on such a board even though the entity's
# own is_on already falls back to reading it correctly.
power_href = args[0] if args else '/power/vs/0'
if power_href == '/power/0':
return ['power', '0'], {'value': bool(value)}
return (['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if value else 'Off'})
return _power_write(args[0] if args else '/power/vs/0', value)
if kind == 'mode':
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [value]}
return None
@@ -272,6 +273,38 @@ FAN = Capability(
),
)
def _wind_strength_fan_write(payload, rep, href=None):
kind, value, *args = payload
if kind == 'power':
return _power_write(args[0] if args else '/power/vs/0', value)
if kind == 'mode':
return ['wind', 'strength', 'vs', '0'], {'x.com.samsung.da.modes': value}
return None
# A-VTWW-TP2-21-COMMON (issue #151): named preset modes like FAN above, but
# on a distinct href with numeric codes ("87"/"89"/"90"/"91") instead of
# self-describing supportedModes -- x.com.samsung.da.modesName gives the
# actual names (SMART/MAX/WINDFREE/Sleep), read live by fan.py's
# LocalThingsAirPurifierFan._label_for_code rather than a hardcoded
# per-model map. modes here is a bare string ('87'), not a single-element
# list like HREF_MODE's -- _wind_strength_fan_write writes it back as-is.
#
# key is 'wind_strength_fan', NOT 'fan' -- FAN above shares this registry
# and also uses a FanDesc; BoundEntity's unique_id is built from key alone
# (entity.py's _key), not href, so two same-key FanDescs in one registry
# would collide if a board ever bound both (see AIRFLOW_GENERIC's own
# comment on this exact hazard -- missed here in the initial cut).
WIND_STRENGTH_FAN = Capability(
href=HREF_WIND_STRENGTH,
poll_tier='warm',
entities=(
FanDesc(key='wind_strength_fan', translation_key='air_purifier_fan',
field='x.com.samsung.da.modes', write_fn=_wind_strength_fan_write),
),
)
# ---------------------------------------------------------------------------
# TP1X_DA-AC-AIR-class additions (issue #130). This board reports several
# resources the older ARTIK051_TVTL family never did.
@@ -425,4 +458,8 @@ COVERAGE = [
# functionState both 'false'). Same "needs a multi-field schedule
# editor" treatment as fridge.py's /defrost/reservation/vs/0.
Capability(href='/dnd/autosleep/vs/0'),
# Empty ({}) on the A-VTWW-TP2-21 dump (issue #151) -- this board's
# convenient-mode-equivalent behavior lives entirely in WIND_STRENGTH_FAN
# above instead.
Capability(href='/mode/convenient/vs/0'),
]
@@ -264,7 +264,7 @@ def _option_token(rep, prefix):
return None
def _is_legacy_board(resources):
def is_legacy_board(resources):
"""True for the board generation whose airflow lives in /airflow/vs/0.
Newer families carry several of the same option tokens (Volume, Sleep,
@@ -275,13 +275,12 @@ def _is_legacy_board(resources):
/airflow/vs/0. Same test as climate.py's _legacy_airflow(), so the entities
below and the climate entity can never disagree about the generation.
"""
return ('/airflow/vs/0' in resources
and '/wind/strength/vs/0' not in resources)
return HREF_AIRFLOW in resources and HREF_WIND_STRENGTH not in resources
def _has_option_token(prefix):
return lambda rep, resources: (
_is_legacy_board(resources) and _option_token(rep, prefix) is not None)
is_legacy_board(resources) and _option_token(rep, prefix) is not None)
def _option_token_on(prefix):
@@ -322,14 +321,20 @@ def _humidity(rep):
(51% observed, matching what the same unit's cloud integration reported at
that moment), then zeroes the field and switches Air monitoring back off by
itself. So 0 reads as "not measuring" and is reported as unknown rather than
as 0% humidity, which would poison long-term history -- which is also why
the boards that do have fivepercentHumidity read a permanent 0 here.
as 0% humidity, which would poison long-term history.
That zero-as-"not measuring" carve-out is specific to the ARTIK051
fallback field's hardware quirk -- every other board's fivepercentHumidity
has never been documented getting stuck at zero, and collapsing a
genuine 0% reading there to unknown is a regression, not a safeguard
(issue #160). So fivepercentHumidity passes 0 through unchanged; only the
humidity fallback applies the zero-collapse.
"""
for field in ('x.com.samsung.da.fivepercentHumidity',
'x.com.samsung.da.humidity'):
if field in rep:
value = _num(rep[field])
return value if value else None
if 'x.com.samsung.da.fivepercentHumidity' in rep:
return _num(rep['x.com.samsung.da.fivepercentHumidity'])
if 'x.com.samsung.da.humidity' in rep:
value = _num(rep['x.com.samsung.da.humidity'])
return value if value else None
return None
@@ -422,13 +427,13 @@ CLIMATE = Capability(
icon='mdi:led-on', entity_category='config'),
# Beep on/off from the `Volume_*` option token (Volume_Mute/Volume_100).
# Single-token option_write; a full options RMW reverts on ARTIK051_PRAC.
# Gated off the legacy ARTIK051 board generation (see _is_legacy_board):
# Gated off the legacy ARTIK051 board generation (see is_legacy_board):
# that generation's own Volume_ token is already modeled as the
# buzzer_volume Number below, and both reading the same options[] slot
# into two entities would be redundant.
SwitchDesc(key='beep', rep_fn=_beep_on,
exists_fn=lambda rep, resources: (
not _is_legacy_board(resources)
not is_legacy_board(resources)
and _option_token(rep, 'Volume') is not None),
write_fn=_beep_write,
icon='mdi:volume-high', entity_category='config'),
@@ -438,7 +443,7 @@ CLIMATE = Capability(
# Sleep_ token is already the good_sleep Number below.
NumberDesc(key='tropical_night_mode', rep_fn=_tropical_night_value,
exists_fn=lambda rep, resources: (
not _is_legacy_board(resources)
not is_legacy_board(resources)
and _option_token(rep, 'Sleep') is not None),
write_fn=_tropical_night_write,
native_min=0, native_max=16, step=1,
@@ -783,6 +788,19 @@ _AC_IGNORED = [
# topology -- indoor/outdoor unit pairing, per-unit serials, MCU info.
# Commissioning-time plumbing, not user-actionable appliance state.
'/sac/installationinfo/vs/0',
# Wind-Free 2-in-1 systems (one outdoor unit driving a floor-standing
# *and* a wall-mounted indoor unit over one shared local IP, e.g.
# TP2X_FAC_BORA_21K, issues #150/#153): an opaque paired-subdevice id
# list, same "remote device ids, not user-actionable locally" role as
# /remotedeviceinfo/vs/0 above. This integration talks to whichever
# single local endpoint the config entry was set up against; the
# second indoor unit isn't independently reachable through this
# resource (or any other in the dump) -- it would need its own local
# DTLS session/IP, which SmartThings pairing doesn't expose here.
'/subdevices/vs/0',
# Undocumented single int (runningMode: 0 on every dump seen), no
# supported-values list to interpret it against -- 'don't guess'.
'/runn/vs/0',
]
# Built as bare no-entity caps; folded into the AC registry (not global).
@@ -91,12 +91,25 @@ def _ml_to_l(v):
def _active_alarm_codes(items):
"""Join active alarm codes; skip retained rows Samsung leaves as Deleted.
"""Join active alarm codes; skip retained rows Samsung leaves as Deleted,
and any code ending in '_OFF'.
Laundry boards keep a Deleted ErrorCode row in /alarms/vs/0 after the
condition clears (see WD7000B diagnostics). Surface only live alarms so
HA doesn't stick on a stale ErrorCode. Range-hood has its own stricter
helper that also drops ErrorCode_OFF.
HA doesn't stick on a stale ErrorCode.
Samsung pre-populates this array with one row per alarm *type* the board
supports, each carrying its own '<Name>_OFF' placeholder code when that
alarm isn't firing -- confirmed across independent device families
(ErrorCode_OFF, FilterAlarm_OFF, OV_E_OFF, CT_E_OFF, WaterTankFull_OFF,
AC_V_0002_OFF all appear in fixtures with no corresponding active
condition). An alarm that's actually firing instead reports a plain,
unsuffixed code (FilterAlarm, DoorA_Opened, SNSF_Reached) -- issue #166's
AC dump has both a FilterAlarm_OFF placeholder and shows what a live
filter alert looks like: code 'FilterAlarm' (no suffix), state
'Created'. Range-hood previously special-cased only the literal
'ErrorCode_OFF' string in its own stricter helper; this generalizes
the same rule to the whole '_OFF' suffix convention.
"""
if not items or not isinstance(items, list):
return 'none'
@@ -105,6 +118,7 @@ def _active_alarm_codes(items):
for i in items
if i.get('x.com.samsung.da.code')
and str(i.get('x.com.samsung.da.state', '')).lower() != 'deleted'
and not str(i.get('x.com.samsung.da.code', '')).lower().endswith('_off')
]
return ', '.join(codes) if codes else 'none'
@@ -53,6 +53,10 @@ IGNORED: list[Capability] = [
Capability(href='/setting/vs/0'), # supported/selected UI language
Capability(href='/timezone/vs/0'), # redundant with HA's own timezone
Capability(href='/wm/setinfo/vs/0'), # model/manufacturing metadata
# Resource-monitoring poll-interval config (a bare minPeriod in
# milliseconds, issue #165's TP1X_REF_21K fridge) -- internal transport
# plumbing, not appliance state.
Capability(href='/rm/control/vs/0'),
# Demand Response Load Control — utility-company grid signals; requires
# cloud registration with a utility program we don't support locally.
@@ -107,6 +111,11 @@ IGNORED: list[Capability] = [
# exposed by dryer.DRYER_COURSE at /course/vs/0
# (x.com.samsung.da.st.dryerMode is "Table_03_Course_<same hex code>").
Capability(href='/st/dryercourse/vs/0'),
# AirDresser counterpart of the above (issue #157): read only for its
# courseTable id (air_dresser.AIR_DRESSER_COURSE's table_href), no
# entity of its own -- same "no entity, just the table id" role as
# /st/washercourse/vs/0 and /st/dryercourse/vs/0.
Capability(href='/st/airdressercourse/vs/0'),
# Empty on every washer dump seen so far.
Capability(href='/wm/welcomemsg/vs/0'),
# User-saved custom course slots (F1-FA). No controllable/observable
@@ -21,7 +21,12 @@ different from an oven, and defined fresh here:
* Lamp: this family's option-array token is bare 'Lamp' (issue #137's
'Lamp_Off'), not oven.py's 'UpperLamp' -- and it's genuinely absent on
the combi dump (issue #121), so it's gated with exists_fn rather than
assumed universal like oven.py's lamp switch.
assumed universal like oven.py's lamp switch. Issue #137's dump only
ever showed 'Off', so 'On' was a guess at the paired value; issue #152's
ME7500D dump is the first to show a real non-Off value, and it's 'High'
(a brightness level, not literally 'On') -- the switch now treats any
non-Off/non-None value as "on" for reads, and writes back 'High'/'Off'
(the two confirmed tokens) rather than the never-confirmed 'On'.
Note: cooking-mode writes are unproven here, same caveat as oven.py's
OVEN_MODE -- exposed as a SelectDesc for fidelity, first real-world write
@@ -107,8 +112,21 @@ def _power_level_watts(v):
return int_or_none(s)
def _cooking_mode_options(resources):
"""Live mode list from the device's own /mode/vs/0 supportedModes when
it reports one (both known dumps do); the union-of-all-dumps
_MICROWAVE_MODES guess otherwise. Same live-first, static-fallback
pattern as oven._oven_mode_options -- a fixed list here would offer
users modes their own unit doesn't have (issue #152's ME7500D reports
only 4 of _MICROWAVE_MODES' 11)."""
rep = resources.get('/mode/vs/0') or {}
live = rep.get('x.com.samsung.da.supportedModes')
return list(live) if live else list(_MICROWAVE_MODES)
def _mode_write(p, rep, href=None):
if p not in _MICROWAVE_MODES:
valid = rep.get('x.com.samsung.da.supportedModes') or _MICROWAVE_MODES
if p not in valid:
return None
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]}
@@ -132,8 +150,12 @@ def _lamp_write(p, rep, href=None):
return None
if not rep.get('x.com.samsung.da.options'):
return None
# 'High' and 'Off' are the two tokens actually confirmed on live dumps
# (issues #137/#152) -- 'On' has never been observed and the device
# likely doesn't recognize it (see module docstring).
token = 'High' if p == 'On' else 'Off'
return ['mode', 'vs', '0'], {
'x.com.samsung.da.options': option_write('Lamp', p),
'x.com.samsung.da.options': option_write('Lamp', token),
}
@@ -178,7 +200,7 @@ MICROWAVE_MODE = Capability(
# SelectDesc first — test_microwave_mode_options_nonempty uses entities[0]
SelectDesc(key='cooking_mode', field='x.com.samsung.da.modes',
icon='mdi:tune',
options=_MICROWAVE_MODES,
options=_cooking_mode_options,
value_fn=lambda v: v[0] if v else None,
write_fn=_mode_write),
SwitchDesc(key='sound', field='x.com.samsung.da.options',
@@ -189,7 +211,7 @@ MICROWAVE_MODE = Capability(
SwitchDesc(key='lamp', field='x.com.samsung.da.options',
icon='mdi:track-light',
exists_fn=_lamp_exists,
value_fn=lambda opts: option_value(opts, 'Lamp') == 'On',
value_fn=lambda opts: option_value(opts, 'Lamp') not in (None, 'Off'),
write_fn=_lamp_write),
),
)
@@ -51,6 +51,14 @@ SETPOINT_STEP_F = 5
# Mode options seen on NV7000BS-class. No dump exists so this list is inferred
# from Samsung documentation and firmware observations. The firmware will
# reject unknown modes; missing entries here are a coverage gap, not a bug.
#
# This is a fallback only, used when a device's own /mode/vs/0 doesn't report
# x.com.samsung.da.supportedModes at all -- see _oven_mode_options/
# _oven_mode_write below. issue #138's range dump (NE63A6511SS/AA) reports
# ConvectionRoast/KeepWarm/BreadProof/AirFryer/Dehydrate/SelfClean/SteamClean
# in its own supportedModes; those are read live rather than added here, per
# the adding-device-support skill's preference for device-reported option
# lists over hardcoded ones.
_OVEN_MODES = (
'NoOperation',
'Bake',
@@ -173,8 +181,20 @@ def _cook_time_write(p, rep, href=None):
}
def _oven_mode_options(resources):
"""Live mode list from the device's own /mode/vs/0 supportedModes when
it reports one; the NV7000BS-era _OVEN_MODES guess otherwise. Mirrors
laundry.py's options_field pattern (buzzer_sound/finish_sound), but
needs the callable form rather than options_field because a static
fallback has to kick in when the device's own field is absent."""
rep = resources.get('/mode/vs/0') or {}
live = rep.get('x.com.samsung.da.supportedModes')
return list(live) if live else list(_OVEN_MODES)
def _oven_mode_write(p, rep, href=None):
if p not in _OVEN_MODES:
valid = rep.get('x.com.samsung.da.supportedModes') or _OVEN_MODES
if p not in valid:
return None
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]}
@@ -351,7 +371,7 @@ OVEN_MODE = Capability(
# SelectDesc first — test_oven_mode_options_nonempty uses entities[0]
SelectDesc(key='oven_mode', field='x.com.samsung.da.modes',
icon='mdi:tune',
options=_OVEN_MODES,
options=_oven_mode_options,
value_fn=lambda v: v[0] if v else None,
write_fn=_oven_mode_write),
SwitchDesc(key='lamp', field='x.com.samsung.da.options',
@@ -73,7 +73,15 @@ def discover(
registry: dict[str, list[Capability]],
pattern_caps: Iterable[Capability] = (),
log: Optional[Callable[[str], None]] = None,
tier_log: Optional[Callable[[str, str], None]] = None,
) -> list[BoundEntity]:
"""`tier_log(href, poll_tier)` fires for every href a capability actually
matches, even a no-entity "coverage-only" capability (see COVERAGE lists
in capabilities/*.py) that `_bind()` turns into zero `BoundEntity` rows.
Callers that need a href's poll cadence (the coordinator's hot/warm
sub-poll and OBSERVE-attempt lists) must use this, not `bound` -- a
coverage-only capability's `poll_tier` would otherwise be silently
dropped since it never appears in `bound`."""
out: list[BoundEntity] = []
for href, rep in resources.items():
@@ -91,6 +99,8 @@ def discover(
inst = instance_suffix(href)
out.extend(_bind(cap, href, inst, _instance_name(cap, rep)))
matched = True
if tier_log is not None:
tier_log(href, cap.poll_tier)
if matched:
continue
@@ -109,6 +119,8 @@ def discover(
segs = [s for s in src.strip('/').split('/') if s and not s.isdigit() and s != 'vs']
out.extend(_bind(cap, href, inst, _instance_name(cap, rep), '_'.join(segs)))
matched = True
if tier_log is not None:
tier_log(href, cap.poll_tier)
break
if not matched and not caps and log is not None:
@@ -120,7 +120,8 @@
"state_attributes": {
"fan_mode": {
"state": {
"turbo": "Turbo"
"turbo": "Turbo",
"max": "Max"
}
},
"preset_mode": {
@@ -120,7 +120,8 @@
"state_attributes": {
"fan_mode": {
"state": {
"turbo": "Turbo"
"turbo": "Turbo",
"max": "Max"
}
},
"preset_mode": {
+209
View File
@@ -0,0 +1,209 @@
{
"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": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.countryCode": "VN"
}
},
{
"href": "/course/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.options": [
"DeviceType_015E",
"UpdateAllow_NotAllowed",
"Course_01",
"SteamPush_None",
"SeamlessControl_Disable",
"KidsLockBypass_On",
"WashingTimes_58",
"DrumCleanProposal_30",
"SpecialFunction_4",
"AvailableDelayTime_0",
"ProgressTimeSet_AA01E0AC00B4B20690",
"WrinklePreventRunning_Off",
"SendToDevice_Off",
"GMT_12",
"Alarm_Off",
"SilentSet_0F0FF0F0F0F0F0F0F0F00F0F0F0FF0F0F00FF0",
"Silent_Off",
"WrinklePreventSet_0F0F0F0F0F0F0F0FF0F00F0F0F0F0F0F0F0FF0",
"DelayEndSet_0F0F0F0FF00F0F0F0F0F0F0F0F0F0F0F0F0F0F",
"OneTimeCourse_761F00200000000000000000000000",
"DownloadCourseList_090A0B12100C140D0F",
"EnergyLevelSet_0502020104030204050405020303010302010305",
"MostUsed_01",
"UsagesDB_ok",
"EnergyKW_396",
"DrumCleanLog_2024-01-01T00:00:00",
"TimeSync_NotSupported"
],
"x.com.samsung.da.supportedOptions": [
"001020403051A1B1C0708"
]
}
},
{
"href": "/cycleinterface/vs/0",
"rep": {}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/energy/consumption/0",
"rep": {}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.cumulativePower": "78200",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.cumulativeDate": "1785272400",
"x.com.samsung.da.cumulativeDateUTC": "1785240000"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "DA_DF_A51_20_COMMON|20261441|3801010200131100020100FF00000000",
"x.com.samsung.da.description": "DA_DF_A51_20_COMMON_DF8600T/DC92-02656A_0018",
"x.com.samsung.da.serialNum": "REDACTED",
"x.com.samsung.da.otnDUID": "REDACTED",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_DF_A51_20_COMMON|20261441|3801010200131100020100FF00000000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02672A230710(E431)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "DA_DF_A51_20_COMMON",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "20060507,20072702",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/kidslock/0",
"rep": {
"value": false
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/operational/state/0",
"rep": {
"currentMachineState": "REDACTED",
"machineStates": "REDACTED",
"jobStates": [
"None",
"Steaming",
"Airwashing",
"Drying",
"Finish"
],
"currentJobState": "None",
"remainingTime": "00:39:00",
"progressPercentage": "1"
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.remainingTime": "00:39: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",
"Steaming",
"Airwashing",
"Drying",
"Finish"
]
}
},
{
"href": "/power/0",
"rep": {
"value": true
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "10",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/remotectrl/0",
"rep": {
"value": false
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "false"
}
},
{
"href": "/washer/vs/0",
"rep": {
"x.com.samsung.da.wrinklePrevent": "Off"
}
},
{
"href": "/wm/jobbeginingstatus/vs/0",
"rep": {}
}
]
}
+282
View File
@@ -0,0 +1,282 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/airdresseroption/sanitize/vs/0",
"rep": {
"x.com.samsung.da.sanitize": "Off",
"x.com.samsung.da.supportedSanitize": [
"On",
"Off"
]
}
},
{
"href": "/alarms/vs/0",
"rep": {}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "3171000000",
"x.com.samsung.da.countryCode": "KR"
}
},
{
"href": "/course/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.options": [
"DeviceType_015E",
"UpdateAllow_NotAllowed",
"Course_22",
"AiOption_On",
"SteamPush_None",
"SeamlessControl_Disable",
"KidsLockBypass_On",
"FilterUsage_384019D5",
"WashingTimes_6",
"DrumCleanProposal_30",
"ProgressTimeSet_AA01A4AC00F0B20690",
"WrinklePreventRunning_Off",
"SendToDevice_Off",
"WeatherPush_Off",
"GMT_12",
"Alarm_Off",
"WrinklePreventSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0FF00F0F0F0F0F0F0F0F0F0F0FF0F0",
"DelayEndSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0FF0F0F00F0F0FF0F0F0F0F00F0F",
"EnergyLevelSet_0502010102010202030303010401040505050503030502040503030303030405",
"UsagesDB_ok",
"DrumCleanLog_2023-04-02T08:08:55|2023-10-02T03:38:19|2024-05-26T03:25:41|2024-10-01T01:07:20|2026-06-16T02:23:29"
],
"x.com.samsung.da.supportedOptions": [
"12261062361020E61020961021261020C61021E61020B61021061020A61061461021361061661022462062562062F62062062040F62042761023061021561021A61021B61021C61022A61022B61022C61022D61022E6102076102086102"
]
}
},
{
"href": "/cycleinterface/vs/0",
"rep": {
"x.com.samsung.da.cycleInterfaceEnabled": "Off"
}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/energy/consumption/0",
"rep": {}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePower": "63600",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.cumulativeDate": "1784404800",
"x.com.samsung.da.cumulativeDateUTC": "1784372400"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "DA_DF_TP2_20_COMMON|20286141|380101010015110F0201000100010000",
"x.com.samsung.da.description": "DA_DF_TP2_20_COMMON_DF9500A/DC92-02888A_0002",
"x.com.samsung.da.serialNum": "REDACTED",
"x.com.samsung.da.otnDUID": "REDACTED",
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
"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": "A00",
"x.com.samsung.da.diagMinVersion": "1.0",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_DF_TP2_20_COMMON|20286141|380101010015110F0201000100010000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02673A250416(F822)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Firmware_1_DB",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "21020115,23030652",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Firmware_2_DB",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "19111852,FFFFFFFF"
}
]
}
},
{
"href": "/kidslock/0",
"rep": {
"value": false
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/operational/state/0",
"rep": {
"currentMachineState": "REDACTED",
"machineStates": "REDACTED",
"jobStates": [
"None",
"Steaming",
"Airwashing",
"Drying",
"Finish"
],
"currentJobState": "None",
"remainingTime": "00:39:00",
"progressPercentage": "1"
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.remainingTime": "00:39: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",
"Steaming",
"Airwashing",
"Drying",
"Finish"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "Micom",
"x.com.samsung.da.newVersionAvailable": "false"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/remotectrl/0",
"rep": {
"value": false
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "false"
}
},
{
"href": "/st/airdressercourse/vs/0",
"rep": {
"x.com.samsung.da.st.airdresserMode": "Table_00_Course_22",
"x.com.samsung.da.st.courseTable": "Table_00"
}
},
{
"href": "/washer/vs/0",
"rep": {
"x.com.samsung.da.wrinklePrevent": "Off"
}
},
{
"href": "/wm/editcourse/vs/0",
"rep": {
"x.com.samsung.da.editCourseList": "EditCourseList_22230C10252F0F0E09121E0B0A14131624202730151A1B1C2A2B2C2D2E0708",
"x.com.samsung.da.fixedCourseList": "FixedCourseList_22270F"
}
},
{
"href": "/wm/jobbeginingstatus/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": "/wm/setinfo/vs/0",
"rep": {
"x.com.samsung.da.isModelSettingWithoutSC": "false",
"x.com.samsung.da.aiCourse": "false",
"x.com.samsung.da.isModelSettingPowerOnOff": "true"
}
},
{
"href": "/wm/welcomemsg/vs/0",
"rep": {}
}
]
}
+310
View File
@@ -0,0 +1,310 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/airlevelcheck/vs/0",
"rep": {
"x.com.samsung.da.periodicSensingActivationState": "Off",
"x.com.samsung.da.periodicSensingInterval": "600",
"x.com.samsung.da.startSensingOnce": "On",
"x.com.samsung.da.sensingState": "NonProcessing",
"x.com.samsung.da.lastSensingTime": "1785195000",
"x.com.samsung.da.lastSensingLevel": "Kr1",
"x.com.samsung.da.autoExeState": "Off",
"x.com.samsung.da.supportedAutoExeState": [
"Off",
"Airpurify",
"Alarm"
],
"x.com.samsung.da.periodicSensingSkipStatus": "Off",
"x.com.samsung.da.periodicSensingSkipTime": "15002100"
}
},
{
"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": "2024-01-01T00:00:00",
"x.com.samsung.da.state": "Deleted"
}
]
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "0000000000000000000000000000",
"x.com.samsung.da.id": "VTL",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "3017000000",
"x.com.samsung.da.airconOptionList": [
"WELCOMECARE",
"AI_PURIFY"
],
"x.com.samsung.da.countryCode": ""
}
},
{
"href": "/da/softreset/vs/0",
"rep": {
"x.com.samsung.da.softwarereset": "false"
}
},
{
"href": "/devicespecificinfo/vs/0",
"rep": {
"x.com.samsung.da.deviceActive": true
}
},
{
"href": "/dnd/autosleep/vs/0",
"rep": {
"x.com.samsung.da.startTime": "14:00:00",
"x.com.samsung.da.endTime": "22:00:00",
"x.com.samsung.da.visible": "true",
"x.com.samsung.da.useTimeSetting": "true",
"x.com.samsung.da.functionState": "true"
}
},
{
"href": "/energy/consumption/0",
"rep": {}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativePower": "196696",
"x.com.samsung.da.cumulativeDate": "1785245199",
"x.com.samsung.da.cumulativeDateUTC": "1785212799",
"x.com.samsung.da.cumulativeUnit": "Wh"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00"
}
},
{
"href": "/filter/hepafilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "0",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterStatus": "normal",
"x.com.samsung.da.filterCapacity": "8760",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"replaceable"
]
}
},
{
"href": "/humidity/0",
"rep": {}
},
{
"href": "/humidity/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "A-VTWW-TP2-21-COMMON|10231241|70000535001511EC0505090201000000",
"x.com.samsung.da.description": "A-VTWW-TP2-21-COMMON",
"x.com.samsung.da.serialNum": "REDACTED",
"x.com.samsung.da.otnDUID": "REDACTED",
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
"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": "101",
"x.com.samsung.da.diagMinVersion": "1.0",
"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": "02308A260310",
"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": "21063003,21063002",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.options": [
"CountOfSlave_0",
"SlavePower01_NotSupported",
"Pollution_Off",
"OptionCode_23418",
"SmartSleep_Off"
]
}
},
{
"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": "A-VTWW-TP2-21-COMMON",
"versions": [
"12260310"
],
"visVersion": "260310"
},
{
"type": "Micom",
"modelId": "09501023124110231341",
"versions": [
"21063003",
"21063002"
],
"visVersion": "210630"
}
]
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "7",
"x.com.samsung.da.longnotisubscription": "true",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/sensors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Sensor for Dust",
"x.com.samsung.da.type": "Dust",
"x.com.samsung.da.value": [
"5",
"1"
]
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Sensor for FineDust",
"x.com.samsung.da.type": "FineDust",
"x.com.samsung.da.value": [
"5",
"1"
]
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Sensor for Odor",
"x.com.samsung.da.type": "Odor",
"x.com.samsung.da.value": [
"1"
]
},
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Sensor for CleanLevel",
"x.com.samsung.da.type": "CleanLevel",
"x.com.samsung.da.value": [
"1"
]
},
{
"x.com.samsung.da.id": "4",
"x.com.samsung.da.description": "Sensor for SuperFineDust",
"x.com.samsung.da.type": "SuperFineDust",
"x.com.samsung.da.value": [
"5",
"1"
]
}
],
"x.com.samsung.da.total": "5"
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "87",
"x.com.samsung.da.supportedModes": [
"87",
"89",
"90",
"91"
],
"x.com.samsung.da.modesName": [
"SMART",
"MAX",
"WINDFREE",
"Sleep"
]
}
}
]
}
+446
View File
@@ -0,0 +1,446 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"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": "2024-01-01T00:00:00"
},
{
"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": "2024-01-01T00:00:00"
}
]
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "000000B4012C0000404B0C000000",
"x.com.samsung.da.id": "FAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "2824500000",
"x.com.samsung.da.airconOptionList": [
"HOMECARE_WIZARD_V2",
"ENERGY_2.0",
"AI_2.0",
"DeviceTypeMaster",
"SingleCommand_1"
]
}
},
{
"href": "/diagnosis/vs/0",
"rep": {}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 0,
"start": "1970-01-01T00:00:00Z",
"duration": 0,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.duration": "00:00:00",
"x.com.samsung.da.drlcStartTime": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off"
}
},
{
"href": "/energy/consumption/0",
"rep": {
"energy": 100.0,
"power": 65278.0
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeConsumption": "100.000000",
"x.com.samsung.da.instantaneousPower": "65278.000000",
"x.com.samsung.da.usageThreshold": "0.000000",
"x.com.samsung.da.cumulativePower": "1278818",
"x.com.samsung.da.cumulativeDate": "1785243600",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "individual"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "100",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterDesiredUsage": "112",
"x.com.samsung.da.filterStatus": "wash",
"x.com.samsung.da.filterCapacity": "112",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"washable"
],
"x.com.samsung.da.supportedFilterDesiredUsage": [
"112",
"224",
"336",
"448"
]
}
},
{
"href": "/humidity/0",
"rep": {
"humidity": 0
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "0.000000",
"x.com.samsung.da.fivepercentHumidity": "73"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP2X_FAC_BORA_21K|10233041|600001110015110006000C1200830000",
"x.com.samsung.da.description": "TP2X_FAC_BORA_21K",
"x.com.samsung.da.serialNum": "REDACTED",
"x.com.samsung.da.otnDUID": "REDACTED",
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
"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": "000",
"x.com.samsung.da.diagMinVersion": "1.0",
"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": "02337A260424",
"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": "2102240021022200",
"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": "2103300110000300"
}
]
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Quiet",
"Speed"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"AIComfort",
"Cool",
"Dry",
"Wind"
],
"x.com.samsung.da.modes": [
"AIComfort"
],
"x.com.samsung.da.options": [
"Operation_Family",
"Blooming_0",
"OnTimer_0",
"OffTimer_0",
"Sleep_16",
"ArtificialWorking_Off",
"ComfortAICooling_Off",
"AiTempChanged_Off",
"AiTemp_270",
"welcomecare_Off",
"Panel_Close",
"Weather_Off",
"Volume_100",
"StopAutoClean_Idle",
"DiagnosisAI_Off",
"Display_Off",
"ProgressDiagnosisAI_1",
"ResultDiagnosisAI_Normal",
"Service_Off",
"SmartCoolClean_Off",
"ProgressSmartClean_0",
"OutDoorVentil_Off",
"FreezeAlarmSetting_On",
"DesiredFreezeAlarm_112",
"OptionCode_529",
"ExtendOptionCode_16975",
"RacInfo_First",
"RacInfo_None_Second",
"ModelInfo_16K_BORA_VENT2",
"UpdateAllow_NotAllowed",
"EnergySaveIcon_Off",
"DurationOn_0",
"welcomecareElapsedTime_0",
"welcomecareThresholdTemp_0",
"welcomecareStartDate_0000",
"welcomecareEndDate_0000",
"welcomecareSeason_None"
]
}
},
{
"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": "/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-KR-TP2-21-AF9X00",
"versions": [
"10260424"
],
"visVersion": "260424"
},
{
"type": "Micom",
"modelId": "04511023304110232941",
"versions": [
"21022400",
"21022200"
],
"visVersion": "210224"
},
{
"type": "Micom",
"modelId": "04511022974110229941",
"versions": [
"21033001",
"10000300"
],
"visVersion": "210330"
},
{
"type": "Micom",
"modelId": "045110230741FFFFFFFF",
"versions": [
"22050300",
"FFFFFFFF"
],
"visVersion": "220503"
}
]
}
},
{
"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": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "true",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/runn/vs/0",
"rep": {
"x.com.samsung.da.runningMode": 0
}
},
{
"href": "/subdevices/vs/0",
"rep": {
"x.com.samsung.da.subdeviceIdList": "REDACTED"
}
},
{
"href": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "1"
}
},
{
"href": "/temperature/current/0",
"rep": {
"range": [
18.0,
30.0
],
"units": "C",
"temperature": 31.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
18.0,
30.0
],
"units": "C",
"temperature": 27.0
}
},
{
"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": "31.0",
"x.com.samsung.da.maximum": "30",
"x.com.samsung.da.minimum": "18",
"x.com.samsung.da.increment": "1.0",
"x.com.samsung.da.unit": "Celsius"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/wind/direction/vs/0",
"rep": {
"x.com.samsung.da.modes": "NotSupported",
"x.com.samsung.da.supportedModes": [
"NotSupported"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "0",
"x.com.samsung.da.supportedModes": [
"0",
"2",
"3",
"4"
],
"x.com.samsung.da.modesName": [
"Auto",
"Mid",
"High",
"Turbo"
]
}
}
]
}
+525
View File
@@ -0,0 +1,525 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"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": "/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": "2024-01-01T00:00:00",
"x.com.samsung.da.state": "Deleted"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "FilterAlarm_OFF",
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00",
"x.com.samsung.da.state": "Deleted"
}
]
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "1DBF00A0012C3F61024804000000",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "1150000000",
"x.com.samsung.da.airconOptionList": [
"SingleCommand_1",
"DR",
"HOMECARE_WIZARD_V2",
"AI_RAC_KOREA_COOLONLY_3.0",
"AI_3.0",
"Auto_To_AI"
]
}
},
{
"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": "/drlc/0",
"rep": {
"DRLevel": 0,
"start": "1970-01-01T00:00:00Z",
"duration": 0,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.duration": "00:00:00",
"x.com.samsung.da.drlcStartTime": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/energy/consumption/0",
"rep": {
"power": 338.0
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "338.000000",
"x.com.samsung.da.cumulativePower": "1362848",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "2",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterDesiredUsage": "500",
"x.com.samsung.da.filterStatus": "normal",
"x.com.samsung.da.filterCapacity": "500",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"replaceable",
"washable"
]
}
},
{
"href": "/humidity/0",
"rep": {
"humidity": 0
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "0",
"x.com.samsung.da.fivepercentHumidity": "42"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01001_0000|10250041|6001051C001711014E00482200912000",
"x.com.samsung.da.description": "TP1X_DA-AC-RAC-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": "AR1",
"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": "02646A260327",
"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": "102500A23012700",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "102472A22120800,102528A10000100",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 0
}
},
{
"href": "/light/vs/0",
"rep": {
"mode": "On",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Quiet",
"Smart",
"Nano",
"NanoSleep"
]
}
},
{
"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": [
"Sleep_16",
"ArtificialWorking_Off",
"ComfortAICooling_Off",
"AiTempChanged_Off",
"AiTemp_280",
"welcomecare_Off",
"OutdoorConnection_Connected",
"OutdoorTemp_86",
"CoolCapa_28",
"WarmCapa_0",
"Volume_100",
"StopAutoClean_Set",
"Autoclean_On",
"DiagnosisAI_Off",
"ProgressDiagnosisAI_0",
"ResultDiagnosisAI_Normal",
"SmartCoolClean_Off",
"ProgressSmartClean_0",
"FreezeAlarmSetting_Off",
"DesiredFreezeAlarm_240",
"WashAlarm_Off",
"OptionCode_56440",
"ExtendOptionCode_246479",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"DurationOn_115",
"welcomecareElapsedTime_0",
"welcomecareThresholdTemp_0",
"welcomecareStartDate_0000",
"welcomecareEndDate_0000",
"welcomecareSeason_None"
]
}
},
{
"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",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"Off"
]
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"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": "ARA-WW-TP1-24-ARXX00",
"versions": [
"11260327"
],
"visVersion": "260327"
},
{
"type": "Micom",
"modelId": "045210250041FFFFFFFF",
"versions": [
"23012700",
"FFFFFFFF"
],
"visVersion": "230127"
},
{
"type": "Micom",
"modelId": "04521024724110252841",
"versions": [
"22120800",
"10000100"
],
"visVersion": "221208"
}
]
}
},
{
"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": "/power/0",
"rep": {
"value": true
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On",
"causeSource": "SMTS"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "true",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/remotedatacontrol/vs/0",
"rep": {
"x.com.samsung.da.status": "Off",
"x.com.samsung.da.connectionStatus": "Disconnected"
}
},
{
"href": "/remotedeviceinfo/vs/0",
"rep": {
"x.com.samsung.da.didList": ""
}
},
{
"href": "/remotetemperature/vs/0",
"rep": {
"x.com.samsung.da.temperature": "",
"x.com.samsung.da.unit": "",
"x.com.samsung.da.error": ""
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "1DFFFFFFFF101E121EFFFFFFFFFFFF01000000012F0100000F000000001C00",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/sensors/vs/0",
"rep": {}
},
{
"href": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "1"
}
},
{
"href": "/temperature/current/0",
"rep": {
"range": [
16,
30
],
"units": "C",
"temperature": 28.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
16,
30
],
"units": "C",
"temperature": 25.0
}
},
{
"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": "25.0",
"x.com.samsung.da.current": "28.0",
"x.com.samsung.da.maximum": "30",
"x.com.samsung.da.minimum": "16",
"x.com.samsung.da.increment": "1.0",
"x.com.samsung.da.unit": "Celsius"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/wind/direction/vs/0",
"rep": {
"x.com.samsung.da.modes": "Fix",
"x.com.samsung.da.supportedModes": [
"Fix",
"Up_And_Low",
"Left_And_Right",
"All"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "32",
"x.com.samsung.da.supportedModes": [
"0",
"31",
"32",
"33",
"34",
"35"
],
"x.com.samsung.da.modesName": [
"Auto",
"1",
"2",
"3",
"4",
"MAX"
]
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "REDACTED",
"macaddressBLE": "REDACTED",
"connectedApSsid": "REDACTED"
}
}
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
"diagnosis_status",
"energy_kwh",
"finish_time",
"job_beginning_status",
"machine_state",
"power_switch",
"progress",
"progress_percentage",
"remote_control",
"wrinkle_prevent"
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
"diagnosis_status",
"energy_kwh",
"finish_time",
"firmware_update",
"job_beginning_status",
"machine_state",
"power_switch",
"progress",
"progress_percentage",
"remote_control",
"sanitize",
"wrinkle_prevent"
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"state_keys": [
"alarm_code",
"clean_level",
"device_active",
"dust",
"energy_kwh",
"fine_dust",
"firmware_update",
"hepa_filter_status",
"hepa_filter_usage",
"odor",
"power_switch",
"super_fine_dust",
"wind_strength_fan"
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"state_keys": [
"air_filter_status",
"air_filter_threshold",
"air_filter_usage",
"air_filter_usage_hours",
"alarm_code",
"auto_clean",
"beep",
"climate",
"current_temperature_c",
"diagnosis_status",
"energy_kwh",
"firmware_update",
"humidity",
"power_energy_kwh",
"power_watts",
"tropical_night_mode"
]
}
@@ -0,0 +1,20 @@
{
"state_keys": [
"air_filter_status",
"air_filter_usage",
"air_filter_usage_hours",
"alarm_code",
"auto_clean",
"beep",
"climate",
"current_temperature_c",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"power_watts",
"tropical_night_mode"
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"state_keys": [
"alarm_code",
"cavity_state",
"child_lock",
"cloud_connected",
"cook_time",
"cooking_mode",
"cycle_active",
"door_open",
"energy_kwh",
"fan",
"finish_time",
"firmware_update",
"lamp",
"machine_state",
"operation_time_minutes",
"power_level",
"progress_percentage",
"sound"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"cloud_connected",
"cook_time",
"cooktop_running_state",
"current_temp_c",
"cycle_active",
"door_open",
"fast_preheat",
"finish_time",
"firmware_update",
"lamp",
"machine_state",
"natural_steam",
"operation_time_minutes",
"oven_mode",
"oven_setpoint",
"oven_state",
"power_switch",
"progress_percentage",
"remote_control",
"sound",
"warming_center_state"
]
}
+31
View File
@@ -0,0 +1,31 @@
{
"state_keys": [
"ai_energy_level",
"alarm_code",
"auto_door_opener",
"brightness_level",
"cabinet_light_dim",
"cabinet_light_switch",
"cooler_setpoint",
"cooler_temperature",
"day_brightness",
"door_alert",
"door_cooler_open",
"door_freezer_open",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"freezer_setpoint",
"freezer_temperature",
"fridge_sound",
"night_end",
"night_start",
"power_energy_kwh",
"power_watts",
"rapid_freezing",
"rapid_fridge",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status"
]
}
+269
View File
@@ -0,0 +1,269 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/alarms/vs/0",
"rep": {
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.s"
],
"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": "OV_E_OFF",
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00"
}
]
}
},
{
"href": "/connected/vs/0",
"rep": {
"x.com.samsung.da.connected": "On",
"rt": [
"x.com.samsung.da.connected"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close"
}
],
"rt": [
"x.com.samsung.da.doors"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePower": "103700",
"x.com.samsung.da.cumulativeUnit": "Wh"
}
},
{
"href": "/hood/fanspeed/vs/0",
"rep": {
"x.com.samsung.da.hood.fanSpeed": "0",
"x.com.samsung.da.hood.supportedFanSpeed": [
"0",
"1",
"2",
"3",
"4"
],
"x.com.samsung.da.hood.settableMaxFanSpeed": "4",
"x.com.samsung.da.hood.settableMinFanSpeed": "0",
"rt": [
"x.com.samsung.da.hood.fanSpeed"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA-KS-MICROWAVE-01051|40475141|50040000011811000A00000000000000",
"x.com.samsung.da.description": "ME7500D-/AA2",
"x.com.samsung.da.serialNum": "REDACTED",
"x.com.samsung.da.otnDUID": "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": "24111400",
"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": "04751A23121900",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "04754B23120800",
"x.com.samsung.da.newVersionAvailable": "0"
}
],
"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": "KM5",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01"
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"NoOperation",
"MicroWave",
"Autocook",
"KeepWarm"
],
"x.com.samsung.da.modes": [
"NoOperation"
],
"x.com.samsung.da.options": [
"DeviceType_ME7500D-/AA2",
"TimeAutoSync_On",
"weight_LBS",
"TimeSystem_12",
"Sound_Off",
"RemindBeep_Off",
"FilterRemind_Off",
"Lamp_High"
],
"x.com.samsung.da.defaultMode": "MicroWave",
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.operationTime": "00:00:00",
"x.com.samsung.da.remainingTime": "00:00:00",
"x.com.samsung.da.progressPercentage": "0",
"rt": [
"x.com.samsung.da.operation"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"flashingProgress": "",
"otnStatus": "None",
"otnList": [
{
"type": "WIFI",
"modelId": "AKS-WW-TP1-23-MICROWAVE-OTR",
"versions": [
"40241114"
],
"visVersion": "241114"
},
{
"type": "Micom",
"modelId": "074240475141FFFFFFFF",
"versions": [
"23121900",
"FFFFFFFF"
],
"visVersion": "231219"
},
{
"type": "Micom",
"modelId": "074240475442FFFFFFFF",
"versions": [
"23120800",
"FFFFFFFF"
],
"visVersion": "231208"
}
]
}
},
{
"href": "/oven/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.recipe": "00000000000000",
"x.com.samsung.da.powerLevel": "0",
"rt": [
"x.com.samsung.da.oven"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "America/New_York",
"offset": "-04:00",
"DST": "ON"
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "REDACTED",
"macaddressBLE": "REDACTED"
}
}
]
}
+200
View File
@@ -0,0 +1,200 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/alarms/vs/0",
"rep": {
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.s"
],
"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": "OV_E_OFF",
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00"
}
]
}
},
{
"href": "/configuration/vs/0",
"rep": {}
},
{
"href": "/connected/vs/0",
"rep": {
"x.com.samsung.da.connected": "On"
}
},
{
"href": "/cooktopmonitoring/vs/0",
"rep": {
"x.com.samsung.da.warmingCenterState": "Off",
"x.com.samsung.da.cooktopMonitoring": "0",
"x.com.samsung.da.cooktopRunningState": "Ready",
"supportedHoodLampStateList": [
"Run",
"Ready"
]
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close",
"x.com.samsung.da.lock": "Unlock"
}
],
"rt": [
"x.com.samsung.da.doors"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Bake",
"Broil",
"ConvectionBake",
"ConvectionRoast",
"KeepWarm",
"BreadProof",
"AirFryer",
"Dehydrate",
"SelfClean",
"SteamClean"
],
"x.com.samsung.da.modes": [
"NoOperation"
],
"x.com.samsung.da.options": [
"DeviceType_NE6516A-/AA0",
"SettingPossible_0",
"UpperLamp_Off",
"Sound_Off",
"AdjustingTemp_0",
"Sabbath_Off",
"EnergySaving_On",
"BurnerOnAlert_Off"
],
"x.com.samsung.da.defaultMode": "ConvectionBake",
"x.com.samsung.da.modeSpec": "[{\"mode\":\"Bake\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"175\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"350\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\"},{\"mode\":\"ConvectionRoast\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\"},{\"mode\":\"AirFryer\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"175\",\"tempMaxC\":\"260\",\"tempDefaultC\":\"220\",\"tempListLengthC\":\"0\",\"tempMinF\":\"350\",\"tempMaxF\":\"500\",\"tempDefaultF\":\"425\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\"}]",
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.operationTime": "00:00:00",
"x.com.samsung.da.remainingTime": "00:00:00",
"x.com.samsung.da.progressPercentage": "1",
"rt": [
"x.com.samsung.da.operation"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false"
}
},
{
"href": "/oven/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"rt": [
"x.com.samsung.da.oven"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On"
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "true",
"rt": [
"x.com.samsung.da.configuration"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"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": "0",
"x.com.samsung.da.current": "175",
"x.com.samsung.da.increment": "1",
"x.com.samsung.da.unit": "Fahrenheit"
}
],
"rt": [
"x.com.samsung.da.temperatures"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
}
]
}
+470
View File
@@ -0,0 +1,470 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/alarms/vs/0",
"rep": {
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/cabinet/light/enhanced/vs/0",
"rep": {
"light.control.status": "Off",
"level.brightness.daytime": "100",
"level.brightness.nighttime": "100",
"night.starttime": "2025-08-08T19:00:00",
"night.duration.minute": "540",
"timezone.offset": "+02:00",
"rt": [
"x.com.samsung.da.light.enhanced"
],
"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": "On",
"rt": [
"x.com.samsung.da.cabinetlight"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "",
"x.com.samsung.da.countryCode": ""
}
},
{
"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": "/dginformation/vs/0",
"rep": {
"enrolmentstatus": "Unknown",
"devicestate": "Unknown",
"lockstatus": "Unknown",
"nextduedate": "",
"workingminutes": 0,
"paymentinfo": {
"emiplan": "Unknown",
"currency": "Unknown",
"totalemi": 0,
"totalemipaid": 0
}
}
},
{
"href": "/door/cooler/0",
"rep": {
"openState": "Close",
"rt": [
"oic.r.door"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/door/freezer/0",
"rep": {
"openState": "Close",
"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": "/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": "/energy/ailevel/vs/0",
"rep": {
"aiLevel": "1",
"supportedAiLevel": [
"1",
"2"
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeConsumption": "281",
"x.com.samsung.da.instantaneousPower": "48",
"x.com.samsung.da.cumulativePower": "216620",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+01:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_REF_21K|00175941|00050126001811344100000020090000",
"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": "RB2",
"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": "24070109, FFFFFFFF, 24062000, FFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"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"
],
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"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": []
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/refrigeration/vs/0",
"rep": {
"x.com.samsung.da.rapidFridge": "Off",
"x.com.samsung.da.rapidFreezing": "Off",
"rt": [
"x.com.samsung.da.fridge"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/rm/control/vs/0",
"rep": {
"minPeriod": "9000"
}
},
{
"href": "/runningmode/vs/0",
"rep": {
"x.com.samsung.da.runningMode": 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": "/settings/sound/alert/door/vs/0",
"rep": {
"alert.door": "1",
"supportedAlert.door": [
"1",
"2",
"3",
"4"
],
"rt": [
"x.com.samsung.alert.door"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/status/lock/vs/0",
"rep": {
"x.com.samsung.da.device.sound": "Off",
"rt": [
"x.com.samsung.da.lockstatus"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/temperature/current/cooler/0",
"rep": {
"temperature": 6.0,
"range": [
1.0,
7.0
],
"units": "C",
"rt": [
"oic.r.temperature"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/temperature/current/freezer/0",
"rep": {
"temperature": -17.0,
"range": [
-23.0,
-15.0
],
"units": "C",
"rt": [
"oic.r.temperature"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/temperature/desired/cooler/0",
"rep": {
"temperature": 6.0,
"range": [
1.0,
7.0
],
"units": "C",
"rt": [
"oic.r.temperature"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/temperature/desired/freezer/0",
"rep": {
"temperature": -17.0,
"range": [
-23.0,
-15.0
],
"units": "C",
"rt": [
"oic.r.temperature"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Freezer",
"x.com.samsung.da.desired": "-17",
"x.com.samsung.da.current": "-17",
"x.com.samsung.da.maximum": "-15",
"x.com.samsung.da.minimum": "-23",
"x.com.samsung.da.unit": "Celsius"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Fridge",
"x.com.samsung.da.desired": "6",
"x.com.samsung.da.current": "6",
"x.com.samsung.da.maximum": "7",
"x.com.samsung.da.minimum": "1",
"x.com.samsung.da.unit": "Celsius"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Europe/Rome",
"offset": "+02:00",
"DST": "ON"
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "REDACTED",
"macaddressBLE": "REDACTED",
"connectedApSsid": "SPRDFL24"
}
}
]
}
+70
View File
@@ -0,0 +1,70 @@
"""Tests for the AirDresser device type (DA_DF_A51_20_COMMON, issue #162)."""
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 tests.conftest import _load_device
def _air_dresser():
resources = _load_device('air_dresser')
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():
reg, resources = _air_dresser()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
return flatten(bound, resources)
def test_resolves_to_air_dresser_registry():
reg, _ = _air_dresser()
assert reg is not None and reg.name == 'air_dresser'
def test_no_unbound_hrefs():
reg, resources = _air_dresser()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_course_select_present_and_reads_current_selection():
"""This was the actual reported gap: /course/vs/0 was previously
entirely unbound, so no cycle/mode select existed at all."""
state = _state()
assert state['cycle'] == '01'
def test_course_options_derived_from_supported_options_fallback():
"""No /wm/editcourse/vs/0 on this board at all, so the option list must
come from laundry.cycle_options' supportedOptions decode rather than
editCourseList -- confirmed distinct codes, current selection included."""
from custom_components.localthings.registry.capabilities.laundry import cycle_options
_, resources = _air_dresser()
codes = cycle_options(resources)
assert codes == ['01', '02', '04', '03', '05', '1A', '1B', '1C', '07', '08']
def test_wrinkle_prevent_present_dry_level_fields_absent():
"""AIR_DRESSER_SETTINGS only binds wrinkle_prevent -- dryLevel/dryTime/
dryerType are dryer-only fields this device never reports, so they
should not appear as always-empty entities."""
state = _state()
assert state['wrinkle_prevent'] is False
for key in ('dry_level', 'dry_time', 'dryer_type'):
assert key not in state, key
def test_expected_entities_present():
state = _state()
for key in (
'power_switch', 'child_lock', 'remote_control', 'cycle',
'machine_state', 'progress', 'progress_percentage', 'finish_time',
'completion_minutes', 'delay_start_hours', 'diagnosis_status',
'job_beginning_status', 'wrinkle_prevent', 'energy_kwh',
):
assert key in state, key
@@ -0,0 +1,82 @@
"""Tests for the DA_DF_TP2_20_COMMON AirDresser (model DF9500A, issue #157).
A different board generation than issue #162's DA_DF_A51_20_COMMON, routed
into the same air_dresser registry (both carry the '_DF_' modelNum token).
Exercises the two things this board does differently: a populated
/wm/editcourse/vs/0 (so cycle_options() never needs the supportedOptions
fallback #162 relies on) and the new /airdresseroption/sanitize/vs/0
capability #162's dump doesn't report at all.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import air_dresser, for_device_by_model
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
def _air_dresser():
resources = _load_device('air_dresser_tp2_20')
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():
reg, resources = _air_dresser()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
return flatten(bound, resources)
def test_resolves_to_air_dresser_registry():
reg, _ = _air_dresser()
assert reg is not None and reg.name == 'air_dresser'
def test_no_unbound_hrefs():
"""Confirms /st/airdressercourse/vs/0 and /airdresseroption/sanitize/vs/0
-- the two hrefs this board reports that #162's dump doesn't -- are
covered (ignored.py and AIR_DRESSER_SANITIZE respectively)."""
reg, resources = _air_dresser()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_course_options_come_from_edit_course_list_not_the_fallback():
"""Unlike issue #162's board, this one populates /wm/editcourse/vs/0
directly, so cycle_options() should never reach the
supportedOptions-decode fallback."""
from custom_components.localthings.registry.capabilities.laundry import cycle_options
_, resources = _air_dresser()
codes = cycle_options(resources)
assert codes[:3] == ['22', '23', '0C']
assert codes == cycle_options({'/wm/editcourse/vs/0': resources['/wm/editcourse/vs/0']})
def test_course_select_reads_current_selection():
state = _state()
assert state['cycle'] == '22'
def test_course_translation_key_falls_back_to_cycle_for_unidentified_table():
"""Table_00's course codes aren't identified yet, so the select's
translation_key resolves to the generic 'cycle' catalog entry rather
than a table-specific one that doesn't exist."""
_, resources = _air_dresser()
desc = air_dresser.REGISTRY.capabilities['/course/vs/0'][0].entities[0]
assert desc.translation_key(resources) == 'cycle'
def test_sanitize_present_and_toggles():
state = _state()
assert state['sanitize'] is False
desc = next(
e for e in air_dresser.REGISTRY.capabilities['/airdresseroption/sanitize/vs/0'][0].entities
if e.key == 'sanitize'
)
path, body = desc.write_fn('On', {})
assert path == ['airdresseroption', 'sanitize', 'vs', '0']
assert body == {'x.com.samsung.da.sanitize': 'On'}
assert desc.write_fn('Sparkle', {}) is None
+128
View File
@@ -0,0 +1,128 @@
"""HA fan-entity mapping tests for the A-VTWW-TP2-21-COMMON BESPOKE Cube Air
(issue #151).
This board's /wind/strength/vs/0 reports numeric wind-strength codes
("87"/"89"/"90"/"91") with a separate modesName array ("SMART"/"MAX"/
"WINDFREE"/"Sleep") giving the actual names, unlike the TP1X_DA-AC-AIR
family's /mode/vs/0 (issue #130) where supportedModes IS the name list
already. LocalThingsAirPurifierFan._label_for_code resolves both shapes
without a per-model map.
"""
from custom_components.localthings.fan import LocalThingsAirPurifierFan
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.capabilities.air_purifier import HREF_WIND_STRENGTH
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import FanDesc
from tests.conftest import _load_device
class _FakeCoordinator:
device_serial = 'TEST-VTWW-SERIAL'
device_info = {}
data = {}
def __init__(self, resources):
self.last_resources = resources
self.commands = []
def resource(self, href):
return self.last_resources.get(href, {})
async def async_send_command(self, bound, payload):
self.commands.append((bound, payload))
def _resources():
return _load_device('air_purifier_vtww')
def _reg(resources):
info = resources['/information/vs/0']
return for_device_by_model(
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
def _entity(resources, coordinator=None):
reg = _reg(resources)
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
fan_bound = next(
item for item in bound
if isinstance(item.desc, FanDesc) and item.href == HREF_WIND_STRENGTH
)
return LocalThingsAirPurifierFan(coordinator or _FakeCoordinator(resources), fan_bound)
def test_resolves_to_air_purifier_registry():
assert _reg(_resources()).name == 'air_purifier'
def test_no_unbound_hrefs():
resources = _resources()
reg = _reg(resources)
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_preset_modes_come_from_modes_name_not_the_raw_codes():
entity = _entity(_resources())
assert entity.preset_modes == ['smart', 'max', 'windfree', 'sleep']
def test_preset_mode_reads_the_current_code_via_modes_name():
"""Fixture's current mode is '87' -> modesName[0] 'SMART'."""
entity = _entity(_resources())
assert entity.preset_mode == 'smart'
async def test_set_preset_mode_writes_back_the_raw_code():
resources = _resources()
coordinator = _FakeCoordinator(resources)
entity = _entity(resources, coordinator)
await entity.async_set_preset_mode('windfree')
assert coordinator.commands[-1][1] == ('mode', '90')
def test_is_on_reads_vendor_power():
entity = _entity(_resources())
assert entity.is_on is False
def test_wind_strength_fan_key_does_not_collide_with_mode_fan():
"""WIND_STRENGTH_FAN and FAN both live in this registry and both are
FanDesc-typed; BoundEntity's unique_id is built from key alone (entity.py's
_key), not href, so a shared key would silently shadow one entity if a
board ever bound both (see AIRFLOW_GENERIC's own comment on this exact
hazard). No real dump reports both hrefs today, but the keys must stay
distinct regardless."""
from custom_components.localthings.registry.capabilities import air_purifier
fan_keys = {
entity.key
for cap in (air_purifier.FAN, air_purifier.WIND_STRENGTH_FAN)
for entity in cap.entities
}
assert fan_keys == {'fan', 'wind_strength_fan'}
def test_both_fan_hrefs_bound_simultaneously_produce_distinct_entities():
"""Synthetic combination (no real dump reports both hrefs) proving the
two FanDescs don't shadow each other in flatten()'s key-based state dict
even if a future board did report both."""
from custom_components.localthings.registry.adapter import flatten
resources = _resources()
resources['/mode/vs/0'] = {
'x.com.samsung.da.modes': ['Smart'],
'x.com.samsung.da.supportedModes': ['Smart', 'Max', 'Mid', 'WindFree', 'Sleep'],
}
reg = _reg(resources)
unbound = []
bound = discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
state = flatten(bound, resources)
assert state['fan'] == 'Smart'
assert state['wind_strength_fan'] == '87'
+18 -1
View File
@@ -24,7 +24,8 @@ from custom_components.localthings.registry.by_type import (
airconditioner, for_device_by_model,
)
from custom_components.localthings.registry.capabilities.airconditioner import (
HREF_AIRFLOW, _option_number_write, _option_switch_write,
HREF_AIRFLOW, HREF_WIND_STRENGTH, _option_number_write,
_option_switch_write, is_legacy_board,
)
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import ClimateDesc
@@ -119,6 +120,22 @@ def test_token_entities_stay_off_newer_boards():
assert key not in state, key
def test_climate_legacy_airflow_gate_agrees_with_is_legacy_board():
"""issue #161: climate.py's _legacy_airflow() delegates to
capabilities/airconditioner.py's is_legacy_board() instead of
re-implementing the same presence/absence check, so the token entities
and the climate card's legacy read/write paths can't drift apart on
which board generation is in play."""
legacy_resources = _load_device(FIXTURE)
assert is_legacy_board(legacy_resources) is True
assert _climate(legacy_resources)._legacy_airflow() == legacy_resources[HREF_AIRFLOW]
newer_resources = _load_device('airconditioner_tp1x_rac')
assert is_legacy_board(newer_resources) is False
assert _climate(newer_resources)._legacy_airflow() == {}
assert HREF_WIND_STRENGTH in newer_resources
def test_absent_token_yields_no_entity():
"""The issue #136 unit of this same model reports no Spi token."""
resources = _load_device(FIXTURE)
+55
View File
@@ -418,6 +418,61 @@ def test_humidity_falls_back_to_the_plain_field_where_five_percent_is_absent():
assert desc.rep_fn({}) is None
def test_humidity_five_percent_field_passes_a_genuine_zero_through():
"""issue #160: fivepercentHumidity's zero-as-"not measuring" carve-out
(added in #146 to cover ARTIK051's plain humidity field) was
over-applied to fivepercentHumidity too, silently turning a real 0%
reading on every other AC board into unknown. Only the humidity
fallback field collapses 0 -- fivepercentHumidity's 0 is a real
reading."""
desc = airconditioner.HUMIDITY.entities[0]
assert desc.rep_fn({'x.com.samsung.da.fivepercentHumidity': '0'}) == 0.0
# ---------------------------------------------------------------------------
# Wind-Free 2-in-1 (TP2X_FAC_BORA_21K, issues #150/#153): a floor-standing +
# wall-mounted indoor unit pair sharing one outdoor unit and one local IP.
# Reported "no climate entity is generated, only power" -- the device simply
# fell back to 'unknown' for lack of a '_FAC_' modelNum routing token; once
# routed, it binds against the exact same CLIMATE composite every other RAC
# family uses, zero unbound hrefs, no new capabilities needed beyond ignoring
# the two hrefs (/subdevices/vs/0, /runn/vs/0) unique to this board.
# ---------------------------------------------------------------------------
def _ac_fac_bora():
resources = _load_device('airconditioner_fac_bora')
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 test_fac_bora_resolves_to_airconditioner_registry():
reg, _ = _ac_fac_bora()
assert reg is not None and reg.name == 'airconditioner'
def test_fac_bora_no_unbound_hrefs():
reg, resources = _ac_fac_bora()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_fac_bora_climate_entity_present():
"""The actual reported gap: only a power switch existed before, no
climate entity at all."""
reg, resources = _ac_fac_bora()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
assert any(isinstance(item.desc, ClimateDesc) for item in bound)
def test_fac_bora_subdevices_and_runningmode_are_ignored_not_guessed():
assert '/subdevices/vs/0' in airconditioner._AC_IGNORED
assert '/runn/vs/0' in airconditioner._AC_IGNORED
# ---------------------------------------------------------------------------
# Additive entities layered on the ARTIK051_PRAC family on top of the upstream
# registry: beep (Volume_* option), tropical night mode (Sleep_<N> option),
@@ -0,0 +1,134 @@
"""TP1X_DA-AC-RAC-01001_0000 fan-strength codes (model AR07C9150HZN, issue
#155).
Its /wind/strength/vs/0 reports supportedModes "0"/"31"/"32"/"33"/"34"/"35"
instead of the "0"-"4" scale climate.py's _DEVICE_TO_FAN was built from
(every other AC fixture in this repo uses "0"-"4", some with a 6th "5" --
see airconditioner_window_ac_device.json). Only "0" matched _DEVICE_TO_FAN,
so fan_modes silently dropped every speed but Auto. The fix reads the
device's own modesName labels (parallel-indexed with supportedModes) for
any code _DEVICE_TO_FAN doesn't already cover, instead of hardcoding a
second numeric scale.
"""
from custom_components.localthings.climate import (
LocalThingsClimate, _DEVICE_TO_FAN, _wind_strength_label,
)
from custom_components.localthings.registry import by_type
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import ClimateDesc
from tests.conftest import _load_device
FIXTURE = 'airconditioner_tp1x_rac_01001'
class _FakeCoordinator:
device_serial = 'TEST-RAC-01001-SERIAL'
device_info = {}
data = {}
def __init__(self, resources):
self.last_resources = resources
self.commands = []
def resource(self, href):
return self.last_resources.get(href, {})
async def async_send_command(self, bound, payload):
self.commands.append((bound, payload))
def _climate(resources, coordinator=None):
info = resources['/information/vs/0']
reg = by_type.for_device_by_model(
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
climate_bound = next(item for item in bound if isinstance(item.desc, ClimateDesc))
return LocalThingsClimate(coordinator or _FakeCoordinator(resources), climate_bound)
def test_wind_strength_label_reads_the_devices_own_modes_name():
rep = {
'x.com.samsung.da.supportedModes': ['0', '31', '32', '33', '34', '35'],
'x.com.samsung.da.modesName': ['Auto', '1', '2', '3', '4', 'MAX'],
}
assert _wind_strength_label('32', rep) == '2'
assert _wind_strength_label('35', rep) == 'max'
def test_wind_strength_label_falls_back_to_raw_code_when_names_absent():
assert _wind_strength_label('32', {}) == '32'
def test_fan_modes_include_every_supported_speed_not_just_auto():
"""Before the fix, only '0' matched _DEVICE_TO_FAN and fan_modes was
['auto'] -- exactly the reported symptom."""
resources = _load_device(FIXTURE)
entity = _climate(resources)
assert entity.fan_modes == ['auto', '1', '2', '3', '4', 'max']
def test_fan_mode_reads_the_current_dynamic_code():
"""Fixture's /wind/strength/vs/0 modes is '32' -> modesName '2'."""
resources = _load_device(FIXTURE)
entity = _climate(resources)
assert entity.fan_mode == '2'
def test_standard_scale_codes_still_use_device_to_fan():
"""A code _DEVICE_TO_FAN already covers keeps its existing friendly
label rather than falling through to the device's own (blunter) one --
no regression for boards using the standard "0"-"4" scale."""
resources = _load_device(FIXTURE)
resources['/wind/strength/vs/0']['x.com.samsung.da.modes'] = '0'
entity = _climate(resources)
assert entity.fan_mode == _DEVICE_TO_FAN['0'] == 'auto'
async def test_set_fan_mode_resolves_a_dynamic_label_back_to_its_code():
resources = _load_device(FIXTURE)
coordinator = _FakeCoordinator(resources)
entity = _climate(resources, coordinator)
await entity.async_set_fan_mode('max')
assert coordinator.commands[-1][1] == ('fan', '35')
async def test_set_fan_mode_still_resolves_standard_scale_labels():
resources = _load_device(FIXTURE)
coordinator = _FakeCoordinator(resources)
entity = _climate(resources, coordinator)
await entity.async_set_fan_mode('auto')
assert coordinator.commands[-1][1] == ('fan', '0')
async def test_set_fan_mode_does_not_misroute_when_static_map_and_live_codes_collide():
"""A board can use non-standard codes ('31'-'33') while modesName still
spells a standard-looking label ('Low'/'High') that _FAN_TO_DEVICE's
static reverse map also happens to have an entry for ('1'/'3') -- but
that entry is for a *different* code this unit never advertises at all.
Resolving the static hit without checking it against this unit's own
supportedModes would silently write a code the device doesn't have.
"""
resources = _load_device(FIXTURE)
resources['/wind/strength/vs/0'] = {
'x.com.samsung.da.modes': '0',
'x.com.samsung.da.supportedModes': ['0', '31', '32', '33'],
'x.com.samsung.da.modesName': ['Auto', 'Low', 'High', 'Turbo'],
}
coordinator = _FakeCoordinator(resources)
entity = _climate(resources, coordinator)
assert entity.fan_modes == ['auto', 'low', 'high', 'turbo']
await entity.async_set_fan_mode('high')
assert coordinator.commands[-1][1] == ('fan', '32')
await entity.async_set_fan_mode('low')
assert coordinator.commands[-1][1] == ('fan', '31')
await entity.async_set_fan_mode('turbo')
assert coordinator.commands[-1][1] == ('fan', '33')
+44 -2
View File
@@ -7,8 +7,8 @@ testable directly.
from homeassistant.components.climate import HVACMode
from custom_components.localthings.climate import (
_AI_COMFORT_MODE, _DEVICE_TO_HVAC, _HVAC_TO_DEVICE, PRESET_AI_COMFORT,
_preset_to_ha,
_AI_COMFORT_MODE, _DEVICE_TO_HVAC, _HVAC_TO_DEVICE,
PRESET_AI_COMFORT, _preset_to_ha,
)
@@ -74,3 +74,45 @@ def test_preset_to_ha_lowercases_other_codes():
assert _preset_to_ha('Sleep') == 'sleep'
assert _preset_to_ha('NanoSleep') == 'nanosleep'
assert _preset_to_ha('MotionIndirect') == 'motionindirect'
def test_fac_bora_wind_strength_codes_fit_the_standard_scale():
"""TP2X_FAC_BORA_21K's (issues #150/#153) /wind/strength/vs/0 codes
(0/2/3/4, skipping 1/'low') and modesName (Auto/Mid/High/Turbo) already
match _DEVICE_TO_FAN's own mapping exactly -- no dynamic modesName
fallback needed for this particular board, unlike issue #155's
TP1X_DA-AC-RAC-01001_0000.
Asserts the live climate entity's actual fan_modes/fan_mode output
(not just that the module constant _DEVICE_TO_FAN happens to have
these keys) -- a bare `code in _DEVICE_TO_FAN` check would still pass
even if fan_modes/fan_mode were completely broken, since it never
touches the entity at all.
"""
from custom_components.localthings.climate import LocalThingsClimate
from custom_components.localthings.registry import by_type
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import ClimateDesc
from tests.conftest import _load_device
class _FakeCoordinator:
device_serial = 'TEST-FAC-BORA-SERIAL'
device_info = {}
data = {}
def __init__(self, resources):
self.last_resources = resources
def resource(self, href):
return self.last_resources.get(href, {})
resources = _load_device('airconditioner_fac_bora')
info = resources['/information/vs/0']
reg = by_type.for_device_by_model(
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
climate_bound = next(item for item in bound if isinstance(item.desc, ClimateDesc))
entity = LocalThingsClimate(_FakeCoordinator(resources), climate_bound)
assert entity.fan_modes == ['auto', 'medium', 'high', 'turbo']
assert entity.fan_mode == 'auto'
+34
View File
@@ -184,6 +184,40 @@ class TestWmSetinfoFlags:
},
]) == 'LE'
def test_off_suffixed_placeholder_codes_filtered(self):
"""issue #166: Samsung pre-populates /alarms/vs/0 with one row per
supported alarm *type*, each carrying a '<Name>_OFF' placeholder
code (no 'Deleted' state at all) when that alarm isn't firing --
confirmed not just for 'ErrorCode_OFF' but also 'FilterAlarm_OFF'
on the same dump. These were previously shown as if they were
active alarms."""
assert common._active_alarm_codes([
{
'x.com.samsung.da.code': 'ErrorCode_OFF',
'x.com.samsung.da.triggeredTime': '2026-07-28T12:59:22',
},
{
'x.com.samsung.da.code': 'FilterAlarm_OFF',
'x.com.samsung.da.triggeredTime': '2026-07-28T12:59:22',
},
]) == 'none'
def test_real_filter_alarm_not_off_suffixed_still_shown(self):
"""issue #166's actual live filter alert: code 'FilterAlarm' (no
'_OFF' suffix), state 'Created' -- distinct from the 'FilterAlarm_OFF'
placeholder and must still surface."""
assert common._active_alarm_codes([
{
'x.com.samsung.da.code': 'ErrorCode_OFF',
'x.com.samsung.da.triggeredTime': '2026-07-28T12:59:22',
},
{
'x.com.samsung.da.code': 'FilterAlarm',
'x.com.samsung.da.state': 'Created',
'x.com.samsung.da.triggeredTime': '2026-07-28T12:59:22',
},
]) == 'FilterAlarm'
class TestKidsLockFallback:
def test_generic_read_write(self):
+16
View File
@@ -209,3 +209,19 @@ def test_discover_rt_filter_gates_binding():
resources = {'/mode/vs/0': {'rt': ['x.com.samsung.da.mode'], 'x.com.samsung.da.mode': 'Bake'}}
bound = discover(resources, {'/mode/vs/0': [oven_mode_cap]})
assert bound == []
def test_discover_tier_log_fires_for_no_entity_coverage_cap():
"""A coverage-only Capability (entities=(), used to mark a href as
handled by e.g. a composite climate entity rather than its own HA
entity) produces zero BoundEntity rows -- `bound` alone can't tell a
caller its poll_tier. tier_log must still report it, or a consumer
(the coordinator's hot/warm href lists) silently treats it as 'cold'
regardless of what poll_tier the capability actually declares."""
coverage_cap = Capability(href='/wind/strength/vs/0', poll_tier='warm')
resources = {'/wind/strength/vs/0': {'x.com.samsung.da.modes': '0'}}
seen = []
bound = discover(resources, {coverage_cap.href: [coverage_cap]},
tier_log=lambda href, tier: seen.append((href, tier)))
assert bound == []
assert seen == [('/wind/strength/vs/0', 'warm')]
+157
View File
@@ -191,6 +191,27 @@ def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_us():
)
def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_eu():
"""TP1X_REF_21K, EU region variant (issue #165) -- self-reports
oneUiVersion "7.0 Refrigerator" like the US variant, but additionally
carries /rm/control/vs/0 (a bare resource-monitoring poll-interval
config, added to the global ignore list) that the US dump doesn't
report at all. Door sensors (the reporter's actual ask) were already
covered by fridge.DOOR_GENERIC/DOORS_FALLBACK -- this href was the only
gap keeping the coverage repair open."""
from tests.conftest import _load_device
resources = _load_device('refrigerator_tp1x_ref_21k_eu')
golden = json.loads(
(GOLDEN / 'refrigerator_tp1x_ref_21k_eu.json').read_text()
)
state_keys = _new_state_keys('refrigerator_tp1x_ref_21k_eu', 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_range_hood():
from tests.conftest import _load_device
resources = _load_device('range_hood')
@@ -457,6 +478,12 @@ def test_registry_reproduces_golden_state_keys_for_induction_cooktop():
resources = _load_device('induction_cooktop')
golden = json.loads((GOLDEN / 'induction_cooktop.json').read_text())
state_keys = _new_state_keys('induction_cooktop', 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_range_no_info():
"""NE63B8411SS (issue #74) -- reports no oneUiVersion *and* no
@@ -544,6 +571,23 @@ def test_registry_reproduces_golden_state_keys_for_range_ne8300d():
)
def test_registry_reproduces_golden_state_keys_for_range_ne63a6511():
"""NE63A6511SS/AA (issue #138) -- reports no /information/vs/0 at all,
same shape as issue #74's NE63B8411SS; resolved via the same
'Bake'-in-supportedModes + /cooktopmonitoring/vs/0 signature in
for_device_by_resources. Binds cleanly against the existing range
registry with zero unbound hrefs."""
from tests.conftest import _load_device
resources = _load_device('range_ne63a6511')
golden = json.loads((GOLDEN / 'range_ne63a6511.json').read_text())
state_keys = _new_state_keys('range_ne63a6511', 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_ara_ww_tp1_22():
"""ARA-WW-TP1-22-COMMON wall-mount RACs (model AR10/13/18BYEAAWKNME,
issues #115/#116/#117/#120) report no oneUiVersion and no
@@ -630,6 +674,22 @@ def test_registry_reproduces_golden_state_keys_for_microwave_me7500d():
)
def test_registry_reproduces_golden_state_keys_for_microwave_me7500d_lamp_high():
"""Same TP1X_DA-KS-MICROWAVE-01051/ME7500D board as microwave_me7500d
above, but this live capture (issue #152) is the first to report a
non-Off Lamp token ('Lamp_High'). Locks in that the lamp switch reads
it as on rather than the previously-hardcoded 'On'-only comparison."""
from tests.conftest import _load_device
resources = _load_device('microwave_me7500d_lamp_high')
golden = json.loads((GOLDEN / 'microwave_me7500d_lamp_high.json').read_text())
state_keys = _new_state_keys('microwave_me7500d_lamp_high', 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_air_purifier_tp1x_da_ac_air():
"""TP1X_DA-AC-AIR-01031_0000 (issue #130) self-reports oneUiVersion
'7.0 Air purifier' and resolves via for_device() onto the existing
@@ -692,6 +752,103 @@ def test_registry_reproduces_golden_state_keys_for_artik051_krac_18k():
)
def test_registry_reproduces_golden_state_keys_for_airconditioner_tp1x_rac_01001():
"""TP1X_DA-AC-RAC-01001_0000 (model AR07C9150HZN, issue #155) -- binds
cleanly against the existing airconditioner registry with zero unbound
hrefs (the registry/discovery side was never the gap here). Its
/wind/strength/vs/0 reports supportedModes "0"/"31"-"35" instead of the
"0"-"4" scale climate.py's _DEVICE_TO_FAN was built from, which silently
dropped every fan speed but Auto -- see
test_airconditioner_tp1x_rac_01001_fan.py for the climate-level fix."""
from tests.conftest import _load_device
resources = _load_device('airconditioner_tp1x_rac_01001')
golden = json.loads(
(GOLDEN / 'airconditioner_tp1x_rac_01001.json').read_text()
)
state_keys = _new_state_keys('airconditioner_tp1x_rac_01001', 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_air_dresser():
"""DA_DF_A51_20_COMMON AirDresser (model DF8600T, issue #162) -- reports
no oneUiVersion; resolved via the '_DF_' modelNum token fallback. Has no
/wm/editcourse/vs/0 at all, so the course select's option list comes
entirely from laundry.cycle_options' supportedOptions fallback. Binds
cleanly against the new air_dresser registry with zero unbound hrefs."""
from tests.conftest import _load_device
resources = _load_device('air_dresser')
golden = json.loads((GOLDEN / 'air_dresser.json').read_text())
state_keys = _new_state_keys('air_dresser', 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_air_dresser_tp2_20():
"""DA_DF_TP2_20_COMMON AirDresser (model DF9500A, issue #157) -- a
different board generation than issue #162's DA_DF_A51_20_COMMON, also
routed via the '_DF_' modelNum fallback into the same air_dresser
registry. Unlike #162's board, this one populates /wm/editcourse/vs/0's
editCourseList directly (no supportedOptions fallback needed) and
reports two AirDresser-specific resources #162 doesn't have:
/st/airdressercourse/vs/0 (course table id, ignored.py) and
/airdresseroption/sanitize/vs/0 (air_dresser.AIR_DRESSER_SANITIZE).
Binds cleanly with zero unbound hrefs."""
from tests.conftest import _load_device
resources = _load_device('air_dresser_tp2_20')
golden = json.loads((GOLDEN / 'air_dresser_tp2_20.json').read_text())
state_keys = _new_state_keys('air_dresser_tp2_20', 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_air_purifier_vtww():
"""A-VTWW-TP2-21-COMMON BESPOKE Cube Air (issue #151) -- reports no
oneUiVersion; resolved via the '-VTWW-' modelNum token fallback into
the existing air_purifier registry. Its fan lives on
/wind/strength/vs/0 (air_purifier.WIND_STRENGTH_FAN) rather than the
/mode/vs/0 FAN the other two board generations in this registry use.
Binds cleanly with zero unbound hrefs."""
from tests.conftest import _load_device
resources = _load_device('air_purifier_vtww')
golden = json.loads((GOLDEN / 'air_purifier_vtww.json').read_text())
state_keys = _new_state_keys('air_purifier_vtww', 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_fac_bora():
"""TP2X_FAC_BORA_21K Wind-Free 2-in-1 (floor-standing + wall-mounted
indoor units sharing one outdoor unit and one local IP, issues
#150/#153) -- reports no oneUiVersion; resolved via the '_FAC_'
modelNum fallback. Binds cleanly against the existing airconditioner
registry (including a real climate entity, the actual reported gap)
with zero unbound hrefs -- /subdevices/vs/0 and /runn/vs/0 are the only
hrefs this board reports that no other AC family does, both added to
_AC_IGNORED as undocumented/not-locally-actionable."""
from tests.conftest import _load_device
resources = _load_device('airconditioner_fac_bora')
golden = json.loads((GOLDEN / 'airconditioner_fac_bora.json').read_text())
state_keys = _new_state_keys('airconditioner_fac_bora', 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
dump = {
+46 -4
View File
@@ -120,9 +120,23 @@ def test_power_level_handles_missing_value():
def test_microwave_mode_options_nonempty():
desc = microwave.MICROWAVE_MODE.entities[0]
assert len(desc.options) > 0
assert 'MicroWave' in desc.options
assert 'AirFryer' in desc.options # distinct spelling from oven.py's 'AirFry'
assert callable(desc.options)
options = desc.options({})
assert len(options) > 0
assert 'MicroWave' in options
assert 'AirFryer' in options # distinct spelling from oven.py's 'AirFry'
def test_microwave_mode_options_reads_live_supported_modes():
"""issue #152's ME7500D reports only 4 of the 11 union-of-all-dumps
_MICROWAVE_MODES -- the live supportedModes list is used verbatim when
present, same live-first pattern as oven._oven_mode_options, instead of
offering users modes their own unit doesn't have."""
desc = microwave.MICROWAVE_MODE.entities[0]
resources = {'/mode/vs/0': {
'x.com.samsung.da.supportedModes': ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm'],
}}
assert desc.options(resources) == ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm']
def test_microwave_mode_write_round_trips():
@@ -137,6 +151,19 @@ def test_microwave_mode_rejects_unknown():
assert desc.write_fn('SpaghettiMode', {}) is None
def test_microwave_mode_write_validates_against_live_supported_modes():
"""A device reporting its own supportedModes is validated against that
list, not the static union-of-all-dumps fallback -- 'AirFryer' is a
valid _MICROWAVE_MODES entry but must still be rejected for a unit
whose own supportedModes doesn't include it."""
desc = microwave.MICROWAVE_MODE.entities[0]
rep = {'x.com.samsung.da.supportedModes': ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm']}
path, body = desc.write_fn('MicroWave', rep)
assert path == ['mode', 'vs', '0']
assert body['x.com.samsung.da.modes'] == ['MicroWave']
assert desc.write_fn('AirFryer', rep) is None
# ---------------------------------------------------------------------------
# MICROWAVE_MODE — lamp/sound options-array writes
# ---------------------------------------------------------------------------
@@ -165,13 +192,28 @@ def test_lamp_gated_present_when_lamp_option_reported():
def test_lamp_write_is_single_token():
"""issue #152: the device has never been observed accepting 'On' --
only 'High'/'Off' -- so the switch's "on" write uses 'High'."""
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
rep = {'x.com.samsung.da.options': ['Lamp_Off']}
path, body = desc.write_fn('On', rep)
assert path == ['mode', 'vs', '0']
assert body == {'x.com.samsung.da.options': ['Lamp_On']}
assert body == {'x.com.samsung.da.options': ['Lamp_High']}
def test_lamp_write_requires_existing_options():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
assert desc.write_fn('On', {}) is None
def test_lamp_reads_off_as_false():
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
assert desc.value_fn(['Lamp_Off']) is False
def test_lamp_reads_any_non_off_level_as_true():
"""issue #152's ME7500D reports 'Lamp_High', not the binary 'Lamp_On'
#137's dump implied -- any non-Off/non-absent value must read as on, not
just a literal 'On'."""
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
assert desc.value_fn(['Lamp_High']) is True
+32 -2
View File
@@ -120,12 +120,29 @@ def test_oven_setpoint_native_bounds_track_live_unit():
# ---------------------------------------------------------------------------
def test_oven_mode_options_nonempty():
assert len(oven.OVEN_MODE.entities[0].options) > 0
desc = oven.OVEN_MODE.entities[0]
assert callable(desc.options)
assert len(desc.options({})) > 0
def test_oven_mode_options_falls_back_when_no_live_supported_modes():
desc = oven.OVEN_MODE.entities[0]
assert desc.options({}) == list(oven._OVEN_MODES)
def test_oven_mode_options_reads_live_supported_modes():
"""issue #138: the device's own supportedModes list is used verbatim
when present, instead of the static _OVEN_MODES guess."""
desc = oven.OVEN_MODE.entities[0]
resources = {'/mode/vs/0': {
'x.com.samsung.da.supportedModes': ['Bake', 'AirFryer', 'SelfClean'],
}}
assert desc.options(resources) == ['Bake', 'AirFryer', 'SelfClean']
def test_oven_mode_write_round_trips():
desc = oven.OVEN_MODE.entities[0]
valid_mode = desc.options[1] # e.g. 'Bake'
valid_mode = desc.options({})[1] # e.g. 'Bake'
path, body = desc.write_fn(valid_mode, {})
assert path == ['mode', 'vs', '0']
assert body['x.com.samsung.da.modes'] == [valid_mode]
@@ -136,6 +153,19 @@ def test_oven_mode_rejects_unknown():
assert desc.write_fn('SpaghettiMode', {}) is None
def test_oven_mode_write_validates_against_live_supported_modes():
"""A device reporting its own supportedModes is validated against that
list, not the static fallback -- issue #138's AirFryer/SelfClean/etc.
are accepted, and a mode outside the device's own list is rejected even
if some other model's static guess would have allowed it."""
desc = oven.OVEN_MODE.entities[0]
rep = {'x.com.samsung.da.supportedModes': ['Bake', 'AirFryer', 'SelfClean']}
path, body = desc.write_fn('AirFryer', rep)
assert path == ['mode', 'vs', '0']
assert body['x.com.samsung.da.modes'] == ['AirFryer']
assert desc.write_fn('FrozenPizzaPlus', rep) is None
# ---------------------------------------------------------------------------
# OVEN_MODE options-array writes (lamp, sound, fast_preheat, natural_steam).
# Confirmed on real hardware (issue #54): a write only needs to carry the
@@ -0,0 +1,68 @@
"""Tests for the NE63A6511SS/AA range (issue #138) -- same no-/information/vs/0,
no-burner-status shape as issue #74's NE63B8411SS, confirming the existing
range_no_info detection path still resolves this model with zero unbound
hrefs, plus oven.OVEN_MODE reading this dump's ConvectionRoast/KeepWarm/
BreadProof/AirFryer/Dehydrate/SelfClean/SteamClean modes live from its own
/mode/vs/0 supportedModes rather than needing them hardcoded."""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_resources
from custom_components.localthings.registry.capabilities import oven
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
def _range():
resources = _load_device('range_ne63a6511')
reg = for_device_by_resources(resources)
return reg, resources
def _state():
reg, resources = _range()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
return flatten(bound, resources)
def test_resolves_to_range_registry():
reg, _ = _range()
assert reg is not None and reg.name == 'range'
def test_no_unbound_hrefs():
reg, resources = _range()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_expected_entities_present():
state = _state()
for key in (
'power_switch', 'oven_setpoint', 'current_temp_c', 'oven_mode',
'machine_state', 'door_open', 'cloud_connected', 'child_lock',
'cooktop_running_state', 'warming_center_state',
):
assert key in state, key
def test_oven_mode_accepts_this_devices_supported_modes():
"""This dump's /mode/vs/0 supportedModes reports ConvectionRoast,
KeepWarm, BreadProof, AirFryer, Dehydrate, SelfClean, and SteamClean --
none of which were in the static oven._OVEN_MODES fallback, so a
hardcoded write-validation list would've silently rejected them even
though the device itself advertises them. write_fn reads supportedModes
off the live rep (the same one it's called with in production -- see
coordinator.py's async_send_command), so this passes without needing
those modes added to any Python list."""
_, resources = _range()
live_rep = resources['/mode/vs/0']
desc = oven.OVEN_MODE.entities[0]
assert desc.options(resources) == live_rep['x.com.samsung.da.supportedModes']
for mode in (
'ConvectionRoast', 'KeepWarm', 'BreadProof', 'AirFryer',
'Dehydrate', 'SelfClean', 'SteamClean',
):
path, body = desc.write_fn(mode, live_rep)
assert path == ['mode', 'vs', '0']
assert body['x.com.samsung.da.modes'] == [mode]